//-----------------------------------------------------------------------------
// Torque Game Engine
// Copyright (C) GarageGames.com, Inc.
//-----------------------------------------------------------------------------

#include "platform/platform.h"
#include "dgl/dgl.h"
#include "game/game.h"
#include "math/mMath.h"
#include "console/simBase.h"
#include "console/console.h"
#include "console/consoleTypes.h"
#include "core/bitStream.h"
#include "core/dnet.h"
#include "game/camera.h"
#include "game/gameConnection.h"
#include "math/mathIO.h"
#include "editor/editor.h"

#define MaxPitch 1.3962
#define CameraRadius    0.05;


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

IMPLEMENT_CO_DATABLOCK_V1(CameraData);

void CameraData::initPersistFields()
{
   Parent::initPersistFields();
}

void CameraData::packData(BitStream* stream)
{
   Parent::packData(stream);
}

void CameraData::unpackData(BitStream* stream)
{
   Parent::unpackData(stream);
}


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

IMPLEMENT_CO_NETOBJECT_V1(Camera);
F32 Camera::mMovementSpeed = 40;

Camera::Camera()
{
   mNetFlags.clear(Ghostable);
   mTypeMask |= CameraObjectType;
   delta.pos = Point3F(0,0,100);
   delta.rot = Point3F(0,0,0);
   delta.posVec = delta.rotVec = VectorF(0,0,0);
   mObjToWorld.setColumn(3,delta.pos);
   mRot = delta.rot;

   mMinOrbitDist = 0;
   mMaxOrbitDist = 0;
   mCurOrbitDist = 0;
   mOrbitObject = NULL;
   mPosition.set(0.f, 0.f, 0.f);
   mObservingClientObject = false;
   mode = 2;
}

Camera::~Camera()
{
}


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

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

   mObjBox.max = mObjScale;
   mObjBox.min = mObjScale;
   mObjBox.min.neg();
   resetWorldBox();

   if(isClientObject())
      gClientContainer.addObject(this);
   else
      gServerContainer.addObject(this);

 //  addToScene();
   return true;
}

void Camera::onEditorEnable()
{
   mNetFlags.set(Ghostable);
}

void Camera::onEditorDisable()
{
   mNetFlags.clear(Ghostable);
}

void Camera::onRemove()
{
//   removeFromScene();
   if (getContainer())
      getContainer()->removeObject(this);

   Parent::onRemove();
}


//----------------------------------------------------------------------------
// check if the object needs to be observed through its own camera...
void Camera::getCameraTransform(F32* pos, MatrixF* mat)
{
   // The camera doesn't support a third person mode,
   // so we want to override the default ShapeBase behavior.
   ShapeBase * obj = dynamic_cast<ShapeBase*>(static_cast<SimObject*>(mOrbitObject));
   if(obj && static_cast<ShapeBaseData*>(obj->getDataBlock())->observeThroughObject)
      obj->getCameraTransform(pos, mat);
   else
      getEyeTransform(mat);
}

F32 Camera::getCameraFov()
{
   ShapeBase * obj = dynamic_cast<ShapeBase*>(static_cast<SimObject*>(mOrbitObject));
   if(obj && static_cast<ShapeBaseData*>(obj->getDataBlock())->observeThroughObject)
      return(obj->getCameraFov());
   else
      return(Parent::getCameraFov());
}

F32 Camera::getDefaultCameraFov()
{
   ShapeBase * obj = dynamic_cast<ShapeBase*>(static_cast<SimObject*>(mOrbitObject));
   if(obj && static_cast<ShapeBaseData*>(obj->getDataBlock())->observeThroughObject)
      return(obj->getDefaultCameraFov());
   else
      return(Parent::getDefaultCameraFov());
}

bool Camera::isValidCameraFov(F32 fov)
{
   ShapeBase * obj = dynamic_cast<ShapeBase*>(static_cast<SimObject*>(mOrbitObject));
   if(obj && static_cast<ShapeBaseData*>(obj->getDataBlock())->observeThroughObject)
      return(obj->isValidCameraFov(fov));
   else
      return(Parent::isValidCameraFov(fov));
}

