--- **Functional** - Keep airbases clean of crashing or colliding airplanes, and kill missiles when being fired at airbases.
--
-- ===
--
-- ## Features:
--
--
--  * Try to keep the airbase clean and operational.
--  * Prevent airplanes from crashing.
--  * Clean up obstructing airplanes from the runway that are standing still for a period of time.
--  * Prevent airplanes firing missiles within the airbase zone.
--
-- ===
--
-- ## Missions:
--
-- [CLA - CleanUp Airbase](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Functional/CleanUp)
--
-- ===
--
-- Specific airbases need to be provided that need to be guarded. Each airbase registered, will be guarded within a zone of 8 km around the airbase.
-- Any unit that fires a missile, or shoots within the zone of an airbase, will be monitored by CLEANUP_AIRBASE.
-- Within the 8km zone, units cannot fire any missile, which prevents the airbase runway to receive missile or bomb hits.
-- Any airborne or ground unit that is on the runway below 30 meters (default value) will be automatically removed if it is damaged.
--
-- This is not a full 100% secure implementation. It is still possible that CLEANUP_AIRBASE cannot prevent (in-time) to keep the airbase clean.
-- The following situations may happen that will still stop the runway of an airbase:
--
--   * A damaged unit is not removed on time when above the runway, and crashes on the runway.
--   * A bomb or missile is still able to dropped on the runway.
--   * Units collide on the airbase, and could not be removed on time.
--
-- When a unit is within the airbase zone and needs to be monitored,
-- its status will be checked every 0.25 seconds! This is required to ensure that the airbase is kept clean.
-- But as a result, there is more CPU overload.
--
-- So as an advise, I suggest you use the CLEANUP_AIRBASE class with care:
--
--   * Only monitor airbases that really need to be monitored!
--   * Try not to monitor airbases that are likely to be invaded by enemy troops.
--     For these airbases, there is little use to keep them clean, as they will be invaded anyway...
--
-- By following the above guidelines, you can add airbase cleanup with acceptable CPU overhead.
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions:
--
-- ===
--
-- @module Functional.CleanUp
-- @image CleanUp_Airbases.JPG

--- @type CLEANUP_AIRBASE.__ Methods which are not intended for mission designers, but which are used interally by the moose designer :-)
-- @field #map<#string,Wrapper.Airbase#AIRBASE> Airbases Map of Airbases.
-- @extends Core.Base#BASE

--- @type CLEANUP_AIRBASE
-- @extends #CLEANUP_AIRBASE.__

--- Keeps airbases clean, and tries to guarantee continuous airbase operations, even under combat.
--
-- # 1. CLEANUP_AIRBASE Constructor
--
-- Creates the main object which is preventing the airbase to get polluted with debris on the runway, which halts the airbase.
--
--      -- Clean these Zones.
--      CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi } )
--
--      -- or
--      CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi )
--      CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi )
--
-- # 2. Add or Remove airbases
--
-- The method @{#CLEANUP_AIRBASE.AddAirbase}() to add an airbase to the cleanup validation process.
-- The method @{#CLEANUP_AIRBASE.RemoveAirbase}() removes an airbase from the cleanup validation process.
--
-- # 3. Clean missiles and bombs within the airbase zone.
--
-- When missiles or bombs hit the runway, the airbase operations stop.
-- Use the method @{#CLEANUP_AIRBASE.SetCleanMissiles}() to control the cleaning of missiles, which will prevent airbases to stop.
-- Note that this method will not allow anymore airbases to be attacked, so there is a trade-off here to do.
--
-- @field #CLEANUP_AIRBASE
CLEANUP_AIRBASE = {
	ClassName = "CLEANUP_AIRBASE",
	TimeInterval = 0.2,
	CleanUpList = {},
}

-- @field #CLEANUP_AIRBASE.__
CLEANUP_AIRBASE.__ = {}

--- @field #CLEANUP_AIRBASE.__.Airbases
CLEANUP_AIRBASE.__.Airbases = {}

--- Creates the main object which is handling the cleaning of the debris within the given Zone Names.
-- @param #CLEANUP_AIRBASE self
-- @param #list<#string> AirbaseNames Is a table of airbase names where the debris should be cleaned. Also a single string can be passed with one airbase name.
-- @return #CLEANUP_AIRBASE
-- @usage
--  -- Clean these Zones.
-- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi )
-- or
-- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi )
-- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi )
function CLEANUP_AIRBASE:New( AirbaseNames )

  local self = BASE:Inherit( self, BASE:New() ) -- #CLEANUP_AIRBASE
	self:F( { AirbaseNames } )

	if type( AirbaseNames ) == 'table' then
    for AirbaseID, AirbaseName in pairs( AirbaseNames ) do
      self:AddAirbase( AirbaseName )
    end
	else
    local AirbaseName = AirbaseNames
    self:AddAirbase( AirbaseName )
	end

	self:HandleEvent( EVENTS.Birth, self.__.OnEventBirth )

  self.__.CleanUpScheduler = SCHEDULER:New( self, self.__.CleanUpSchedule, {}, 1, self.TimeInterval )

  self:HandleEvent( EVENTS.EngineShutdown , self.__.EventAddForCleanUp )
  self:HandleEvent( EVENTS.EngineStartup, self.__.EventAddForCleanUp )
  self:HandleEvent( EVENTS.Hit, self.__.EventAddForCleanUp )
  self:HandleEvent( EVENTS.PilotDead, self.__.OnEventCrash )
  self:HandleEvent( EVENTS.Dead, self.__.OnEventCrash )
  self:HandleEvent( EVENTS.Crash, self.__.OnEventCrash )

  for UnitName, Unit in pairs( _DATABASE.UNITS ) do
    local Unit = Unit -- Wrapper.Unit#UNIT
    if Unit:IsAlive() ~= nil then
      if self:IsInAirbase( Unit:GetVec2() ) then
        self:F( { UnitName = UnitName } )
        self.CleanUpList[UnitName] = {}
        self.CleanUpList[UnitName].CleanUpUnit = Unit
        self.CleanUpList[UnitName].CleanUpGroup = Unit:GetGroup()
        self.CleanUpList[UnitName].CleanUpGroupName = Unit:GetGroup():GetName()
        self.CleanUpList[UnitName].CleanUpUnitName = Unit:GetName()
      end
    end
  end

	return self
