//-----------------------------------------------------------------------------
// 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 "environment/timeOfDay.h"

#include "console/consoleTypes.h"
#include "core/stream/bitStream.h"
#include "T3D/gameBase/gameConnection.h"
#include "environment/sun.h"
#include "console/engineAPI.h"


TimeOfDayUpdateSignal TimeOfDay::smTimeOfDayUpdateSignal;

IMPLEMENT_CO_NETOBJECT_V1(TimeOfDay);

ConsoleDocClass( TimeOfDay,
   "@brief Environmental object that triggers a day/night cycle in level.\n\n"

   "@note TimeOfDay only works in Advanced Lighting with a Sub object or ScatterSky\n\n"

   "@tsexample\n"
   "new TimeOfDay(tod)\n"
   "{\n"
   "   axisTilt = \"23.44\";\n"
   "   dayLength = \"120\";\n"
   "   startTime = \"0.15\";\n"
   "   time = \"0.15\";\n"
   "   play = \"0\";\n"
   "   azimuthOverride = \"572.958\";\n"
   "   dayScale = \"1\";\n"
   "   nightScale = \"1.5\";\n"
   "   position = \"598.399 550.652 196.297\";\n"
   "   rotation = \"1 0 0 0\";\n"
   "   scale = \"1 1 1\";\n"
   "   canSave = \"1\";\n"
   "   canSaveDynamicFields = \"1\";\n"
   "};\n"
   "@endtsexample\n\n"
   "@ingroup enviroMisc"
);

TimeOfDay::TimeOfDay() 
   :  mStartTimeOfDay( 0.5f ),   // High noon
      mDayLen( 120.0f ),         // 2 minutes
      mAxisTilt( 23.44f ),       // 35 degree tilt
      mAzimuth( 0.0f ),
      mElevation( 0.0f ),
      mTimeOfDay( 0.0f ),        // initialized to StartTimeOfDay in onAdd
      mDayScale( 1.0f ),
      mPlay( true ),
      mNightScale( 1.5f ),
      mAnimateTime( 0.0f ),
      mAnimateSpeed( 0.0f ),
      mAnimate( false )
{
   mNetFlags.set( Ghostable | ScopeAlways );
   mTypeMask = EnvironmentObjectType;

   // Sets the sun vector directly overhead for lightmap generation
   // The value of mSunVector is grabbed by the terrain lighting stuff.
   /*
   F32 ele, azi;
   ele = azi = TORADIANS(90);
   MathUtils::getVectorFromAngles(mSunVector, azi, ele);
   */
   mPrevElevation = 0;
   mNextElevation = 0;
   mAzimuthOverride = 1.0f;

   _initColors();
}

TimeOfDay::~TimeOfDay()
{
}

bool TimeOfDay::setTimeOfDay( void *object, const char *index, const char *data )
{
   TimeOfDay *tod = static_cast<TimeOfDay*>(object);
   tod->setTimeOfDay( dAtof( data ) );

   return false;
}

bool TimeOfDay::setPlay( void *object, const char *index, const char *data )
{
   TimeOfDay *tod = static_cast<TimeOfDay*>(object);
   tod->setPlay( dAtob( data ) );

   return false;
}

bool TimeOfDay::setDayLength( void *object, const char *index, const char *data )
{
   TimeOfDay *tod = static_cast<TimeOfDay*>(object);
   F32 length = dAtof( data );
   if( length != 0 )
      tod->setDayLength( length );

   return false;

}

void TimeOfDay::initPersistFields()
{
	  addGroup( "TimeOfDay" );

      addField( "axisTilt", TypeF32, Offset( mAxisTilt, TimeOfDay ),
            "The angle in degrees between global equator and tropic." );

      addProtectedField( "dayLength", TypeF32, Offset( mDayLen, TimeOfDay ), &setDayLength, &defaultProtectedGetFn,
            "The length of a virtual day in real world seconds." );

      addField( "startTime", TypeF32, Offset( mStartTimeOfDay, TimeOfDay ),
         "" );

      addProtectedField( "time", TypeF32, Offset( mTimeOfDay, TimeOfDay ), &setTimeOfDay, &defaultProtectedGetFn, "Current time of day." );

      addProtectedField( "play", TypeBool, Offset( mPlay, TimeOfDay ), &setPlay, &defaultProtectedGetFn, "True when the TimeOfDay object is operating." );

      addField( "azimuthOverride", TypeF32, Offset( mAzimuthOverride, TimeOfDay ), "" );

      addField( "dayScale", TypeF32, Offset( mDayScale, TimeOfDay ), "Scalar applied to time that elapses while the sun is up." );

      addField( "nightScale", TypeF32, Offset( mNightScale, TimeOfDay ), "Scalar applied to time that elapses while the sun is down." );

   endGroup( "TimeOfDay" );

	Parent::initPersistFields();
}

