//
// Copyright(C) 2014-2015 Samuel Villarreal
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// DESCRIPTION:
//      Turret Class
//

//-----------------------------------------------------------------------------
//
// Flags
//
//-----------------------------------------------------------------------------

const int16 TURRET_FLAG_LINK_TRIGGERED          = (1 << 11);
const int16 TURRET_FLAG_INITIALLY_INVISIBLE     = (1 << 12);

enum turretMotionTypes
{
    TURRET_MOTION_NONE  = 0,
    TURRET_MOTION_PINGPONG,
    TURRET_MOTION_TRACK,
    NUMTURRETMOTIONTYPES
}

//-----------------------------------------------------------------------------
//
// Modes
//
//-----------------------------------------------------------------------------

// modes 0 - 55 are already reserved by the enemy ai component
enum turretModes
{
    TURRET_MODE_WAIT_FOR_TRIGGER  = 0,
    TURRET_MODE_ACTIVATE,
    TURRET_MODE_IDLE,
    TURRET_MODE_FIRE,
    TURRET_MODE_DEATH,
    NUMTURRETMODES
}

//-----------------------------------------------------------------------------
//
// Messages
//
//-----------------------------------------------------------------------------

const int8 TURRET_MSG_ACTIVATE      = 1;
const int8 TURRET_MSG_VISIBLE       = 2;

#define TURRET_CLIPFLAG (CF_NOCLIPSTATICS|CF_NOCLIPACTORS|CF_ALLOWRESTRICTEDAREAS|CF_ALLOWPLAYERRESTRICTED)

