/******************************************************************
  
  Module:  cameracontroller.cpp
  
  Author: Sean Craig
  
  Copyright 2005 Sony Online Entertainment.  All rights reserved.
  
*******************************************************************/

//-------------------------------------------------------- Includes
#include "cameracontroller.h"
#include "gameobj.h"
#include "SyScene.h"
#include "registry.h"
#include "TiCameraDirector.h"
#include "animcontroller.h" // for AngleNormalize
#include "tuning.h"

#include "graphic.h"
//---------------------------------------------- Class Declarations


//--------------------------------------------------------- Globals
//----------------------------------------- Functions Declarations

// note that all these variables are modified by the tuning system.
float l_Distance[NUM_CAMERA_TYPES];
float l_Pitch[NUM_CAMERA_TYPES];
float l_FOV[NUM_CAMERA_TYPES];
float l_AspectRatio[NUM_CAMERA_TYPES];
float l_LeadDistance[NUM_CAMERA_TYPES];
float l_Y_Offset[NUM_CAMERA_TYPES];

float l_ZoomSpeed   = 10.0f; // meters/sec
float l_PanSpeed    = SY_DEG_TO_RAD(180.0f); // rads/sec
float l_ZoomAcceleration = 1.0f;
float l_PitchSpeed  = SY_DEG_TO_RAD(90.0f);
float l_PitchAcceleration = SY_DEG_TO_RAD(9.0f);
float l_PitchResetSpeed  = SY_DEG_TO_RAD(90.0f);
float l_PitchResetAcceleration = SY_DEG_TO_RAD(9.0f);
float l_Drift = SY_DEG_TO_RAD(10.0f);

//------------------------------------ Member Functions Definitions

//------------------------------------ cTransient
cTransient::cTransient() :
  mValue(0.0f),
  mTarget(0.0f),
  mVelocity(0.0f),
  mMaxVelocity(1.0f),
  mAcceleration(1.0f)
{
};
 

void
cTransient::CheckCloseEnough(float time)
{

  float maxDeltaV = mAcceleration * time;

  float distance = mVelocity * time;
  float delta = mTarget - mValue;
  float next_distance = delta - distance;

  if ((next_distance <= maxDeltaV*time && next_distance >= -maxDeltaV*time ) && 
    (mVelocity <= maxDeltaV && mVelocity >= -maxDeltaV))
  {
    if (mVelocity != 0.0f)
    {
      SyAssertf(mVelocity!=0.0f,""); // debug hook
    }
    mVelocity = 0.0f;
    mValue = mTarget;
  }
}
float 
cTransient::DetermineDeltaV(float time)
{
  float delta = mTarget - mValue;

  float maxDeltaV = mAcceleration * time;

  float max_decel_time = mMaxVelocity/mAcceleration;
  float max_decel_dist = mMaxVelocity * max_decel_time - mAcceleration * max_decel_time * max_decel_time * 0.5f;

  if (delta > 0.0001f)
  {
    if (mVelocity < 0.0f) 
    {
      // we're going the wrong way
      return maxDeltaV;
    }

    if (delta > max_decel_dist)
    {
      return maxDeltaV; // accelerate to maximum velocity
    }

    float target_vel = mMaxVelocity * mMaxVelocity - 2 * mAcceleration * (max_decel_dist - delta);
    target_vel = sqrtf(target_vel);
    float ideal_deltaV = target_vel - mVelocity;
    if (ideal_deltaV > maxDeltaV)
    {
      ideal_deltaV = maxDeltaV;
    }
    return (ideal_deltaV);
  }
  else if (delta < -0.0001f)
  {
    if (mVelocity > 0.0f) 
    {
      // we're going the wrong way
      return -maxDeltaV;
    }

    if (delta < -max_decel_dist)
    {
      return -maxDeltaV; // accelerate to maximum velocity
    }

    float target_vel = mMaxVelocity * mMaxVelocity - 2 * mAcceleration * (max_decel_dist + delta);
    target_vel = -sqrtf(target_vel);
    float ideal_deltaV =  target_vel - mVelocity;

    if (ideal_deltaV < -maxDeltaV)
    {
      ideal_deltaV = -maxDeltaV;
    }
    return ideal_deltaV;
  }

  return 0.0f;
}

