-- $LastChangedDate: 2010-05-25 21:26:05 -0400 (Tue, 25 May 2010) $
-- LEGAL: COPYRIGHT (C) 2010 JIM E. BROOKS WWW.PALOMINO3D.ORG
-- Actor classes.
-- See module.txt for documentation and pitfalls.
--
-- Actors are assigned a team number so the object will know how to behave.
-- Team number is used by Game when an Actor is attached.
--
-- Although PlayerActor is a singleton, writing ":" is necessary,
-- despite it confusingly hinting of an instantiatable class.
-- For example, this will fail:
--   local name = PlayerActor.GetName()  -- wrong
--   local name = PlayerActor:GetName()  -- right
-- The reason is that the base Actor:Get*() methods require the "self" arg.
--
-- Derivative should completely override GetObject() and omit .object data member.
-- Reason is that multiple references to C++ Object complicates detaching from World.
--
-- Convention for naming Actor instances is: "Player", "Sam", etc.
-- C++ game::Hud will show them upper-case.
--
-- Remember that C++ Object::Zombify() schedules zombification at the next tick.
-- So ObjectDestroyEvent() won't be immediately broadcast.

-----------------
-- Actor class --
-----------------

Actor = {
    SHOW_IN_HUD      = true,
    DONT_SHOW_IN_HUD = false,
}

-- ctor
-- teamNum arg precedes the object arg because some derivatives won't be passed an object arg.
function Actor:New( name, teamNum, object, showInHud )
    showInHud = lib.Default( showInHud, true )

    self = lib.NewInstanceChain( Actor, self )
    self.name      = name
    self.teamNum   = teamNum
    self.object    = object  -- ok if nil
    self.showInHud = showInHud

    Timer:Register( self, defs.ACTOR_TICK_FREQ )

    return self
end

-- In Lua, a destructor must be user-defined and explicitly called.
-- Order of destruction should be derivative-to-base.
function Actor:Destroy()
    if ( not self.destroyed ) then  -- tolerate repeated calls
        -- Order is tricky!!

        self.destroyed = true

        Game:DetachActor( self )  -- while self:GetObject() remains valid

        -- Destroy 3D Object.  Some derivatives don't define self.object.
        sim.ZombifyObject( self, "object" )

        Game:BroadcastUnreferenceActor( self )

        Timer:Unregister( self )
    end
end

-- Means both the Lua object and the underlying 3D Object were destroyed.
function Actor:IfDestroyed()
    if ( self.destroyed ) then
        return true
    else
        -- Lua thinks it isn't destroyed.
        -- But has C++ detected a fatal collison?
        local object = self:GetObject()
        if ( object ) then
            if ( object:IfCollisionFatal() ) then
                self:Destroy()  -- sync with C++ condition
                return true
            else
                -- No collision and self.destroyed==false.
                return false
            end         
        else
            -- Has no Object and self.destroyed==false.
            return false
        end
    end
end

-- Reset doesn't remake the 3D Object!
function Actor:Reset()
    self.destroyed = false  -- resurrect
    -- If 3D Object still exists, forget collision.
    local object = self:GetObject()
    if ( object ) then object:SetCollision( sim.COLLISION_NONE ) end
end

function Actor:GetObject()
    return self.object
end

function Actor:GetName()
    return self.name
end

function Actor:GetTeamNum()
    return self.teamNum
end

-- If Actor should be shown in HUD.
function Actor:IfShowInHud()
    return self.showInHud
end

function Actor:Tick()
    -- NOP
end

-- Game will broadcast an "unreference Actor" event to all Actors.
function Actor:UnreferenceActor( actor )
    -- NOP
end

-- Better place to put this?
function Actor:RandomMissileMalfunction()
    return math.random(20) <= 1
end

-------------------
-- AircraftActor --
-------------------

-- FYI:
-- AircraftCtor = { }
-- This is wrong if "AircraftCtor." is ever written.
-- Of course, because it's an empty table, it won't derive anything.

AircraftActor = lib.DerivedClass( Actor )

-- ctor.
function AircraftActor:New( name, teamNum, aircraftType, RadarClass )
    self = lib.NewInstanceChain( AircraftActor, self )
    Actor.New( self, name, teamNum, nil )  -- object=nil
    self.aircraft     = sim.Aircraft:New( Vec3(0,0,0), aircraftType, sim.ModelCache.LOAD_COPY )
    self.aircraftType = aircraftType
    RadarClass = RadarClass or Radar
    self.radar        = RadarClass:New( self, defs.AIRCRAFT_RADAR_RANGE_METERS )

    return self
end

