------------------------------------------------------------------
-- VEAF combat mission functions for DCS World
-- By zip (2020)
--
-- Features:
-- ---------
-- * A combat mission consists in spawning enemy aircrafts
-- * It also contains a mass briefing, optional objectives (timed, number of kills, ...) and can trigger the activation of one or more combat zones
-- * For each mission, a specific radio sub-menu is created, allowing common actions (get mission status, weather, briefing, start and stop the mission, etc.)
--
-- See the documentation : https://veaf.github.io/documentation/
------------------------------------------------------------------

veafCombatMission = {}

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Global settings. Stores the script constants
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Identifier. All output in DCS.log will start with this.
veafCombatMission.Id = "COMBATMISSION"

--- Version.
veafCombatMission.Version = "2.2.0"

-- trace level, specific to this module
--veafCombatMission.LogLevel = "trace"

veaf.loggers.new(veafCombatMission.Id, veafCombatMission.LogLevel)

--- Number of seconds between each check of the watchdog function
veafCombatMission.SecondsBetweenWatchdogChecks = 30

veafCombatMission.RadioMenuName = "MISSIONS"

veafCombatMission.MinimumSpacingBetweenClones = 300 -- minimum spawn distance between clones of a group

veafCombatMission.RemoteCommandParser = "([[a-zA-Z0-9]+)%s?([^%s]*)%s?(.*)"

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Do not change anything below unless you know what you are doing!
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Radio menus paths
veafCombatMission.rootPath = nil

-- Missions list (table of VeafCombatMission objects)
veafCombatMission.missionsList = {}

-- Missions dictionary (map of VeafCombatMission objects by mission name)
veafCombatMission.missionsDict = {}

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Utility methods
-------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- VeafCombatMissionObjective object
-------------------------------------------------------------------------------------------------------------------------------------------------------------
VeafCombatMissionObjective = {}

VeafCombatMissionObjective.FAILED = -1
VeafCombatMissionObjective.SUCCESS = 1
VeafCombatMissionObjective.NOTHING = 0

function VeafCombatMissionObjective:new(objectToCopy)
    local objectToCreate = objectToCopy or {} -- create object if user does not provide one
    setmetatable(objectToCreate, self)
    self.__index = self

    -- init the new object

    -- technical name
    objectToCreate.name = nil
    -- description for the briefing
    objectToCreate.description = nil
    -- message when the objective is completed
    objectToCreate.message = nil
    -- parameters
    objectToCreate.parameters = {}
    -- function that is call when the mission starts
    objectToCreate.onStartupFunction = nil
    -- function that is called when the completion check watchdog runs (should check for objective completion and return one of the FAILED, SUCCESS or NOTHING constants)
    objectToCreate.onCheckFunction = nil

    return objectToCreate
end

function VeafCombatMissionObjective:copy()
    local copy = VeafCombatMissionObjective:new()

    -- copy the attributes
    copy.name = self.name
    copy.description = self.description
    copy.onStartupFunction = self.onStartupFunction
    copy.onCheckFunction = self.onCheckFunction

    -- deep copy the collections
    copy.parameters = {}
    for name, value in pairs(self.parameters) do
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("copying parameter %s : ",tostring(name)))
        copy.parameters[name]=value
    end

    return copy
end

---
--- setters and getters
---

function VeafCombatMissionObjective:setName(value)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective.setName([%s])",value or ""))
    self.name = value
    return self
end

function VeafCombatMissionObjective:getName()
    return self.name
end

function VeafCombatMissionObjective:setDescription(value)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].setDescription([%s])", self:getName() or "", value or ""))
    self.description = value
    return self
end

function VeafCombatMissionObjective:getDescription()
    return self.description
end

function VeafCombatMissionObjective:setMessage(value)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].setMessage([%s])", self:getName() or "", value or ""))
    self.message = value
    return self
end

function VeafCombatMissionObjective:getMessage()
    return self.message
end

function VeafCombatMissionObjective:setParameters(value)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].setParameters([%s])", self:getName() or "", veaf.p(value or "")))
    self.parameters = value
    return self
end

function VeafCombatMissionObjective:getParameters()
    return self.parameters
end

function VeafCombatMissionObjective:setOnCheck(value)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].setOnCheck(some function)",self:getName()))
    self.onCheckFunction = value
    return self
end

function VeafCombatMissionObjective:getOnCheck()
    return self.onCheckFunction
end

function VeafCombatMissionObjective:setOnStartup(value)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].setOnStartup(some function)", self:getName()))
    self.onStartupFunction = value
    return self
end

function VeafCombatMissionObjective:getOnStartup()
    return self.onStartupFunction
end

---
--- other methods
---

function VeafCombatMissionObjective:onCheck(mission)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].onCheck([%s])", self:getName() or "", mission:getName() or ""))
    if self.onCheckFunction then
        return self.onCheckFunction(mission, self.parameters)
    else
        return VeafCombatMissionObjective.NOTHING
    end
end

function VeafCombatMissionObjective:onStartup(mission)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].onStartup([%s])", self:getName() or "", mission:getName() or ""))
    if self.onStartupFunction then
        return self.onStartupFunction(self.parameters)
    end
end

function VeafCombatMissionObjective:configureAsTimedObjective(timeInSeconds)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].configureAsTimedObjective()",self:getName()))

    local function onCheck(mission, parameters)
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective.NewTimedObjective.onCheck()"))
        local timeout = parameters.timeout
        local startTime = parameters.startTime
        if timer.getTime() > startTime + timeout then
            return VeafCombatMissionObjective.FAILED
        else
            return VeafCombatMissionObjective.NOTHING
        end
    end

    return self
        :setParameters({timeout=timeInSeconds})
        :setOnStartup(
            function(parameters)
                parameters["startTime"] = timer.getTime()
            end
        )
        :setOnCheck(onCheck)
end