void
cTransient::Update(float time) 
{
  CheckCloseEnough(time);

  float delta_v = DetermineDeltaV(time);

  float newVel = mVelocity + delta_v;
  if (newVel > mMaxVelocity)
  {
    delta_v = mMaxVelocity - mVelocity;
  }
  else if (newVel < -mMaxVelocity)
  {
    delta_v = -mMaxVelocity - mVelocity;
  }
  
  mVelocity += delta_v;
  float delta = mVelocity * time + 0.5f * delta_v * time;

  mValue += delta;
};

//------------------------------------ cCameraController


cCameraController::cCameraController(Titan *titan) :
  mTitan(titan),
  mTarget(ID_NONE),
  mDirector(NULL),
  mCutTime(0.0f),
  mCutDir(0,0,0),
  mCutStored(false),
  mPrevDirStored(false),
  mPrevDir(1,0,0),
  mOverrideCameraFlag(false),
  mSettingChangeState(SETTING_CHANGE_NONE),
  mSettingChangeTime(0.0f),
  mSetting(CAMERA_MEDIUM)
{

}

cCameraController::~cCameraController()
{
  OnExit();
}
 
  /// Initializes the application
bool 
cCameraController::OnInit()
{
  // set up scene
  SyVect3 Loc( 0.0f, 0.0f, 0.0f );
  SyVect3 Dir( 0.0f, 0.0f, 1.0f );

  float32 ViewAngle = 45.0f;
  float32 AspectRatio = 1.0f;

  float32 mfNear( 0.1f );
  float32 mfFar( 100.0f );

  SyScene *scene = mTitan->GetScene();

  mCamera.Init(*scene->GetRasterDev(), Loc, Dir, ViewAngle, AspectRatio, mfNear, mfFar );

  mDirector = new TiCameraDirector; 

  RegisterTuningVariables();


  return true;
}

  /// Called on exit
void 
cCameraController::OnExit()
{
  delete mDirector;
  mDirector = NULL;
}