void TimeOfDay::consoleInit()
{
   Parent::consoleInit();

   //addVariable( "$TimeOfDay::currentTime", &TimeOfDay::smCurrentTime );
   //addVariable( "$TimeOfDay::timeScale", TypeF32, &TimeOfDay::smTimeScale );
}

void TimeOfDay::inspectPostApply()
{
   _updatePosition();
   setMaskBits( OrbitMask );
}

void TimeOfDay::_onGhostAlwaysDone()
{
   _updatePosition();
}

bool TimeOfDay::onAdd()
{
   if ( !Parent::onAdd() )
      return false;
   
   // The server initializes to the specified starting values.
   // The client initializes itself to the server time from
   // unpackUpdate.
   if ( isServerObject() )
   {
      mTimeOfDay = mStartTimeOfDay;
      _updatePosition();
   }

   // We don't use a bounds.
   setGlobalBounds();
   resetWorldBox();
   addToScene();

   // Lets receive ghost events so we can resolve
   // the sun object.
   if ( isClientObject() )
      NetConnection::smGhostAlwaysDone.notify( this, &TimeOfDay::_onGhostAlwaysDone );

   if ( isServerObject() )   
      Con::executef( this, "onAdd" );   

   setProcessTick( true );

   return true;
}

void TimeOfDay::onRemove()
{
   if ( isClientObject() )
      NetConnection::smGhostAlwaysDone.remove( this, &TimeOfDay::_onGhostAlwaysDone );

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

U32 TimeOfDay::packUpdate(NetConnection *conn, U32 mask, BitStream *stream )
{
   U32 retMask = Parent::packUpdate( conn, mask, stream );

   if ( stream->writeFlag( mask & OrbitMask ) )
   {
      stream->write( mStartTimeOfDay );
      stream->write( mDayLen );
      stream->write( mTimeOfDay );
      stream->write( mAxisTilt );
      stream->write( mAzimuthOverride );

      stream->write( mDayScale );
      stream->write( mNightScale );

      stream->writeFlag( mPlay );
   }

   if ( stream->writeFlag( mask & AnimateMask ) )
   {
      stream->write( mAnimateTime );
      stream->write( mAnimateSpeed );
   }

   return retMask;
}

void TimeOfDay::unpackUpdate( NetConnection *conn, BitStream *stream )
{
   Parent::unpackUpdate( conn, stream );

   if ( stream->readFlag() ) // OrbitMask
   {
      stream->read( &mStartTimeOfDay );
      stream->read( &mDayLen );
      stream->read( &mTimeOfDay );
      stream->read( &mAxisTilt );
      stream->read( &mAzimuthOverride );

      stream->read( &mDayScale );
      stream->read( &mNightScale );

      mPlay = stream->readFlag();

      _updatePosition();
   }

   if ( stream->readFlag() ) // AnimateMask
   {
      F32 time, speed;
      stream->read( &time );
      stream->read( &speed );

      if( isProperlyAdded() )
         animate( time, speed );
   }
}

void TimeOfDay::processTick( const Move *move )
{
   if ( mAnimate )
   {
      F32 current = mTimeOfDay * 360.0f;      
      F32 next = current + (mAnimateSpeed * TickSec);

      // Protect for wrap around.
      while ( next > 360.0f )
         next -= 360.0f;

      // Clamp to make sure we don't pass the target time.
      if ( next >= mAnimateTime )
      {
         next = mAnimateTime;
         mAnimate = false;
      }

      // Set the new time of day.
      mTimeOfDay = next / 360.0f;

      _updatePosition();
      _updateTimeEvents();

      if ( !mAnimate && isServerObject() )
         Con::executef( this, "onAnimateDone" );
   }
   else if ( mPlay )
   {
      F32 dt = TickSec;
      F32 current = mRadToDeg( mNextElevation );

      if ( current > 350.0f || ( 0.0f <= current && current < 190.0f ) )
         dt *= mDayScale;
      else
         dt *= mNightScale;

      mTimeOfDay += dt / mDayLen;

      // It could be possible for more than a full day to 
      // pass in a single advance time, so I put this inside a loop
      // but timeEvents will not actually be called for the
      // skipped day.
      while ( mTimeOfDay > 1.0f )
         mTimeOfDay -= 1.0f;

      _updatePosition();
      _updateTimeEvents();
   }
   else
      _updatePosition();
}

void TimeOfDay::_updatePosition()
{
   mPrevElevation = mNextElevation;

   if ( mFabs( mAzimuthOverride ) )
   {
      mElevation = mDegToRad( mTimeOfDay * 360.0f );
      mAzimuth = mAzimuthOverride;

      mNextElevation = mElevation;  // already normalized
   }
   else
   {
      //// Full azimuth/elevation calculation.
      //// calculate sun decline and meridian angle (in radians)
      //F32 sunDecline = mSin( M_2PI * mTimeOfYear ) * mDegToRad( mAxisTilt );
      //F32 meridianAngle = mTimeOfDay * M_2PI - mDegToRad( mLongitude );

      //// calculate the elevation and azimuth (in radians)
      //mElevation = _calcElevation( mDegToRad( mLatitude ), sunDecline, meridianAngle );
      //mAzimuth = _calcAzimuth( mDegToRad( mLatitude ), sunDecline, meridianAngle );

      // Simplified azimuth/elevation calculation.
      // calculate sun decline and meridian angle (in radians)
      F32 sunDecline = mDegToRad( mAxisTilt );
      F32 meridianAngle = mTimeOfDay * M_2PI;

      // calculate the elevation and azimuth (in radians)
      mElevation = _calcElevation( 0.0f, sunDecline, meridianAngle );
      mAzimuth = _calcAzimuth( 0.0f, sunDecline, meridianAngle );

      // calculate 'normalized' elevation (0=sunrise, PI/2=zenith, PI=sunset, 3PI/4=nadir)
      F32 normElevation = M_PI_F * mElevation / ( 2 * _calcElevation( 0.0f, sunDecline, 0.0f ) );
      if ( mAzimuth > M_PI_F )
         normElevation = M_PI_F - normElevation;
      else if ( mElevation < 0 )
         normElevation = M_2PI_F + normElevation;

      mNextElevation = normElevation;
   }

   // Only the client updates the sun position!
   if ( isClientObject() )
      smTimeOfDayUpdateSignal.trigger( this, mTimeOfDay );
}

F32 TimeOfDay::_calcElevation( F32 lat, F32 dec, F32 mer )
{
   return mAsin( mSin(lat) * mSin(dec) + mCos(lat) * mCos(dec) * mCos(mer) );
}

F32 TimeOfDay::_calcAzimuth( F32 lat, F32 dec, F32 mer )
{
   // Add PI to normalize this from the range of -PI/2 to PI/2 to 0 to 2 * PI;
	  return mAtan2( mSin(mer), mCos(mer) * mSin(lat) - mTan(dec) * mCos(lat) ) + M_PI_F;
}

void TimeOfDay::_getSunColor( LinearColorF *outColor ) const
{
	  const COLOR_TARGET *ct = NULL;

   F32 ele = mClampF( M_2PI_F - mNextElevation, 0.0f, M_PI_F );
	  F32 phase = -1.0f;
	  F32 div;

   if (!mColorTargets.size())
   {
      outColor->set(1.0f,1.0f,1.0f);
      return;
   }

   if (mColorTargets.size() == 1)
   {
      ct = &mColorTargets[0];
      outColor->set(ct->color.red, ct->color.green, ct->color.blue);
      return;
   }

   //simple check
   if ( mColorTargets[0].elevation != 0.0f )
   {
      AssertFatal(0, "TimeOfDay::GetColor() - First elevation must be 0.0 radians");
      outColor->set(1.0f, 1.0f, 1.0f);
      //mBandMod = 1.0f;
      //mCurrentBandColor = color;
      return;
   }

   if ( mColorTargets[mColorTargets.size()-1].elevation != M_PI_F )
   {
      AssertFatal(0, "Celestails::GetColor() - Last elevation must be PI");
      outColor->set(1.0f, 1.0f, 1.0f);
      //mBandMod = 1.0f;
      //mCurrentBandColor = color;
      return;
   }

   //we need to find the phase and interp... also loop back around
   U32 count=0;
   for (;count < mColorTargets.size() - 1; count++)
   {
      const COLOR_TARGET *one = &mColorTargets[count];
      const COLOR_TARGET *two = &mColorTargets[count+1];

      if (ele >= one->elevation && ele <= two->elevation)
      {
			      div = two->elevation - one->elevation;
			
         //catch bad input divide by zero
         if ( mFabs( div ) < 0.01f )
            div = 0.01f;
			
			      phase = (ele - one->elevation) / div;
			      outColor->interpolate( one->color, two->color, phase );

			      //mCurrentBandColor.interpolate(one->bandColor, two->bandColor, phase);
			      //mBandMod = one->bandMod * (1.0f - phase) + two->bandMod * phase;

			      return;
		    }
	  }

	  AssertFatal(0,"This isn't supposed to happen");
}

void TimeOfDay::_initColors()
{
   // NOTE: The elevation targets represent distances 
   // from PI/2 radians (strait up).

   LinearColorF c;
   LinearColorF bc;

   // e is for elevation
   F32 e = M_PI_F / 13.0f; // (semicircle in radians)/(number of color target entries);

   // Day
   c.set(1.0f,1.0f,1.0f);
   _addColorTarget(0, c, 1.0f, c); // High noon at equanox
   c.set(.9f,.9f,.9f);
   _addColorTarget(e * 1.0f, c, 1.0f, c);
   c.set(.9f,.9f,.9f);
   _addColorTarget(e * 2.0f, c, 1.0f, c);
   c.set(.8f,.75f,.75f);
   _addColorTarget(e * 3.0f, c, 1.0f, c);
   c.set(.7f,.65f,.65f);
   _addColorTarget(e * 4.0f, c, 1.0f, c);

   //Dawn and Dusk (3 entries)
   c.set(.7f,.65f,.65f);
   bc.set(.8f,.6f,.3f);
   _addColorTarget(e * 5.0f, c, 3.0f, bc);
   c.set(.65f,.54f,.4f);
   bc.set(.75f,.5f,.4f);
   _addColorTarget(e * 6.0f, c, 2.75f, bc);
   c.set(.55f,.45f,.25f);
   bc.set(.65f,.3f,.3f);
   _addColorTarget(e * 7.0f, c, 2.5f, bc);

   //NIGHT
   c.set(.3f,.3f,.3f);
   bc.set(.7f,.4f,.2f);
   _addColorTarget(e * 8.0f, c, 1.25f, bc);
   c.set(.25f,.25f,.3f);
   bc.set(.8f,.3f,.2f);
   _addColorTarget(e * 9.0f, c, 1.00f, bc);
   c.set(.25f,.25f,.4f);
   _addColorTarget(e * 10.0f, c, 1.0f, c);
   c.set(.2f,.2f,.35f);
   _addColorTarget(e * 11.0f, c, 1.0f, c);
   c.set(.15f,.15f,.2f);
   _addColorTarget(M_PI_F, c, 1.0f, c); // Midnight at equanox.
}

void TimeOfDay::_addColorTarget( F32 ele, const LinearColorF &color, F32 bandMod, const LinearColorF &bandColor )
{
   COLOR_TARGET  newTarget;

   newTarget.elevation = ele;
   newTarget.color = color;
   newTarget.bandMod = bandMod;
   newTarget.bandColor = bandColor;

   mColorTargets.push_back(newTarget);
}

void TimeOfDay::_updateTimeEvents()
{
   if ( mTimeEvents.empty() )
      return;

   // Get the prev, next elevation in degrees since TimeOfDayEvent is specified
   // in degrees.
   F32 prevElevation = mRadToDeg( mPrevElevation );
   F32 nextElevation = mRadToDeg( mNextElevation );

   // If prevElevation is less than nextElevation then its the next day.
   // Unroll it so we can just loop forward in time and simplify our loop.
   if ( nextElevation < prevElevation )
      nextElevation += 360.0f;

   const U32 evtCount = mTimeEvents.size();

   // Find where in the event list we need to start...
   // The first timeEvent with elevation greater than our previous elevation.
   
   U32 start = 0;   
   for ( ; start < evtCount; start++ )
   {
      if ( mTimeEvents[start].triggerElevation > prevElevation )
         break;
   }

   bool onNextDay = false;

   // Nothing between prevElevation and the end of the day...
   // Check between start of the day and nextElevation...
   if ( start == evtCount )
   {
      start = 0;
      for ( ; start < evtCount; start++ )
      {
         if ( mTimeEvents[start].triggerElevation <= nextElevation )
         {
            onNextDay = true;
            break;
         }
      }
   }

   // No events were hit...
   if ( start == evtCount )
      return;

   U32 itr = start;
   while ( true )
   {
      TimeOfDayEvent &timeEvent = mTimeEvents[itr];
      
      F32 elev = timeEvent.triggerElevation;
      if ( onNextDay )
         elev += 360.0f;

      // Hit an event that happens later after nextElevation so we
      // have checked everything within the range and are done.
      if ( elev > nextElevation )
         break;

      // If its not greater than the nextElevation it must be less, and if
      // we are here we already know its greater than prevElevation.
      
      AssertFatal( elev >= prevElevation && elev <= nextElevation, "TimeOfDay::_updateTimeEvents - Logical error in here!" );
      AssertFatal( !timeEvent.deleteMe, "TimeOfDay::_updateTimeEvents - tried to fire the same event twice!" );

      _onTimeEvent( timeEvent.identifier );

      if ( timeEvent.oneShot )
         timeEvent.deleteMe = true;

      // On to the next time event...
      itr++;

      // We hit the end of the day?
      if ( itr == evtCount )
      {
         // We are already on the next day so we have checked everything.
         if ( onNextDay )
            break;
         // Check events for the next day
         else
         {            
            itr = 0;
            onNextDay = true;
         }         
      }
   }

   // Cleanup one-shot events that fired...

   for ( S32 i = 0; i < mTimeEvents.size(); i++ )
   {
      if ( mTimeEvents[i].deleteMe )
      {
         // Don't use erase_fast, there are ordered.
         mTimeEvents.erase( i );
         i--;
      }
   }
}

void TimeOfDay::addTimeEvent( F32 triggerElevation, const UTF8 *identifier )
{
   // Insert in ascending order of elevation.
   // Note that having more than one TimeEvent with the same triggerElevation
   // may cause undefined behavior.

   TimeOfDayEvent *pEvent = NULL;
   
   if ( mTimeEvents.empty() || mTimeEvents.last().triggerElevation <= triggerElevation )
   {
      mTimeEvents.increment();
      pEvent = &mTimeEvents.last();
   }
   else 
   {   
      for ( S32 i = 0; i < mTimeEvents.size(); i++ )
      {
         if ( mTimeEvents[i].triggerElevation > triggerElevation )
         {
            mTimeEvents.insert( i );
            pEvent = &mTimeEvents[i];
            break;
         }
      }
   }

   AssertFatal( pEvent, "TimeOfDay::addTimeEvent - could not find place to insert event." );

   pEvent->triggerElevation = triggerElevation;
   pEvent->identifier = identifier;
   pEvent->oneShot = false;
      
   pEvent->deleteMe = false;
}

void TimeOfDay::setTimeOfDay( F32 time )
{ 
   mTimeOfDay = time;

   while ( mTimeOfDay > 1.0f )
      mTimeOfDay -= 1.0f;
   while ( mTimeOfDay < 0.0f )
      mTimeOfDay += 1.0f;

   _updatePosition();

   //if ( isServerObject() )
   _updateTimeEvents();

   setMaskBits( OrbitMask ); 
}

void TimeOfDay::_onTimeEvent( const String &identifier )
{
   // Client doesn't do onTimeEvent callbacks.
   if ( isClientObject() )
      return;

   String strCurrentTime = String::ToString( "%g", mTimeOfDay );

   F32 elevation = mRadToDeg( mNextElevation );
   while( elevation < 0 )
      elevation += 360.0f;
   while( elevation > 360.0f )
      elevation -= 360.0f;

   String strCurrentElevation = String::ToString( "%g", elevation );

   Con::executef( this, "onTimeEvent", identifier.c_str(), strCurrentTime.c_str(), strCurrentElevation.c_str() );
}

void TimeOfDay::animate( F32 time, F32 speed )
{
   // Stop any existing animation... this one
   // becomes the new one.
   mAnimate = false;

   // Set the target time to hit.
   mAnimateTime = mClamp(time, 0.0f, 360.0f);

   F32 current = mTimeOfDay * 360.0f;
   F32 target = mAnimateTime;
   if ( target < current )
      target += 360.0f;

   // If we're already at the current time then
   // we have nothing more to do... the animation is here.
   F32 dif = target - current;
   if ( mIsZero( dif ) )
      return;

   // Start playback.
   mAnimateSpeed = speed;
   mAnimate = true;

   if ( isServerObject() )
   {
      Con::executef( this, "onAnimateStart" );
      setMaskBits( AnimateMask );
   }
}

DefineEngineMethod( TimeOfDay, addTimeOfDayEvent, void, (F32 elevation, const char *identifier ),,
   "" )
{
   object->addTimeEvent( elevation, identifier );
}

DefineEngineMethod( TimeOfDay, setTimeOfDay, void, ( F32 time ),,
   "" )
{
   object->setTimeOfDay( time );
}

DefineEngineMethod( TimeOfDay, setPlay, void, ( bool enabled ),,
   "")
{
   object->setPlay( enabled );
}

DefineEngineMethod( TimeOfDay, setDayLength, void, ( F32 seconds ),,
   "" )
{
   if ( seconds > 0.0f )
      object->setDayLength( seconds );
}

DefineEngineMethod( TimeOfDay, animate, void, ( F32 elevation, F32 degreesPerSecond ),,
   "")
{
   object->animate( elevation, degreesPerSecond );
}