-- In Lua, a destructor must be user-defined and explicitly called.
-- Order of destruction should be derivative-to-base.
function AircraftActor:Destroy()
    if ( not self.destroyed ) then
        -- Zombify/free 3D object.
        sim.ZombifyObject( self, "aircraft" )

        -- Destroy Radar.
        lib.Destroy( self, "radar" )

        -- Top-down destruction.
        Actor.Destroy( self )

        -- Mark destroyed.
        self.destroyed = true
    end
end

function AircraftActor:GetObject()
    return self.aircraft  -- self.object=nil
end

function AircraftActor:GetAircraft()
    return self.aircraft
end

function AircraftActor:Tick()
    self.radar:Tick()
end

-----------------------------
-- PlayerActor (singleton) --
-----------------------------

PlayerActor = lib.DerivedClass( AircraftActor )
PlayerActor.swingWingsFraction = 0.0  -- 0.0:forward
PlayerActor.swingWingsFractionIncForKey = 0.035
PlayerActor.swingWingsFractionIncForJoy = 0.010
if ( sim.argvMap["-ossim"] ) then
    PlayerActor.defaultAircraftType = sim.Aircraft.type_SpaceShuttle
else
    PlayerActor.defaultAircraftType = sim.Aircraft.type_F14
end

-- Make/remake the player.
-- If aircraftType is nil, determine from cmd-line args.
function PlayerActor:New( aircraftType )
    -- In case of remake, first detach object before overwriting self.aircraft.
    sim.ZombifyObject( self, "aircraft" )

    AircraftActor.New( self,
                       "Player",
                       Game.TEAM_NUM_PLAYER,
                       aircraftType or self:GetAircraftType(),
                       PlayerRadar )

    self.replay         = Replay:New( self.aircraft )
    self.selectedTarget = nil  -- Actor
    self.savedState     = nil
    self.showInHud      = false
    self.destroyed      = false

    self:Init()

    return self
end

-- (private)
function PlayerActor:Init()
    -- special-case
    Game:AttachActor( self )

    -- Before loading model into scene-graph, set its attributes.
    self.aircraft:SetState( sim.Craft.eState_PARKED ) -- default
    self.aircraft:SetShadowCaster( true )

    -- Now load player's aircraft into scene.
    -- Also register functor to reload it when scene is reloaded.
    sim.SceneGraph:AttachObject( self.aircraft )
    Scene:RegisterLoadObjectFunctor( PlayerActor.LoadObjectIntoScene )

    sim.Control:SetCurrentCraft( self.aircraft )

    -- Enable coordinated-turning if this is a propeller aircraft.
    if ( sim.argvMap["-coturn"] ) then
        self.aircraft:EnableCoordinatedTurn( true )
        Log( "Enabled coturn." )
    elseif ( sim.argvMap["-no-coturn"] ) then
        self.aircraft:EnableCoordinatedTurn( false )
        Log( "Disabled coturn." )
    else
        self.aircraft:EnableCoordinatedTurn( self.aircraft:IfHasPropeller() )
    end
end

-- Functor to reload player's aircraft when scene is reloaded (rarely used).
function PlayerActor:LoadObjectIntoScene()
    -- PlayerActor.Init() initially loads its 3D model (NOP for first loading of scene).
    -- Thereafter, if scene is reloaded, then PlayerActor has to reload its model.
    if ( Scene:IfReload() ) then
        sim.SceneGraph:AttachObject( PlayerActor.aircraft )
    end
end

function PlayerActor:Destroy()
    -- PlayerActor must survive.
    -- Therefore, keep the C++ Aircraft object in case of reset
    -- when player keeps same type of aircraft (speeds reset).
    self.destroyed = true
end

-- (private)
function PlayerActor:GetAircraftType()
    if ( self.aircraftType ) then
        -- Was stored.
        return self.aircraftType
    else
        -- Parse cmdline args to select aircraft for player.
        aircraftType = self.defaultAircraftType
        for k,v in KEYS_VALS(sim.argvMap) do
            if ( sim.Aircraft.typeArgMap[k] ) then
                aircraftType = sim.Aircraft.typeArgMap[k]
            end
        end
        return aircraftType
    end
end

-- Save player state.
function PlayerActor:Save()
    local physics = self.aircraft:GetPhysics()
    self.savedState = {
        aircraftState = self.aircraft:GetState(),
        matrix        = self.aircraft:GetMatrix(),
        throttle      = self.aircraft:GetThrottle(),
        speed         = physics:ComputeSpeed(),
        landingGear   = self.aircraft:IfLandingGear(),
    }
end