function VeafCombatMissionObjective:configureAsKillEnemiesObjective(nbKillsToWin, whatsInAKill)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].configureAsKillEnemiesObjective()",self:getName()))

    local function onCheck(mission, parameters)
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective.configureAsKillEnemiesObjective.onCheck()"))
        if mission:isActive() then
            local nbKillsToWin = parameters.nbKillsToWin
            local whatsInAKill = parameters.whatsInAKill
            veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbKillsToWin = %d",nbKillsToWin))
            veaf.loggers.get(veafCombatMission.Id):trace(string.format("whatsInAKill = %d",whatsInAKill))

            local nbLiveUnits, nbDamagedUnits, nbDeadUnits = mission:getRemainingEnemies(whatsInAKill)

            veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbLiveUnits = %d",nbLiveUnits))
            veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbDamagedUnits = %d",nbDamagedUnits))
            veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbDeadUnits = %d",nbDeadUnits))

            if (nbKillsToWin == -1 and nbLiveUnits == 0) or (nbKillsToWin >= 0 and nbDeadUnits >= nbKillsToWin) then
                -- objective is achieved
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("objective is achieved"))
                local msg = string.format(self:getMessage(), nbDeadUnits)
                if not mission:isSilent() then
                    trigger.action.outText(msg, 15)
                end
                return VeafCombatMissionObjective.SUCCESS
            else
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("objective is NOT achieved"))
                return VeafCombatMissionObjective.NOTHING
            end
        end
        return VeafCombatMissionObjective.NOTHING
    end

    return self
            :setParameters({nbKillsToWin=nbKillsToWin or -1, whatsInAKill=whatsInAKill or 0})
            :setOnCheck(onCheck)
end

function VeafCombatMissionObjective:configureAsPreventDestructionOfSceneryObjectsInZone(zones, objects)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective[%s].configureAsPreventDestructionOfSceneryObjectsInZone()",self:getName()))

    local function onCheck(mission, parameters)
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("VeafCombatMissionObjective.configureAsPreventDestructionOfSceneryObjectsInZone.onCheck()"))
        if mission:isActive() then

            local zones = parameters.zones
            local objects = parameters.objects
            local failed = false
            local killedObjectsNames = nil

            local killedObjects = mist.getDeadMapObjsInZones(zones)
            ----veaf.loggers.get(veafCombatMission.Id):trace(veaf.serialize("killedObjects", killedObjects))

            for _, object in pairs(killedObjects) do
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("checking id_ = [%s]", object.object.id_))
                if objects[object.object.id_] then
                    veaf.loggers.get(veafCombatMission.Id):trace(string.format("found [%s]", objects[object.object.id_]))
                    if killedObjectsNames then
                        killedObjectsNames = killedObjectsNames .. ", " .. objects[object.object.id_]
                    else
                        killedObjectsNames = objects[object.object.id_]
                    end
                    failed = true
                end
            end

            if failed then
                -- objective is failed
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("objective is failed"))
                local msg = string.format(self:getMessage(), killedObjectsNames)
                if not mission:isSilent() then
                    trigger.action.outText(msg, 15)
                end
                return VeafCombatMissionObjective.FAILED
            else
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("objective is NOT failed"))
                return VeafCombatMissionObjective.NOTHING
            end
        end
        return VeafCombatMissionObjective.NOTHING
    end

    return self
            :setParameters({zones=zones, objects=objects})
            :setOnCheck(onCheck)
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- VeafCombatMissionElement object
-------------------------------------------------------------------------------------------------------------------------------------------------------------
VeafCombatMissionElement = {}

function VeafCombatMissionElement:new(objectToCopy)
    local objectToCreate = objectToCopy or {} -- create object if user does not provide one
    setmetatable(objectToCreate, self)
    self.__index = self

    -- init the new object

    -- name
    objectToCreate.name = nil
    -- groups : a list of group names that compose this element
    objectToCreate.groups = {}
    -- skill ("Average", "Good", "High", "Excellent" or "Random"), defaults to "Random"
    objectToCreate.skill = "Random" -- SPAWN:InitSkill(Skill)
    -- spawn radius in meters (randomness introduced in the respawn mechanism)
    objectToCreate.spawnRadius = 0 -- SPAWN:InitRandomizePosition
    -- spawn chance in percent (xx chances in 100 that the unit is spawned - or the command run)
    objectToCreate.spawnChance = 100
    -- the element can be multiplied to scale the mission
    objectToCreate.scale = 1
    -- if true, the element is scalable
    objectToCreate.scalable = true
    -- init tables (no need to init simple values)

    return objectToCreate
end

function VeafCombatMissionElement:copy()
    local copy = VeafCombatMissionElement:new()

    -- copy the attributes
    copy.name = self.name
    copy.skill = self.skill
    copy.spawnRadius = self.spawnRadius
    copy.spawnChance = self.spawnChance
    copy.scale = self.scale
    copy.scalable = self.scalable

    -- deep copy the collections
    copy.groups = {}
    for _, group in pairs(self.groups) do
        table.insert(copy.groups, group)
    end
    copy.spawnPoints = {}
    for groupName, spawnPoint in pairs(self.spawnPoints) do
        copy.spawnPoints[groupName] = spawnPoint
    end

    return copy
end

---
--- setters and getters
---

function VeafCombatMissionElement:setName(value)
    self.name = value
    return self
end

function VeafCombatMissionElement:getName()
    return self.name
end

function VeafCombatMissionElement:setGroups(value)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMissionElement[%s]:setGroups(%s)",veaf.p(self.name), veaf.p(value)))
    self.groups = value
    self.spawnPoints = {}
    for _, groupName in pairs(self.groups) do
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("processing groupName=%s",veaf.p(groupName)))
        local _group = Group.getByName(groupName)
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("_group=%s",veaf.p(_group)))
        if _group then
            local _unit1 = _group:getUnit(1)
            veaf.loggers.get(veafCombatMission.Id):trace(string.format("_unit1=%s",veaf.p(_unit1)))
            if _unit1 then
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("_unit1:getPoint()=%s",veaf.p(_unit1:getPoint())))
                self.spawnPoints[groupName] = _unit1:getPoint()
            end
        end
    end
    return self
end

function VeafCombatMissionElement:getGroups()
    return self.groups
end

function VeafCombatMissionElement:setSkill(value)
    self.skill = value
    return self
end

function VeafCombatMissionElement:getSkill()
    return self.skill
end

function VeafCombatMissionElement:setSpawnRadius(value)
    self.spawnRadius = tonumber(value)
    return self
end

function VeafCombatMissionElement:getSpawnRadius()
    return self.spawnRadius
end

function VeafCombatMissionElement:setSpawnChance(value)
    self.spawnChance = tonumber(value)
    return self
end

function VeafCombatMissionElement:getSpawnChance()
    return self.spawnChance
end

