#include "precompiled.h"

#include "Projectile.h"

#include "ScriptObject.h"
#include "graphics/GameView.h"
#include "graphics/Model.h"
#include "graphics/ObjectEntry.h"
#include "graphics/ObjectManager.h"
#include "graphics/Terrain.h"
#include "graphics/Unit.h"
#include "maths/Matrix3D.h"
#include "ps/CLogger.h"
#include "ps/Game.h"
#include "simulation/Collision.h"
#include "simulation/Entity.h"

const double GRAVITY = 0.00005;
const double GRAVITY_2 = GRAVITY * 0.5;

CProjectile::CProjectile( const CModel* Actor, const CVector3D& Position, const CVector3D& Target, float Speed, CEntity* Originator, const CScriptObject& ImpactScript, const CScriptObject& MissScript )
{
    m_Actor = Actor->Clone();
    m_Position = m_Position_Previous = m_Position_Graphics = Position;
    m_Speed_H = Speed;
    m_Originator = Originator;
    m_ImpactEventHandler = ImpactScript;
    m_MissEventHandler = MissScript;

    AddHandler( EVENT_IMPACT, &m_ImpactEventHandler );
    AddHandler( EVENT_MISS, &m_MissEventHandler );

    // That was the easy stuff.
    // We want horizontal distance only:

    m_Axis = Target -Position;
    double s = m_Axis.Length();
    m_Axis /= (float)s; 

    // Now vertical distance:
    double d_h = Target.Y -Position.Y;
    // Time of impact:
    double t = s / m_Speed_H;
    // Required vertical velocity at launch:
    m_Speed_V = (float)( d_h / t + GRAVITY_2 * t );
}

CProjectile::~CProjectile()
{
    CProjectileManager& projectileManager = g_Game->GetWorld()->GetProjectileManager();
    std::vector<CProjectile*>::iterator it;
    for( it = projectileManager.m_Projectiles.begin(); it != projectileManager.m_Projectiles.end(); ++it )
        if( *it == this )
        {
            projectileManager.m_Projectiles.erase( it );
            break;
        }
    delete( m_Actor );
}

bool CProjectile::Update( size_t timestep_millis )
{
    m_Position_Previous = m_Position;
    m_Position.X += timestep_millis * m_Axis.x * m_Speed_H;
    m_Position.Z += timestep_millis * m_Axis.y * m_Speed_H;
    
    m_Position.Y += (float)( timestep_millis * ( m_Speed_V -timestep_millis * GRAVITY_2 ) );
    m_Speed_V -= (float)( timestep_millis * GRAVITY );

    float height = m_Position.Y -g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel( m_Position.X, m_Position.Z );
    
    if( height < 0.0f )
    {
        // We appear to have missed.
        CEventProjectileMiss evt( m_Originator, m_Position );
        DispatchEvent( &evt );
        // Not going to let this be cancelled.
        return( false );
    }
    RayIntersects& r = GetProjectileIntersection( m_Position_Previous, m_Axis, timestep_millis * m_Speed_H );
    RayIntersects::iterator it;
    for( it = r.begin(); it != r.end(); it++ )
    {
        // Hit something?
        if( *it != m_Originator ) /* That wouldn't be fair at all... */
        {
            // Low enough to hit it?
            if( height < (*it)->m_bounds->m_height )
            {
                CEventProjectileImpact evt( m_Originator, *it, m_Position );
                if( DispatchEvent( &evt ) )
                    return( false );
            }
        }
    }
    return( true );
}

void CProjectile::Interpolate( size_t timestep_millis )
{
    m_Position_Graphics.X = m_Position_Previous.X + timestep_millis * m_Speed_H * m_Axis.x;
    m_Position_Graphics.Z = m_Position_Previous.Z + timestep_millis * m_Speed_H * m_Axis.y;
    m_Position_Graphics.Y = (float)( m_Position_Previous.Y + timestep_millis * ( m_Speed_V -timestep_millis * GRAVITY_2 ) );
    float dh_dt = (float)( m_Speed_V -timestep_millis * GRAVITY );
    float scale = 1 / sqrt( m_Speed_H * m_Speed_H + dh_dt * dh_dt );
    float scale2 = m_Speed_H * scale;
    
    float y = dh_dt * scale;
    CMatrix3D rotateInc;
    rotateInc.SetIdentity();
    rotateInc._22 = rotateInc._33 = y;
    rotateInc._23 = -( rotateInc._32 = scale2 );
    
    CMatrix3D rotateDir;
    rotateDir.SetIdentity();
    rotateDir._11 = rotateDir._33 = m_Axis.y;
    rotateDir._31 = -( rotateDir._13 = m_Axis.x );
    rotateInc.Concatenate( rotateDir );
    
    rotateInc._14 = m_Position_Graphics.X;
    rotateInc._24 = m_Position_Graphics.Y;
    rotateInc._34 = m_Position_Graphics.Z;

    m_Actor->SetTransform( rotateInc );
}

void CProjectile::ScriptingInit()
{
    CJSObject<CProjectile>::ScriptingInit( "Projectile", Construct, 4 );
}