void
cCameraController::RegisterTuningVariables()
{
  gTuningSys.AddFloat(&l_Distance[CAMERA_NEAR],"Camera_Distance_Near");
  gTuningSys.AddAngle(&l_Pitch[CAMERA_NEAR],"Camera_Pitch_Near");
  gTuningSys.AddFloat(&l_AspectRatio[CAMERA_NEAR],"Camera_Aspect_Ratio_Near");
  gTuningSys.AddFloat(&l_FOV[CAMERA_NEAR],"Camera_FieldOfView_Near"); // stored as degrees, not radians, so we don't use "AddAngle"
  gTuningSys.AddFloat(&l_LeadDistance[CAMERA_NEAR],"Camera_LeadDistance_Near"); 
  gTuningSys.AddFloat(&l_Y_Offset[CAMERA_NEAR],"Camera_Y_Offset_Near"); 
 

  gTuningSys.AddFloat(&l_Distance[CAMERA_MEDIUM],"Camera_Distance_Medium");
  gTuningSys.AddAngle(&l_Pitch[CAMERA_MEDIUM],"Camera_Pitch_Medium");
  gTuningSys.AddFloat(&l_AspectRatio[CAMERA_MEDIUM],"Camera_Aspect_Ratio_Medium");
  gTuningSys.AddFloat(&l_FOV[CAMERA_MEDIUM],"Camera_FieldOfView_Medium"); // stored as degrees, not radians, so we don't use "AddAngle"
  gTuningSys.AddFloat(&l_LeadDistance[CAMERA_MEDIUM],"Camera_LeadDistance_Medium"); 
  gTuningSys.AddFloat(&l_Y_Offset[CAMERA_MEDIUM],"Camera_Y_Offset_Medium"); 


  gTuningSys.AddFloat(&l_Distance[CAMERA_FAR],"Camera_Distance_Far");
  gTuningSys.AddAngle(&l_Pitch[CAMERA_FAR],"Camera_Pitch_Far");
  gTuningSys.AddFloat(&l_AspectRatio[CAMERA_FAR],"Camera_Aspect_Ratio_Far");
  gTuningSys.AddFloat(&l_FOV[CAMERA_FAR],"Camera_FieldOfView_Far"); // stored as degrees, not radians, so we don't use "AddAngle"
  gTuningSys.AddFloat(&l_LeadDistance[CAMERA_FAR],"Camera_LeadDistance_Far"); 
  gTuningSys.AddFloat(&l_Y_Offset[CAMERA_FAR],"Camera_Y_Offset_Far"); 

  gTuningSys.AddFloat(&l_Distance[CAMERA_TWOPLAYER],"Camera_Distance_TwoP");
  gTuningSys.AddAngle(&l_Pitch[CAMERA_TWOPLAYER],"Camera_Pitch_TwoP");
  gTuningSys.AddFloat(&l_AspectRatio[CAMERA_TWOPLAYER],"Camera_Aspect_Ratio_TwoP");
  gTuningSys.AddFloat(&l_FOV[CAMERA_TWOPLAYER],"Camera_FieldOfView_TwoP"); // stored as degrees, not radians, so we don't use "AddAngle"
  gTuningSys.AddFloat(&l_LeadDistance[CAMERA_TWOPLAYER],"Camera_LeadDistance_TwoP"); 
  gTuningSys.AddFloat(&l_Y_Offset[CAMERA_TWOPLAYER],"Camera_Y_Offset_TwoP"); 

  gTuningSys.AddAngle(&l_PanSpeed,"Camera_PanSpeed");

  gTuningSys.AddFloat(&l_ZoomSpeed,"Camera_ZoomSpeed");
  gTuningSys.AddFloat(&l_ZoomAcceleration,"Camera_ZoomAcceleration");

  gTuningSys.AddAngle(&l_PitchSpeed,"Camera_PitchSpeed");
  gTuningSys.AddAngle(&l_PitchAcceleration,"Camera_PitchAcceleration");

  gTuningSys.AddAngle(&l_PitchResetSpeed,"Camera_PitchResetSpeed");
  gTuningSys.AddAngle(&l_PitchResetAcceleration,"Camera_PitchResetAcceleration");

  gTuningSys.AddAngle(&l_Drift,"Camera_Drift");


}

void 
cCameraController::LoadCameraFile(const char *camFileName)
{
  SyAssertf(mDirector!=NULL,"OnInit not called, or called after OnExit");
 // mDirector->LoadCameraFile(camFileName); // BIJAN - TEMP comment. Need to generate new .CAM file (format changed).
  mCutStored = false;
  mPrevDirStored = false;
  
}

void 
cCameraController::SetTarget(tGameID id)
{
  if (id != mTarget)
  {
    mTarget=id;
    mCutStored = false;
    mPrevDirStored = false;

    cGameObject *target = mTitan->GetRegistry()->Fetch(mTarget);
    mPrevLoc = target->GetLocation();

    mDistance.SetValue(l_Distance[mSetting]);
    mPitch.SetValue(l_Pitch[mSetting]);
    mYOffset.SetValue(l_Y_Offset[mSetting]);
    mLeadDistance.SetValue(l_LeadDistance[mSetting]);
    mFOV.SetValue(l_FOV[mSetting]);
  }


};