end

--- Adds an airbase to the airbase validation list.
-- @param #CLEANUP_AIRBASE self
-- @param #string AirbaseName
-- @return #CLEANUP_AIRBASE
function CLEANUP_AIRBASE:AddAirbase( AirbaseName )
  self.__.Airbases[AirbaseName] = AIRBASE:FindByName( AirbaseName )
  self:F({"Airbase:", AirbaseName, self.__.Airbases[AirbaseName]:GetDesc()})

  return self
end

--- Removes an airbase from the airbase validation list.
-- @param #CLEANUP_AIRBASE self
-- @param #string AirbaseName
-- @return #CLEANUP_AIRBASE
function CLEANUP_AIRBASE:RemoveAirbase( AirbaseName )
  self.__.Airbases[AirbaseName] = nil
  return self
end

--- Enables or disables the cleaning of missiles within the airbase zones.
-- Airbase operations stop when a missile or bomb is dropped at a runway.
-- Note that when this method is used, the airbase operations won't stop if
-- the missile or bomb was cleaned within the airbase zone, which is 8km from the center of the airbase.
-- However, there is a trade-off to make. Attacks on airbases won't be possible anymore if this method is used.
-- Note, one can also use the method @{#CLEANUP_AIRBASE.RemoveAirbase}() to remove the airbase from the control process as a whole,
-- when an enemy unit is near. That is also an option...
-- @param #CLEANUP_AIRBASE self
-- @param #string CleanMissiles (Default=true) If true, missiles fired are immediately destroyed. If false missiles are not controlled.
-- @return #CLEANUP_AIRBASE
function CLEANUP_AIRBASE:SetCleanMissiles( CleanMissiles )

  if CleanMissiles then
    self:HandleEvent( EVENTS.Shot, self.__.OnEventShot )
  else
    self:UnHandleEvent( EVENTS.Shot )
  end
end

function CLEANUP_AIRBASE.__:IsInAirbase( Vec2 )

  local InAirbase = false
  for AirbaseName, Airbase in pairs( self.__.Airbases ) do
    local Airbase = Airbase -- Wrapper.Airbase#AIRBASE
    if Airbase:GetZone():IsVec2InZone( Vec2 ) then
      InAirbase = true
      break;
    end
  end

  return InAirbase
end



--- Destroys a @{Wrapper.Unit} from the simulator, but checks first if it is still existing!
-- @param #CLEANUP_AIRBASE self
-- @param Wrapper.Unit#UNIT CleanUpUnit The object to be destroyed.
function CLEANUP_AIRBASE.__:DestroyUnit( CleanUpUnit )
	self:F( { CleanUpUnit } )

	if CleanUpUnit then
	  local CleanUpUnitName = CleanUpUnit:GetName()
		local CleanUpGroup = CleanUpUnit:GetGroup()
    -- TODO DCS BUG - Client bug in 1.5.3
		if CleanUpGroup:IsAlive() then
			local CleanUpGroupUnits = CleanUpGroup:GetUnits()
			if #CleanUpGroupUnits == 1 then
				local CleanUpGroupName = CleanUpGroup:GetName()
				CleanUpGroup:Destroy()
			else
				CleanUpUnit:Destroy()
			end
			self.CleanUpList[CleanUpUnitName] = nil
		end
	end
end



--- Destroys a missile from the simulator, but checks first if it is still existing!
-- @param #CLEANUP_AIRBASE self
-- @param DCS#Weapon MissileObject
function CLEANUP_AIRBASE.__:DestroyMissile( MissileObject )
	self:F( { MissileObject } )

	if MissileObject and MissileObject:isExist() then
		MissileObject:destroy()
		self:T( "MissileObject Destroyed")
	end
end

--- @param #CLEANUP_AIRBASE self
-- @param Core.Event#EVENTDATA EventData
function CLEANUP_AIRBASE.__:OnEventBirth( EventData )
  self:F( { EventData } )
  
  if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() ~= nil then
    if self:IsInAirbase( EventData.IniUnit:GetVec2() ) then
      self.CleanUpList[EventData.IniDCSUnitName] = {}
      self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnit = EventData.IniUnit
      self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroup = EventData.IniGroup
      self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroupName = EventData.IniDCSGroupName
      self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnitName = EventData.IniDCSUnitName
    end
  end

end


--- Detects if a crash event occurs.
-- Crashed units go into a CleanUpList for removal.
-- @param #CLEANUP_AIRBASE self
-- @param Core.Event#EVENTDATA Event
function CLEANUP_AIRBASE.__:OnEventCrash( Event )
	self:F( { Event } )

  --TODO: DCS BUG - This stuff is not working due to a DCS bug. Burning units cannot be destroyed.
	-- self:T("before getGroup")
	-- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired
	-- self:T("after getGroup")
	-- _grp:destroy()
	-- self:T("after deactivateGroup")
	-- event.initiator:destroy()

  if Event.IniDCSUnitName and Event.IniCategory == Object.Category.UNIT then
    self.CleanUpList[Event.IniDCSUnitName] = {}
    self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniUnit
    self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniGroup
    self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName
    self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName
  end

end

--- Detects if a unit shoots a missile.
-- If this occurs within one of the airbases, then the weapon used must be destroyed.
-- @param #CLEANUP_AIRBASE self
-- @param Core.Event#EVENTDATA Event
function CLEANUP_AIRBASE.__:OnEventShot( Event )
	self:F( { Event } )

	-- Test if the missile was fired within one of the CLEANUP_AIRBASE.AirbaseNames.
	if self:IsInAirbase( Event.IniUnit:GetVec2() ) then
		-- Okay, the missile was fired within the CLEANUP_AIRBASE.AirbaseNames, destroy the fired weapon.
    self:DestroyMissile( Event.Weapon )
	end
end

--- Detects if the Unit has an S_EVENT_HIT within the given AirbaseNames. If this is the case, destroy the unit.
-- @param #CLEANUP_AIRBASE self
-- @param Core.Event#EVENTDATA Event
function CLEANUP_AIRBASE.__:OnEventHit( Event )
	self:F( { Event } )

	if Event.IniUnit then
		if self:IsInAirbase( Event.IniUnit:GetVec2() ) then
			self:T( { "Life: ", Event.IniDCSUnitName, ' = ',  Event.IniUnit:GetLife(), "/", Event.IniUnit:GetLife0() } )
			if Event.IniUnit:GetLife() < Event.IniUnit:GetLife0() then
				self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName )
        CLEANUP_AIRBASE.__:DestroyUnit( Event.IniUnit )
			end
		end
	end

	if Event.TgtUnit then
		if self:IsInAirbase( Event.TgtUnit:GetVec2() ) then
			self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtUnit:GetLife(), "/", Event.TgtUnit:GetLife0() } )
			if Event.TgtUnit:GetLife() < Event.TgtUnit:GetLife0() then
				self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName )
        CLEANUP_AIRBASE.__:DestroyUnit( Event.TgtUnit )
			end
		end
	end