function VeafCombatMissionElement:setScale(value)
    self.scale = tonumber(value)
    return self
end

function VeafCombatMissionElement:getScale()
    return self.scale
end

function VeafCombatMissionElement:setScalable(value)
    self.scalable = value
    return self
end

function VeafCombatMissionElement:isScalable()
    return self.scalable
end

---
--- other methods
---

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- VeafCombatMission object
-------------------------------------------------------------------------------------------------------------------------------------------------------------

VeafCombatMission = {
    -- mission name (technical)
    name = nil,
    -- mission name (human-friendly)
    friendlyName = nil,
    -- mission briefing
    briefing = nil,
    -- secured :  if true, the radio menu will be secured
    secured = false,
    -- list of objectives
    objectives = {},
    -- list of the elements defined in the mission
    elements = {},
    -- mission is active
    active = false,
    -- mission is a training mission
    training = false,
    -- DCS groups that have been spawned (for cleaning up later)
    spawnedGroups = {},
    --- Radio menus paths
    radioMarkersPath = nil,
    radioTargetInfoPath = nil,
    radioRootPath = nil,
    -- the watchdog function checks for mission objectives completion
    watchdogFunctionId = nil,
    -- if false, the mission will not appear in the radio menu
    radioMenuEnabled = false,
    -- if true, no message will be displayed when activating/deactivating the mission
    hidden = false,
    -- same as hidden but only valid for one activation of the mission (will be reset to *hidden* at next start)
    silent = false,
    spawnedUnitsCountByGroup = {},
    spawnedNamesIndex = {}
}

function VeafCombatMission:new(objectToCopy)
    local objectToCreate = objectToCopy or {} -- create object if user does not provide one
    setmetatable(objectToCreate, self)
    self.__index = self
    
    -- init the new object
    
    -- mission name (technical)
    objectToCreate.name = nil
    -- mission name (human-friendly)
    objectToCreate.friendlyName = nil
    -- mission briefing
    objectToCreate.briefing = nil
    -- secured :  if true, the radio menu will be secured
    objectToCreate.secured = false
    -- list of objectives
    objectToCreate.objectives = {}
    -- list of the elements defined in the mission
    objectToCreate.elements = {}
    -- mission is active
    objectToCreate.active = false
    -- mission is a training mission
    objectToCreate.training = false
    -- DCS groups that have been spawned (for cleaning up later)
    objectToCreate.spawnedGroups = {}
    --- Radio menus paths
    objectToCreate.radioMarkersPath = nil
    objectToCreate.radioTargetInfoPath = nil
    objectToCreate.radioRootPath = nil
    -- the watchdog function checks for mission objectives completion
    objectToCreate.watchdogFunctionId = nil
    -- if false, the mission will not appear in the radio menu
    objectToCreate.radioMenuEnabled = false
    -- if true, no message will be displayed when activating/deactivating the mission
    objectToCreate.hidden = false
    -- same as hidden but only valid for one activation of the mission (will be reset to *hidden* at next start)
    objectToCreate.silent = false
    objectToCreate.spawnedUnitsCountByGroup = {}
    objectToCreate.spawnedNamesIndex = {}

    return objectToCreate
end

function VeafCombatMission:copy(newSkill, newScale)
    local copy = VeafCombatMission:new()

    -- copy the attributes
    copy.name = self.name
    copy.friendlyName = self.friendlyName
    copy.secured = self.secured
    copy.briefing = self.briefing
    copy.active = self.active
    copy.training = self.training
    copy.hidden = self.hidden
    copy.radioMenuEnabled = self.radioMenuEnabled
    copy.silent = self.silent

    -- deep copy the collections
    for _, objective in pairs(self.objectives) do
        copy:addObjective(objective:copy())
    end
    for _, element in pairs(self.elements) do
        local elementCopy = element:copy()
        if element:isScalable() and newScale then
            elementCopy:setScale(newScale)
        end
        if newSkill then
            elementCopy:setSkill(newSkill)
        end
        copy:addElement(elementCopy)
    end

    return copy
end

---
--- setters and getters
---

function VeafCombatMission:setName(value)
    self.name = value
    return self
end

function VeafCombatMission:getName()
    return self.name
end

function VeafCombatMission:setSecured(value)
    self.secured = value
    return self
end

function VeafCombatMission:isSecured()
    return self.secured
end

function VeafCombatMission:getRadioMenuName()
    return self:getFriendlyName()
end

function VeafCombatMission:setFriendlyName(value)
    self.friendlyName = value
    return self
end

function VeafCombatMission:getFriendlyName()
    return self.friendlyName
end

function VeafCombatMission:setBriefing(value)
    self.briefing = value
    return self
end

function VeafCombatMission:getBriefing()
    return self.briefing
end

function VeafCombatMission:isActive()
    return self.active
end

function VeafCombatMission:setActive(value)
    self.active = value
    return self
end

function VeafCombatMission:isTraining()
    return self.training
end

function VeafCombatMission:setTraining(value)
    self.training = value
    return self
end

function VeafCombatMission:addElement(value)
    table.insert(self.elements, value)
    return self
end

function VeafCombatMission:addSpawnedGroup(group)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:addSpawnedGroup(%s)",self.name or "", group:getName() or ""))
    if not self.spawnedGroups then
        self.spawnedGroups = {}
    end
    table.insert(self.spawnedGroups, group)

    -- count units in group
    self.spawnedUnitsCountByGroup[group:getName()] = #group:getUnits()
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("%s units in group [%s]",tostring(self.spawnedUnitsCountByGroup[group:getName()]), tostring(group:getName())))

    return self
end

function VeafCombatMission:getSpawnedGroups()
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:getSpawnedGroups()",self.name or ""))
    for _, group in pairs(self.spawnedGroups) do
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("spawnedGroups[%s]",group:getName()))
    end
    return self.spawnedGroups
end

function VeafCombatMission:clearSpawnedGroups()
    self.spawnedGroups = {}
    return self
end

function VeafCombatMission:addObjective(objective)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:addObjective(%s)",self.name or "", objective:getName() or ""))
    if not self.objectives then
        self.objectives = {}
    end
    table.insert(self.objectives, objective)
    return self
end

function VeafCombatMission:isHidden()
    return self.hidden
end

function VeafCombatMission:setHidden(value)
    self.hidden = value
    return self
end

function VeafCombatMission:isSilent()
    return self.silent
