//-----------------------------------------------------------------------------
// Copyright (c) 2012 GarageGames, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------

#include "platform/platform.h"
#include "T3D/components/collision/collisionTrigger.h"

#include "scene/sceneRenderState.h"
#include "console/consoleTypes.h"
#include "console/engineAPI.h"
#include "collision/boxConvex.h"

#include "core/stream/bitStream.h"
#include "math/mathIO.h"
#include "gfx/gfxTransformSaver.h"
#include "renderInstance/renderPassManager.h"
#include "gfx/gfxDrawUtil.h"
#include "T3D/physics/physicsPlugin.h"
#include "T3D/physics/physicsBody.h"
#include "T3D/physics/physicsCollision.h"


bool CollisionTrigger::smRenderCollisionTriggers = false;

//-----------------------------------------------------------------------------

//----------------------------------------------------------------------------
//--------------------------------------------------------------------------

IMPLEMENT_CO_NETOBJECT_V1(CollisionTrigger);

ConsoleDocClass(CollisionTrigger,
   "@brief A CollisionTrigger is a volume of space that initiates script callbacks "
   "when objects pass through the CollisionTrigger.\n\n"

   "CollisionTriggerData provides the callbacks for the CollisionTrigger when an object enters, stays inside "
   "or leaves the CollisionTrigger's volume.\n\n"

   "@see CollisionTriggerData\n"
   "@ingroup gameObjects\n"
   );

IMPLEMENT_CALLBACK(CollisionTrigger, onAdd, void, (U32 objectId), (objectId),
   "@brief Called when the CollisionTrigger is being created.\n\n"
   "@param objectId the object id of the CollisionTrigger being created\n");

IMPLEMENT_CALLBACK(CollisionTrigger, onRemove, void, (U32 objectId), (objectId),
   "@brief Called just before the CollisionTrigger is deleted.\n\n"
   "@param objectId the object id of the CollisionTrigger being deleted\n");

CollisionTrigger::CollisionTrigger()
{
   // Don't ghost by default.
   mNetFlags.set(Ghostable | ScopeAlways);

   mTypeMask |= TriggerObjectType;

   mObjScale.set(1, 1, 1);
   mObjToWorld.identity();
   mWorldToObj.identity();

   mLastThink = 0;
   mCurrTick = 0;

   mConvexList = new Convex;

   mPhysicsRep = NULL;
}

CollisionTrigger::~CollisionTrigger()
{
   delete mConvexList;
   mConvexList = NULL;
   SAFE_DELETE(mPhysicsRep);
}

bool CollisionTrigger::castRay(const Point3F &start, const Point3F &end, RayInfo* info)
{
   // Collide against bounding box
   F32 st, et, fst = 0, fet = 1;
   F32 *bmin = &mObjBox.minExtents.x;
   F32 *bmax = &mObjBox.maxExtents.x;
   F32 const *si = &start.x;
   F32 const *ei = &end.x;

   for (S32 i = 0; i < 3; i++)
   {
      if (*si < *ei)
      {
         if (*si > *bmax || *ei < *bmin)
            return false;
         F32 di = *ei - *si;
         st = (*si < *bmin) ? (*bmin - *si) / di : 0;
         et = (*ei > *bmax) ? (*bmax - *si) / di : 1;
      }
      else
      {
         if (*ei > *bmax || *si < *bmin)
            return false;
         F32 di = *ei - *si;
         st = (*si > *bmax) ? (*bmax - *si) / di : 0;
         et = (*ei < *bmin) ? (*bmin - *si) / di : 1;
      }
      if (st > fst) fst = st;
      if (et < fet) fet = et;
      if (fet < fst)
         return false;
      bmin++; bmax++;
      si++; ei++;
   }

   info->normal = start - end;
   info->normal.normalizeSafe();
   getTransform().mulV(info->normal);

   info->t = fst;
   info->object = this;
   info->point.interpolate(start, end, fst);
   info->material = 0;
   return true;
}