#define DEFINE_TURRET_MODE(mode, anim, flags) \
    DefineMode("TurretModeTable", mode, "void MODE_Setup"#mode"(void)", "void MODE_Tick"#mode"(void)", TURRET_CLIPFLAG, anim, "", 0, 5, 0, flags);
    
#define DEFINE_TURRET_MODE_ANIMFUNC(mode, anim, flags) \
    DefineMode("TurretModeTable", mode, "void MODE_Setup"#mode"(void)", "void MODE_Tick"#mode"(void)", TURRET_CLIPFLAG, 0, "int MODE_Anim"#anim"(void)", 0, 5, 0, flags);


/*
==============================================================
InitTurretModeTable
==============================================================
*/

void InitTurretModeTable(void)
{
    DEFINE_TURRET_MODE(TURRET_MODE_IDLE, ANIM_TURRET_IDLE, MF_NOROOTMOTION);
    DEFINE_TURRET_MODE(TURRET_MODE_FIRE, ANIM_TURRET_FIRE, MF_NOROOTMOTION);
    DEFINE_TURRET_MODE(TURRET_MODE_DEATH, ANIM_TURRET_DEATH, MF_NOROOTMOTION);
    
    DEFINE_TURRET_MODE_ANIMFUNC(TURRET_MODE_WAIT_FOR_TRIGGER, Activate, MF_NOROOTMOTION);
    DEFINE_TURRET_MODE_ANIMFUNC(TURRET_MODE_ACTIVATE, Activate, MF_NOROOTMOTION);
}

#undef DEFINE_TURRET_MODE
#undef DEFINE_TURRET_MODE_ANIMFUNC

#define TURRET_MODE_FUNCTION(mode, Setup, Tick) \
    void MODE_Setup##mode(void) \
    Setup   \
    void MODE_Tick##mode(void)  \
    Tick

/*
==============================================================
TurokTurret
==============================================================
*/

class TurokTurret : ScriptObject
{
    kActor@                 self;
    uint                    m_dwFlags;
    uint                    m_dwCommonFlags;
    
    int32                   m_startHealth;
    
    uint16                  m_wStartSound;
    uint16                  m_wPainSound;
    
    uint32                  m_dwStartAnim;
    
    uint32                  m_dwDropPickupFlags1;
    uint32                  m_dwDropPickupFlags2;
    
    int32                   m_gunNode;
    int32                   m_rotNode;
    
    float                   m_fRotSpeed;
    float                   m_fRotLimit;
    
    float                   m_fSightRadius;
    float                   m_fSightAngle;
    
    float                   m_fFireRadius;
    float                   m_fFireAngle;
    
    float                   m_fVertDist;
    float                   m_fHorizDist;
    float                   m_fHorizDir;
    float                   m_fMoveSpeed;
    float                   m_fStartOffset;
    
    uint8                   m_targetVisibleMotionType;
    uint8                   m_targetNotVisibleMotionType;
    
    float                   m_fTiltRot;
    kAngle                  m_anStartYaw;
    float                   m_fMoveTime;
    float                   m_fTurretVel;
    bool                    m_bPong;
    kVec3                   m_vStartPos;
    kVec3                   m_vDesiredPos;
    
    TurokTurret(kActor @actor)
    {
        @self = actor;
        m_fTiltRot = 0.0f;
        m_fMoveTime = 0;
        m_fTurretVel = 0;
        m_bPong = false;
    }
    
    //--------------------------------------------------
    void OnLevelLoad(kDictMem@ pDict)
    {
        pDict.GetInt("flags", m_dwFlags);
        pDict.GetInt("commonFlags", m_dwCommonFlags);
        pDict.GetInt("startHealth", m_startHealth);
        pDict.GetInt("startSoundID", m_wStartSound);
        pDict.GetInt("painSoundID", m_wPainSound);
        pDict.GetInt("startAnimID", m_dwStartAnim);
        pDict.GetInt("deathItemDropFlags1", m_dwDropPickupFlags1);
        pDict.GetInt("deathItemDropFlags2", m_dwDropPickupFlags2);
        pDict.GetInt("gunNode", m_gunNode);
        pDict.GetInt("rotNode", m_rotNode);
        pDict.GetFloat("rotSpeed", m_fRotSpeed);
        pDict.GetFloat("rotLimit", m_fRotLimit);
        pDict.GetFloat("sightRadius", m_fSightRadius);
        pDict.GetFloat("sightAngle", m_fSightAngle);
        pDict.GetFloat("fireRadius", m_fFireRadius);
        pDict.GetFloat("fireAngle", m_fFireAngle);
        pDict.GetFloat("vertDist", m_fVertDist);
        pDict.GetFloat("horizDist", m_fHorizDist);
        pDict.GetFloat("horizDir", m_fHorizDir);
        pDict.GetFloat("moveSpeed", m_fMoveSpeed);
        pDict.GetFloat("startOffset", m_fStartOffset);
        pDict.GetInt("targetVisibleMotionType", m_targetVisibleMotionType);
        pDict.GetInt("targetNotVisibleMotionType", m_targetNotVisibleMotionType);
        
        self.Health() = m_startHealth;
    }
    
    //--------------------------------------------------
    void OnSerialize(kDict& out dict)
    {
        SERIALIZE(m_startHealth);
        SERIALIZE(m_dwFlags);
        SERIALIZE(m_dwCommonFlags);
        SERIALIZE(m_startHealth);
        SERIALIZE(m_wStartSound);
        SERIALIZE(m_wPainSound);
        SERIALIZE(m_dwStartAnim);
        SERIALIZE(m_dwDropPickupFlags1);
        SERIALIZE(m_dwDropPickupFlags2);
        SERIALIZE(m_gunNode);
        SERIALIZE(m_rotNode);
        SERIALIZE(m_fRotSpeed);
        SERIALIZE(m_fRotLimit);
        SERIALIZE(m_fSightRadius);
        SERIALIZE(m_fSightAngle);
        SERIALIZE(m_fFireRadius);
        SERIALIZE(m_fFireAngle);
        SERIALIZE(m_fVertDist);
        SERIALIZE(m_fHorizDist);
        SERIALIZE(m_fHorizDir);
        SERIALIZE(m_fMoveSpeed);
        SERIALIZE(m_fStartOffset);
        SERIALIZE(m_targetVisibleMotionType);
        SERIALIZE(m_targetNotVisibleMotionType);
    }
    
    //--------------------------------------------------
    void OnDeserialize(kDict& in dict)
    {
        DESERIALIZE_INT(m_startHealth);
        DESERIALIZE_INT(m_startHealth);
        DESERIALIZE_INT(m_dwFlags);
        DESERIALIZE_INT(m_dwCommonFlags);
        DESERIALIZE_INT(m_startHealth);
        DESERIALIZE_INT(m_wStartSound);
        DESERIALIZE_INT(m_wPainSound);
        DESERIALIZE_INT(m_dwStartAnim);
        DESERIALIZE_INT(m_dwDropPickupFlags1);
        DESERIALIZE_INT(m_dwDropPickupFlags2);
        DESERIALIZE_INT(m_gunNode);
        DESERIALIZE_INT(m_rotNode);
        DESERIALIZE_FLOAT(m_fRotSpeed);
        DESERIALIZE_FLOAT(m_fRotLimit);
        DESERIALIZE_FLOAT(m_fSightRadius);
        DESERIALIZE_FLOAT(m_fSightAngle);
        DESERIALIZE_FLOAT(m_fFireRadius);
        DESERIALIZE_FLOAT(m_fFireAngle);
        DESERIALIZE_FLOAT(m_fVertDist);
        DESERIALIZE_FLOAT(m_fHorizDist);
        DESERIALIZE_FLOAT(m_fHorizDir);
        DESERIALIZE_FLOAT(m_fMoveSpeed);
        DESERIALIZE_FLOAT(m_fStartOffset);
        DESERIALIZE_INT(m_targetVisibleMotionType);
        DESERIALIZE_INT(m_targetNotVisibleMotionType);
        
        self.Health() = m_startHealth;
    }
    
    //--------------------------------------------------
    void OnSpawn(void)
    {
        if(self.Deserialized())
        {
            return;
        }
        
        self.AddComponent("kexModeStateComponent", true);
        if(!(self.ModeStateComponent() is null))
        {
            self.ModeStateComponent().AssignModeTable("TurretModeTable");
        }
        
        // if this actor was marked in the persistent data, then its already dead
        if(self.IsMarked())
        {
            // force death animation to be set and then set the last frame. this will
            // execute all keyframe actions that could unblock door regions
            self.AnimTrackComponent().Set(ANIM_TURRET_DEATH, 0, 0);
            self.AnimTrackComponent().SetLastFrame(true);
            
            kDamageInfo cDamageInfo;
        
            cDamageInfo.radius = 10;
            cDamageInfo.flags = DF_NORMAL;
            @cDamageInfo.inflictor = @self;
            @cDamageInfo.source = @self;
            @cDamageInfo.target = @self;
            @cDamageInfo.particle = null;
            cDamageInfo.hits = self.Health() * 4.0f;
            
            self.InflictDamage(cDamageInfo);
            return;
        }
            
        m_anStartYaw = self.Yaw();
        m_vStartPos = self.Origin();
    
        if(!(self.ModeStateComponent() is null))
        {
            if((m_dwCommonFlags & TURRET_FLAG_LINK_TRIGGERED) != 0)
            {
                self.ModeStateComponent().SetMode(TURRET_MODE_WAIT_FOR_TRIGGER);
            }
            else
            {
                self.ModeStateComponent().SetMode(TURRET_MODE_ACTIVATE);
            }
        }
        else
        {
            Sys.Print("Missing mode component on turret (" + self.TID() + ")!");
        }
        
        if((m_dwCommonFlags & TURRET_FLAG_INITIALLY_INVISIBLE) != 0)
        {
            self.Flags() |= AF_HIDDEN;
            self.AnimTrackComponent().Toggle(false);
        }
    }
    
    //--------------------------------------------------
    void OnTick(void)
    {
        self.Pitch() = -m_fTiltRot;
        
        if(m_gunNode >= 0)
        {
            self.RenderMeshComponent().SetRotationOffset(m_gunNode, self.Pitch(), 1, 0, 0);
        }
    }

    //--------------------------------------------------
    void OnTrigger(kActor@ pInstigator, const int msg)
    {
        if(self.TriggerMessageID() == TURRET_MSG_ACTIVATE)
        {
            self.Flags() &= ~AF_HIDDEN;
            self.AnimTrackComponent().Toggle(true);
            
            if(self.ModeStateComponent().Mode() == TURRET_MODE_WAIT_FOR_TRIGGER)
            {
                self.ModeStateComponent().SetMode(TURRET_MODE_ACTIVATE);
            }
        }
        else if(self.TriggerMessageID() == TURRET_MSG_VISIBLE)
        {
            self.Flags() &= ~AF_HIDDEN;
            self.AnimTrackComponent().Toggle(true);
        }
        
        Sys.Print("Turret (" + self.TID() + ") recieved MSG: " +
            self.TriggerMessageID() + " with data: " + self.TriggerData());
    }
    
    //--------------------------------------------------
    void OnDeath(kDamageInfo& in dmgInfo)
    {
        self.TriggerMessageID() = TURRET_MSG_ACTIVATE;
        self.Trigger(null);
        self.Mark(true);
        
        self.ModeStateComponent().SetMode(TURRET_MODE_DEATH);
    }
    
    //--------------------------------------------------
    void Move(const int motionType, kActor@ pTarget)
    {
        float fSpeed = 0;
        float t = m_fMoveTime;
        
        if(m_fHorizDist != 0.0f || m_fVertDist != 0.0f)
        {
            fSpeed = m_fMoveSpeed / Math::Sqrt(m_fHorizDist*m_fHorizDist+m_fVertDist*m_fVertDist);
        }
        
        switch(motionType)
        {
        case TURRET_MOTION_PINGPONG:
            m_fTurretVel = ((m_bPong ? -fSpeed : fSpeed) - m_fTurretVel) * 0.25f + m_fTurretVel;
            t += m_fTurretVel;
            
            if(!m_bPong)
            {
                if(t > 1.0f)
                {
                    m_fTurretVel = 0;
                    m_bPong = true;
                }
            }
            else
            {
                if(t < 0.0f)
                {
                    m_fTurretVel = 0;
                    m_bPong = false;
                }
            }
            break;
            
        case TURRET_MOTION_TRACK:
            if(!(pTarget is null))
            {
                float fTarget = 0;
                
                if(m_fHorizDir != 0.0f)
                {
                    kVec3 vPos = pTarget.Origin() - m_vStartPos;
                    kVec3 vRotPos = (vPos * kQuat(-m_fHorizDir, 0, 0));
                    fTarget = vRotPos.z / m_fHorizDist;
                }
                
                float fVel = fTarget - t;
                if(fVel >  fSpeed) fVel =  fSpeed;
                if(fVel < -fSpeed) fVel = -fSpeed;
                
                m_fTurretVel = (fVel - m_fTurretVel) * 0.25f + m_fTurretVel;
                t += m_fTurretVel;
            }
            break;
            
        default:
            break;
        }
        
        if(t > 1) t = 1;
        if(t < 0) t = 0;
        
        m_fMoveTime = t;
        
        kVec3 vEndPos = m_vStartPos;
        
        vEndPos.x += m_fHorizDist * Math::Sin(m_fHorizDir);
        vEndPos.y += m_fHorizDist * Math::Cos(m_fHorizDir);
        vEndPos.z += m_fVertDist;
        
        m_vDesiredPos = (vEndPos - m_vStartPos) * t + m_vStartPos;
    }
    
    //--------------------------------------------------
    void Turn(const float maxSpeed, const float angle)
    {
        float delta = angle;
        if(delta < -maxSpeed) delta = -maxSpeed;
        if(delta >  maxSpeed) delta =  maxSpeed;

        delta *= GAME_FRAME_TIME;
        if(Math::Fabs(delta) > Math::Fabs(angle))
        {
            delta = angle;
        }
        
        self.Yaw() += delta;
    }
    
    //--------------------------------------------------
    bool TrackTarget(void)
    {
        bool    bCanFire        = false;
        float   fTargetTiltRot  = 0;
        float   fTargetYawRot   = 0;
        float   fDeltaRotYaw    = 0;
        kAngle  anRotYaw        = self.Yaw() - m_anStartYaw;
        
        kActor@ pTarget = LocalPlayer.Actor().CastToActor();
        float fDistXY = 0;
        float fDeltaAngle = anRotYaw;
        
        if(!(pTarget is null))
        {
            kVec3 vTargOrg = pTarget.Origin();
            
            if(!(pTarget.WorldComponent() is null))
            {
                vTargOrg.z += pTarget.WorldComponent().Height() * 0.9f;
                vTargOrg.z += pTarget.WorldComponent().HeightOffset();
            }
            
            fDistXY = self.Origin().Distance(vTargOrg);
            fDeltaAngle = self.GetDeltaFromPoint(vTargOrg);
        }
        
        if(!(pTarget is null) && fDistXY <= m_fSightRadius && Math::Fabs(fDeltaAngle) < (m_fSightAngle*0.5f))
        {
            if(fDistXY <= m_fFireRadius && Math::Fabs(fDeltaAngle) < (m_fFireAngle*0.5f))
            {
                // kaiser@04092017 - adding new HitScan method to ensure that turrets won't fire if target is behind a door
                if(!(self.WorldComponent() is null))
                {
                    kVec3 vTargOrg = pTarget.Origin();
                    kVec3 vTurret = self.Origin();
                    
                    if(!(self.WorldComponent() is null))
                    {
                        vTurret.z += self.WorldComponent().Height() * 0.5f;
                    }
            
                    if(!(pTarget.WorldComponent() is null))
                    {
                        vTargOrg.z += pTarget.WorldComponent().Height() * 0.9f;
                        vTargOrg.z += pTarget.WorldComponent().HeightOffset();
                    }
                    
                    kClipInfo clipInfo;
                    self.WorldComponent().HitScan(
                        clipInfo,
                        vTurret,
                        vTargOrg,
                        uint(CF_POLYCOLLISION|CF_COLLIDEJOINTS|CF_EXPANDJOINTNODES));
                        
                    // start firing if the trace has hit the target
                    if(clipInfo.fraction < 1.0f && !(clipInfo.pContactCollider is null))
                    {
                        if(clipInfo.pContactCollider.Owner() is pTarget)
                        {
                            bCanFire = true;
                        }
                    }
                }
                else
                {
                    bCanFire = true;
                }
            }
            
            kVec3 vTurret = self.Origin();
            if(!(self.WorldComponent() is null))
            {
                vTurret.z += self.WorldComponent().Height() * 0.5f;
            }
            
            kVec3 vTarget = pTarget.Origin();
            if(!(pTarget.WorldComponent() is null))
            {
                vTarget.z += pTarget.WorldComponent().Height() * 0.9f;
                vTarget.z += pTarget.WorldComponent().HeightOffset() * 0.5f;
            }
            
            kVec3 vDir = vTarget - vTurret;
            
            fTargetTiltRot = vDir.ToPitch();
            fDeltaRotYaw = fDeltaAngle;
            
            if(m_fRotLimit < Math::Deg2Rad(360))
            {
                float fLimit = m_fRotLimit * 0.5f;
                fTargetYawRot = anRotYaw + fDeltaRotYaw;
                
                     if(fTargetYawRot >  fLimit) fDeltaRotYaw =  fLimit - anRotYaw;
                else if(fTargetYawRot < -fLimit) fDeltaRotYaw = -fLimit - anRotYaw;
            }
            
            Move(m_targetVisibleMotionType, pTarget);
            
            if(fDeltaRotYaw == 0.0f)
            {
                fDeltaRotYaw = fDeltaAngle;
            }
        }
        else
        {
            fDeltaRotYaw = fDeltaAngle;
            fTargetTiltRot = 0;
            Move(m_targetNotVisibleMotionType, pTarget);
        }
        
        Turn(m_fRotSpeed, fDeltaRotYaw);
        m_fTiltRot = (fTargetTiltRot - m_fTiltRot) * m_fRotSpeed + m_fTiltRot;
        
        return bCanFire;
    }
    
    //--------------------------------------------------
    int MODE_AnimActivate(void)
    {
        if(m_wStartSound >= 0)
        {
            self.PlaySound(m_wStartSound);
        }
        
        if(m_dwStartAnim > 0)
        {
            return m_dwStartAnim;
        }

        return ANIM_TURRET_IDLE;
    }
    
    //--------------------------------------------------
    TURRET_MODE_FUNCTION(TURRET_MODE_WAIT_FOR_TRIGGER, {}, {});
    
    //--------------------------------------------------
    TURRET_MODE_FUNCTION(TURRET_MODE_ACTIVATE, {},
    {
        if(self.AnimTrackComponent() is null)
        {
            return;
        }
        
        if(!self.AnimTrackComponent().CycleCompleted())
        {
            return;
        }
        
        self.ModeStateComponent().SetMode(TURRET_MODE_IDLE);
    });
    
    //--------------------------------------------------
    TURRET_MODE_FUNCTION(TURRET_MODE_IDLE, {},
    {
        if(TrackTarget())
        {
            self.ModeStateComponent().SetMode(TURRET_MODE_FIRE);
        }
    });
    
    //--------------------------------------------------
    TURRET_MODE_FUNCTION(TURRET_MODE_FIRE, {},
    {
        if(!TrackTarget())
        {
            self.ModeStateComponent().SetMode(TURRET_MODE_IDLE);
        }
    });
    
    //--------------------------------------------------
    TURRET_MODE_FUNCTION(TURRET_MODE_DEATH, 
    {  
        if(!(self.WorldComponent() is null))
        {
            self.WorldComponent().Flags() |= WCF_NONSOLID;
        }
    }, {});
};

#undef TURRET_MODE_FUNCTION
