--- **AI** - Models the intelligent transportation of infantry and other cargo.
--
-- ===
--
-- ### Author: **acrojason** (derived from AI_Cargo_APC by FlightControl)
--
-- ===
--
-- @module AI.AI_Cargo_Ship
-- @image AI_Cargo_Dispatcher.JPG

-- @type AI_CARGO_SHIP
-- @extends AI.AI_Cargo#AI_CARGO

--- Brings a dynamic cargo handling capability for an AI naval group.
--
-- Naval ships can be utilized to transport cargo around the map following naval shipping lanes.
-- The AI_CARGO_SHIP class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. 
-- @{Cargo.Cargo} must be declared within the mission or warehouse to make the AI_CARGO_SHIP recognize the cargo.
-- Please consult the @{Cargo.Cargo} module for more information.
--
-- ## Cargo loading.
-- 
-- The module will automatically load cargo when the Ship is within boarding or loading radius. 
-- The boarding or loading radius is specified when the cargo is created in the simulation and depends on the type of 
-- cargo and the specified boarding radius.
--
-- ## Defending the Ship when enemies are nearby
-- This is not supported for naval cargo because most tanks don't float. Protect your transports...
--
-- ## Infantry or cargo **health**.
-- When cargo is unboarded from the Ship, the cargo is actually respawned into the battlefield.
-- As a result, the unboarding cargo is very _healthy_ every time it unboards.
-- This is due to the limitation of the DCS simulator, which is not able to specify the health of newly spawned units as a parameter.
-- However, cargo that was destroyed when unboarded and following the Ship won't be respawned again (this is likely not a thing for
-- naval cargo due to the lack of support for defending the Ship mentioned above). Destroyed is destroyed.
-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance
-- this has marginal impact on the overall battlefield simulation. Given the relatively short duration of DCS missions and the somewhat
-- lengthy naval transport times, most units entering the Ship as cargo will be freshly en route to an amphibious landing or transporting 
-- between warehouses.
--
-- ## Control the Ships on the map.
-- 
-- Currently, naval transports can only be controlled via scripts due to their reliance upon predefined Shipping Lanes created in the Mission
-- Editor. An interesting future enhancement could leverage new pathfinding functionality for ships in the Ops module.
--
-- ## Cargo deployment.
--
-- Using the @{#AI_CARGO_SHIP.Deploy}() method, you are able to direct the Ship towards a Deploy zone to unboard/unload the cargo at the
-- specified coordinate. The Ship will follow the Shipping Lane to ensure consistent cargo transportation within the simulation environment.
--
-- ## Cargo pickup.
--
-- Using the @{#AI_CARGO_SHIP.Pickup}() method, you are able to direct the Ship towards a Pickup zone to board/load the cargo at the specified
-- coordinate. The Ship will follow the Shipping Lane to ensure consistent cargo transportation within the simulation environment.
--
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- @field #AI_CARGO_SHIP
AI_CARGO_SHIP = {
    ClassName = "AI_CARGO_SHIP",
    Coordinate = nil -- Core.Point#COORDINATE
}

--- Creates a new AI_CARGO_SHIP object.
-- @param #AI_CARGO_SHIP self
-- @param Wrapper.Group#GROUP Ship  The carrier Ship group
-- @param Core.Set#SET_CARGO CargoSet  The set of cargo to be transported
-- @param #number CombatRadius  Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. When CombatRadius is 0, no defense will occur.
-- @param #table ShippingLane  Table containing list of Shipping Lanes to be used
-- @return #AI_CARGO_SHIP
function AI_CARGO_SHIP:New( Ship, CargoSet, CombatRadius, ShippingLane )

    local self = BASE:Inherit( self, AI_CARGO:New( Ship, CargoSet ) ) -- #AI_CARGO_SHIP

    self:AddTransition( "*", "Monitor", "*" )
    self:AddTransition( "*", "Destroyed", "Destroyed" )
    self:AddTransition( "*", "Home", "*" )

    self:SetCombatRadius( 0 )  -- Don't want to deploy cargo in middle of water to defend Ship, so set CombatRadius to 0
    self:SetShippingLane ( ShippingLane )

    self:SetCarrier( Ship )

    return self
end

--- Set the Carrier
-- @param #AI_CARGO_SHIP self
-- @param Wrapper.Group#GROUP CargoCarrier
-- @return #AI_CARGO_SHIP
function AI_CARGO_SHIP:SetCarrier( CargoCarrier )
    self.CargoCarrier = CargoCarrier -- Wrapper.Group#GROUIP
    self.CargoCarrier:SetState( self.CargoCarrier, "AI_CARGO_SHIP", self )

    CargoCarrier:HandleEvent( EVENTS.Dead )

    function CargoCarrier:OnEventDead( EventData )
        self:F({"dead"})
        local AICargoTroops = self:GetState( self, "AI_CARGO_SHIP" )
        self:F({AICargoTroops=AICargoTroops})
        if AICargoTroops then
            self:F({})
            if not AICargoTroops:Is( "Loaded" ) then
                -- Better hope they can swim!
                AICargoTroops:Destroyed()
            end
        end
    end

    self.Zone = ZONE_UNIT:New( self.CargoCarrier:GetName() .. "-Zone", self.CargoCarrier, self.CombatRadius )
    self.Coalition = self.CargoCarrier:GetCoalition()

    self:SetControllable( CargoCarrier )

    return self
end


--- FInd a free Carrier within a radius
-- @param #AI_CARGO_SHIP self
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Radius
-- @return Wrapper.Group#GROUP NewCarrier
function AI_CARGO_SHIP:FindCarrier( Coordinate, Radius )

    local CoordinateZone = ZONE_RADIUS:New( "Zone", Coordinate:GetVec2(), Radius )
    CoordinateZone:Scan( { Object.Category.UNIT } )
    for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do
        local NearUnit = UNIT:Find( DCSUnit )
        self:F({NearUnit=NearUnit})
        if not NearUnit:GetState( NearUnit, "AI_CARGO_SHIP" ) then
            local Attributes = NearUnit:GetDesc()
            self:F({Desc=Attributes})
            if NearUnit:HasAttributes( "Trucks" ) then
                return NearUnit:GetGroup()
            end
        end
    end

    return nil
end

function AI_CARGO_SHIP:SetShippingLane( ShippingLane )
    self.ShippingLane = ShippingLane

    return self
end

function AI_CARGO_SHIP:SetCombatRadius( CombatRadius )
    self.CombatRadius = CombatRadius or 0

    return self
end


--- Follow Infantry to the Carrier
-- @param #AI_CARGO_SHIP self
-- @param #AI_CARGO_SHIP Me
-- @param Wrapper.Unit#UNIT ShipUnit
-- @param Cargo.CargoGroup#CARGO_GROUP Cargo
-- @return #AI_CARGO_SHIP
function AI_CARGO_SHIP:FollowToCarrier( Me, ShipUnit, CargoGroup )

    local InfantryGroup = CargoGroup:GetGroup()

    self:F( { self=self:GetClassNameAndID(), InfantryGroup = InfantryGroup:GetName() } )

    if ShipUnit:IsAlive() then
        -- Check if the Cargo is near the CargoCarrier
        if InfantryGroup:IsPartlyInZone( ZONE_UNIT:New( "Radius", ShipUnit, 1000 ) ) then

            -- Cargo does not need to navigate to Carrier
            Me:Guard()
        else

            self:F( { InfantryGroup = InfantryGroup:GetName() } )
            if InfantryGroup:IsAlive() then

                self:F( { InfantryGroup = InfantryGroup:GetName() } )
                local Waypoints = {}

                -- Calculate new route
                local FromCoord = InfantryGroup:GetCoordinate()
                local FromGround = FromCoord:WaypointGround( 10, "Diamond" )
                self:F({FromGround=FromGround})
                table.insert( Waypoints, FromGround )

                local ToCoord = ShipUnit:GetCoordinate():GetRandomCoordinateInRadius( 10, 5 )
                local ToGround = ToCoord:WaypointGround( 10, "Diamond" )
                self:F({ToGround=ToGround})
                table.insert( Waypoints, ToGround )

                local TaskRoute = InfantryGroup:TaskFunction( "AI_CARGO_SHIP.FollowToCarrier", Me, ShipUnit, CargoGroup )

                self:F({Waypoints=Waypoints})
                local Waypoint = Waypoints[#Waypoints]
                InfantryGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone

                InfantryGroup:Route( Waypoints, 1 ) -- Move after a random number of seconds to the Route.  See Route method for details
            end
        end
    end
end


function AI_CARGO_SHIP:onafterMonitor( Ship, From, Event, To )
    self:F( { Ship, From, Event, To, IsTransporting = self:IsTransporting() } )

    if self.CombatRadius > 0 then
        -- We really shouldn't find ourselves in here for Ships since the CombatRadius should always be 0.
        -- This is to avoid Unloading the Ship in the middle of the sea.
        if Ship and Ship:IsAlive() then
            if self.CarrierCoordinate then
                if self:IsTransporting() == true then
                    local Coordinate = Ship:GetCoordinate()
                        if self:Is( "Unloaded" ) or self:Is( "Loaded" ) then
                            self.Zone:Scan( { Object.Category.UNIT } )
                            if self.Zone:IsAllInZoneOfCoalition( self.Coalition ) then
                                if self:Is( "Unloaded" ) then
                                    -- There are no enemies within combat radius. Reload the CargoCarrier.
                                    self:Reload()
                                end
                            else
                                if self:Is( "Loaded" ) then
                                    -- There are enemies within combat radius. Unload the CargoCarrier.
                                    self:__Unload( 1, nil, true ) -- The 2nd parameter is true, which means that the unload is for defending the carrier, not to deploy!
                                else
                                    if self:Is( "Unloaded" ) then
                                    --self:Follow()
                                    end
                                    self:F( "I am here" .. self:GetCurrentState() )
                                    if self:Is( "Following" ) then
                                        for Cargo, ShipUnit in pairs( self.Carrier_Cargo ) do
                                            local Cargo = Cargo -- Cargo.Cargo#CARGO
                                            local ShipUnit = ShipUnit -- Wrapper.Unit#UNIT
                                            if Cargo:IsAlive() then
                                                if not Cargo:IsNear( ShipUnit, 40 ) then
                                                    ShipUnit:RouteStop()
                                                    self.CarrierStopped = true
                                                else
                                                    if self.CarrierStopped then
                                                        if Cargo:IsNear( ShipUnit, 25 ) then
                                                            ShipUnit:RouteResume()
                                                            self.CarrierStopped = nil
                                                        end
                                                    end
                                                end
                                            end
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
            self.CarrierCoordinate = Ship:GetCoordinate()
        end
        self:__Monitor( -5 )
    end
end

--- Check if cargo ship is alive and trigger Load event
-- @param Wrapper.Group#Group Ship
-- @param #AI_CARGO_SHIP self
function AI_CARGO_SHIP._Pickup( Ship, self, Coordinate, Speed, PickupZone )
    
    Ship:F( { "AI_CARGO_Ship._Pickup:", Ship:GetName() } )

    if Ship:IsAlive() then
        self:Load( PickupZone )
    end
end

--- Check if cargo ship is alive and trigger Unload event. Good time to remind people that Lua is case sensitive and Unload != UnLoad
-- @param Wrapper.Group#GROUP Ship
-- @param #AI_CARGO_SHIP self
function AI_CARGO_SHIP._Deploy( Ship, self, Coordinate, DeployZone )
    Ship:F( { "AI_CARGO_Ship._Deploy:", Ship } )

    if Ship:IsAlive() then
        self:Unload( DeployZone )
    end
end

--- on after Pickup event.
-- @param AI_CARGO_SHIP Ship
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE  Coordinate of the pickup point
-- @param #number Speed  Speed in km/h to sail to the pickup coordinate. Default is 50% of max speed for the unit
-- @param #number Height  Altitude in meters to move to the pickup coordinate. This parameter is ignored for Ships
-- @param Core.Zone#ZONE PickupZone (optional)  The zone where the cargo will be picked up. The PickupZone can be nil if there was no PickupZoneSet provided
function AI_CARGO_SHIP:onafterPickup( Ship, From, Event, To, Coordinate, Speed, Height, PickupZone )
    
    if Ship and Ship:IsAlive() then
        AI_CARGO_SHIP._Pickup( Ship, self, Coordinate, Speed, PickupZone )
        self:GetParent( self, AI_CARGO_SHIP ).onafterPickup( self, Ship, From, Event, To, Coordinate, Speed, Height, PickupZone )
    end
end

--- On after Deploy event.
-- @param #AI_CARGO_SHIP self
-- @param Wrapper.Group#GROUP SHIP
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate  Coordinate of the deploy point
-- @param #number Speed  Speed in km/h to sail to the deploy coordinate. Default is 50% of max speed for the unit
-- @param #number Height  Altitude in meters to move to the deploy coordinate. This parameter is ignored for Ships
-- @param Core.Zone#ZONE  DeployZone The zone where the cargo will be deployed.
function AI_CARGO_SHIP:onafterDeploy( Ship, From, Event, To, Coordinate, Speed, Height, DeployZone )

  if Ship and Ship:IsAlive() then
    
    Speed = Speed or Ship:GetSpeedMax()*0.8
    local lane = self.ShippingLane

    if lane then
      local Waypoints = {}

      for i=1, #lane do
        local coord = lane[i]
        local Waypoint = coord:WaypointGround(_speed)
        table.insert(Waypoints, Waypoint)
      end

      local TaskFunction = Ship:TaskFunction( "AI_CARGO_SHIP._Deploy", self, Coordinate, DeployZone )
      local Waypoint = Waypoints[#Waypoints]
      Ship:SetTaskWaypoint( Waypoint, TaskFunction )
      Ship:Route(Waypoints, 1)  
      self:GetParent( self, AI_CARGO_SHIP ).onafterDeploy( self, Ship, From, Event, To, Coordinate, Speed, Height, DeployZone )
    else
      self:E(self.lid.."ERROR: No shipping lane defined for Naval Transport!")
    end
  end
end

--- On after Unload event.
-- @param #AI_CARGO_SHIP self
-- @param Wrapper.Group#GROUP Ship
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
function AI_CARGO_SHIP:onafterUnload( Ship, From, Event, To, DeployZone, Defend )
    self:F( { Ship, From, Event, To, DeployZone, Defend = Defend } )
  
    local UnboardInterval = 5
    local UnboardDelay = 5
  
    if Ship and Ship:IsAlive() then
      for _, ShipUnit in pairs( Ship:GetUnits() ) do
        local ShipUnit = ShipUnit -- Wrapper.Unit#UNIT
        Ship:RouteStop()
        for _, Cargo in pairs( ShipUnit:GetCargo() ) do
          self:F( { Cargo = Cargo:GetName(), Isloaded = Cargo:IsLoaded() } )
          if Cargo:IsLoaded() then
            local unboardCoord = DeployZone:GetRandomPointVec2()
            Cargo:__UnBoard( UnboardDelay, unboardCoord, 1000)
            UnboardDelay = UnboardDelay + Cargo:GetCount() * UnboardInterval
            self:__Unboard( UnboardDelay, Cargo, ShipUnit, DeployZone, Defend )
            if not Defend == true then
              Cargo:SetDeployed( true )
            end
          end 
        end
      end
    end
end

function AI_CARGO_SHIP:onafterHome( Ship, From, Event, To, Coordinate, Speed, Height, HomeZone )
    if Ship and Ship:IsAlive() then
        
        self.RouteHome = true
        Speed = Speed or Ship:GetSpeedMax()*0.8
        local lane = self.ShippingLane
 
        if lane then
            local Waypoints = {}

            -- Need to find a more generalized way to do this instead of reversing the shipping lane.
            -- This only works if the Source/Dest route waypoints are numbered 1..n and not n..1
            for i=#lane, 1, -1 do
                local coord = lane[i]
                local Waypoint = coord:WaypointGround(_speed)
                table.insert(Waypoints, Waypoint)
            end

            local Waypoint = Waypoints[#Waypoints]
            Ship:Route(Waypoints, 1)
        
        else
            self:E(self.lid.."ERROR: No shipping lane defined for Naval Transport!")
        end
    end
end