JSBool CProjectile::Construct( JSContext* cx, JSObject* UNUSED(obj), uint argc, jsval* argv, jsval* rval )
{
    debug_assert( argc >= 4 );
    CStr ModelString;
    CVector3D Here, There;
    float Speed;
    CEntity* Temp, *Originator = NULL;
    CObjectEntry* oe = NULL;
    CModel* Model = NULL;
    CScriptObject Impact, Miss;
    const char* err = NULL;


    Temp = ToNative<CEntity>( argv[0] );
    if(Temp)
    {
        Model = Temp->m_actor->GetObject()->m_ProjectileModel;
        if( !Model )
        {
            err = "No projectile model is defined for that entity's actor.";
            goto fail;
        }
    }
    else if( !ToPrimitive<CStr>( cx, argv[0], ModelString ) || NULL == ( oe = g_Game->GetView()->GetObjectManager().FindObject( ModelString ) ) || NULL == ( Model = oe->m_Model ) )
    {
        err = "Invalid actor";
        goto fail;
    }

    Temp = ToNative<CEntity>( argv[1] );
    if(Temp)
    {
        // Use the position vector of this entity, add a bit (so the arrow doesn't appear out of the ground)
        // In future, find the appropriate position from the entity (location of a specific prop point?)
        Here = Temp->m_position;
        Here.Y = g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel( Here.X, Here.Z ) + 2.5f;
    }
    else if( !( ToPrimitive<CVector3D>( cx, argv[1], Here ) ) )
    {
        err = "Invalid vector";
        goto fail;
    }

    Temp = ToNative<CEntity>( argv[2] );
    if(Temp)
    {
        // Use the position vector of this entity.
        // TODO: Maybe: Correct for the movement of this entity.
        //       Then again, that doesn't belong here.
        There = Temp->m_position;
        There.Y = g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel( There.X, There.Z ) + 2.5f;
    }
    else if( !( ToPrimitive<CVector3D>( cx, argv[2], There ) ) )
    {
        err = "Invalid vector";
        goto fail;
    }

    Speed = ToPrimitive<float>( cx, argv[3] );
    if( Speed == 0.0f )
    {
        // Either wasn't specified, or was zero. In either case,
        // can't allow it: div/0 errors in the physics
        err = "Invalid speed";
        goto fail;
    }
    // Ignore errors in these last few and use the defaults if there's a problem.

    if( argc >= 5 )
        Originator = ToNative<CEntity>( argv[4] );
    if( argc >= 6 )
        Impact = argv[5]; // Script to run on impact with an entity.
    if( argc >= 7 ) 
        Miss = argv[6]; // Script to run on impact with the floor.

    {
        CProjectile* p = g_Game->GetWorld()->GetProjectileManager()
            .AddProjectile( Model, Here, There, Speed / 1000.0f, Originator, Impact, Miss );

        *rval = ToJSVal<CProjectile>( *p );
        return( JS_TRUE );
    }

fail:
    *rval = JSVAL_NULL;
    JS_ReportError( cx, err );
    return( JS_TRUE );
}

CEventProjectileImpact::CEventProjectileImpact( CEntity* Originator, CEntity* Impact, const CVector3D& Position ) : CScriptEvent( L"ProjectileImpact", EVENT_IMPACT )
{
    m_Originator = Originator;
    m_Impact = Impact;
    m_Position = Position;
    AddLocalProperty( L"originator", &m_Originator, true );
    AddLocalProperty( L"impacted", &m_Impact );
    AddLocalProperty( L"position", &m_Position );
}

CEventProjectileMiss::CEventProjectileMiss( CEntity* Originator, const CVector3D& Position ) : CScriptEvent( L"ProjectileMiss", EVENT_MISS )
{
    m_Originator = Originator;
    m_Position = Position;
    AddLocalProperty( L"originator", &m_Originator, true );
    AddLocalProperty( L"position", &m_Position );
}

CProjectileManager::CProjectileManager()
{
    m_LastTurnLength = 0;
}

CProjectileManager::~CProjectileManager()
{
    DeleteAll();
}
    
void CProjectileManager::DeleteAll()
{
    while( !( m_Projectiles.empty() ) )
        delete( m_Projectiles[0] ); // removes itself from m_Projectiles
}

CProjectile* CProjectileManager::AddProjectile( const CModel* Actor, const CVector3D& Position, const CVector3D& Target, float Speed, CEntity* Originator, const CScriptObject& ImpactScript, const CScriptObject& MissScript )
{
    CProjectile* p = new CProjectile( Actor, Position, Target, Speed, Originator, ImpactScript, MissScript );
    m_Projectiles.push_back( p );
    return( p );
}

void CProjectileManager::DeleteProjectile( CProjectile* p )
{
    delete( p );
}

void CProjectileManager::UpdateAll( size_t timestep )
{
    m_LastTurnLength = timestep;
    for( size_t i = 0; i < m_Projectiles.size(); )
        if( !( m_Projectiles[i]->Update( timestep ) ) )
            delete( m_Projectiles[i] ); // removes itself from m_Projectiles
        else
            i++;
}

void CProjectileManager::InterpolateAll( double relativeOffset )
{
    size_t absoluteOffset = (size_t)( (double)m_LastTurnLength * relativeOffset );
    std::vector<CProjectile*>::iterator it;
    for( it = m_Projectiles.begin(); it != m_Projectiles.end(); ++it )
        (*it)->Interpolate( absoluteOffset );
}