-- Reset player.
function PlayerActor:Reset()
    -- If PlayerActor was "destroyed" (fatal collision), then renew it.
    if ( self.destroyed ) then
        PlayerActor:New()
    end

    AircraftActor:Reset()

    -- Restore player from saved state.
    if ( self.savedState ) then
        local placement = sim.BeginPlacement()
            local physics = self.aircraft:GetPhysics()
            self.aircraft:Reset()
            self.aircraft:SetState(       self.savedState.aircraftState )
            self.aircraft:SetMatrix(      self.savedState.matrix )
            self.aircraft:SetThrottle(    self.savedState.throttle )
            self.aircraft:SetLandingGear( self.savedState.landingGear )
            physics:SetSpeed(             self.savedState.speed )
        sim.EndPlacement( placement )
    end

    self:ResetTargets()

    -- Reset views.
    ChaseView:Init( sim.argvMap["-ossim"] )
    sim.SuperView:Reset()

    -- SUBTLE: Although the Replay object is reassigned and no longer referenced,
    -- Lua delays garbage-collecting it.  Replay::Stop() is called to stop
    -- its timer-ticks so that it becomes inactive (zombie) else it would
    -- content with the new Replay object!
    self.replay:Stop()  -- disable its timer-ticks (turn into zombie)
    self.replay = Replay:New( self.aircraft )  -- remake

    -- Misc.
    self.swingWingsFraction = 0.0  -- 0.0:forward
end

Events:RegisterReset( function () PlayerActor:Reset() end )  -- need anon func

-- Get Replay object.
function PlayerActor:GetReplay()
    return self.replay
end

-- Get target selected by player else nil.
function PlayerActor:GetSelectedTarget()
    return self.selectedTarget
end

-- Let player select a target (show in HUD).
-- If actor=nil, deselect.
function PlayerActor:SelectTarget( actor, radarLock )
    if ( actor and not actor:IfDestroyed() ) then
        -- Lua
        self.selectedTarget = actor

        -- C++
        sim.GameHud:SelectTarget( actor:GetObject(), radarLock or false )
    else
        -- Lua
        self.selectedTarget = nil

        -- C++
        sim.GameHud:DeselectTarget()
    end
end

-- Reset the selection of a target.
function PlayerActor:ResetTargets()
    self.selectedTarget = nil
    self:CycleTargets()
end

-- Switch to next target.
function PlayerActor:CycleTargets()
    local nextSelectedTarget = Game:CycleActor( Game.TEAM_NUM_ENEMY, self.selectedTarget )
    self:SelectTarget( nextSelectedTarget )
end

-- Incrementally adjust swing-wings.
-- +1:forward or -1:backward
-- input = "key" or "joy"
function PlayerActor:IncSwingWings( dir, input )
    -- Inc or dec.
    -- But at a slow rate for joystick which is polled faster than keyboard.
    local inc = self.swingWingsFractionIncForKey
    if ( input == "joy" ) then
        inc = self.swingWingsFractionIncForJoy
    end

    -- Compute fraction of swing-wings (0.0,...,1.0).
    local swingWingsFraction = self.swingWingsFraction + dir * inc

    -- Apply (unless outside range).
    if ( swingWingsFraction >= 0 and swingWingsFraction <= 1 ) then
        self.swingWingsFraction = swingWingsFraction -- commit
        self.aircraft:SwingWings( swingWingsFraction )
    end
end

-- Fire selected weapon.
function PlayerActor:FireWeapon()
    self:FireMissile()
end

-- Fire guns.
function PlayerActor:FireGun()
    Log( "fire gun TODO" )
end

-- Fire missile.
function PlayerActor:FireMissile()
    -- Does radar have lock?
    local target = self.radar:GetTarget()
    if ( not target ) then
        Hud:AddStatusText( defs.HUD_STATUS_NO_RADAR_LOCK )
    else
        Hud:RemoveStatusText( defs.HUD_STATUS_NO_RADAR_LOCK )

        -- Create missile (as an Actor).
        local launcher = PlayerActor
        local launcherSpeed = self.aircraft:GetPhysics():ComputeSpeed()
        local malfunction = self:RandomMissileMalfunction()
        local particleRadiusGeo = 8.0  -- smaller trail for player
        local missile = MissileActor:New( target, launcher, launcherSpeed,
                                          particleRadiusGeo, malfunction )

        if ( malfunction ) then
            Hud:AddStatusText( defs.HUD_STATUS_MISSILE_MALFUNCTION )
        else
            Hud:RemoveStatusText( defs.HUD_STATUS_MISSILE_MALFUNCTION )
        end

        -- Contain missile.
        Game:AttachActor( missile )

        -- Play missile sound.
        sim.Sound:Play( "missile.wav.gz", sim.Sound.PLAY_ONCE, 1.0 )
    end