void 
cCameraController::Update(float time)
{
  if (mTarget == ID_NONE)
  {
    return;
  }

  if (mCutStored)
  {
    mCutTime += time;
  }

  mSettingChangeTime += time;
  DetermineSettings();

  mHeading.Update(time);
  mDistance.Update(time);
  mPitch.Update(time);
  mYOffset.Update(time);
  mLeadDistance.Update(time);
  mFOV.Update(time);

  cGameObject *target = mTitan->GetRegistry()->Fetch(mTarget);
  SyVect3 Loc = target->GetLocation();
  SyVect3 Cam_Loc, Cam_Dir,Forward;
  float32 Cam_Fov; // vertical

   // TiCameraDirector time update
  mDirector->UpdateTime (time);

  bool calculateCamera = true;
  // Check to see where we're getting our camera from
  if (UseOverrideCamera())  // cut scene
  {
    // cut scenes
    Cam_Loc = mOverrideCameraPosition; 
    Cam_Dir = mOverrideCameraDirection; 
    Cam_Fov = mOverrideCameraFov;

    calculateCamera = false;
  }

  else
  {
    // placed camera
    if (mDirector->GetCamInfo(Loc,&Cam_Loc,&Cam_Dir, &Cam_Fov))
    {
      calculateCamera = false;
    }
  }


  if (calculateCamera)
  {
    // new CON style camera  

    Forward(-SY_SIN(mHeading.GetValue()), 0.0f,-SY_COS(mHeading.GetValue()));

    Cam_Dir(SY_SIN(mPitch.GetValue())* Forward.X, -SY_COS(mPitch.GetValue()),SY_SIN(mPitch.GetValue())*Forward.Z);


    Cam_Loc = Loc;
    SyVect3 offset;
    offset.Mul(Cam_Dir,mDistance.GetValue());
    Cam_Loc -= offset; // meters
    Cam_Loc.X += -SY_SIN(mHeading.GetValue()) * mLeadDistance.GetValue(); 
    Cam_Loc.Y += mYOffset.GetValue();
    Cam_Loc.Z += -SY_COS(mHeading.GetValue()) * mLeadDistance.GetValue(); 
    Cam_Fov = mFOV.GetValue();
  }


  float32 ViewAngle = Cam_Fov ; 

  float32 mfNear( 0.1f );
  float32 mfFar( 100.0f );

  SyScene *scene = mTitan->GetScene();

  mCamera.Init(*scene->GetRasterDev(), Cam_Loc, Cam_Dir, ViewAngle, l_AspectRatio[mSetting], mfNear, mfFar );

  if (mPrevDirStored)
  {
    // sgc apparently % is the cross product operator

    float cross = (mPrevDir % Cam_Dir).Magnitude();
    float max = SY_SIN(SY_DEG_TO_RAD(30));
    if (cross > max || cross < -max )
    {
      mCutStored = true;
      mCutDir = mPrevDir;
      mCutTime = 0.0f;
    }
  }
  mPrevDirStored = true;
  mPrevDir = Cam_Dir;

}

float 
cCameraController::GetControlOffset(float time_held_down)
{
  // eventually, use time held down to see if a cut has occurred with that time frame...
  // (after MAXing time_held_down to something reasonable, say half a second
  // if so, use the old camera angle

  static const float MAX_CAMERA_LATENCY = 0.5f;
  if (time_held_down > MAX_CAMERA_LATENCY)
  {
    time_held_down = MAX_CAMERA_LATENCY;
  }
  const SyVect3 *dir;
  if (mCutStored && mCutTime < time_held_down)
  {
    dir = &mCutDir;
  }
  else
  {
    dir = &mCamera.GetDir();
  }
  float heading = SY_ATAN2(-dir->X,-dir->Z);  // left-right is +x, forward is -z (maya coord system)

  return heading;
}

void
cCameraController::OverrideCamera (const SyVect3& cameraPosition, const SyVect3& cameraDirection, const float32& cameraFov)
{
  mOverrideCameraFlag = true;
  mOverrideCameraPosition = cameraPosition;
  mOverrideCameraDirection = cameraDirection;
  mOverrideCameraFov = cameraFov;
}

void
cCameraController::ClearOverrideCamera ()
{
  mOverrideCameraFlag = false;
  
}

bool
cCameraController::UseOverrideCamera()
{
  return mOverrideCameraFlag;
}