end

function VeafCombatMission:setSilent(value)
    self.silent = value
    return self
end

function VeafCombatMission:isRadioMenuEnabled()
    return self.radioMenuEnabled
end

function VeafCombatMission:setRadioMenuEnabled(value)
    self.radioMenuEnabled = value
    return self
end

function VeafCombatMission:setAllElementsSkill(skill)
    for _, element in self.elements do
        element:setSkill(skill)
    end
    return self
end
---
--- other methods
---

function VeafCombatMission:scheduleWatchdogFunction()
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:scheduleWatchdogFunction()",self.name or ""))
    self.watchdogFunctionId = mist.scheduleFunction(veafCombatMission.CompletionCheck,{self.name},timer.getTime()+veafCombatMission.SecondsBetweenWatchdogChecks)
    return self
end

function VeafCombatMission:unscheduleWatchdogFunction()
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:unscheduleWatchdogFunction()",self.name or ""))
    if self.watchdogFunctionId then
        veaf.loggers.get(veafCombatMission.Id):debug(string.format("mist.removeFunction()"))
        mist.removeFunction(self.watchdogFunctionId)
        self.watchdogFunctionId = nil
    end
    return self
end

function VeafCombatMission:getObjectives()
    return self.objectives
end

function VeafCombatMission:addDefaultObjectives()
    -- TODO
    return self
end

function VeafCombatMission:initialize()
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:initialize()",self.name or ""))

    -- check parameters
    if not self.name then
        return self
    end
    if not self.friendlyName then
        self:setFriendlyName(self.name)
    end
    if #self.objectives == 0 then
        self:addDefaultObjectives()
    end

    -- refresh the radio menu
    self:updateRadioMenu()

    return self
end

function VeafCombatMission:getRemainingEnemiesString()
    local nbLiveUnits, nbDamagedUnits, nbDeadUnits = self:getRemainingEnemies()
    return string.format("%d alive (%d damaged), %d dead", nbLiveUnits, nbDamagedUnits, nbDeadUnits)
end

function VeafCombatMission:getRemainingEnemies(whatsInAKill)
    local whatsInAKill = whatsInAKill or 0.01
    local nbLiveUnits = 0
    local nbDamagedUnits = 0
    local nbDeadUnits = 0
    for _, group in pairs(self:getSpawnedGroups()) do
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("processing group [%s]",group:getName()))
        local groupLiveUnits = 0
        local groupDamagedUnits = 0
        if group and group:getUnits() then
            for _, unit in pairs(group:getUnits()) do
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("processing unit [%s]",unit:getName()))
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("veaf.getUnitLifeRelative(unit) = %f",veaf.getUnitLifeRelative(unit)))
                if veaf.getUnitLifeRelative(unit) == 1.0 then
                    veaf.loggers.get(veafCombatMission.Id):trace(string.format("unit[%s] is alive",unit:getName()))
                    groupLiveUnits = groupLiveUnits + 1
                elseif veaf.getUnitLifeRelative(unit) > whatsInAKill then
                    veaf.loggers.get(veafCombatMission.Id):trace(string.format("unit[%s] is damaged (%d %%)",unit:getName(), veaf.getUnitLifeRelative(unit)*100 ))
                    groupDamagedUnits = groupDamagedUnits + 1
                    groupLiveUnits = groupLiveUnits + 1
                else
                    veaf.loggers.get(veafCombatMission.Id):trace(string.format("unit[%s] is dead",unit:getName()))
                    -- should never come to that, Moose do not return dead units in getUnits()
                end
            end
        else
            groupLiveUnits = 0
        end
        local groupDeadUnits = (self.spawnedUnitsCountByGroup[group:getName()] or 0) - groupLiveUnits
        if groupDeadUnits < 0 then -- should never happen but who knows ? This is DCS !
            groupDeadUnits = 0
        end

        veaf.loggers.get(veafCombatMission.Id):trace(string.format("groupLiveUnits = %d",groupLiveUnits))
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("groupDamagedUnits = %d",groupDamagedUnits))
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("groupDeadUnits = %d",groupDeadUnits))

        nbLiveUnits = nbLiveUnits + groupLiveUnits
        nbDamagedUnits = nbDamagedUnits + groupDamagedUnits
        nbDeadUnits = nbDeadUnits + groupDeadUnits
    end

    veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbLiveUnits = %d",nbLiveUnits))
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbDamagedUnits = %d",nbDamagedUnits))
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("nbDeadUnits = %d",nbDeadUnits))

    return nbLiveUnits, nbDamagedUnits, nbDeadUnits
end