end

--- Add the @{DCS#Unit} to the CleanUpList for CleanUp.
-- @param #CLEANUP_AIRBASE self
-- @param DCS#UNIT CleanUpUnit
-- @oaram #string CleanUpUnitName
function CLEANUP_AIRBASE.__:AddForCleanUp( CleanUpUnit, CleanUpUnitName )
	self:F( { CleanUpUnit, CleanUpUnitName } )

	self.CleanUpList[CleanUpUnitName] = {}
	self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit
	self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName

	local CleanUpGroup = CleanUpUnit:GetGroup()

	self.CleanUpList[CleanUpUnitName].CleanUpGroup = CleanUpGroup
	self.CleanUpList[CleanUpUnitName].CleanUpGroupName = CleanUpGroup:GetName()
	self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime()
	self.CleanUpList[CleanUpUnitName].CleanUpMoved = false

	self:T( { "CleanUp: Add to CleanUpList: ", CleanUpGroup:GetName(), CleanUpUnitName } )

end

--- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given AirbaseNames. If this is the case, add the Group to the CLEANUP_AIRBASE List.
-- @param #CLEANUP_AIRBASE.__ self
-- @param Core.Event#EVENTDATA Event
function CLEANUP_AIRBASE.__:EventAddForCleanUp( Event )

  self:F({Event})


	if Event.IniDCSUnit and Event.IniUnit and Event.IniCategory == Object.Category.UNIT then
		if self.CleanUpList[Event.IniDCSUnitName] == nil then
			if self:IsInAirbase( Event.IniUnit:GetVec2() ) then
				self:AddForCleanUp( Event.IniUnit, Event.IniDCSUnitName )
			end
		end
	end

	if Event.TgtDCSUnit and Event.TgtUnit and Event.TgtCategory == Object.Category.UNIT then
		if self.CleanUpList[Event.TgtDCSUnitName] == nil then
			if self:IsInAirbase( Event.TgtUnit:GetVec2() ) then
				self:AddForCleanUp( Event.TgtUnit, Event.TgtDCSUnitName )
			end
		end
	end

end


--- At the defined time interval, CleanUp the Groups within the CleanUpList.
-- @param #CLEANUP_AIRBASE self
function CLEANUP_AIRBASE.__:CleanUpSchedule()

  local CleanUpCount = 0
	for CleanUpUnitName, CleanUpListData in pairs( self.CleanUpList ) do
	  CleanUpCount = CleanUpCount + 1

		local CleanUpUnit = CleanUpListData.CleanUpUnit -- Wrapper.Unit#UNIT
		local CleanUpGroupName = CleanUpListData.CleanUpGroupName

		if CleanUpUnit and CleanUpUnit:IsAlive() ~= nil then

		  if self:IsInAirbase( CleanUpUnit:GetVec2() ) then

  			if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then

  				local CleanUpCoordinate = CleanUpUnit:GetCoordinate()

          self:T( { "CleanUp Scheduler", CleanUpUnitName } )
          if CleanUpUnit:GetLife() <= CleanUpUnit:GetLife0() * 0.95 then
  					if CleanUpUnit:IsAboveRunway() then
  						if CleanUpUnit:InAir() then

  							local CleanUpLandHeight = CleanUpCoordinate:GetLandHeight()
  							local CleanUpUnitHeight = CleanUpCoordinate.y - CleanUpLandHeight

  							if CleanUpUnitHeight < 100 then
  								self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } )
  								self:DestroyUnit( CleanUpUnit )
  							end
  						else
  							self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because on runway and damaged." } )
  							self:DestroyUnit( CleanUpUnit )
  						end
  					end
  				end
  				-- Clean Units which are waiting for a very long time in the CleanUpZone.
  				if CleanUpUnit and (CleanUpUnit.GetPlayerName == nil or not CleanUpUnit:GetPlayerName()) then
  					local CleanUpUnitVelocity = CleanUpUnit:GetVelocityKMH()
  					if CleanUpUnitVelocity < 1 then
  						if CleanUpListData.CleanUpMoved then
  							if CleanUpListData.CleanUpTime + 180 <= timer.getTime() then
  								self:T( { "CleanUp Scheduler", "Destroy due to not moving anymore " .. CleanUpUnitName } )
  								self:DestroyUnit( CleanUpUnit )
  							end
  						end
  					else
  						CleanUpListData.CleanUpTime = timer.getTime()
  						CleanUpListData.CleanUpMoved = true
  					end
  				end
        else
          -- not anymore in an airbase zone, remove from cleanup list.
          self.CleanUpList[CleanUpUnitName] = nil
        end
			else
				-- Do nothing ...
				self.CleanUpList[CleanUpUnitName] = nil
			end
		else
			self:T( "CleanUp: Group " .. CleanUpUnitName .. " cannot be found in DCS RTE, removing ..." )
			self.CleanUpList[CleanUpUnitName] = nil
		end
	end
	self:T(CleanUpCount)

	return true
end