//-----------------------------------------------------------------------------
void CollisionTrigger::consoleInit()
{
   Con::addVariable("$CollisionTrigger::renderCollisionTriggers", TypeBool, &smRenderCollisionTriggers,
      "@brief Forces all CollisionTrigger's to render.\n\n"
      "Used by the Tools and debug render modes.\n"
      "@ingroup gameObjects");
}

void CollisionTrigger::initPersistFields()
{
   addField("polyhedron", TypeTriggerPolyhedron, Offset(mCollisionTriggerPolyhedron, CollisionTrigger),
      "@brief Defines a non-rectangular area for the CollisionTrigger.\n\n"
      "Rather than the standard rectangular bounds, this optional parameter defines a quadrilateral "
      "CollisionTrigger area.  The quadrilateral is defined as a corner point followed by three vectors "
      "representing the edges extending from the corner.\n");

   addProtectedField("enterCommand", TypeCommand, Offset(mEnterCommand, CollisionTrigger), &setEnterCmd, &defaultProtectedGetFn,
      "The command to execute when an object enters this CollisionTrigger. Object id stored in %%obj. Maximum 1023 characters.");
   addProtectedField("leaveCommand", TypeCommand, Offset(mLeaveCommand, CollisionTrigger), &setLeaveCmd, &defaultProtectedGetFn,
      "The command to execute when an object leaves this CollisionTrigger. Object id stored in %%obj. Maximum 1023 characters.");
   addProtectedField("tickCommand", TypeCommand, Offset(mTickCommand, CollisionTrigger), &setTickCmd, &defaultProtectedGetFn,
      "The command to execute while an object is inside this CollisionTrigger. Maximum 1023 characters.");

   Parent::initPersistFields();
}

bool CollisionTrigger::setEnterCmd(void *object, const char *index, const char *data)
{
   static_cast<CollisionTrigger*>(object)->setMaskBits(EnterCmdMask);
   return true; // to update the actual field
}

bool CollisionTrigger::setLeaveCmd(void *object, const char *index, const char *data)
{
   static_cast<CollisionTrigger*>(object)->setMaskBits(LeaveCmdMask);
   return true; // to update the actual field
}

bool CollisionTrigger::setTickCmd(void *object, const char *index, const char *data)
{
   static_cast<CollisionTrigger*>(object)->setMaskBits(TickCmdMask);
   return true; // to update the actual field
}

//--------------------------------------------------------------------------

bool CollisionTrigger::onAdd()
{
   if (!Parent::onAdd())
      return false;

   onAdd_callback(getId());

   Polyhedron temp = mCollisionTriggerPolyhedron;
   setTriggerPolyhedron(temp);

   addToScene();

   if (isServerObject())
      scriptOnAdd();

   return true;
}

void CollisionTrigger::onRemove()
{
   onRemove_callback(getId());

   mConvexList->nukeList();

   removeFromScene();
   Parent::onRemove();
}

bool CollisionTrigger::onNewDataBlock(GameBaseData *dptr, bool reload)
{
   return true;
}

void CollisionTrigger::onDeleteNotify(SimObject *obj)
{
   GameBase* pScene = dynamic_cast<GameBase*>(obj);

   if (pScene != NULL)
   {
      for (U32 i = 0; i < mObjects.size(); i++)
      {
         if (pScene == mObjects[i])
         {
            mObjects.erase(i);
            //onLeaveCollisionTrigger_callback(this, pScene);
            break;
         }
      }
   }

   Parent::onDeleteNotify(obj);
}

void CollisionTrigger::inspectPostApply()
{
   setTriggerPolyhedron(mCollisionTriggerPolyhedron);
   setMaskBits(PolyMask);
   Parent::inspectPostApply();
}

//--------------------------------------------------------------------------