function VeafCombatMission:getInformation()
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:getInformation()",self.name or ""))
    local message =      "COMBAT MISSION "..self:getFriendlyName().." \n\n"
    if (self:getBriefing()) then
        message = message .. "BRIEFING: \n"
        message = message .. self:getBriefing()
        message = message .. "\n\n"
    end
    if (self:getObjectives() and #self:getObjectives() > 0) then
        message = message .. "OBJECTIVES: \n"
        for _, objective in pairs(self:getObjectives()) do
            message = message .. " - " .. objective:getDescription() .. "\n"
        end
        message = message .. "\n\n"
    end
    if self:isActive() then

        -- generate information dispatch
        message = message .. "ENEMIES : " ..self:getRemainingEnemiesString() .."\n"

        if self:isTraining() then
            -- TODO find the position of the enemies
        end

    else
        message = message .. "mission is not yet active."
    end

    return message
end

-- activate the mission
function VeafCombatMission:activate(silent)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:activate(%s)",self:getName(), tostring(silent)))

    -- don't start twice
    if self:isActive() then
        return nil
    end

    self:setActive(true)
    self:setSilent(self:isHidden() or silent)

    for _, missionElement in pairs(self.elements) do
        veaf.loggers.get(veafCombatMission.Id):debug(string.format("processing element [%s]",missionElement:getName()))
        local chance = math.random(0, 100)
        if chance <= missionElement:getSpawnChance() then
            -- spawn the element
            veaf.loggers.get(veafCombatMission.Id):debug(string.format("chance hit (%d <= %d)",chance, missionElement:getSpawnChance()))
            for _, groupName in pairs(missionElement:getGroups()) do
                local _spawnPoint = missionElement.spawnPoints[groupName]
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("_spawnPoint=%s",veaf.p(_spawnPoint)))
                local _spawnRadius = missionElement:getSpawnRadius()
                if (missionElement:getScale() > 1 and _spawnRadius < veafCombatMission.MinimumSpacingBetweenClones) then
                    _spawnRadius = veafCombatMission.MinimumSpacingBetweenClones
                end
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("_spawnRadius=%s",veaf.p(_spawnRadius)))

                local vars = {}
                vars.gpName = groupName
                vars.action = 'clone'
                vars.point = _spawnPoint
                vars.radius = _spawnRadius
                vars.disperse = false
                vars.route = mist.getGroupRoute(groupName, 'task')
                --veaf.loggers.get(veafCombatMission.Id):trace(string.format("vars=%s",veaf.p(vars)))

                for i=1,missionElement:getScale() do
                    if not self.spawnedNamesIndex[groupName] then
                        self.spawnedNamesIndex[groupName] = 1
                    else
                        self.spawnedNamesIndex[groupName] = self.spawnedNamesIndex[groupName] + 1
                    end
                    local spawnedGroupName = string.format("%s #%04d", groupName, self.spawnedNamesIndex[groupName])
                    veaf.loggers.get(veafCombatMission.Id):trace(string.format("spawnedGroupName=%s",veaf.p(spawnedGroupName)))
                    local _group = mist.teleportToPoint(vars, true)
                    if _group then
                        for _, unit in pairs(_group.units) do
                            unit.skill = missionElement:getSkill()
                        end
                    end
                    _group.groupName = spawnedGroupName
                    if _group then
                        for _, unit in pairs(_group.units) do
                            local unitName = unit.unitName
                            veaf.loggers.get(veafCombatMission.Id):trace(string.format("unitName=%s",veaf.p(unitName)))
                            if not self.spawnedNamesIndex[unitName] then
                                self.spawnedNamesIndex[unitName] = 1
                            else
                                self.spawnedNamesIndex[unitName] = self.spawnedNamesIndex[unitName] + 1
                            end
                            local spawnedUnitName = string.format("%s #%04d", unitName, self.spawnedNamesIndex[unitName])
                            unit.groupName = spawnedUnitName
                            veaf.loggers.get(veafCombatMission.Id):trace(string.format("spawnedUnitName=%s",veaf.p(spawnedUnitName)))
                        end
                    end
                    veaf.loggers.get(veafCombatMission.Id):trace(string.format("_group=%s",veaf.p(_group)))
                    local _spawnedGroup = mist.dynAdd(_group)
                    if _spawnedGroup then
                        veaf.loggers.get(veafCombatMission.Id):trace(string.format("_spawnedGroup.name=%s",veaf.p(_spawnedGroup.name)))
                        local _dcsSpawnedGroup = Group.getByName(_spawnedGroup.name)
                        veaf.loggers.get(veafCombatMission.Id):trace(string.format("_spawnedGroup.name=%s",veaf.p(_dcsSpawnedGroup:getName())))
                        for _, unit in pairs(_dcsSpawnedGroup:getUnits()) do
                            veaf.loggers.get(veafCombatMission.Id):trace(string.format("_spawnedGroup.unit.name=%s",veaf.p(unit:getName())))
                        end

                        self:addSpawnedGroup(_dcsSpawnedGroup)
                        -- add the group to the Hound Elint, if there is one
                        if veafHoundElint then
                            veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafHoundElint.addPlatformToSystem(%s)",veaf.p(_dcsSpawnedGroup:getName())))
                            veafHoundElint.addPlatformToSystem(_dcsSpawnedGroup)
                        end
                    end
                end
            end
        else
            veaf.loggers.get(veafCombatMission.Id):debug(string.format("chance missed (%d > %d)",chance, missionElement:getSpawnChance()))
        end
    end

    -- start all the objectives
    for _, objective in pairs(self.objectives) do
        objective:onStartup(self)
    end

    -- start the completion watchdog
    self:scheduleWatchdogFunction()

    -- refresh the radio menu
    self:updateRadioMenu()

    return self
end

-- desactivate the mission
function VeafCombatMission:desactivate()
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:desactivate()",self.name or ""))
    self:setActive(false)
    self:unscheduleWatchdogFunction()

    for _, group in pairs(self:getSpawnedGroups()) do
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("trying to destroy group [%s]",group:getName()))
        group:destroy()
    end
    self:clearSpawnedGroups()

    -- refresh the radio menu
    self:updateRadioMenu()

    return self
end

-- check if there are still units in mission
function VeafCombatMission:completionCheck()
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:completionCheck()",self.name or ""))

    VeafCombatMissionObjective.FAILED = -1
    VeafCombatMissionObjective.SUCCESS = 1
    VeafCombatMissionObjective.NOTHING = 0

    local reschedule = true

    -- check all the objectives
    for _, objective in pairs(self.objectives) do
        local result = objective:onCheck(self)
        if result == VeafCombatMissionObjective.FAILED then
            -- mission is failed
            local message = string.format([[
Objective not met : %s
The mission %s will now end.
You can replay by starting it again, in the radio menu.]], objective:getDescription(), self:getFriendlyName())
            if not self:isSilent() then
                trigger.action.outText(message, 15)
            end
            self:desactivate()
            reschedule = false
        elseif result == VeafCombatMissionObjective.SUCCESS then
            -- mission is won
            local message = string.format([[
All objectives were met !
The mission %s is a success ! It will now end.
You can replay by starting it again, in the radio menu.]], self:getFriendlyName())
            if not self:isSilent() then
                trigger.action.outText(message, 15)
            end
            self:desactivate()
            reschedule = false
        end
    end

    if reschedule then
        -- reschedule
        self:scheduleWatchdogFunction()
    end
end