end

function PlayerActor:UnreferenceActor( actor )
    if ( actor == self:GetObject() ) then
        -- NOP.  Keep holding reference to the same player's Aircraft.
        -- As a special-case, the C++ core doesn't zombify the current craft (object/zombie.cc).
    elseif ( actor == self.selectedTarget ) then
        self.selectedTarget = nil
    end
end

-- Abbrev.
function GetPlayerAircraft()
    return PlayerActor:GetAircraft()
end

------------------------
-- Create PlayerActor --
------------------------

-- After all its methods have been defined.

PlayerActor:New()  -- do it

-------------------
-- MissileActor --
-------------------

MissileActor = lib.DerivedClass( Actor )

-- ctor.
function MissileActor:New( target, launcher, launcherSpeed, particleRadiusGeo, malfunction )
    self = lib.NewInstanceChain( MissileActor, self )

    Actor.New( self, "Missile", launcher:GetTeamNum(), nil )

    self.target  = target
    self.missile = sim.Missile:New( target:GetObject(), launcher:GetObject(),
                                    launcherSpeed, particleRadiusGeo, malfunction )
    self.showInHud = false

    return self
end

function MissileActor:Destroy()
    if ( not self.destroyed ) then
        sim.ZombifyObject( self, "missile" )
        Actor.Destroy( self )  -- top-down destruction
        self.destroyed = true
    end
end

-- Destroy missiles after program reset.
function MissileActor:Reset()
    Game:DetachActor( self ) -- TODO/FIXME may disrupt Game iteration!
    self:Destroy()
end

function MissileActor:GetObject()
    return self.missile
end

function MissileActor:Tick()
    -- NOP
end

-- Orient missile.
function MissileActor:Rotate( axis, rad )
    self.missile:Rotate( axis, rad )
end

--------------
-- SamActor --
--------------

SamActor = lib.DerivedClass( Actor )

-- ctor.
function SamActor:New( name, teamNum, object )
    self = lib.NewInstanceChain( SamActor, self )

    Actor.New( self, name, teamNum, object )

    self.radar   = Radar:New( self, defs.SAM_RADAR_RANGE_METERS )
    self.target  = nil
    self.missile = nil

    return self
end

function SamActor:Destroy()
    lib.Destroy( self, "radar" )
    lib.Destroy( self, "target" )
    -- Its missile will survive.
    Actor.Destroy( self )
end

-- private (Mission:Reset() calls this).
function SamActor:Reset()
    Actor.Reset( self )
    self.radar:Reset()
    self.target = nil  -- forget target
end

-- private
function SamActor:LaunchMissile()
    -- Missile needs target to aim at.
    if ( self.target ) then
        -- Create MissileActor and attach to Game.
        local launcher = self
        local speed = sim.Physics.KPH2Speed( 600.0 )  -- slow to make game easy
        --speed = PlayerActor:GetAircraft():GetPhysics():ComputeSpeed() -- ideal
        local particleRadiusGeo = M2W( 20.0 )  -- larger
        local malfunction = self:RandomMissileMalfunction()
        self.missile = MissileActor:New( self.target, launcher, speed, particleRadiusGeo, malfunction )
        Game:AttachActor( self.missile )
        
        -- Orient missile to shoot upward.
        self.missile:Rotate( AXIS_PITCH, Deg2Rad( 30.0 ) )
        self.missile:Rotate( AXIS_YAW, Deg2Rad( 20.0 ) )
        
        -- Warn player if missile is fired at player.
        if ( self.target == PlayerActor ) then
            Hud:AddStatusText( defs.HUD_STATUS_MISSILE_WARNING )
            sim.Sound:Play( "beep2.wav.gz", sim.Sound.PLAY_ONCE, 1.0 )
        end
    end
end

-- Tick to pulse animation.
function SamActor:Tick()
    if ( self:IfDestroyed() ) then return end

    self.radar:Tick()

    -- Launch one missile when enemy Actor transitions into the range of SAM.
    -- Radar class handles transitions within/outside randar range.
    local target = self.radar:GetTarget()
    if ( target ) then
        -- Radar has locked-on to target. Same or different target?
        if ( target ~= self.target ) then
            -- Different target.  Launch missile.
            self.target = target
            self:LaunchMissile()
        end
    end
end

function SamActor:UnreferenceActor( actor )
    -- Release target but SAM might not have a target yet!
    if ( self.target == target ) then
        self.target = nil
    end

    -- Propagate to Radar.
    self.radar:UnreferenceActor( actor )
end
