------------------------------------------------------------------
-- VEAF helper for Skynet-IADS
-- By zip (2021)
--
-- Features:
-- ---------
-- * This module offers support for integrating Skynet-IADS in a mission
--
-- See the documentation : https://veaf.github.io/documentation/
------------------------------------------------------------------

veafSkynet = {}

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Global settings. Stores the script constants
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Identifier. All output in DCS.log will start with this.
veafSkynet.Id = "SKYNET"

--- Version.
veafSkynet.Version = "3.1.1"

-- trace level, specific to this module
--veafSkynet.LogLevel = "trace"

veaf.loggers.new(veafSkynet.Id, veafSkynet.LogLevel)

-- delay before the mission groups are added to the IADS' at start
veafSkynet.DelayForStartup = 1

-- delay before restarting the IADS when adding a single group
veafSkynet.DelayForRestart = 20

-- maximum x or y (z in DCS) between a SAM site and it's point defenses in meters
veafSkynet.MaxPointDefenseDistanceFromSite = 10000

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Do not change anything below unless you know what you are doing!
-------------------------------------------------------------------------------------------------------------------------------------------------------------

veafSkynet.initialized = false
--flag to know if all units present on the map should be loaded at init or not into their team's main IADS network
veafSkynet.loadAllAtInit = {
    [tostring(coalition.side.BLUE)] = true,
    [tostring(coalition.side.RED)] = true
}
--table containing the default IADS network names initialized for each coalition
veafSkynet.defaultIADS = {
    [tostring(coalition.side.BLUE)] = "blue iads",
    [tostring(coalition.side.RED)] = "red iads",
}
veafSkynet.iadsSamUnitsTypes = {}
veafSkynet.iadsEwrUnitsTypes = {}

veafSkynet.GroupIntegrationModes =
{
    Strict = 0, -- groups will only be included in the skynet networks if they contains only units known to skynet
    Lenient = 1 -- groups will be included in the skynet networks even if some units are not known to skynet
}
veafSkynet.GroupIntegrationMode = veafSkynet.GroupIntegrationModes.Lenient -- lenient by default

-- Management of point defences (Flogas)
-- when active, veafSkynet will attempt to set eligible groups as point defence of nearby defencible groups
-- as per Skynet database types :
--  - "single" groups will be eligible to defend "complex" or "ewr" groups
--  - "complex" groups will be eligible to defend "ewr" groups
--  - "single" groups will never be defended
veafSkynet.PointDefenceModes =
{
    None = 0, -- point defences will not be created
    Skynet = 1, -- point defences will be defined with the skynet logic
    Dcs = 2 -- point defences will be defined as not a part of the skynet network and left to the dcs ai
}
veafSkynet.PointDefenceMode = veafSkynet.PointDefenceModes.None -- no point defences by default

veafSkynet.DynamicSpawn = false -- false by default

veafSkynet.SkynetElementStates =
{
    Autonomous = 0,
    Live = 1,
    Dark = 2
}

--table containing the structure of each IADS network, first level is accessed with the IADS name. This contains the .coalitionID of the network, the IADS network (.iads), the groups added to the network (.groups) stored by groupName, 
--wether this network should appear on the radio menu (.includeInRadio) and lastly if this network is in debug mode (.debugFlag). The groups store whether the group was .forceEwr or .pointDefense.
veafSkynet.structure = {}

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Utility methods
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function veafSkynet.getStringSkynetElement(skynetElement)
    local s = skynetElement.dcsName

    if (not skynetElement.dcsRepresentation:isExist()) then
        return s .. " (dcs object does not exist)"    
    end

    local id = skynetElement.dcsRepresentation:getID()
    local category = getmetatable(skynetElement.dcsRepresentation)
    local sCategory = "unknown"
    if (category == Group) then
        sCategory = "group"
    elseif (category == Unit) then
        sCategory = "unit"
    elseif (category == StaticObject) then
        sCategory = "static"
    end

    if (skynetElement.getNatoName) then
        s = s .. " [" .. skynetElement:getNatoName() .. "]"
    else
        s = s .. " [" .. skynetElement.typeName .. "]"
    end

    s = s .. " [id=" .. id .. "]" .. " [" .. sCategory .. "]"
    return s
end

function veafSkynet.getDcsGroupFromSkynetElement(skynetElement)
    if (skynetElement.dcsRepresentation and skynetElement.dcsRepresentation:isExist()) then
        local category = getmetatable(skynetElement.dcsRepresentation)
        if (category == Group) then
            return skynetElement.dcsRepresentation
        elseif (category == Unit) then
            return Unit.getGroup(skynetElement.dcsRepresentation)
        end
    end
    
    return nil
end