-- updates the radio menu according to the mission state
function VeafCombatMission:updateRadioMenu(inBatch)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("VeafCombatMission[%s]:updateRadioMenu(%s)",self.name or "", tostring(inBatch)))

    -- do not update the radio menu for a mission that has no menu
    if not self:isRadioMenuEnabled() then
        return self
    end

    -- do not update the radio menu if not yet initialized
    if not veafCombatMission.rootPath then
        return self
    end

    -- reset the radio menu
    if self.radioRootPath then
        veaf.loggers.get(veafCombatMission.Id):trace("reset the radio submenu")
        veafRadio.clearSubmenu(self.radioRootPath)
    end

    -- populate the radio menu
    veaf.loggers.get(veafCombatMission.Id):trace("populate the radio menu")
    -- global commands
    veafRadio.addCommandToSubmenu("Get info", self.radioRootPath, veafCombatMission.GetInformationOnMission, self.name, veafRadio.USAGE_ForAll)
    if self:isActive() then
        -- mission is active, set up accordingly (desactivate mission, get information, pop smoke, etc.)
        veaf.loggers.get(veafCombatMission.Id):trace("mission is active")
        if self:isSecured() then
            veafRadio.addSecuredCommandToSubmenu('Desactivate mission', self.radioRootPath, veafCombatMission.DesactivateMission, self.name, veafRadio.USAGE_ForAll)
        else
            veafRadio.addCommandToSubmenu('Desactivate mission', self.radioRootPath, veafCombatMission.DesactivateMission, self.name, veafRadio.USAGE_ForAll)
        end
    else
        -- mission is not active, set up accordingly (activate mission)
        veaf.loggers.get(veafCombatMission.Id):trace("mission is not active")
        if self:isSecured() then
            veafRadio.addSecuredCommandToSubmenu('Activate mission', self.radioRootPath, veafCombatMission.ActivateMission, self.name, veafRadio.USAGE_ForAll)
        else
            veafRadio.addCommandToSubmenu('Activate mission', self.radioRootPath, veafCombatMission.ActivateMission, self.name, veafRadio.USAGE_ForAll)
        end
    end

    if not inBatch then veafRadio.refreshRadioMenu() end
    return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- global functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------

function veafCombatMission.GetMissionNumber(number)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.GetMissionNumber([%s])",tostring(number)))
    local mission = veafCombatMission.missionsList[number]
    return mission
end

function veafCombatMission.GetMission(name)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.GetMission([%s])",name or ""))
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("Searching for mission with name [%s]", name))
    local mission = veafCombatMission.missionsDict[name:lower()]
    if not mission then
        local message = string.format("VeafCombatMission [%s] was not found !",name)
        veaf.loggers.get(veafCombatMission.Id):error(message)
        trigger.action.outText(message,5)
    end
    return mission
end

-- add a mission
function veafCombatMission.AddMission(mission)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.AddMission([%s])",mission:getName() or ""))
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("Adding mission [%s]", mission:getName()))
    mission:initialize()
    table.insert(veafCombatMission.missionsList, mission)
    veafCombatMission.missionsDict[mission:getName():lower()] = mission
    return mission
end

-- add a mission and create copies with different skills
function veafCombatMission.AddMissionsWithSkillAndScale(mission, includeOriginal, skills, scales)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.AddMissionsWithSkill([%s])",mission:getName() or ""))
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("skills=%s",veaf.p(skills)))
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("scales=%s",veaf.p(scales)))

    if (mission:isRadioMenuEnabled() and includeOriginal) then
        veafCombatMission.AddMission(mission)
    end

    local skills = skills or  {"Average", "Good", "High", "Excellent", "Random"}
    local scales = scales or {1, 2, 3, 4}

    for _, scale in pairs(scales) do
        for _, skill in pairs(skills) do
            local copy = mission:copy(skill, scale)--:setRadioMenuEnabled(false)
            copy:setName(mission:getName().."/"..skill.."/"..scale)
            copy:setFriendlyName(mission:getFriendlyName())
            veafCombatMission.AddMission(copy)
        end
    end

    return mission
end

-- activate a mission by number
function veafCombatMission.ActivateMissionNumber(number, silent)
    local mission = veafCombatMission.GetMissionNumber(number)
    if mission then
        veafCombatMission.ActivateMission(mission:getName(), silent)
    end
end

-- activate a mission
function veafCombatMission.ActivateMission(name, silent, unitName)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.ActivateMission([%s])",name or ""))
    local mission = veafCombatMission.GetMission(name)
    local result = mission:activate(silent)
    if not silent and not mission:isSilent() then
        if result then
            veaf.outTextForUnit(unitName, "VeafCombatMission "..mission:getFriendlyName().." has been activated.", 10)
            mist.scheduleFunction(veafCombatMission.GetInformationOnMission,{{name}},timer.getTime()+1)
        else
            veaf.outTextForUnit(unitName, "VeafCombatMission "..mission:getFriendlyName().." was already active.", 10)
        end
    end
    veafCombatMission.buildRadioMenu()
end

-- desactivate a mission by number
function veafCombatMission.DesactivateMissionNumber(number, silent)
    local mission = veafCombatMission.GetMission(number)
    if mission then
        veafCombatMission.DesactivateMission(mission:getName(), silent)
    end
end

-- desactivate a mission
function veafCombatMission.DesactivateMission(name, silent, unitName)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.DesactivateMission([%s])",name or ""))
    local mission = veafCombatMission.GetMission(name)
    mission:desactivate()
    if not silent and not mission:isSilent() then
        veaf.outTextForUnit(unitName, "VeafCombatMission "..mission:getFriendlyName().." has been desactivated.", 10)
    end
    veafCombatMission.buildRadioMenu()
end

-- print information about a mission
function veafCombatMission.GetInformationOnMission(parameters)
    local name, unitName = veaf.safeUnpack(parameters)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.GetInformationOnMission([%s])",name or ""))
    local mission = veafCombatMission.GetMission(name)
    local text = mission:getInformation()
    if unitName then
        veaf.outTextForGroup(unitName, text, 30)
    else
        trigger.action.outText(text, 30)
    end
end