void CollisionTrigger::buildConvex(const Box3F& box, Convex* convex)
{
   // These should really come out of a pool
   mConvexList->collectGarbage();

   Box3F realBox = box;
   mWorldToObj.mul(realBox);
   realBox.minExtents.convolveInverse(mObjScale);
   realBox.maxExtents.convolveInverse(mObjScale);

   if (realBox.isOverlapped(getObjBox()) == false)
      return;

   // Just return a box convex for the entire shape...
   Convex* cc = 0;
   CollisionWorkingList& wl = convex->getWorkingList();
   for (CollisionWorkingList* itr = wl.wLink.mNext; itr != &wl; itr = itr->wLink.mNext) {
      if (itr->mConvex->getType() == BoxConvexType &&
         itr->mConvex->getObject() == this) {
         cc = itr->mConvex;
         break;
      }
   }
   if (cc)
      return;

   // Create a new convex.
   BoxConvex* cp = new BoxConvex;
   mConvexList->registerObject(cp);
   convex->addToWorkingList(cp);
   cp->init(this);

   mObjBox.getCenter(&cp->mCenter);
   cp->mSize.x = mObjBox.len_x() / 2.0f;
   cp->mSize.y = mObjBox.len_y() / 2.0f;
   cp->mSize.z = mObjBox.len_z() / 2.0f;
}


//------------------------------------------------------------------------------

void CollisionTrigger::setTransform(const MatrixF & mat)
{
   Parent::setTransform(mat);

   if (mPhysicsRep)
      mPhysicsRep->setTransform(mat);

   if (isServerObject()) {
      MatrixF base(true);
      base.scale(Point3F(1.0 / mObjScale.x,
         1.0 / mObjScale.y,
         1.0 / mObjScale.z));
      base.mul(mWorldToObj);
      mClippedList.setBaseTransform(base);

      setMaskBits(TransformMask | ScaleMask);
   }
}

void CollisionTrigger::prepRenderImage(SceneRenderState *state)
{
   // only render if selected or render flag is set
   if (!smRenderCollisionTriggers && !isSelected())
      return;

   ObjectRenderInst *ri = state->getRenderPass()->allocInst<ObjectRenderInst>();
   ri->renderDelegate.bind(this, &CollisionTrigger::renderObject);
   ri->type = RenderPassManager::RIT_Editor;
   ri->translucentSort = true;
   ri->defaultKey = 1;
   state->getRenderPass()->addInst(ri);
}

void CollisionTrigger::renderObject(ObjectRenderInst *ri,
   SceneRenderState *state,
   BaseMatInstance *overrideMat)
{
   if (overrideMat)
      return;

   GFXStateBlockDesc desc;
   desc.setZReadWrite(true, false);
   desc.setBlend(true);

   // CollisionTrigger polyhedrons are set up with outward facing normals and CCW ordering
   // so can't enable backface culling.
   desc.setCullMode(GFXCullNone);

   GFXTransformSaver saver;

   MatrixF mat = getRenderTransform();
   mat.scale(getScale());

   GFX->multWorld(mat);

   GFXDrawUtil *drawer = GFX->getDrawUtil();

   drawer->drawPolyhedron(desc, mCollisionTriggerPolyhedron, ColorI(255, 192, 0, 45));

   // Render wireframe.

   desc.setFillModeWireframe();
   drawer->drawPolyhedron(desc, mCollisionTriggerPolyhedron, ColorI::BLACK);
}

void CollisionTrigger::setTriggerPolyhedron(const Polyhedron& rPolyhedron)
{
   mCollisionTriggerPolyhedron = rPolyhedron;

   if (mCollisionTriggerPolyhedron.mPointList.size() != 0) {
      mObjBox.minExtents.set(1e10, 1e10, 1e10);
      mObjBox.maxExtents.set(-1e10, -1e10, -1e10);
      for (U32 i = 0; i < mCollisionTriggerPolyhedron.mPointList.size(); i++) {
         mObjBox.minExtents.setMin(mCollisionTriggerPolyhedron.mPointList[i]);
         mObjBox.maxExtents.setMax(mCollisionTriggerPolyhedron.mPointList[i]);
      }
   }
   else {
      mObjBox.minExtents.set(-0.5, -0.5, -0.5);
      mObjBox.maxExtents.set(0.5, 0.5, 0.5);
   }

   MatrixF xform = getTransform();
   setTransform(xform);

   mClippedList.clear();
   mClippedList.mPlaneList = mCollisionTriggerPolyhedron.mPlaneList;
   //   for (U32 i = 0; i < mClippedList.mPlaneList.size(); i++)
   //      mClippedList.mPlaneList[i].neg();

   MatrixF base(true);
   base.scale(Point3F(1.0 / mObjScale.x,
      1.0 / mObjScale.y,
      1.0 / mObjScale.z));
   base.mul(mWorldToObj);

   mClippedList.setBaseTransform(base);

   SAFE_DELETE(mPhysicsRep);

   if (PHYSICSMGR)
   {
      PhysicsCollision *colShape = PHYSICSMGR->createCollision();

      MatrixF colMat(true);
      colMat.displace(Point3F(0, 0, mObjBox.getExtents().z * 0.5f * mObjScale.z));

      colShape->addBox(mObjBox.getExtents() * 0.5f * mObjScale, colMat);
      //MatrixF colMat( true );
      //colMat.scale( mObjScale );
      //colShape->addConvex( mCollisionTriggerPolyhedron.pointList.address(), mCollisionTriggerPolyhedron.pointList.size(), colMat );

      PhysicsWorld *world = PHYSICSMGR->getWorld(isServerObject() ? "server" : "client");
      mPhysicsRep = PHYSICSMGR->createBody();
      mPhysicsRep->init(colShape, 0, PhysicsBody::BF_TRIGGER | PhysicsBody::BF_KINEMATIC, this, world);
      mPhysicsRep->setTransform(getTransform());
   }
}