void 
cCameraController::Zoom(float amount)
{

  float THRESHOLD_HIGH = 0.8f;
  float THRESHOLD_LOW = 0.5f;
  float SETTING_CHANGE_REPEAT = 0.3f;

  switch( mSettingChangeState)
  {
    case SETTING_CHANGE_DOWN:
    {
      if (amount > THRESHOLD_HIGH)
      {
        SettingUp();
        mSettingChangeState = SETTING_CHANGE_UP;
        mSettingChangeTime = 0.0f;
      }
      else if (amount > -THRESHOLD_LOW)
      {
        mSettingChangeState = SETTING_CHANGE_NONE;
        mSettingChangeTime = 0.0f;
      }
      else if (mSettingChangeTime > SETTING_CHANGE_REPEAT)
      {
        SettingDown();
        mSettingChangeTime = 0.0f;
      }
    }
    break;
    case SETTING_CHANGE_NONE:
    {
      if (amount > THRESHOLD_HIGH)
      {
        SettingUp();
        mSettingChangeState = SETTING_CHANGE_UP;
        mSettingChangeTime = 0.0f;
      }
      else if (amount < -THRESHOLD_HIGH)
      {
        SettingDown();
        mSettingChangeState = SETTING_CHANGE_DOWN;
        mSettingChangeTime = 0.0f;
      }
    }
    break;
    case SETTING_CHANGE_UP:
    {
      if (amount < -THRESHOLD_HIGH)
      {
        SettingDown();
        mSettingChangeState = SETTING_CHANGE_DOWN;
        mSettingChangeTime = 0.0f;
      }
      else if (amount < THRESHOLD_LOW)
      {
        mSettingChangeState = SETTING_CHANGE_NONE;
        mSettingChangeTime = 0.0f;
      }
      else if (mSettingChangeTime > SETTING_CHANGE_REPEAT)
      {
        SettingUp();
        mSettingChangeTime = 0.0f;
      }
    }
    break;
  }
}


void
cCameraController::SettingUp()
{
  
  if (mSetting > CAMERA_NEAR)
  {
    mSetting = (eCameraSetting)(mSetting - 1);
    mPitch.SetConstants(l_PitchSpeed,l_PitchAcceleration);
  }
}

void
cCameraController::SettingDown()
{
  
  if (mSetting < CAMERA_FAR)
  {
    mSetting = (eCameraSetting)(mSetting + 1);
    mPitch.SetConstants(l_PitchSpeed,l_PitchAcceleration);
  }
}

void 
cCameraController::Pan(float amount)
{

  amount *= l_PanSpeed;

  // todo: make sure both players are on the screen after camera motion.
  float new_value =  mHeading.GetValue() - amount;
  AngleNormalize(new_value);
  mHeading.SetValue(new_value);
  mHeading.SetTarget(new_value);
}

void  
cCameraController::SetHeading(float value)
{
  mHeading.SetValue(value);
  mHeading.SetTarget(value);
};

void  
cCameraController::DetermineSettings()
{
  SyAssert(mSetting >=CAMERA_NEAR && mSetting < CAMERA_TWOPLAYER);
  if (mSetting == CAMERA_NEAR)
  {
    Drift();
  }
  // determine where the camera wants to be (according to setting)

  SyVect3 cam_loc;

  float pitch = l_Pitch[mSetting];
 
  GetCameraLocation(&cam_loc,
                    l_Distance[mSetting],
                    pitch,
                    mHeading.GetValue(),
                    l_LeadDistance[mSetting],
                    l_Y_Offset[mSetting]);

  // collision test from player to camera to see if that's a valid location...

  SyVect3 collision_point;
  cGameObject *target = mTitan->GetRegistry()->Fetch(mTarget);
  SyVect3 target_loc = target->GetLocation();
  target_loc.Y += 1.5f;

  static const float PITCH_INC = SY_DEG_TO_RAD(2.5f);
  static const float MIN_PITCH = SY_DEG_TO_RAD(10.0f);
  

  while (Collide(&collision_point,cam_loc,target_loc))
  {
    pitch -= PITCH_INC;
    if (pitch < MIN_PITCH) 
    {
      pitch = MIN_PITCH;
      break;
    }
    
    GetCameraLocation(&cam_loc,
                      l_Distance[mSetting],
                      pitch,
                      mHeading.GetValue(),
                      l_LeadDistance[mSetting],
                      l_Y_Offset[mSetting]);

  }


  if (mPitch.GetTarget() != pitch)
  {
    if (mPitch.GetValue() < pitch )
    {
      // we're going down
      mPitch.SetConstants(l_PitchResetSpeed,l_PitchResetAcceleration);
    }
    else
    {
      // we're going up
      mPitch.SetConstants(l_PitchSpeed,l_PitchAcceleration);
    }
  }


  mDistance.SetConstants(l_ZoomSpeed,l_ZoomAcceleration);
  mYOffset.SetConstants(l_ZoomSpeed,l_ZoomAcceleration);
  mLeadDistance.SetConstants(l_ZoomSpeed,l_ZoomAcceleration);
  mFOV.SetConstants(l_PitchSpeed,l_PitchAcceleration);


  mDistance.SetTarget(l_Distance[mSetting]);
  mPitch.SetTarget(pitch);
  mYOffset.SetTarget(l_Y_Offset[mSetting]);
  mLeadDistance.SetTarget(l_LeadDistance[mSetting]);
  mFOV.SetTarget(l_FOV[mSetting]);
}