-- call the completion watchdog methods
function veafCombatMission.CompletionCheck(name)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.CompletionCheck([%s])",name or ""))
    local mission = veafCombatMission.GetMission(name)
    mission:completionCheck()
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Radio menu and help
-------------------------------------------------------------------------------------------------------------------------------------------------------------
local function _groupMissions()
    local missionGroups = {}
    local activeGroups = {}

    for _, mission in pairs(veafCombatMission.missionsDict) do
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("grouping missionName=%s", mission:getName()))
        if mission:isRadioMenuEnabled() then
            local regex = ("^([^/]+)/([^/]+)/(.+)$")
            local name, skill, scale = mission:getName():match(regex)
            veaf.loggers.get(veafCombatMission.Id):trace(string.format("name=%s, skill=%s, scale=%s", tostring(name), tostring(skill), tostring(scale)))
            local groupName = name
            if not groupName then groupName = mission:getName() end
            if not(missionGroups[groupName]) then
                missionGroups[groupName] = {}
                --veaf.loggers.get(veafCombatMission.Id):trace(string.format("creating group %s", groupName))
            end
            table.insert(missionGroups[groupName], mission)
            if mission:isActive() then
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("mission %s is active", mission:getName()))
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("activating group %s", groupName))
                if not(activeGroups[groupName]) then
                    activeGroups[groupName] = {}
                end
                if skill then
                    veaf.loggers.get(veafCombatMission.Id):trace(string.format("activating skill %s", skill))
                    if not(activeGroups[groupName][skill]) then
                        activeGroups[groupName][skill] = {}
                    end
                    if scale then
                        veaf.loggers.get(veafCombatMission.Id):trace(string.format("activating scale %s", scale))
                        activeGroups[groupName][skill][scale] = true
                    end
                end
            end
        end
    end
    --veaf.loggers.get(veafCombatMission.Id):trace(string.format("missionGroups=%s",veaf.p(missionGroups)))
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("activeGroups=%s",veaf.p(activeGroups)))
    return missionGroups, activeGroups
end

function veafCombatMission._buildMissionRadioMenu(menu, title, element)
    local missions = element.missions
    if #missions == 1 then
        -- one simple mission
        local mission = missions[1]
        if mission:isActive() then title = "* "..title end
        mission.radioRootPath = veafRadio.addSubMenu(title, menu)
        mission:updateRadioMenu(true)
    else
        -- group by skill and scale
        veaf.loggers.get(veafCombatMission.Id):trace("group by skill and scale")
        local skills = {}
        for _, mission in pairs(missions) do
            local regex = ("^([^/]+)/([^/]+)/(%d+)$")
            local name, skill, scale = mission:getName():match(regex)
            veaf.loggers.get(veafCombatMission.Id):trace(string.format("missionName=[%s], name=%s, skill=%s, scale=%s", tostring(mission:getName()), tostring(name), tostring(skill), tostring(scale)))
            if not skills[skill] then
                skills[skill] = {}
            end
            skills[skill][scale] = mission
        end

        veaf.loggers.get(veafCombatMission.Id):trace(string.format("skills=%s", veaf.p(skills)))

        -- create the radio menus
        local title = title
        if element.activeGroups then title = "* "..title end
        local missionPath = veafRadio.addSubMenu(title, menu)
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("  %s", title))
        local skillsNames = {}
        for skill, _ in pairs(skills) do
            table.insert(skillsNames, skill)
        end
        table.sort(skillsNames)
        for _, skill in pairs(skillsNames) do
            local scales = skills[skill]
            local skillTitle = skill
            if element.activeGroups and element.activeGroups[skill] then skillTitle = "* "..skillTitle end
            local skillPath = veafRadio.addSubMenu(skillTitle, missionPath)
            veaf.loggers.get(veafCombatMission.Id):trace(string.format("    %s", skill))
            local scalesNames = {}
            for scale, _ in pairs(scales) do
                table.insert(scalesNames, scale)
            end
            table.sort(scalesNames)
            for _, scale in pairs(scalesNames) do
                local mission = scales[scale]
                local scaleTitle = "scale "..scale
                if element.activeGroups and element.activeGroups[skill] and element.activeGroups[skill][scale] then scaleTitle = "* "..scaleTitle end
                local scalePath = veafRadio.addSubMenu(scaleTitle, skillPath)
                veaf.loggers.get(veafCombatMission.Id):trace(string.format("      %s", scale))
                mission.radioRootPath = scalePath
                mission:updateRadioMenu(true)
            end
        end
    end
end

--- Build the initial radio menu
function veafCombatMission.buildRadioMenu()
    veaf.loggers.get(veafCombatMission.Id):debug("buildRadioMenu()")

    -- don't create an empty menu
    if veaf.length(veafCombatMission.missionsDict) == 0 then
        return
    end

    if veafCombatMission.rootPath then
        veafRadio.clearSubmenu(veafCombatMission.rootPath)
    else
        veafCombatMission.rootPath = veafRadio.addMenu(veafCombatMission.RadioMenuName)
    end
    if not(veafRadio.skipHelpMenus) then
        veafRadio.addCommandToSubmenu("HELP", veafCombatMission.rootPath, veafCombatMission.help, nil, veafRadio.USAGE_ForGroup)
    end
    veafRadio.addCommandToSubmenu("List available", veafCombatMission.rootPath, veafCombatMission.listAvailableMissions, nil, veafRadio.USAGE_ForAll)
    veafRadio.addCommandToSubmenu("List active", veafCombatMission.rootPath, veafCombatMission.listActiveMissions, nil, veafRadio.USAGE_ForAll)

    local missions = {}
    local missionGroups, activeGroups = _groupMissions()
    for groupName, missionsInGroup in pairs(missionGroups) do
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("processing groupName=%s",groupName))
        missions[groupName] = {title=missionsInGroup[1]:getRadioMenuName(), sort=missionsInGroup[1]:getFriendlyName(), missions=missionsInGroup, activeGroups=activeGroups[groupName]}
    end
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("missions=%s",veaf.p(missions)))
    --veaf.loggers.get(veafCombatMission.Id):trace(string.format("#missions=%d",#missions))
    veafRadio.addPaginatedRadioElements(veafCombatMission.rootPath, veafCombatMission._buildMissionRadioMenu, missions)
    veafRadio.refreshRadioMenu()
end

function veafCombatMission.help(unitName)
    local text =
        'Combat missions are defined by the mission maker, and listed here\n' ..
        'You can start and stop them at will,\n' ..
        'as well as ask for information about their status.'

    veaf.outTextForGroup(unitName, text, 30)
end

function veafCombatMission.listAvailableMissions(unitName)
    -- sort the missions alphabetically
    local sortedMissions = {}
    local groupedMissions = _groupMissions()
    for groupName, missionsInGroup in pairs(groupedMissions) do
        table.insert(sortedMissions, groupName)
    end
    table.sort(sortedMissions)

    local text =
    'List of all available combat missions:\n'

    for _, missionName in pairs(sortedMissions) do
        text = text .. " - " .. missionName .. "\n"
    end
    veaf.outTextForUnit(unitName, text, 20)
end

function veafCombatMission.listActiveMissions()
    -- sort the missions alphabetically
    local sortedMissions = {}
    for _, mission in pairs(veafCombatMission.missionsDict) do
        if mission:isActive() then
            table.insert(sortedMissions, mission:getName() .. ' : ' .. mission:getRemainingEnemiesString())
        end
    end
    table.sort(sortedMissions)

    local text =
    'No active combat mission !'

    if #sortedMissions > 0 then
        text =
        'List of active combat missions:\n'

        for _, missionName in pairs(sortedMissions) do
            text = text .. " - " .. missionName .. "\n"
        end
    end

    trigger.action.outText(text, 20)
end

-- add a standard CAP mission with a single group
function veafCombatMission.addCapMission(missionName, missionDescription, missionBriefing, secured, radioMenuEnabled, skills, scales, spawnRadius)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("veafCombatMission.addCapMission(%s)",tostring(missionName)))

    local groupName = "OnDemand-"..missionName
    local spawnRadius = spawnRadius
    if spawnRadius == nil then spawnRadius = 20000 end
    local secured = secured
    if secured == nil then secured = true end
    local radioMenuEnabled = radioMenuEnabled
    if radioMenuEnabled == nil then radioMenuEnabled = false end
    local skills = skills
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("checking skills"))
    if not skills then
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("skills is nil"))
        if radioMenuEnabled then
            skills = {"Good", "Excellent"}
        else
            skills = nil
        end
    end
    local scales = scales
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("checking scales"))
    if not scales then
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("scales is nil"))
        if radioMenuEnabled then
            scales = {1, 2}
        else
            scales = nil
        end
    end

    veaf.loggers.get(veafCombatMission.Id):trace(string.format("skills=(%s)", veaf.p(skills)))
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("scales=(%s)", veaf.p(scales)))

    veafCombatMission.AddMissionsWithSkillAndScale(
		VeafCombatMission:new()
		:setSecured(secured)
		:setRadioMenuEnabled(radioMenuEnabled)
		:setName(missionName)
		:setFriendlyName(missionDescription)
		:setBriefing(missionBriefing)
		:addElement(
			VeafCombatMissionElement:new()
			:setName(groupName)
            :setGroups({groupName})
            :setSkill("Random")
            :setScalable(true)
            :setSpawnRadius(spawnRadius)
		)
		:addObjective(
			VeafCombatMissionObjective:new()
			:setName("Kill all the ennemies")
			:setDescription("you must kill all of the ennemies")
			:setMessage("%d ennemies destroyed !")
			:configureAsKillEnemiesObjective()
		)
		:initialize()
    ,false, skills, scales)