void Camera::setCameraFov(F32 fov)
{
   ShapeBase * obj = dynamic_cast<ShapeBase*>(static_cast<SimObject*>(mOrbitObject));
   if(obj && static_cast<ShapeBaseData*>(obj->getDataBlock())->observeThroughObject)
      obj->setCameraFov(fov);
   else
      Parent::setCameraFov(fov);
}

//----------------------------------------------------------------------------
void Camera::processTick(const Move* move)
{
   Parent::processTick(move);
   Point3F vec,pos;
   if (move) 
   {
      // If using editor then force camera into fly mode
      if(gEditingMission && mode != FlyMode)
         setFlyMode();

      // Update orientation
      delta.rotVec = mRot;
      mObjToWorld.getColumn(3,&delta.posVec);

      mRot.x += move->pitch;
      if(mRot.x > MaxPitch)
         mRot.x = MaxPitch;
      else if(mRot.x < -MaxPitch)
         mRot.x = -MaxPitch;

      mRot.z += move->yaw;

      if(mode == OrbitObjectMode || mode == OrbitPointMode)
      {
         if(mode == OrbitObjectMode && bool(mOrbitObject)) 
         {
            // If this is a shapebase, use its render eye transform
            // to avoid jittering.
            GameBase *castObj = mOrbitObject;
            ShapeBase* shape = dynamic_cast<ShapeBase*>(castObj);
            if( shape != NULL ) 
            {
               MatrixF ret;
               shape->getRenderEyeTransform( &ret );
               mPosition = ret.getPosition();
            } 
            else 
            {
               // Hopefully this is a static object that doesn't move,
               // because the worldbox doesn't get updated between ticks.
               mOrbitObject->getWorldBox().getCenter(&mPosition);
            }
         }
         setPosition(mPosition, mRot);
         validateEyePoint(1.0f, &mObjToWorld);
         pos = mPosition;
      }
      else
      {
         // Update pos
         bool faster = move->trigger[0] || move->trigger[1];
         F32 scale = mMovementSpeed * (faster + 1);

         mObjToWorld.getColumn(3,&pos);
         mObjToWorld.getColumn(0,&vec);
         pos += vec * move->x * TickSec * scale;
         mObjToWorld.getColumn(1,&vec);
         pos += vec * move->y * TickSec * scale;
         mObjToWorld.getColumn(2,&vec);
         pos += vec * move->z * TickSec * scale;
         setPosition(pos,mRot);
      }

      // If on the client, calc delta for backstepping
      if (isClientObject()) 
      {
         delta.pos = pos;
         delta.rot = mRot;
         delta.posVec = delta.posVec - delta.pos;
         delta.rotVec = delta.rotVec - delta.rot;
      }
      setMaskBits(MoveMask);
   }

   if(getControllingClient() && mContainer)
      updateContainer();
}

void Camera::onDeleteNotify(SimObject *obj)
{
   Parent::onDeleteNotify(obj);
   if (obj == (SimObject*)mOrbitObject)
   {
      mOrbitObject = NULL;

      if(mode == OrbitObjectMode)
         mode = OrbitPointMode;
   }
}

void Camera::interpolateTick(F32 dt)
{
   Parent::interpolateTick(dt);
   Point3F rot = delta.rot + delta.rotVec * dt;

   if(mode == OrbitObjectMode || mode == OrbitPointMode)
   {
      if(mode == OrbitObjectMode && bool(mOrbitObject))
      {
         // If this is a shapebase, use its render eye transform
         // to avoid jittering.
         GameBase *castObj = mOrbitObject;
         ShapeBase* shape = dynamic_cast<ShapeBase*>(castObj);
         if( shape != NULL ) 
         {
            MatrixF ret;
            shape->getRenderEyeTransform( &ret );
            mPosition = ret.getPosition();
         } 
         else 
         {
            // Hopefully this is a static object that doesn't move,
            // because the worldbox doesn't get updated between ticks.
            mOrbitObject->getWorldBox().getCenter(&mPosition);
         }
      }
      setRenderPosition(mPosition, rot);
      validateEyePoint(1.0f, &mObjToWorld);
   }
   else 
   {
      Point3F pos = delta.pos + delta.posVec * dt;
      setRenderPosition(pos,rot);
   }
}