//--------------------------------------------------------------------------

bool CollisionTrigger::testObject(GameBase* enter)
{
   if (mCollisionTriggerPolyhedron.mPointList.size() == 0)
      return false;

   mClippedList.clear();

   SphereF sphere;
   sphere.center = (mWorldBox.minExtents + mWorldBox.maxExtents) * 0.5;
   VectorF bv = mWorldBox.maxExtents - sphere.center;
   sphere.radius = bv.len();

   enter->buildPolyList(PLC_Collision, &mClippedList, mWorldBox, sphere);
   return mClippedList.isEmpty() == false;
}


void CollisionTrigger::potentialEnterObject(GameBase* enter)
{
   for (U32 i = 0; i < mObjects.size(); i++) {
      if (mObjects[i] == enter)
         return;
   }

   if (testObject(enter) == true) {
      mObjects.push_back(enter);
      deleteNotify(enter);

      if (!mEnterCommand.isEmpty())
      {
         String command = String("%obj = ") + enter->getIdString() + ";" + mEnterCommand;
         Con::evaluate(command.c_str());
      }

      //onEnterCollisionTrigger_callback(this, enter);
   }
}


void CollisionTrigger::processTick(const Move* move)
{
   Parent::processTick(move);

   //
   if (mObjects.size() == 0)
      return;

   if (mLastThink + 100 < mCurrTick)
   {
      mCurrTick = 0;
      mLastThink = 0;

      for (S32 i = S32(mObjects.size() - 1); i >= 0; i--)
      {
         if (testObject(mObjects[i]) == false)
         {
            GameBase* remove = mObjects[i];
            mObjects.erase(i);
            clearNotify(remove);

            if (!mLeaveCommand.isEmpty())
            {
               String command = String("%obj = ") + remove->getIdString() + ";" + mLeaveCommand;
               Con::evaluate(command.c_str());
            }

            //onLeaveCollisionTrigger_callback(this, remove);
         }
      }

      if (!mTickCommand.isEmpty())
         Con::evaluate(mTickCommand.c_str());

      //if (mObjects.size() != 0)
      //   onTickCollisionTrigger_callback(this);
   }
   else
   {
      mCurrTick += TickMs;
   }
}

//--------------------------------------------------------------------------