end

function veafCombatMission.dumpMissionsList(export_path)

    local jsonify = function(key, value)
        veaf.loggers.get(veafCombatMission.Id):trace("jsonify()")
        veaf.loggers.get(veafCombatMission.Id):trace("key=%s", veaf.p(key))
        veaf.loggers.get(veafCombatMission.Id):trace("value=%s", veaf.p(value))
        if veaf.json then
            return veaf.json.stringify(veafCombatMission.missionsDict[value])
        else
            return ""
        end
    end

    -- sort the missions alphabetically
    local sortedMissions = {}
    for _, mission in pairs(veafCombatMission.missionsDict) do
        veaf.loggers.get(veafCombatMission.Id):trace("mission=%s", veaf.p(mission))
        table.insert(sortedMissions, mission:getName():lower())
    end
    table.sort(sortedMissions)

    local _filename = "CombatMissionsList.json"
    if veaf.config.MISSION_NAME then
        _filename = "CombatMissionsList_" .. veaf.config.MISSION_NAME .. ".json"
    end
    if not(veaf.DO_NOT_EXPORT_JSON_FILES) then
        veaf.exportAsJson(sortedMissions, "combatMissions", jsonify, _filename, export_path)
    end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- remote interface
-------------------------------------------------------------------------------------------------------------------------------------------------------------

-- execute command from the remote interface
function veafCombatMission.executeCommandFromRemote(parameters)
    veaf.loggers.get(veafCombatMission.Id):debug(string.format("veafCombatMission.executeCommandFromRemote()"))
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("parameters= %s", veaf.p(parameters)))
    local _pilot, _pilotName, _unitName, _command = unpack(parameters)
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("_pilot= %s", veaf.p(_pilot)))
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("_pilotName= %s", veaf.p(_pilotName)))
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("_unitName= %s", veaf.p(_unitName)))
    veaf.loggers.get(veafCombatMission.Id):trace(string.format("_command= %s", veaf.p(_command)))
    if not _pilot or not _command then
        return false
    end

    if _command then
        -- parse the command
        local _action, _missionName, _parameters = _command:match(veafCombatMission.RemoteCommandParser)
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("_action=%s",veaf.p(_action)))
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("_missionName=%s",veaf.p(_missionName)))
        veaf.loggers.get(veafCombatMission.Id):trace(string.format("_parameters=%s",veaf.p(_parameters)))
        if _action and _action:lower() == "list" then
            veaf.loggers.get(veafCombatMission.Id):info(string.format("[%s] is listing air missions)",veaf.p(_pilot.name)))
            veafCombatMission.listAvailableMissions(_unitName)
            return true
        elseif _action and _action:lower() == "start" and _missionName then
            local _silent = _parameters and _parameters:lower() == "silent"
            veaf.loggers.get(veafCombatMission.Id):info(string.format("[%s] is starting air mission [%s] %s)",veaf.p(_pilot.name), veaf.p(_missionName), veaf.p(_parameters)))
            veafCombatMission.ActivateMission(_missionName, _silent, _unitName)
            return true
        elseif _action and _action:lower() == "stop" then
            local _silent = _parameters and _parameters:lower() == "silent"
            veaf.loggers.get(veafCombatMission.Id):info(string.format("[%s] is stopping air mission [%s] %s)",veaf.p(_pilot.name), veaf.p(_missionName), veaf.p(_parameters)))
            veafCombatMission.DesactivateMission(_missionName, _silent, _unitName)
            return true
        end
    end
    return false
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- initialisation
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function veafCombatMission.initialize()
    veaf.loggers.get(veafCombatMission.Id):info("Initializing module")
    veafCombatMission.buildRadioMenu()
    veafCombatMission.dumpMissionsList(veaf.config.MISSION_EXPORT_PATH)
end

veaf.loggers.get(veafCombatMission.Id):info(string.format("Loading version %s", veafCombatMission.Version))