void Camera::setPosition(const Point3F& pos,const Point3F& rot)
{
   MatrixF xRot, zRot;
   xRot.set(EulerF(rot.x, 0, 0));
   zRot.set(EulerF(0, 0, rot.z));
   MatrixF temp;
   temp.mul(zRot, xRot);
   temp.setColumn(3, pos);
   Parent::setTransform(temp);
   mRot = rot;
}

void Camera::setRenderPosition(const Point3F& pos,const Point3F& rot)
{
   MatrixF xRot, zRot;
   xRot.set(EulerF(rot.x, 0, 0));
   zRot.set(EulerF(0, 0, rot.z));
   MatrixF temp;
   temp.mul(zRot, xRot);
   temp.setColumn(3, pos);
   Parent::setRenderTransform(temp);
   mRot = rot;
}

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

void Camera::writePacketData(GameConnection *connection, BitStream *bstream)
{
   // Update client regardless of status flags.
   Parent::writePacketData(connection, bstream);

   Point3F pos;
   mObjToWorld.getColumn(3,&pos);
   bstream->setCompressionPoint(pos);
   mathWrite(*bstream, pos);
   bstream->write(mRot.x);
   bstream->write(mRot.z);

   U32 writeMode = mode;
   Point3F writePos = mPosition;
   S32 gIndex = -1;
   if(mode == OrbitObjectMode)
   {
      gIndex = bool(mOrbitObject) ? connection->getGhostIndex(mOrbitObject): -1;
      if(gIndex == -1)
      {
         writeMode = OrbitPointMode;
         mOrbitObject->getWorldBox().getCenter(&writePos);
      }
   }
   bstream->writeRangedU32(writeMode, CameraFirstMode, CameraLastMode);

   if (writeMode == OrbitObjectMode || writeMode == OrbitPointMode)
   {
      bstream->write(mMinOrbitDist);
      bstream->write(mMaxOrbitDist);
      bstream->write(mCurOrbitDist);
      if(writeMode == OrbitObjectMode)
      {
         bstream->writeFlag(mObservingClientObject);
         bstream->writeInt(gIndex, NetConnection::GhostIdBitSize);
      }
      if (writeMode == OrbitPointMode)
         bstream->writeCompressedPoint(writePos);
   }
}

void Camera::readPacketData(GameConnection *connection, BitStream *bstream)
{
   Parent::readPacketData(connection, bstream);
   Point3F pos,rot;
   mathRead(*bstream, &pos);
   bstream->setCompressionPoint(pos);
   bstream->read(&rot.x);
   bstream->read(&rot.z);

   GameBase* obj = 0;
   mode = bstream->readRangedU32(CameraFirstMode, CameraLastMode);
   mObservingClientObject = false;
   if (mode == OrbitObjectMode || mode == OrbitPointMode) {
      bstream->read(&mMinOrbitDist);
      bstream->read(&mMaxOrbitDist);
      bstream->read(&mCurOrbitDist);

      if(mode == OrbitObjectMode)
      {
         mObservingClientObject = bstream->readFlag();
         S32 gIndex = bstream->readInt(NetConnection::GhostIdBitSize);
         obj = static_cast<GameBase*>(connection->resolveGhost(gIndex));
      }
      if (mode == OrbitPointMode)
         bstream->readCompressedPoint(&mPosition);
   }
   if (obj != (GameBase*)mOrbitObject) {
      if (mOrbitObject) {
         clearProcessAfter();
         clearNotify(mOrbitObject);
      }
      mOrbitObject = obj;
      if (mOrbitObject) {
         processAfter(mOrbitObject);
         deleteNotify(mOrbitObject);
      }
   }

   setPosition(pos,rot);
   delta.pos = pos;
   delta.rot = rot;
   delta.rotVec.set(0,0,0);
   delta.posVec.set(0,0,0);
}

U32 Camera::packUpdate(NetConnection *con, U32 mask, BitStream *bstream)
{
   Parent::packUpdate(con,mask,bstream);

   // The rest of the data is part of the control object packet update.
   // If we're controlled by this client, we don't need to send it.
   if(bstream->writeFlag(getControllingClient() == con && !(mask & InitialUpdateMask)))
      return 0;

   if (bstream->writeFlag(mask & MoveMask)) {
      Point3F pos;
      mObjToWorld.getColumn(3,&pos);
      bstream->write(pos.x);
      bstream->write(pos.y);
      bstream->write(pos.z);
      bstream->write(mRot.x);
      bstream->write(mRot.z);
   }

   return 0;
}