U32 CollisionTrigger::packUpdate(NetConnection* con, U32 mask, BitStream* stream)
{
   U32 i;
   U32 retMask = Parent::packUpdate(con, mask, stream);

   if (stream->writeFlag(mask & TransformMask))
   {
      stream->writeAffineTransform(mObjToWorld);
   }

   // Write the polyhedron
   if (stream->writeFlag(mask & PolyMask))
   {
      stream->write(mCollisionTriggerPolyhedron.mPointList.size());
      for (i = 0; i < mCollisionTriggerPolyhedron.mPointList.size(); i++)
         mathWrite(*stream, mCollisionTriggerPolyhedron.mPointList[i]);

      stream->write(mCollisionTriggerPolyhedron.mPlaneList.size());
      for (i = 0; i < mCollisionTriggerPolyhedron.mPlaneList.size(); i++)
         mathWrite(*stream, mCollisionTriggerPolyhedron.mPlaneList[i]);

      stream->write(mCollisionTriggerPolyhedron.mEdgeList.size());
      for (i = 0; i < mCollisionTriggerPolyhedron.mEdgeList.size(); i++) {
         const Polyhedron::Edge& rEdge = mCollisionTriggerPolyhedron.mEdgeList[i];

         stream->write(rEdge.face[0]);
         stream->write(rEdge.face[1]);
         stream->write(rEdge.vertex[0]);
         stream->write(rEdge.vertex[1]);
      }
   }

   if (stream->writeFlag(mask & EnterCmdMask))
      stream->writeLongString(CMD_SIZE - 1, mEnterCommand.c_str());
   if (stream->writeFlag(mask & LeaveCmdMask))
      stream->writeLongString(CMD_SIZE - 1, mLeaveCommand.c_str());
   if (stream->writeFlag(mask & TickCmdMask))
      stream->writeLongString(CMD_SIZE - 1, mTickCommand.c_str());

   return retMask;
}

void CollisionTrigger::unpackUpdate(NetConnection* con, BitStream* stream)
{
   Parent::unpackUpdate(con, stream);

   U32 i, size;

   // Transform
   if (stream->readFlag())
   {
      MatrixF temp;
      stream->readAffineTransform(&temp);
      setTransform(temp);
   }

   // Read the polyhedron
   if (stream->readFlag())
   {
      Polyhedron tempPH;
      stream->read(&size);
      tempPH.mPointList.setSize(size);
      for (i = 0; i < tempPH.mPointList.size(); i++)
         mathRead(*stream, &tempPH.mPointList[i]);

      stream->read(&size);
      tempPH.mPlaneList.setSize(size);
      for (i = 0; i < tempPH.mPlaneList.size(); i++)
         mathRead(*stream, &tempPH.mPlaneList[i]);

      stream->read(&size);
      tempPH.mEdgeList.setSize(size);
      for (i = 0; i < tempPH.mEdgeList.size(); i++) {
         Polyhedron::Edge& rEdge = tempPH.mEdgeList[i];

         stream->read(&rEdge.face[0]);
         stream->read(&rEdge.face[1]);
         stream->read(&rEdge.vertex[0]);
         stream->read(&rEdge.vertex[1]);
      }
      setTriggerPolyhedron(tempPH);
   }

   if (stream->readFlag())
   {
      char buf[CMD_SIZE];
      stream->readLongString(CMD_SIZE - 1, buf);
      mEnterCommand = buf;
   }
   if (stream->readFlag())
   {
      char buf[CMD_SIZE];
      stream->readLongString(CMD_SIZE - 1, buf);
      mLeaveCommand = buf;
   }
   if (stream->readFlag())
   {
      char buf[CMD_SIZE];
      stream->readLongString(CMD_SIZE - 1, buf);
      mTickCommand = buf;
   }
}

//ConsoleMethod( CollisionTrigger, getNumObjects, S32, 2, 2, "")
DefineEngineMethod(CollisionTrigger, getNumObjects, S32, (), ,
   "@brief Get the number of objects that are within the CollisionTrigger's bounds.\n\n"
   "@see getObject()\n")
{
   return object->getNumCollisionTriggeringObjects();
}

//ConsoleMethod( CollisionTrigger, getObject, S32, 3, 3, "(int idx)")
DefineEngineMethod(CollisionTrigger, getObject, S32, (S32 index), ,
   "@brief Retrieve the requested object that is within the CollisionTrigger's bounds.\n\n"
   "@param index Index of the object to get (range is 0 to getNumObjects()-1)\n"
   "@returns The SimObjectID of the object, or -1 if the requested index is invalid.\n"
   "@see getNumObjects()\n")
{
   if (index >= object->getNumCollisionTriggeringObjects() || index < 0)
      return -1;
   else
      return object->getObject(U32(index))->getId();
}