function veafSkynet.getSkynetData(skynetElement)
    local function skynetDatabaseMatchType(skynetElementTypeList, skynetDatabaseTypeList)
        if (skynetDatabaseTypeList and skynetElementTypeList and #skynetElementTypeList > 0) then
            for i = 1, #skynetElementTypeList do
                if (skynetDatabaseTypeList[skynetElementTypeList[i].typeName]) then
                    return true
                end
            end
        end

        return false
    end

    for skynetDataName, skynetData in pairs(SkynetIADS.database) do
        -- first check the launchers as they are the most unique thing
        if(skynetDatabaseMatchType(skynetElement.launchers, skynetData["launchers"])) then
            veaf.loggers.get(veafSkynet.Id):trace("Matched by launcher : " .. veafSkynet.getStringSkynetElement(skynetElement) .. " > " .. skynetDataName)
            return skynetData
        end

        -- tracking and search radars can be used by multiple sites
        if(skynetDatabaseMatchType(skynetElement.trackingRadars, skynetData["trackingRadar"])) then
            veaf.loggers.get(veafSkynet.Id):trace("Matched by TR : " .. veafSkynet.getStringSkynetElement(skynetElement) .. " > " .. skynetDataName)
            return skynetData
        end
        if(skynetDatabaseMatchType(skynetElement.searchRadars, skynetData["searchRadar"])) then
            veaf.loggers.get(veafSkynet.Id):trace("Matched by SR : " .. veafSkynet.getStringSkynetElement(skynetElement) .. " > " .. skynetDataName)
            return skynetData
        end
    end

    veaf.loggers.get(veafSkynet.Id):trace("No match : " .. veafSkynet.getStringSkynetElement(skynetElement))
    return nil
end

function veafSkynet.removeSkynetElement(skynetElement, veafSkynetNetwork)
    local iads = veafSkynetNetwork.iads

    veaf.loggers.get(veafSkynet.Id):trace("Remove skynet element [" .. veafSkynet.getStringSkynetElement(skynetElement) .. "]")

    local function _removeSkynetElementFromList(list, skynetElement)
        local iIndex = -1
        if (list and #list > 0) then
            for i = 1, #list do
                if (list[i] == skynetElement) then
                    iIndex = i
                    break
                end
            end
    
            if (iIndex > 0) then
                table.remove(list, iIndex) 
            end
        end
    end

    skynetElement:cleanUp()
    skynetElement:getDCSRepresentation():enableEmission(true)

    local list = iads.samSites
    veaf.loggers.get(veafSkynet.Id):trace("Sam sites count: " .. #list)

    _removeSkynetElementFromList(list, skynetElement)
    --_removeSkynetElementFromList(iads:getEarlyWarningRadars(), skynetElement)

    veaf.loggers.get(veafSkynet.Id):trace("Sam sites count: " .. #list) -- not removed here

    local dcsGroup = veafSkynet.getDcsGroupFromSkynetElement(skynetElement)
    veafSkynetNetwork.groups[dcsGroup:getName()] = nil
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- core functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------

-- get the IADS network for a given name ("blue iads", "red iads"")
function veafSkynet.getNetwork(networkName)
    local network = nil
    if networkName then
        network = veafSkynet.structure[networkName]
    end
    return network
end

-- get the IADS object for a given name ("blue iads", "red iads"")
function veafSkynet.getIADS(networkName)
    local iads = nil
    local network = veafSkynet.getNetwork(networkName)
    if network then
        iads = network.iads
    end
    return iads
end

-- calling SkynetIADS:activate() after a delay, to avoid calling it at each time a group is added to the IADS
function veafSkynet.delayedActivate(networkName)
    veaf.loggers.get(veafSkynet.Id):debug("veafSkynet.delayedActivate(%s)", veaf.p(networkName))
    local network = veafSkynet.structure[networkName]
    if network then
        if network.delayedActivation then
            veaf.loggers.get(veafSkynet.Id):trace(string.format("IADS %s already has a delayed activation", veaf.p(networkName)))
        else
            veaf.loggers.get(veafSkynet.Id):trace(string.format("IADS %s will be activated in %d seconds", veaf.p(networkName), veafSkynet.DelayForRestart))
            network.delayedActivation = mist.scheduleFunction(veafSkynet._activateIADS, {networkName}, timer.getTime() + veafSkynet.DelayForRestart)
        end
    end
end

function veafSkynet._activateIADS(networkName)
    veaf.loggers.get(veafSkynet.Id):debug("veafSkynet._activateIADS(%s)", veaf.p(networkName))

    local network = veafSkynet.structure[networkName]
    if network then
        network.delayedActivation = nil
        local iads = network.iads
        if iads then
            veaf.loggers.get(veafSkynet.Id):debug("calling iads:activate()")
            iads:activate()
        end
    end
end

function veafSkynet.getIadsOfCoalition(networkName, coa)
    local iads = nil
    if veafSkynet.structure[networkName] and coa == veafSkynet.structure[networkName].coalitionID then
        iads = veafSkynet.structure[networkName].iads
    end
    return iads
end

function veafSkynet.getNearestIADSSite(networkName, dcsGroup)
    
    if not dcsGroup then
        veaf.loggers.get(veafSkynet.Id):trace("No group to find the nearest IADS site for")
        return false
    end

    local coa = dcsGroup:getCoalition()
    veaf.loggers.get(veafSkynet.Id):trace(string.format("Ref coalition : %s", veaf.p(coa))) 

    local iads = veafSkynet.getIadsOfCoalition(networkName, coa)
    if not iads then
        veaf.loggers.get(veafSkynet.Id):trace(string.format("IADS named %s for the coalition of the group %s does not exist", veaf.p(networkName), tostring(dcsGroup:getName())))
        return false
    end
    
    local currentGroup = dcsGroup:getName()
    veaf.loggers.get(veafSkynet.Id):trace(string.format("networkName : %s", veaf.p(networkName))) 
    local groupPos = veaf.getAveragePosition(dcsGroup)
    veaf.loggers.get(veafSkynet.Id):debug(string.format("Ref Position : %s", veaf.p(groupPos))) 


    local nearestEWRname = nil
    local minEWRDistance = veafSkynet.MaxPointDefenseDistanceFromSite
    local nearestSAMname = nil
    local minSAMDistance = veafSkynet.MaxPointDefenseDistanceFromSite

    local CoalitionSites = nil

    local searchForGroup = function(CoalitionSites, pos, currentGroupName, ewrFlag)
        local minDistance = veafSkynet.MaxPointDefenseDistanceFromSite
        local FoundGroup = nil

        for site, site_info in pairs(CoalitionSites) do
            local site_name = site_info.dcsName -- For EWRs it looks like this gives the unit's name which would need to be reversed to the group to get position data
            veaf.loggers.get(veafSkynet.Id):trace(string.format("Checked Site groupName : %s and isEWR : %s", veaf.p(site_name), veaf.p(ewrFlag))) 
            
            if site_name and currentGroupName ~= site_name then
                if ewrFlag then
                    local unit = Unit.getByName(site_name)
                    local group = Unit.getGroup(unit)
                    site_name = Group.getName(group)
                end
                veaf.loggers.get(veafSkynet.Id):trace(string.format("Checked Site groupName : %s", veaf.p(site_name)))

                local groupAvgPosition = veaf.getAveragePosition(site_name)
                veaf.loggers.get(veafSkynet.Id):debug(string.format("Checked Site groupAvgPosition : %s", veaf.p(groupAvgPosition)))

                if groupAvgPosition then
                    local distance = math.sqrt((pos.x-groupAvgPosition.x)^2+(pos.z-groupAvgPosition.z)^2)
                    veaf.loggers.get(veafSkynet.Id):trace(string.format("Distance between checked site and pointDefense : %s", veaf.p(distance)))

                    if distance <= minDistance then
                        veaf.loggers.get(veafSkynet.Id):trace("This site is closer")
                        FoundGroup = site_name
                        minDistance = distance
                    end
                end
            end
        end 
       
        return FoundGroup, minDistance
    end

    --start by going through the EWRs
    CoalitionSites = iads:getEarlyWarningRadars()
    nearestEWRname, minEWRDistance = searchForGroup(CoalitionSites, groupPos, currentGroup, true)

    --search for SAM sites
    CoalitionSites = iads:getSAMSites()
    nearestSAMname, minSAMDistance = searchForGroup(CoalitionSites, groupPos, currentGroup)    

    if minEWRDistance <= minSAMDistance then
        return nearestEWRname
    end
    return nearestSAMname
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Management of point defences (Flogas)
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function veafSkynet.canBePointDefence(skynetData)
    if (skynetData == nil) then
        return false
    end
    if (skynetData["can_engage_harm"] ~= true) then
        return false
    end
    if (skynetData["type"] ~= "complex" and skynetData["type"] ~= "single") then
        return false
    end

    return true
end

function veafSkynet.findSkynetElementToDefend(skynetElementPointDefence, skynetDataPointDefence)
    local dcsGroupPointDefence = veafSkynet.getDcsGroupFromSkynetElement(skynetElementPointDefence)
    local iads = skynetElementPointDefence.iads
    local pointDefenceType = skynetDataPointDefence["type"]

    local pointDefencePosition = veaf.getAveragePosition(dcsGroupPointDefence)
    if (pointDefencePosition == nil) then
        return nil
    end

    local function findClosestSkynetElementInList(skynetElementList)
        if (skynetElementList == nil or #skynetElementList <= 0) then
            return nil
        end

        local iMinDistance = veafSkynet.MaxPointDefenseDistanceFromSite
        local foundElement
        for i = 1, #skynetElementList do
            local skynetElement = skynetElementList[i]
            local skynetData = veafSkynet.getSkynetData(skynetElement)
            if (skynetData and (skynetData["type"] == "ewr" or (skynetData["type"] == "complex" and pointDefenceType == "single"))) then
                -- ewr are always defencible, and complex sam sites only defencible by single groups
                local dcsGroup = veafSkynet.getDcsGroupFromSkynetElement(skynetElementList[i])
                local position = veaf.getAveragePosition(dcsGroup)

                if (position) then
                    local iDistance = math.sqrt((position.x - pointDefencePosition.x)^2 + (position.z - pointDefencePosition.z)^2)
             
                    if (iDistance < iMinDistance) then
                        foundElement = skynetElement
                        iMinDistance = iDistance
                    end
                end
            end
        end

        return foundElement
    end

    local elementToDefend = findClosestSkynetElementInList (iads:getEarlyWarningRadars())
    if (elementToDefend == nil and pointDefenceType == "single") then
        elementToDefend = findClosestSkynetElementInList (iads:getSAMSites())
    end

    if (elementToDefend) then
        local sDescriptorPointDefence = veafSkynet.getStringSkynetElement(skynetElementPointDefence)
        local sDescriptorToDefend = veafSkynet.getStringSkynetElement(elementToDefend)
        veaf.loggers.get(veafSkynet.Id):trace("Identified [ " .. sDescriptorToDefend .. " ] to be defended by [ " .. sDescriptorPointDefence .. " ]")
    end

    return elementToDefend
end

function veafSkynet.removePointDefencesFromSkynetElement(skynetElement)
    if(skynetElement and skynetElement.pointDefences and #skynetElement.pointDefences > 0) then
        for i = 1, #skynetElement.pointDefences do
            skynetElement.pointDefences[i]:setIsAPointDefence(false)
        end
        skynetElement.pointDefences = {}
    end

    skynetElement.pointDefences = {}
end

function veafSkynet.removePointDefences(iads)
    -- not tested
    local ewrs = iads:getEarlyWarningRadars()
    if (ewrs and #ewrs > 0) then
        for i = 1, #ewrs do
            veafSkynet.removePointDefencesFromSkynetElement(ewrs[i])
        end
    end
    local samSites = iads:getSAMSites()
    if (samSites and #samSites > 0) then
        for i = 1, #samSites do
            veafSkynet.removePointDefencesFromSkynetElement(samSites[i])
        end
    end
end

function veafSkynet.initializePointDefenceSamSite(samSite, veafSkynetNetwork)
    if (samSite:getIsAPointDefence()) then
        return
    end

    local skynetData = veafSkynet.getSkynetData(samSite)
    if (not veafSkynet.canBePointDefence(skynetData)) then
        return
    end

    local elementToDefend = veafSkynet.findSkynetElementToDefend(samSite, skynetData)
    if (elementToDefend) then
        if (veafSkynet.PointDefenceMode == veafSkynet.PointDefenceModes.Skynet) then
            veaf.loggers.get(veafSkynet.Id):debug("Point defence: add as skynet")
            elementToDefend:addPointDefence(samSite)
        elseif (veafSkynet.PointDefenceMode == veafSkynet.PointDefenceModes.Dcs) then
            veaf.loggers.get(veafSkynet.Id):debug("Point defence: add as dcs")
            veafSkynet.removeSkynetElement(samSite, veafSkynetNetwork)
        end
    end
end

function veafSkynet.initializePointDefences(veafSkynetNetwork)
    if (veafSkynet.PointDefenceMode ~= veafSkynet.PointDefenceModes.Skynet and veafSkynet.PointDefenceMode ~= veafSkynet.PointDefenceModes.Dcs) then
        return
    end
  
    veaf.loggers.get(veafSkynet.Id):debug("Analyzing network to create point defences")

    local iads = veafSkynetNetwork.iads
    local samSites = iads:getSAMSites()
    if (samSites and #samSites > 0) then
        for i = 1, #samSites do
            veafSkynet.initializePointDefenceSamSite(samSites[i], veafSkynetNetwork)
        end
    end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Management of dynamic group spawns (Flogas)
-- Option to integrate late spawned units into the existing networks
-------------------------------------------------------------------------------------------------------------------------------------------------------------
veafSkynet.monitorDynamicSpawnHandlerId = nil

function veafSkynet.OnDynamicSpawn(event)
    if(not veafSkynet.initialized) then
        return
    end
    if(event.id ~= world.event.S_EVENT_BIRTH) then
        return
    end
    if (event.initiator == nil or Object.getCategory(event.initiator) ~= Object.Category.UNIT) then
        return
    end

    -- birth event will be triggered for each unit of a spawned group, but we want to manage the group, so we only work for for the first unit
    local dcsGroup = Unit.getGroup (event.initiator)
    local firstDcsUnit = dcsGroup:getUnit(1)
    if(firstDcsUnit == nil) then
        return
    end
    if(firstDcsUnit:getID() ~= event.initiator:getID()) then
        return 
    end

    local coalition = dcsGroup:getCoalition()
    local networkName = veafSkynet.defaultIADS[tostring(coalition)]
    if not networkName then
        veaf.loggers.get(veafSkynet.Id):error("No default IADS network for coalition " .. coalition)
        return
    end

    veaf.loggers.get(veafSkynet.Id):debug("DYNAMIC SPAWN adding spawned group [" .. dcsGroup:getName() .. "] [id=" .. dcsGroup:getID() .. "] to IADS network [" .. networkName .. "]")
    if (veafSkynet.addGroupToNetwork(networkName, dcsGroup, false, false)) then
        veafSkynet.initializePointDefences(veafSkynet.getNetwork(networkName))
        --iads:buildRadarCoverage()
    end
end

function veafSkynet.monitorDynamicSpawn(bMonitor)
    if (bMonitor) then
        if (veafSkynet.monitorDynamicSpawnHandlerId ~= nil) then
            return -- already active
        end
        veaf.loggers.get(veafSkynet.Id):debug("DYNAMIC SPAWN monitoring ON")
        veafSkynet.monitorDynamicSpawnHandlerId = mist.addEventHandler(veafSkynet.OnDynamicSpawn)
    else
        if (veafSkynet.monitorDynamicSpawnHandlerId == nil) then
            return -- already inactive
        end
        veaf.loggers.get(veafSkynet.Id):debug("DYNAMIC SPAWN monitoring OFF")
        mist.removeEventHandler(veafSkynet.monitorDynamicSpawnHandlerId)
        veafSkynet.monitorDynamicSpawnHandlerId = nil
    end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- initialisation
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function veafSkynet.isGroupUsable(dcsGroup)
    if (veafSkynet.GroupIntegrationMode == veafSkynet.GroupIntegrationModes.Strict) then
        for _, dcsUnit in pairs(dcsGroup:getUnits()) do
            local dcsUnitType = dcsUnit:getTypeName()
            if (not veafSkynet.iadsEwrUnitsTypes[dcsUnitType] and not veafSkynet.iadsSamUnitsTypes[dcsUnitType]) then
                return false
            end
        end

        return true
    else
        for _, dcsUnit in pairs(dcsGroup:getUnits()) do
            local dcsUnitType = dcsUnit:getTypeName()
            if (veafSkynet.iadsEwrUnitsTypes[dcsUnitType] or veafSkynet.iadsSamUnitsTypes[dcsUnitType]) then
                return true
            end
        end

        return false
    end
end

function veafSkynet.addGroupToNetwork(networkName, dcsGroup, forceEwr, pointDefense, alreadyAddedGroups, silent)
    veaf.loggers.get(veafSkynet.Id):debug("ADD GROUP START [" .. dcsGroup:getName() .. "] [id=" .. dcsGroup:getID() .. "] to IADS network [" .. networkName .. "]")

    if not dcsGroup then
        veaf.loggers.get(veafSkynet.Id):error("No group to find to add to network")
        return false
    end
    
    if (not veafSkynet.isGroupUsable(dcsGroup)) then
        veaf.loggers.get(veafSkynet.Id):trace("Group is not usable for skynet")
        return false        
    end
    
    local forceEwr = false or forceEwr
    local pointDefense = false or pointDefense
    local silent = false or silent

    local batchMode = (alreadyAddedGroups ~= nil)
    local alreadyAddedGroups = alreadyAddedGroups or {}
    local groupName = dcsGroup:getName()
    local coa = dcsGroup:getCoalition()
    local iads = veafSkynet.getIadsOfCoalition(networkName, coa)
    if not(iads) then
        veaf.loggers.get(veafSkynet.Id):debug(string.format("IADS named %s for the coalition of the group %s does not exist", veaf.p(networkName), tostring(groupName)))
        return false
    end
    local didSomething = false

    veaf.loggers.get(veafSkynet.Id):trace(string.format("batchMode=%s forceEwr=%s PointDefense=%s", tostring(batchMode), tostring(forceEwr), tostring(pointDefense)))

    local defended_name = nil
    if pointDefense and not(forceEwr) then
        veaf.loggers.get(veafSkynet.Id):trace(string.format("SAM is requested as pointDefense"))
        
        if pointDefense == true then 
            veaf.loggers.get(veafSkynet.Id):trace(string.format("Find nearest site to defend"))
            defended_name = veafSkynet.getNearestIADSSite(networkName, dcsGroup)
        else
            defended_name = pointDefense
            local defended_SAM = iads:getSAMSiteByGroupName(defended_name)
            local defended_EWR = iads:getEarlyWarningRadars(defended_name)

            local defended_site = defended_EWR
            if defended_SAM then
                defended_site = defended_SAM
            end

            if defended_site then
                local defended_pos = veaf.getAvgGroupPos(defended_name)
                local dcsGroup_pos = veaf.getAvgGroupPos(groupName)
                local distance = math.sqrt((dcsGroup_pos.x-defended_pos.x)^2+(dcsGroup_pos.z-defended_pos.z)^2)
                veaf.loggers.get(veafSkynet.Id):trace(string.format("Distance between requested site and pointDefense : %s", veaf.p(distance)))

                if distance > veafSkynet.MaxPointDefenseDistanceFromSite then
                    defended_name = nil
                    veaf.loggers.get(veafSkynet.Id):info("User requested SAM Site out of reach for point defense")
                end
            else
                defended_name = nil
            end 
        end
    end

    local dcsGroupName = dcsGroup:getName()

    for _, dcsUnit in pairs(dcsGroup:getUnits()) do
        local dcsUnitName = dcsUnit:getName()
        local dcsUnitType = dcsUnit:getTypeName()
        
        local addedSite = nil
        local sLog = string.format(" - iads[%s] unit[%s][%d][%s] group[%s][%d]", iads.name, dcsUnitName, dcsUnit:getID(), dcsUnitType, dcsGroupName, dcsGroup:getID())
    
        if(not veafSkynet.iadsEwrUnitsTypes[dcsUnitType] and not veafSkynet.iadsSamUnitsTypes[dcsUnitType]) then
            veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => unit type is not eligible for skynet")
        elseif(alreadyAddedGroups[groupName]) then
            veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => group as already been marked as added by veafSkynet")
            break -- if group is added no need to go check the other units
        else
            local bShouldBeAdded = true
            local sams = iads:getSAMSites()
            for i = 1, #sams do
                local sam = sams[i]
                if (sam.dcsName == dcsGroupName) then
                    veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => already in network as SAM group")
                    bShouldBeAdded = false
                    break
                end
            end
        
            local ewrs = iads:getEarlyWarningRadars()
            for i = 1, #ewrs do
                local ewr = ewrs[i]
                local dcsGroupEwr = veafSkynet.getDcsGroupFromSkynetElement(ewr)

                if (not dcsGroupEwr) then
                    veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => group not found for skynet ewr " .. veafSkynet.getStringSkynetElement(ewr))
                elseif (dcsGroupEwr:getName() == dcsGroupName) then
                    veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => already in network as EWR group")
                    bShouldBeAdded = false
                    break
                end
            end

            if (not bShouldBeAdded) then
                break -- if group is added no need to go check the other units
            end

            if(veafSkynet.iadsSamUnitsTypes[dcsUnitType]) then
                local samsite = iads:addSAMSite(groupName)
                if (samsite) then
                    addedSite = samsite
                    didSomething = true
                    alreadyAddedGroups[groupName] = true
                    veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => added as SAM")
                end
            end

            local ewrFlag = false
            if (addedSite == nil and veafSkynet.iadsEwrUnitsTypes[dcsUnitType]) then
                local ewr = iads:addEarlyWarningRadar(dcsUnitName)
                if (ewr) then 
                    addedSite = ewr
                    didSomething = true
                    ewrFlag = true
                    veaf.loggers.get(veafSkynet.Id):trace(sLog .. " => added as EWR")
                end
            end

            --user requested configuration
            if addedSite and pointDefense and not(forceEwr) and not(ewrFlag) then
                if defended_name then
                    local text = string.format("Point Defense added to site : %s", string.format(defended_name))
                    veaf.loggers.get(veafSkynet.Id):info(text)
                    if not silent then trigger.action.outText(text,10) end
                    if iads:getSAMSiteByGroupName(defended_name) then
                        veaf.loggers.get(veafSkynet.Id):trace(string.format("adding pointDefense to SAM -> OK"))
                        iads:getSAMSiteByGroupName(defended_name):addPointDefence(addedSite)
                    else
                        veaf.loggers.get(veafSkynet.Id):trace(string.format("adding pointDefense to EWR -> OK"))
                        iads:getEarlyWarningRadars(defended_name):addPointDefence(addedSite)

                        --confirm that the addition of point defenses to the EWR which is gathered through it's group name but only the unit name is stored in the structure
                        --local site_info = iads:getEarlyWarningRadars(defended_name)
                        --veaf.loggers.get(veafSkynet.Id):debug(string.format("Recovered EWR name : %s", veaf.p(site_info[1].dcsName)))
                        --local pointDefenses = site_info[1].pointDefences
                        --veaf.loggers.get(veafSkynet.Id):debug(string.format("Recover pointDefense name : %s", veaf.p(pointDefenses[#pointDefenses].dcsName)))
                    end
                else
                    veaf.loggers.get(veafSkynet.Id):info("Could not find SAM site within range to add point defenses to")
                    if not silent then trigger.action.outText("Could not find SAM site within range to add point defenses to", 15) end
                end
            elseif forceEwr then
                veaf.loggers.get(veafSkynet.Id):trace(string.format("SAM/EWR is forced EWR"))

                if addedSite then
                    veaf.loggers.get(veafSkynet.Id):trace("Unit Forced as EWR")
                    addedSite:setActAsEW(true)
                end
            end

            if (addedSite) then
                break -- if something has been added for this group no need to check the remaining units
            end
        end
    end

    if didSomething then
        if not(batchMode) and not(forceEwr) and not(pointDefense) then
            -- specific configurations, for each SAM type
            veaf.loggers.get(veafSkynet.Id):trace("Specific configuration applied")

            iads:getSAMSitesByNatoName('SA-10'):setActAsEW(false)
            iads:getSAMSitesByNatoName('SA-6'):setActAsEW(false)
            iads:getSAMSitesByNatoName('SA-5'):setActAsEW(false)
            iads:getSAMSitesByNatoName('Patriot'):setActAsEW(false)
            iads:getSAMSitesByNatoName('Hawk'):setActAsEW(false)
        end       

        -- reactivate (rebuild coverage) the IADS
        veaf.loggers.get(veafSkynet.Id):trace("reactivate (rebuild coverage) the IADS")
        veafSkynet.delayedActivate(networkName)

        --add the added site to the structure of the network it was added to
        veafSkynet.structure[networkName].groups[groupName] = { forceEwr = forceEwr, pointDefense = defended_name }
    end

    return didSomething
end

local function initializeIADS(networkName, coa, inRadio, debug)
    local iads = veafSkynet.getIadsOfCoalition(networkName, coa)
    if not(iads) then
        veaf.loggers.get(veafSkynet.Id):trace(string.format("IADS named %s for coalition %s does not exist", veaf.p(networkName), veaf.p(coa)))
        return false
    end
    veaf.loggers.get(veafSkynet.Id):trace(string.format("initializeIADS %s",tostring(iads:getCoalitionString())))

    if debug then
        veaf.loggers.get(veafSkynet.Id):debug("adding debug information")
        local iadsDebug = iads:getDebugSettings()
        iadsDebug.IADSStatus = true
        iadsDebug.radarWentDark = true -- FG iadsDebug.samWentDark = true
        iadsDebug.contacts = true
        iadsDebug.radarWentLive = true
        iadsDebug.noWorkingCommmandCenter = false
        iadsDebug.ewRadarNoConnection = false
        iadsDebug.samNoConnection = false
        iadsDebug.jammerProbability = true
        iadsDebug.addedEWRadar = true
        iadsDebug.hasNoPower = false
        iadsDebug.harmDefence = true
        iadsDebug.samSiteStatusEnvOutput = true
        iadsDebug.earlyWarningRadarStatusEnvOutput = true
    end

    local alreadyAddedGroups = {}
    local dcsGroups = coalition.getGroups(coa)
    for _, dcsGroup in pairs(dcsGroups) do
        if veafSkynet.structure[networkName] then
            local groupName = dcsGroup:getName()
            if groupName then
                local structureData = veafSkynet.structure[networkName].groups[groupName]
                local forceEwr = false
                local pointDefense = false
                if structureData then
                    if structureData.forceEwr then
                        forceEwr = structureData.forceEwr
                    end
                    if structureData.pointDefense then
                        pointDefense = structureData.pointDefense
                    end
                end
                if veafSkynet.loadAllAtInit[tostring(coa)] or structureData then 
                    veafSkynet.addGroupToNetwork(networkName, dcsGroup, forceEwr, pointDefense, alreadyAddedGroups, true)
                end 
            end 
        end
    end

    if veafSkynet.loadAllAtInit[tostring(coa)] then
        veafSkynet.loadAllAtInit[tostring(coa)] = false
    end

    veaf.loggers.get(veafSkynet.Id):trace("Specific configuration applied")
    -- specific configurations, for each SAM type
    iads:getSAMSitesByNatoName('SA-10'):setActAsEW(false)
    iads:getSAMSitesByNatoName('SA-6'):setActAsEW(false)
    iads:getSAMSitesByNatoName('SA-5'):setActAsEW(false)
    iads:getSAMSitesByNatoName('Patriot'):setActAsEW(false)
    iads:getSAMSitesByNatoName('Hawk'):setActAsEW(false)

    veafSkynet.initializePointDefences(veafSkynet.getNetwork(networkName)) -- Management of point defences (Flogas) - initialization

    if inRadio then
        --activate the radio menu to toggle IADS Status output
        iads:addRadioMenu()
    end

    --activate (build coverage) the IADS
	veaf.loggers.get(veafSkynet.Id):debug("activate (build coverage) the IADS")
    veafSkynet.delayedActivate(networkName)
end

local function createNetwork(networkName, coa, loadUnits, UserAdd)
   
    local UserAdd = UserAdd or false
    local loadUnits = loadUnits or false

    local networkName = networkName
    if networkName then 
        networkName = tostring(networkName) 
    else
        veaf.loggers.get(veafSkynet.Id):error("networkName is of invalid format")
        return false
    end
    local coa = coa
    if coa then 
        coa = tonumber(coa) 
    else
        veaf.loggers.get(veafSkynet.Id):error("Coalition specified is of invalid format")
        return false
    end
    veaf.loggers.get(veafSkynet.Id):trace("networkName= %s", veaf.p(networkName))
    veaf.loggers.get(veafSkynet.Id):trace("CoalitionID= %s", veaf.p(coa))
    veaf.loggers.get(veafSkynet.Id):trace("loadUnits= %s", veaf.p(loadUnits))
    veaf.loggers.get(veafSkynet.Id):trace("UserAdd= %s", veaf.p(UserAdd))

    if networkName and coa then
        if (UserAdd and not veafSkynet.structure[networkName]) or not UserAdd then
            local debugFlag = veafSkynet.debugBlue
            local includeInRadio = veafSkynet.includeBlueInRadio

            if coa == coalition.side.RED then
                debugFlag = veafSkynet.debugRed
                includeInRadio = veafSkynet.includeRedInRadio
            end

            veaf.loggers.get(veafSkynet.Id):trace("creating network...")
            local iads = SkynetIADS:create(networkName)
            iads.coalitionID = coa
            if iads then
                if not veafSkynet.structure[networkName] then
                    veaf.loggers.get(veafSkynet.Id):trace("network is new")
                    veafSkynet.structure[networkName] = {}
                    veafSkynet.structure[networkName].coalitionID = coa
                    veafSkynet.structure[networkName].includeInRadio = includeInRadio
                    veafSkynet.structure[networkName].debugFlag = debugFlag
                    veafSkynet.structure[networkName].groups = {}
                end
                veafSkynet.structure[networkName].iads = iads

                if veaf.loggers.get(veafSkynet.Id):wouldLogTrace() then
                    veaf.loggers.get(veafSkynet.Id):trace("Stored structure for network named %s :", veaf.p(networkName))
                    for index,_ in pairs(veafSkynet.structure[networkName]) do
                        veaf.loggers.get(veafSkynet.Id):trace("-> %s", veaf.p(index))
                    end
                    veaf.loggers.get(veafSkynet.Id):trace("Stored IADS structure for network named %s :", veaf.p(networkName))
                    for index,_ in pairs(veafSkynet.structure[networkName].iads) do
                        veaf.loggers.get(veafSkynet.Id):trace("-> %s", veaf.p(index))
                    end
                    veaf.loggers.get(veafSkynet.Id):trace("CoalitionID for network named %s :", veaf.p(networkName))
                    veaf.loggers.get(veafSkynet.Id):trace("-> %s", veaf.p(veafSkynet.structure[networkName].iads.coalitionID))
                    veaf.loggers.get(veafSkynet.Id):trace("-> %s", veaf.p(veafSkynet.structure[networkName].iads:getCoalitionString()))
                end

                if loadUnits then
                    initializeIADS(networkName, coa, includeInRadio, debugFlag)
                end
                return true
            end
        else
            local text = string.format("The network name \"%s\" already exists", veaf.p(networkName))
            veaf.loggers.get(veafSkynet.Id):info(text)
        end
    end
    return false
end

-- reset an IADS network, useful when many additions are made at once to harmonize the structure
function veafSkynet.reinitializeNetwork(networkName)
    if not veafSkynet.initialized then 
        return false 
    end

    if networkName and veafSkynet.structure[networkName] then
        local networkStructure = veafSkynet.structure[networkName]
        if networkStructure.iads then
            veaf.loggers.get(veafSkynet.Id):trace("Stored structure for network named %s has IADS, deactivating", veaf.p(networkName))
            if networkStructure.includeInRadio then 
                veaf.loggers.get(veafSkynet.Id):trace("Removing radio menu...")
                networkStructure.iads:removeRadioMenu()
            end
            networkStructure.iads:deactivate()
        end
        createNetwork(networkName, networkStructure.coalitionID, true)
    end
end

-- reset an IADS networks, useful when many additions/destructions are made at once to harmonize the structures on the skynet side
function veafSkynet.reinitialize()
    if not veafSkynet.initialized then 
        return false 
    end

    for networkName,_ in pairs(veafSkynet.structure) do
        veafSkynet.reinitializeNetwork(networkName)
    end
end

function veafSkynet.initialize(includeRedInRadio, debugRed, includeBlueInRadio, debugBlue)
    veaf.loggers.get(veafSkynet.Id):info(string.format("initializing Skynet in %s seconds", tostring(veafSkynet.DelayForStartup)))
    mist.scheduleFunction(veafSkynet._initialize,{includeRedInRadio, debugRed, includeBlueInRadio, debugBlue}, timer.getTime()+veafSkynet.DelayForStartup)
end

function veafSkynet._initialize(includeRedInRadio, debugRed, includeBlueInRadio, debugBlue)
    veafSkynet.includeRedInRadio = includeRedInRadio or false
    veafSkynet.debugRed = debugRed or false
    veafSkynet.includeBlueInRadio = includeBlueInRadio or false
    veafSkynet.debugBlue = debugBlue or false

    veaf.loggers.get(veafSkynet.Id):info("Initializing module")
    
    veaf.loggers.get(veafSkynet.Id):debug(string.format("includeRedInRadio=%s",veaf.p(includeRedInRadio)))
    veaf.loggers.get(veafSkynet.Id):debug(string.format("debugRed=%s",veaf.p(debugRed)))
    veaf.loggers.get(veafSkynet.Id):debug(string.format("includeBlueInRadio=%s",veaf.p(includeBlueInRadio)))
    veaf.loggers.get(veafSkynet.Id):debug(string.format("debugBlue=%s",veaf.p(debugBlue)))
    
    -- prepare the list of units supported by Skynet IADS
    for _, groupData in pairs(SkynetIADS.database) do
        for _, listName in pairs({ "searchRadar", "trackingRadar", "launchers", "misc" }) do
            if groupData['type'] ~= 'ewr' then
                local list = groupData[listName]
                if list then
                    for unitType, _ in pairs(list) do
                        veaf.loggers.get(veafSkynet.Id):trace(string.format("-> SAM"))
                        veafSkynet.iadsSamUnitsTypes[unitType] = true
                    end
                end
            end
        end
    end
    veaf.loggers.get(veafSkynet.Id):trace(string.format("veafSkynet.iadsSamUnitsTypes=%s",veaf.p(veafSkynet.iadsSamUnitsTypes)))
    
    -- add EWR-capable units
    for _, unit in pairs(dcsUnits.DcsUnitsDatabase) do
        if unit then
            veaf.loggers.get(veafSkynet.Id):trace(string.format("testing unit %s",veaf.p(unit.type)))
            if unit.attribute then
                veaf.loggers.get(veafSkynet.Id):trace(string.format("unit.attribute = %s",veaf.p(unit.attribute)))
                if (unit.attribute["SAM SR"]) then
                    veafSkynet.iadsEwrUnitsTypes[unit.type] = true
                    veaf.loggers.get(veafSkynet.Id):trace(string.format("-> EWR"))
                elseif (unit.attribute["EWR"]) then
                    veafSkynet.iadsEwrUnitsTypes[unit.type] = true
                    veaf.loggers.get(veafSkynet.Id):trace(string.format("-> EWR"))
                elseif (unit.attribute["AWACS"]) then
                    veafSkynet.iadsEwrUnitsTypes[unit.type] = true
                    veaf.loggers.get(veafSkynet.Id):trace(string.format("-> EWR"))
                end
            end
        end
    end
    veaf.loggers.get(veafSkynet.Id):trace(string.format("veafSkynet.iadsEwrUnitsTypes=%s",veaf.p(veafSkynet.iadsEwrUnitsTypes)))
    

    veaf.loggers.get(veafSkynet.Id):info("Creating IADS for BLUE")
    createNetwork(veafSkynet.defaultIADS[tostring(coalition.side.BLUE)], coalition.side.BLUE, true)

    veaf.loggers.get(veafSkynet.Id):info("Creating IADS for RED")
    createNetwork(veafSkynet.defaultIADS[tostring(coalition.side.RED)], coalition.side.RED, true)

    veafSkynet.initialized = true

    if (veafSkynet.CommandCentersPreinitialize and #veafSkynet.CommandCentersPreinitialize > 0) then
        for _, commandCenter in pairs(veafSkynet.CommandCentersPreinitialize) do
            veafSkynet.addCommandCenterOfCoalition(commandCenter.CoalitionId, commandCenter.CommandCenterName) 
        end

        veafSkynet.CommandCentersPreinitialize = {}
    end
    
    if(veafSkynet.DynamicSpawn) then
        veafSkynet.monitorDynamicSpawn(true)
    end

    veaf.loggers.get(veafSkynet.Id):info(string.format("Skynet IADS has been initialized"))
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Command centers and network deactivation
-------------------------------------------------------------------------------------------------------------------------------------------------------------
veafSkynet.CommandCentersPreinitialize = {} -- this is to memorize the command centers requested for a coalition, if the network of the coalition does not exist yet

function veafSkynet.addCommandCenter(veafSkynetNetwork, sCommandCenterName)
    local iads = veafSkynetNetwork.iads
    
    local dcsCommandCenterObject = StaticObject.getByName(sCommandCenterName)
    if (not dcsCommandCenterObject) then
        dcsCommandCenterObject = Unit.getByName(sCommandCenterName)
    end
    if (not dcsCommandCenterObject) then
        local dcsCommandCenterGroup = Group.getByName(sCommandCenterName)
        if (dcsCommandCenterGroup) then
            dcsCommandCenterObject = dcsCommandCenterGroup:getUnit(1)
        end
    end

    if (not dcsCommandCenterObject) then
        veaf.loggers.get(veafSkynet.Id):error("Requested command center not found: " .. sCommandCenterName)
        return
    end
    
    iads:addCommandCenter(dcsCommandCenterObject)
    veaf.loggers.get(veafSkynet.Id):trace("Command center unit added [" .. sCommandCenterName .. "]")
    iads:buildRadarCoverage() -- as command center is added after the network initialisation, coverage should be rebuilt
end

function veafSkynet.addCommandCenterOfCoalition(iCoalitionId, sCommandCenterName)
    if (veafSkynet.initialized) then
        local veafSkynetNetwork = veafSkynet.getNetwork(veafSkynet.defaultIADS[tostring(iCoalitionId)])

        if (veafSkynetNetwork == nil) then
            veaf.loggers.get(veafSkynet.Id):error("Veaf skynet network not found. Please ensure that veafSkynetIadsHelper has been initialized for coalition [" .. iCoalitionId .. "]")
            return    
        end
        
        veafSkynet.addCommandCenter(veafSkynetNetwork, sCommandCenterName)  
    else
        veaf.loggers.get(veafSkynet.Id):trace("Veaf skynet not initialized. Command center [" .. sCommandCenterName .. "] stored to be added later for [" .. iCoalitionId .. "]")
        table.insert (veafSkynet.CommandCentersPreinitialize, {CoalitionId = iCoalitionId, CommandCenterName = sCommandCenterName})
    end
end

function veafSkynet.destroyCommandCenters(veafSkynetNetwork, iExplosionStrength)
    iExplosionStrength = iExplosionStrength or 200 -- default explosion may not be enough to destroy certain bunkers
    local iads = veafSkynetNetwork.iads

    if (not iads:isCommandCenterUsable()) then
        veaf.loggers.get(veafSkynet.Id):trace("Network has no usable command center")
        return
    end

    local ccs = iads:getCommandCenters()

    for i = 1, #ccs do
        local cc = ccs[i]

        local dcsObject = cc.dcsRepresentation

        if (dcsObject:isExist()) then
            local category = getmetatable(dcsObject)
            if (category == Group) then
                for _, dcsUnit in pairs(dcsObject:getUnits()) do
                    veaf.loggers.get(veafSkynet.Id):trace("Command center unit exploded: " .. dcsUnit:getName())
                    trigger.action.explosion(dcsUnit:getPosition().p, iExplosionStrength)
                end
            else
                veaf.loggers.get(veafSkynet.Id):trace("Command center unit exploded: " .. dcsObject:getName())
                trigger.action.explosion(dcsObject:getPosition().p, iExplosionStrength)
            end
        end
    end
end

function veafSkynet.destroyCommandCentersOfCoalition(iCoalitionId, iExplosionStrength)
    local veafSkynetNetwork = veafSkynet.getNetwork(veafSkynet.defaultIADS[tostring(iCoalitionId)])
    veafSkynet.destroyCommandCenters(veafSkynetNetwork, iExplosionStrength)
end

function veafSkynet.deactivateNetwork(veafSkynetNetwork, elementStates)
    local elementState = elementStates or veafSkynet.SkynetElementStates.Live
    local iads = veafSkynetNetwork.iads

    local sElementState = "live"
    if (elementState == veafSkynet.SkynetElementStates.Autonomous) then
        sElementState = "autonomous"
    elseif (elementState == veafSkynet.SkynetElementStates.Dark) then
        sElementState = "dark"
    end
    veaf.loggers.get(veafSkynet.Id):trace("Deactivating network " .. iads:getCoalitionString() .. ". Network elements will go " .. sElementState)

    local function setGroupState(skynetElement)
        skynetElement:finishHarmDefence()
        skynetElement:cleanUp()

        veafSkynet.removePointDefencesFromSkynetElement(skynetElement)
        if (elementState == veafSkynet.SkynetElementStates.Autonomous) then
            skynetElement:resetAutonomousState()
            skynetElement:goAutonomous()
        elseif (elementState == veafSkynet.SkynetElementStates.Dark) then
            skynetElement:goDark() -- goDark will not always turn the radar off - eg an EWR that is tracking targets will stay on
        else
            skynetElement:goLive() -- goLive will not always turn the radar on - eg a SAM site out of ammo will stay off
        end
    end

    veafSkynet.monitorDynamicSpawn(false)
    iads:deactivate()

    local ewrs = iads:getEarlyWarningRadars()
    for i = 1, #ewrs do
        local ewr = ewrs[i]
        setGroupState(ewr)
    end
    local sams = iads:getSAMSites()
    for i = 1, #sams do
        local sam = sams[i]
        setGroupState(sam)        
    end

    -- Copying the elements and emptying the Skynet lists before doing the state switch to ensure that Skynet does not keep controlling the elements after deactivation.
    -- Does not seem necessary after all, and keeping the lists in the Skynet network allows to reactivate it later if needed.
    -- Note that as it is, reactivating will not rebuild point defences, so it is more of a testing thing still.
    -- Code kept here just in case.
    --[[
    local skynetGroups = {}
    for i = 1, #iads.earlyWarningRadars do
        table.insert(skynetGroups, iads.earlyWarningRadars[i])
    end
    for i = 1, #iads.samSites do
        table.insert(skynetGroups, iads.samSites[i])
    end
    
    iads.earlyWarningRadars = {}
    iads.samSites = {}

    for i = 1, #skynetGroups do
        local skynetGroup = skynetGroups[i]
        setGroupState(skynetGroup)
    end
    ]]
end

function veafSkynet.deactivateNetworkOfCoalition(iCoalitionId, elementStates)
    local veafSkynetNetwork = veafSkynet.getNetwork(veafSkynet.defaultIADS[tostring(iCoalitionId)])
    veafSkynet.deactivateNetwork(veafSkynetNetwork, elementStates)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Load module
-------------------------------------------------------------------------------------------------------------------------------------------------------------
veaf.loggers.get(veafSkynet.Id):info(string.format("Loading version %s", veafSkynet.Version))