void Camera::unpackUpdate(NetConnection *con, BitStream *bstream)
{
   Parent::unpackUpdate(con,bstream);

   // controlled by the client?
   if(bstream->readFlag())
      return;

   if (bstream->readFlag()) {
      Point3F pos,rot;
      bstream->read(&pos.x);
      bstream->read(&pos.y);
      bstream->read(&pos.z);
      bstream->read(&rot.x);
      bstream->read(&rot.z);
      setPosition(pos,rot);

      // New delta for client side interpolation
      delta.pos = pos;
      delta.rot = rot;
      delta.posVec = delta.rotVec = VectorF(0,0,0);
   }
}


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

void Camera::initPersistFields()
{
   Parent::initPersistFields();
}

void Camera::consoleInit()
{
   Con::addVariable("Camera::movementSpeed",TypeF32,&mMovementSpeed);
}

Point3F &Camera::getPosition()
{
   static Point3F position;
   mObjToWorld.getColumn(3, &position);
   return position;
}

ConsoleMethod( Camera, getPosition, const char *, 2, 2, "()"
              "Get the position of the camera.\n\n"
              "@returns A string of form \"x y z\".")
{
   static char buffer[100];

   Point3F& pos = object->getPosition();
   dSprintf(buffer, sizeof(buffer),"%g %g %g",pos.x,pos.y,pos.z);
   return buffer;
}

ConsoleMethod( Camera, setOrbitMode, void, 7, 8, "(GameBase orbitObject, transform mat, float minDistance,"
                                           " float maxDistance, float curDistance, bool ownClientObject)"
              "Set the camera to orbit around some given object.\n\n"
              "@param   orbitObject  Object we want to orbit.\n"
              "@param   mat          A set of fields: posX posY posZ aaX aaY aaZ aaTheta\n"
              "@param   minDistance  Minimum distance to keep from object.\n"
              "@param   maxDistance  Maximum distance to keep from object.\n"
              "@param   curDistance  Distance to set initially from object.\n"
              "@param   ownClientObj Are we observing an object owned by us?")
{
   Point3F pos;
   AngAxisF aa;
   F32 minDis, maxDis, curDis;

   GameBase *orbitObject = NULL;
   if(Sim::findObject(argv[2],orbitObject) == false)
   {
      Con::warnf("Cannot orbit non-existing object.");
      object->setFlyMode();
      return;
   }

    dSscanf(argv[3],"%g %g %g %g %g %g %g",
       &pos.x,&pos.y,&pos.z,&aa.axis.x,&aa.axis.y,&aa.axis.z,&aa.angle);
   minDis = dAtof(argv[4]);
   maxDis = dAtof(argv[5]);
   curDis = dAtof(argv[6]);

   object->setOrbitMode(orbitObject, pos, aa, minDis, maxDis, curDis, (argc == 8) ? dAtob(argv[7]) : false);
}

ConsoleMethod( Camera, setFlyMode, void, 2, 2, "()"
              "Set the camera to be able to fly freely.")
{
   object->setFlyMode();
}

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

void Camera::renderImage(SceneState*, SceneRenderImage*)
{
   if(gEditingMission)
   {
      glPushMatrix();
      dglMultMatrix(&mObjToWorld);
      glScalef(mObjScale.x,mObjScale.y,mObjScale.z);
      wireCube(Point3F(1, 1, 1),Point3F(0,0,0));
      glPopMatrix();
   }
}


//----------------------------------------------------------------------------
//----------------------------------------------------------------------------
//    NEW Observer Code
//----------------------------------------------------------------------------
//----------------------------------------------------------------------------
void Camera::setFlyMode()
{
   mode = FlyMode;

   if(bool(mOrbitObject)) {
      clearProcessAfter();
      clearNotify(mOrbitObject);
   }
   mOrbitObject = NULL;
}