void
cCameraController::GetCameraLocation(SyVect3 *result,float distance, 
                                                      float pitch, 
                                                      float heading, 
                                                      float lead_distance,
                                                      float y_offset)
{
  cGameObject *target = mTitan->GetRegistry()->Fetch(mTarget);
  SyVect3 Loc = target->GetLocation();

  SyVect3 Cam_Loc, Cam_Dir,Forward;


  Forward(-SY_SIN(heading), 0.0f,-SY_COS(heading));
 
  Cam_Dir(SY_SIN(pitch)* Forward.X, -SY_COS(pitch),SY_SIN(pitch)*Forward.Z);
            
  
  *result = Loc;

  SyVect3 offset;

  offset.Mul(Cam_Dir,distance);
  *result -= offset; // meters
  result->X += -SY_SIN(heading) * lead_distance;
  result->Y += y_offset;
  result->Z += -SY_COS(heading) * lead_distance;
};


bool
cCameraController::Collide(SyVect3 *col_point,SyVect3 start,SyVect3 end)
{
  cGameObject *target = mTitan->GetRegistry()->Fetch(mTarget);
  cGraphicCharacter* graphic = prop_cast<cGraphicCharacter *>(target->GetGraphic());
  SyAssert(graphic);

  SyActorHandle handle = graphic->GetActorHandle();
  SySceneFilter Filter;
  
  SyScene *scene = mTitan->GetScene();

  Filter.Init( handle);
  SyVect3 location = target->GetLocation();

  static const float collision_radius = 0.25f; // meters
  SyCollSphere CollSphere;
  CollSphere.Init( start, end, collision_radius ); 

  /* Collide with the world */

  if (scene->Collide( CollSphere, Filter ))
  {
    *col_point = CollSphere.GetHitPoint();
     return true;
  }
  return false;
};


void
cCameraController::Drift()
{

  cGameObject *target = mTitan->GetRegistry()->Fetch(mTarget);
  SyVect3 Loc = target->GetLocation();

  float motion = mPrevLoc.Distance(Loc); 
  if (motion < 3.0f && motion > 0.01f) 
  {
    float heading_change = l_Drift * motion; 
    float heading = mHeading.GetValue();
    SyVect3 delta;
    delta.Sub(mPrevLoc,Loc);
    float drift_direction = SY_ATAN2(delta.X,delta.Z);

    float delta_heading = AngleDifference(heading,drift_direction);

    if (delta_heading < heading_change && delta_heading > -heading_change)
    {
      heading = drift_direction;
    }
    else
    {
      if (delta_heading > 0)
      {
        heading -= heading_change;
      }
      else
      {
        heading += heading_change;
      }
    }
    mHeading.SetValue(heading);
    mHeading.SetTarget(heading);

  }
  mPrevLoc = Loc;

}

// EOF