void Camera::setOrbitMode(GameBase *obj, Point3F &pos, AngAxisF &rot, F32 minDist, F32 maxDist, F32 curDist, bool ownClientObject)
{
   mObservingClientObject = ownClientObject;

   rot;
   if(bool(mOrbitObject)) {
      clearProcessAfter();
      clearNotify(mOrbitObject);
   }
   mOrbitObject = obj;
   if(bool(mOrbitObject))
   {
      processAfter(mOrbitObject);
      deleteNotify(mOrbitObject);
      mOrbitObject->getWorldBox().getCenter(&mPosition);
      mode = OrbitObjectMode;
   }
   else
   {
      mode = OrbitPointMode;
      mPosition = pos;
   }

   QuatF q(rot);
   MatrixF tempMat(true);
   q.setMatrix(&tempMat);
   Point3F dir;
   tempMat.getColumn(1, &dir);

   setPosition(mPosition, dir);

   mMinOrbitDist = minDist;
   mMaxOrbitDist = maxDist;
   mCurOrbitDist = curDist;
}


void Camera::validateEyePoint(F32 pos, MatrixF *mat)
{
   if (pos != 0) 
   {
      // Use the eye transform to orient the camera
      Point3F dir;
      mat->getColumn(1, &dir);
      pos *= mMaxOrbitDist - mMinOrbitDist;
      
      // Use the camera node's pos.
      Point3F startPos = getRenderPosition();
      Point3F endPos;

      // Make sure we don't extend the camera into anything solid
      if(mOrbitObject)
         mOrbitObject->disableCollision();
      disableCollision();
      RayInfo collision;
      U32 mask = TerrainObjectType |
                 InteriorObjectType |
                 WaterObjectType |
                 StaticShapeObjectType |
                 PlayerObjectType |
                 ItemObjectType |
                 VehicleObjectType;

      Container* pContainer = isServerObject() ? &gServerContainer : &gClientContainer;
      if (!pContainer->castRay(startPos, startPos - dir * 2.5 * pos, mask, &collision))
         endPos = startPos - dir * pos;
      else
      {
         float dot = mDot(dir, collision.normal);
         if(dot > 0.01)
         {
            float colDist = mDot(startPos - collision.point, dir) - (1 / dot) * CameraRadius;
            if(colDist > pos)
               colDist = pos;
            if(colDist < 0)
               colDist = 0;
            endPos = startPos - dir * colDist;
         }
         else
            endPos = startPos - dir * pos;
      }
      mat->setColumn(3,endPos);
      enableCollision();
      if(mOrbitObject)
         mOrbitObject->enableCollision();
   }
}

void Camera::setPosition(const Point3F& pos, const Point3F& rot, MatrixF *mat)
{
   MatrixF xRot, zRot;
   xRot.set(EulerF(rot.x, 0, 0));
   zRot.set(EulerF(0, 0, rot.z));
   mat->mul(zRot, xRot);
   mat->setColumn(3,pos);
   mRot = rot;
}

void Camera::setTransform(const MatrixF& mat)
{
   // This method should never be called on the client.

   // This currently converts all rotation in the mat into
   // rotations around the z and x axis.
   Point3F pos,vec;
   mat.getColumn(1,&vec);
   mat.getColumn(3,&pos);
   Point3F rot(-mAtan(vec.z, mSqrt(vec.x*vec.x + vec.y*vec.y)),0,-mAtan(-vec.x,vec.y));
   setPosition(pos,rot);
}

void Camera::setRenderTransform(const MatrixF& mat)
{
   // This method should never be called on the client.

   // This currently converts all rotation in the mat into
   // rotations around the z and x axis.
   Point3F pos,vec;
   mat.getColumn(1,&vec);
   mat.getColumn(3,&pos);
   Point3F rot(-mAtan(vec.z, mSqrt(vec.x*vec.x + vec.y*vec.y)),0,-mAtan(-vec.x,vec.y));
   setRenderPosition(pos,rot);
}

F32 Camera::getDamageFlash() const
{
   if (mode == OrbitObjectMode && isServerObject() && bool(mOrbitObject))
   {
      const GameBase *castObj = mOrbitObject;
      const ShapeBase* psb = dynamic_cast<const ShapeBase*>(castObj);
      if (psb)
         return psb->getDamageFlash();
   }

   return mDamageFlash;
}

F32 Camera::getWhiteOut() const
{
   if (mode == OrbitObjectMode && isServerObject() && bool(mOrbitObject))
   {
      const GameBase *castObj = mOrbitObject;
      const ShapeBase* psb = dynamic_cast<const ShapeBase*>(castObj);
      if (psb)
         return psb->getWhiteOut();
   }

   return mWhiteOut;
}