------------------------------------------------------------------
-- VEAF combat zone functions for DCS World
-- By zip (2019-20)
--
-- Features:
-- ---------
-- * Zones can be defined in the mission editor that are then managed by this script.
-- * For each zone, a specific radio sub-menu is created, allowing common actions on all specific zone (get coordinates, enemy presence, weather, pop smoke and flares, read a briefing, stop and start dynamic activity on the zone, etc.)
--
-- See the documentation : https://veaf.github.io/documentation/
------------------------------------------------------------------

veafCombatZone = {}

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Global settings. Stores the script constants
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Identifier. All output in DCS.log will start with this.
veafCombatZone.Id = "COMBATZONE"

--- Version.
veafCombatZone.Version = "1.22.0"

-- trace level, specific to this module
--veafCombatZone.LogLevel = "trace"

veaf.loggers.new(veafCombatZone.Id, veafCombatZone.LogLevel)

--- Number of seconds between each check of the zone watchdog function
veafCombatZone.SecondsBetweenWatchdogChecks = 60

--- Number of seconds between each smoke request on the zones
veafCombatZone.SecondsBetweenSmokeRequests = 180

--- Number of seconds between each flare request on the zones
veafCombatZone.SecondsBetweenFlareRequests = 120

veafCombatZone.DefaultSpawnRadiusForUnits = 50

veafCombatZone.DefaultSpawnRadiusForStatics = 0

veafCombatZone.RadioMenuName = "COMBAT ZONES"

-- Combat zones specific radio menu name
veafCombatZone.CombatZoneRadioMenuName = nil

-- Combat operations specific radio menu name
veafCombatZone.OperationRadioMenuName = nil

veafCombatZone.EventMessages = {
    CombatZoneComplete = [[
    Well done ! All enemies in zone %s have been destroyed or routed.
    The zone will now be desactivated.
    You can replay by activating it again, in the radio menu.]],
    PopSmokeRequest = "Copy RED smoke requested on %s !",
    UseFlareRequest = "Copy illumination flare requested on %s !",
    CombatOperationComplete = "Operation %s is over. Congratulations !"
}


-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Do not change anything below unless you know what you are doing!
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Radio menus paths
veafCombatZone.rootPath = nil

--- Combat Zones radio menus paths
veafCombatZone.combatZoneRootPath = nil
--- Operation radio menus paths
veafCombatZone.operationRootPath = nil

-- Zones list (table of VeafCombatZone objects)
veafCombatZone.zonesList = {}

-- Zones dictionary (map of VeafCombatZone objects by zone name)
veafCombatZone.zonesDict = {}

-- Radio groups dictionary (map of radio menu paths by radio group name)
veafCombatZone.radioGroupsDict = {}
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Utils
-------------------------------------------------------------------------------------------------------------------------------------------------------------

local messageSeparator = "\n=====================================================\n"

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- VeafCombatZoneElement object
-------------------------------------------------------------------------------------------------------------------------------------------------------------
VeafCombatZoneElement = {}

function VeafCombatZoneElement: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
    -- position on the map
    objectToCreate.position = nil
    -- if true, this is a simple dcs static
    objectToCreate.dcsStatic = false
    -- if true, this is a simple dcs group
    objectToCreate.dcsGroup = false
    -- if true, this is a VEAF command
    objectToCreate.veafCommand = nil
    --  coalition (0 = neutral, 1 = red, 2 = blue)
    objectToCreate.coalition = nil
    -- route, only for veaf commands (groups already have theirs)
    objectToCreate.route = nil
    -- spawn radius in meters (randomness introduced in the respawn mechanism)
    objectToCreate.spawnRadius = 0
    -- spawn chance in percent (xx chances in 100 that the unit is spawned - or the command run)
    objectToCreate.spawnChance = 100
    -- grouping elements (spawnGroup) so that a certain number (spawnCount) is guaranteed to spawn, by running the spawn random chance computation as often as necessary
    objectToCreate.spawnGroup = nil
    -- grouping elements (spawnGroup) so that a certain number (spawnCount) is guaranteed to spawn, by running the spawn random chance computation as often as necessary
    objectToCreate.spawnCount = 1

    return objectToCreate
end

---
--- setters and getters
---

function VeafCombatZoneElement:setName(value)
    self.name = value
    return self
end

function VeafCombatZoneElement:getName()
    return self.name
end

function VeafCombatZoneElement:setPosition(value)
    self.position = value
    return self
end

function VeafCombatZoneElement:getPosition()
    return self.position
end

function VeafCombatZoneElement:setDcsStatic(value)
    self.dcsStatic = value
    return self
end

function VeafCombatZoneElement:isDcsStatic()
    return self.dcsStatic
end

function VeafCombatZoneElement:setDcsGroup(value)
    self.dcsGroup = value
    return self
end

function VeafCombatZoneElement:isDcsGroup()
    return self.dcsGroup
end

function VeafCombatZoneElement:setVeafCommand(value)
    self.veafCommand = value
    return self
end

function VeafCombatZoneElement:getVeafCommand()
    return self.veafCommand
end

function VeafCombatZoneElement:setRoute(value)
    self.route = value
    return self
end

function VeafCombatZoneElement:getRoute()
    return self.route
end

function VeafCombatZoneElement:setCoalition(value)
    self.coalition = value
    return self
end

function VeafCombatZoneElement:getCoalition()
    return self.coalition
end

function VeafCombatZoneElement:setSpawnRadius(value)
    self.spawnRadius = tonumber(value)
    return self
end

function VeafCombatZoneElement:getSpawnRadius()
    return self.spawnRadius
end

function VeafCombatZoneElement:setSpawnChance(value)
    self.spawnChance = tonumber(value)
    return self
end

function VeafCombatZoneElement:getSpawnChance()
    return self.spawnChance
end

function VeafCombatZoneElement:setSpawnGroup(value)
    self.spawnGroup = value
    return self
end

function VeafCombatZoneElement:getSpawnGroup()
    return self.spawnGroup
end

function VeafCombatZoneElement:setSpawnDelay(value)
    if type(value) ~= "number" then
        value = tonumber(value)
    end
    self.spawnDelay = value
    return self
end

function VeafCombatZoneElement:getSpawnDelay()
    return self.spawnDelay
end

function VeafCombatZoneElement:setSpawnCount(value)
    self.spawnCount = tonumber(value)
    return self
end

function VeafCombatZoneElement:getSpawnCount()
    return self.spawnCount
end

---
--- other methods
---

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- VeafCombatZone object
-------------------------------------------------------------------------------------------------------------------------------------------------------------

VeafCombatZone = {}

function VeafCombatZone:new(objectToCopy)
    local objectToCreate = objectToCopy or {} -- create object if user does not provide one
    setmetatable(objectToCreate, self)
    self.__index = self

    -- init the new object

    -- zone name (human-friendly)
    objectToCreate.friendlyName = nil
    -- technical zone name (in the mission editor)
    objectToCreate.missionEditorZoneName = nil
    -- mission briefing
    objectToCreate.briefing = nil
    -- list of defined objectives
    objectToCreate.objectives = {}
    -- list of the elements defined in the zone
    objectToCreate.elements = {}
    objectToCreate.elementGroups = {}
    -- the trigger zone object
    objectToCreate.triggerZone = nil
    -- the zone center
    objectToCreate.zoneCenter = nil
    -- zone is active
    objectToCreate.active = false
    -- zone is a training zone
    objectToCreate.training = false
    -- display the remaining units
    objectToCreate.showUnitsList = true
    -- display the zone coordinates and weather
    objectToCreate.showZonePositionInfo = true
    -- zone is completable (i.e. disable it when all ennemies are dead)
    objectToCreate.completable = true
    -- DCS groups that have been spawned (for cleaning up later)
    objectToCreate.spawnedGroups = {}
    objectToCreate.delayedSpawners = {}
    -- Whether we want the combat zone to be added to populate the radio menu
    objectToCreate.enableRadioMenu = true
    -- Whether we want the combat zone to be cleaned when it is over
    objectToCreate.enableJunkCleanup = true
    -- whether the zone can be activated/deactivated by user via radio menu. If false, the zone won't be added to radio menu until activated
    objectToCreate.enableUserActivation = true
    -- whether we want to allow ground marking of the zone
    objectToCreate.enableSmokeAndFlare = true
    -- list of chained combat zones; this is are list of combat zones, that are activated randomly
    objectToCreate.chainedCombatZones = nil
    -- delay (in seconds) between the end of this combat zone and the start of the other; can be a randomizable numeric (e.g. "[1-5]")
    objectToCreate.chainedCombatZonesDelay = nil
    --- Radio menus
    objectToCreate.radioGroupName = nil
    objectToCreate.radioMenuPrefix = nil
    objectToCreate.radioParentPath = nil
    objectToCreate.radioMarkersPath = nil
    objectToCreate.radioTargetInfoPath = nil
    objectToCreate.radioRootPath = nil
    -- the watchdog function checks for zone objectives completion
    objectToCreate.watchdogFunctionId = nil
    -- "pop smoke" command reset function id
    objectToCreate.smokeResetFunctionId = nil
    -- "pop flare" command reset function id
    objectToCreate.flareResetFunctionId = nil
    -- function to call when combat zone is over. The function is passed self combat zone
    objectToCreate.onCompletedHook = nil

    return objectToCreate
  end

---
--- setters and getters
---
function VeafCombatZone:setOnCompletedHook(onCompletedFunction)
    self.onCompletedHook = onCompletedFunction
    return self
end

function VeafCombatZone:disableRadioMenu()
    self.enableRadioMenu = false
    return self
end

function VeafCombatZone:disableJunkCleanup()
    self.enableJunkCleanup = false
    return self
end

-- make sure users cannot activate the zone (it won't be in the radio menu unless it's already active)
function VeafCombatZone:disableUserActivation()
    self.enableUserActivation = false
    return self
end

-- make sure users can activate the zone (it will be in the radio menu even if inactive - that's the default)
function VeafCombatZone:enableUserActivation()
    self.enableUserActivation = true
    return self
end

function VeafCombatZone:setEnableUserActivation(value)
    self.enableUserActivation = value
    return self
end

function VeafCombatZone:setEnableSmokeAndFlare(value)
    self.enableSmokeAndFlare = value
    return self
end

function VeafCombatZone:getRadioMenuName(asActive)
    local active = ""
    if asActive then
        active = "* "
    end
    local prefix = ""
    if self:getRadioMenuPrefix() then
        prefix = self:getRadioMenuPrefix() .. " "
    end
    return prefix .. active .. self:getFriendlyName()
end
function VeafCombatZone:setFriendlyName(value)
    self.friendlyName = value
    return self
end

function VeafCombatZone:getFriendlyName()
    return self.friendlyName
end

function VeafCombatZone:getRadioMenuPrefix()
    return self.radioMenuPrefix
end

function VeafCombatZone:setRadioMenuPrefix(value)
    self.radioMenuPrefix = value
    return self
end

function VeafCombatZone:setBriefing(value)
    self.briefing = value
    return self
end

function VeafCombatZone:getBriefing()
    return self.briefing
end

function VeafCombatZone:setMissionEditorZoneName(value)
    self.missionEditorZoneName = value
    return self
end

function VeafCombatZone:getMissionEditorZoneName()
    return self.missionEditorZoneName
end

function VeafCombatZone:isActive()
    return self.active
end

function VeafCombatZone:setActive(value)
    self.active = value
    return self
end

function VeafCombatZone:isTraining()
    return self.training
end

function VeafCombatZone:setTraining(value)
    self.training = value
    if value then 
        self.showUnitsList = true
        self.showZonePositionInfo = true
    end
    return self
end

function VeafCombatZone:isShowUnitsList()
    return self.showUnitsList
end

function VeafCombatZone:setShowUnitsList(value)
    self.showUnitsList = value
    return self
end

function VeafCombatZone:isShowZonePositionInfo()
    return self.showZonePositionInfo
end

function VeafCombatZone:setShowZonePositionInfo(value)
    self.showZonePositionInfo = value
    return self
end

function VeafCombatZone:isCompletable()
    return self.completable
end

function VeafCombatZone:setCompletable(value)
    self.completable = value
    return self
end

function VeafCombatZone:getTriggerZone()
    return self.triggerZone
end

function VeafCombatZone:getCenter()
    return self.zoneCenter
end

function VeafCombatZone:setRadioParentPath(value)
    self.radioParentPath = value
    return self
end

function VeafCombatZone:getRadioParentPath()
    return self.radioParentPath
end

function VeafCombatZone:setRadioGroupName(value)
    self.radioGroupName = value
    return self
end

function VeafCombatZone:getRadioGroupName()
    return self.radioGroupName
end

function VeafCombatZone:addSpawnedGroup(groupOrName)
    local groupName = groupOrName
    if type(groupName) ~= "string" then
        groupName = tostring(groupName)
    end
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:addSpawnedGroup(%s)",veaf.p(self.missionEditorZoneName), veaf.p(groupName)))
    if not self.spawnedGroups then
        self.spawnedGroups = {}
    end
    table.insert(self.spawnedGroups, groupName)
    return self
end

function VeafCombatZone:getSpawnedGroups()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getSpawnedGroups()",veaf.p(self.missionEditorZoneName)))
    veaf.loggers.get(veafCombatZone.Id):trace(veaf.serialize("self.spawnedGroups", self.spawnedGroups))
    return self.spawnedGroups
end

function VeafCombatZone:clearSpawnedGroups()
    self.spawnedGroups = {}
    return self
end

function VeafCombatZone:addDelayedSpawner(id)
    veaf.loggers.get(veafCombatZone.Id):trace("VeafCombatZone[%s]:addDelayedSpawner(%s)", veaf.p(self.missionEditorZoneName), veaf.p(id))
    if not self.delayedSpawners then
        self.delayedSpawners = {}
    end
    table.insert(self.delayedSpawners, id)
    return self
end

function VeafCombatZone:getDelayedSpawners()
    veaf.loggers.get(veafCombatZone.Id):trace("VeafCombatZone[%s]:getDelayedSpawners()", veaf.p(self.missionEditorZoneName))
    veaf.loggers.get(veafCombatZone.Id):trace("self.delayedSpawners=%s", self.delayedSpawners)
    return self.delayedSpawners
end

function VeafCombatZone:clearDelayedSpawners()
    self.delayedSpawners = {}
    return self
end

function VeafCombatZone:addZoneElement(element)
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:addZoneElement(%s)",veaf.p(self.missionEditorZoneName), veaf.p(element:getName())))
    if not self.elements then
        self.elements = {}
    end
    if not self.elementGroups then
        self.elementGroups = {}
    end
    table.insert(self.elements, element)
    if not self.elementGroups[element:getSpawnGroup()] then
        local elementGroup = {}
        elementGroup.spawnGroup = element:getSpawnGroup()
        elementGroup.spawnCount = element:getSpawnCount()
        elementGroup.elements = {}
        self.elementGroups[element:getSpawnGroup()] = elementGroup
    end
    local elementGroup = self.elementGroups[element:getSpawnGroup()]
    table.insert(elementGroup.elements, element)
    return self
end

function VeafCombatZone:addZoneElementsFromZoneNamed(zoneName)
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:addZoneElementsFromZoneNamed(%s)",veaf.p(self.missionEditorZoneName), veaf.p(zoneName)))
    if not zoneName then
        return self
    end
    local zone = veafCombatZone.GetZone(zoneName)
    if not zone then
        return self
    end
    local elements = zone:getZoneElements()
    if not elements then
        return self
    end
    for _, element in pairs(elements) do
        self:addZoneElement(element)
    end
    return self
end


function VeafCombatZone:getZoneElements()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getZoneElement()",veaf.p(self.missionEditorZoneName)))
    veaf.loggers.get(veafCombatZone.Id):trace(veaf.serialize("self.elements", self.elements))
    return self.elements
end

function VeafCombatZone:getZoneElementsGroups()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getZoneElementsGroups()",veaf.p(self.missionEditorZoneName)))
    return self.elementGroups
end

-- get the list of chained combat zones; this is are list of combat zones, that are activated randomly
function VeafCombatZone:getChainedCombatZones()
    if not self.chainedCombatZones then
        veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getChainedCombatZones() - Initializing",veaf.p(self.missionEditorZoneName)))
        self.chainedCombatZones = {}
    end
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getChainedCombatZones() = %s",veaf.p(self.missionEditorZoneName), veaf.p(self.chainedCombatZones)))
    return self.chainedCombatZones
end

-- add a chained combat zone (by name); the zone does not have to exist at the time the function is called
function VeafCombatZone:addChainedCombatZone(combatZoneName)
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:addChainedCombatZone([%s])",veaf.p(self.missionEditorZoneName), veaf.p(combatZoneName)))
    table.insert(self:getChainedCombatZones(), combatZoneName)
    return self
end

-- get the next chained combat zone; if the list is more than 1 zone long, get one at random
function VeafCombatZone:getNextChainedCombatZone()
    local nextZoneName = veaf.randomlyChooseFrom(self:getChainedCombatZones())
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getNextChainedCombatZone() = [%s]",veaf.p(self.missionEditorZoneName), veaf.p(nextZoneName)))
    return nextZoneName
end

-- get the delay (in seconds) between the end of this combat zone and the start of the other; if the set value was a randomizable numeric, randomize it
function VeafCombatZone:getChainedCombatZonesDelay()
    return veaf.getRandomizableNumeric(self.chainedCombatZonesDelay or 0)
end

-- set the delay (in seconds) between the end of this combat zone and the start of the other; can be a randomizable numeric (e.g. "1-5")
function VeafCombatZone:setChainedCombatZonesDelay(value)
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:setChainedCombatZonesDelay([%s])",veaf.p(self.missionEditorZoneName), veaf.p(value)))
    if not value then value = 0 end
    self.chainedCombatZonesDelay = value
    return self
end

---
--- other methods
---
function VeafCombatZone:scheduleWatchdogFunction()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:scheduleWatchdogFunction()",veaf.p(self.missionEditorZoneName)))
    if self:isCompletable() then
        self.watchdogFunctionId = mist.scheduleFunction(veafCombatZone.CompletionCheck,{self.missionEditorZoneName},timer.getTime()+veafCombatZone.SecondsBetweenWatchdogChecks)
    end
    return self
end

function VeafCombatZone:unscheduleWatchdogFunction()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:unscheduleWatchdogFunction()",veaf.p(self.missionEditorZoneName)))
    if self.watchdogFunctionId then
        mist.removeFunction(self.watchdogFunctionId)
    end
    self.watchdogFunctionId = nil
    return self
end

function VeafCombatZone:addObjective(value)
    table.insert(self.objectives, value)
    return self
end

function VeafCombatZone:addDefaultObjectives()
    -- TODO
    return self
end

function VeafCombatZone:initialize()
    veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatZone[%s]:initialize()",veaf.p(self.missionEditorZoneName)))

    -- check parameters
    if not self.missionEditorZoneName then
        return self
    else
        self.triggerZone = veaf.getTriggerZone(self.missionEditorZoneName)
        if not self.triggerZone then
            local message = string.format("Trigger zone [%s] does not exist in the mission !",veaf.p(self.missionEditorZoneName))
            veaf.loggers.get(veafCombatZone.Id):error(message)
            trigger.action.outText(message,5)
        return self
        end
    end
    if not self.friendlyName then
        self:setFriendlyName(self.missionEditorZoneName)
    end
    if #self.objectives == 0 then
        self:addDefaultObjectives()
    end

    -- find the trigger zone center
    self.zoneCenter = mist.utils.zoneToVec3(self.missionEditorZoneName)
    if not self.zoneCenter then
        local message = string.format("Trigger zone [%s] does not exist in the mission !",veaf.p(self.missionEditorZoneName))
        veaf.loggers.get(veafCombatZone.Id):error(message)
        trigger.action.outText(message,5)
        return self
    end
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("zone center = [%s]",veaf.vecToString(self.zoneCenter)))

    -- find units in the trigger zone
    local units
    units, _ = veaf.safeUnpack(self:findUnitsInCombatZone())

    -- process special commands in the units 
    local alreadyAddedGroups = {}
    for _,unit in pairs(units) do
        local zoneElement = VeafCombatZoneElement:new()
        zoneElement:setCoalition(unit:getCoalition())
        local unitName = unit:getName()
        veaf.loggers.get(veafCombatZone.Id):trace(string.format("processing unit [%s] of coalition [%d]", unitName, unit:getCoalition()))
        zoneElement:setPosition(unit:getPosition().p)
        local spawnRadius, command, spawnChance, spawnGroup, spawnCount, spawnDelay
        _, _, spawnRadius = unitName:lower():find("#spawnradius%s*=%s*(%d+)")
        _, _, command = unitName:lower():find("#command%s*=%s*\"([^\"]+)\"")
        _, _, spawnChance = unitName:lower():find("#spawnchance%s*=%s*(%d+)")
        _, _, spawnGroup = unitName:lower():find("#spawngroup%s*=%s*\"([^\"]+)\"")
        _, _, spawnCount = unitName:lower():find("#spawncount%s*=%s*(%d+)")
        _, _, spawnDelay = unitName:lower():find("#spawndelay%s*=%s*(%d+)")
        if spawnRadius then
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnRadius = [%d]", spawnRadius))
            zoneElement:setSpawnRadius(spawnRadius)
        end
        if spawnChance then
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnChance = [%d]", spawnChance))
            zoneElement:setSpawnChance(spawnChance)
        end
        if spawnCount then
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnCount = [%d]", spawnCount))
            zoneElement:setSpawnCount(spawnCount)
        end
        if spawnGroup then
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnGroup = [%s]", spawnGroup))
            zoneElement:setSpawnGroup(spawnGroup)
        end
        if spawnDelay then
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnDelay = [%s]", spawnDelay))
            zoneElement:setSpawnDelay(spawnDelay)
        end
        if command then
            -- it's a fake unit transporting a VEAF command
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("command = [%s]", command))
            command = command .. ", czName " .. self:getMissionEditorZoneName() -- add the combat zone name to the command
            zoneElement:setVeafCommand(command)
            local groupName = unit:getGroup():getName()
            zoneElement:setName(groupName)
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("groupName = [%s]", groupName))
            local route = mist.getGroupRoute(groupName, 'task')
            zoneElement:setRoute(route)
            if not zoneElement:getSpawnGroup() then zoneElement:setSpawnGroup(groupName) end -- default the spawn group to the group name in case there is no spawn group  defined
        else
            -- it's a group or a static unit
            local groupName = nil
            local objectCategory = Object.getCategory(unit)
            veaf.loggers.get(veafCombatZone.Id):trace("objectCategory=%s", veaf.p(objectCategory))
            if objectCategory == 1 then
                local unitCategory = Unit.getCategory(unit)
                veaf.loggers.get(veafCombatZone.Id):trace("unitCategory=%s", veaf.p(unitCategory))
            end
            if objectCategory == 3 or objectCategory == 6 then -- 3 is static objects, 6 is cargo (a kind of static object)
                groupName = unitName -- default for static objects = groups themselves
                zoneElement:setDcsStatic(true)
                if not zoneElement:getSpawnRadius() then
                    zoneElement:setSpawnRadius(veafCombatZone.DefaultSpawnRadiusForStatics)
                end
            else
                groupName = unit:getGroup():getName()
                zoneElement:setDcsGroup(true)
                if not zoneElement:getSpawnRadius() then
                    zoneElement:setSpawnRadius(veafCombatZone.DefaultSpawnRadiusForUnits)
                end
            end
            if not zoneElement:getSpawnGroup() then zoneElement:setSpawnGroup(groupName) end -- default the spawn group to the group name in case there is no spawn group  defined
            if not alreadyAddedGroups[groupName] then
                -- add a group element
                veaf.loggers.get(veafCombatZone.Id):trace(string.format("adding group [%s]", groupName))
                alreadyAddedGroups[groupName] = groupName
                zoneElement:setName(groupName)
            else
                veaf.loggers.get(veafCombatZone.Id):trace(string.format("skipping group [%s]", groupName))
                zoneElement = nil -- don't add this element, it's a group that has already been added
            end
        end

        if zoneElement then self:addZoneElement(zoneElement) end
    end

    -- deactivate the zone
    veaf.loggers.get(veafCombatZone.Id):trace("desactivate the zone")
    self:desactivate()

    -- remove all units in the trigger zone (we want it CLEAN !)
    local _, groupNames = veaf.safeUnpack(self:findUnitsInCombatZone())
    if (groupNames) then
        for _, groupName in pairs(groupNames) do

            veaf.loggers.get(veafCombatZone.Id):trace(string.format("destroying group [%s]",groupName))
            local group = Group.getByName(groupName)
            if not group then
                group = StaticObject.getByName(groupName)
            end
            if group then
                group:destroy()
            end
        end
    end
    return self
end

function VeafCombatZone:getInformation()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:getInformation()",veaf.p(self.missionEditorZoneName)))
    local message =      "COMBAT ZONE "..self:getFriendlyName().." \n\n"
    if (self:getBriefing()) then
        message = message .. "BRIEFING: \n"
        message = message .. self:getBriefing()
        message = message .. "\n\n"
    end
    if self:isActive() then

        -- generate information dispatch
        local nbShipsR = 0
        local nbVehiclesR = 0
        local nbInfantryR = 0
        local nbStaticsR = 0
        local nbShipsB = 0
        local nbVehiclesB = 0
        local nbInfantryB = 0
        local nbStaticsB = 0
        local unitsByTypeR = {}
        local unitsByTypeB = {}

        for _, groupName in pairs(self:getSpawnedGroups()) do
            local group = Group.getByName(groupName)
            if group then
                for _, u in pairs(group:getUnits()) do
                    local coa = u:getCoalition()
                    if Object.getCategory(u) == 3 then
                        if coa == 1 then
                            nbStaticsR = nbStaticsR + 1
                        elseif coa == 2 then
                            nbStaticsB = nbStaticsB + 1
                        end
                    else
                        local typeName = u:getTypeName()
                        if typeName then
                            local unit = veafUnits.findUnit(typeName)
                            if unit then
                                if coa == 1 then
                                    if not(unitsByTypeR[typeName]) then
                                        unitsByTypeR[typeName] = 0
                                    end
                                    unitsByTypeR[typeName] = unitsByTypeR[typeName] + 1
                                    if unit.vehicle then
                                        nbVehiclesR = nbVehiclesR + 1
                                    elseif unit.naval then
                                        nbShipsR = nbShipsR + 1
                                    else
                                        nbInfantryR = nbInfantryR + 1
                                    end
                                elseif coa == 2 then
                                    if not(unitsByTypeB[typeName]) then
                                        unitsByTypeB[typeName] = 0
                                    end
                                    unitsByTypeB[typeName] = unitsByTypeB[typeName] + 1
                                    if unit.vehicle then
                                        nbVehiclesB = nbVehiclesB + 1
                                    elseif unit.naval then
                                        nbShipsB = nbShipsB + 1
                                    else
                                        nbInfantryB = nbInfantryB + 1
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end

        if nbShipsB+nbStaticsB+nbVehiclesB+nbInfantryB > 0 and self:isShowUnitsList() then
            local msgs = {}
            if nbShipsB > 0 then
                table.insert(msgs, nbShipsB .. " ship(s)")
            end
            if nbStaticsB > 0 then
                table.insert(msgs, nbStaticsB .. " structure(s)")
            end
            if nbVehiclesB > 0 then
                table.insert(msgs, nbVehiclesB .. " vehicle(s)")
            end
            if nbInfantryB > 0 then
                table.insert(msgs, nbInfantryB .. " soldier(s)")
            end
            message = message .. "FRIENDS: ".. table.concat(msgs, ",") .." remaining.\n"
            if self:isTraining() then
                local firstUnit = true
                for name, count in pairs(unitsByTypeB) do
                    local separator = ", "
                    if firstUnit then
                        separator = ""
                        firstUnit = false
                    end
                    message = message .. string.format("%s%d %s",separator, count, name)
                end
                message = message .. "\n"
            end
        end
        if nbShipsR+nbStaticsR+nbVehiclesR+nbInfantryR > 0 and self:isShowUnitsList() then
            local msgs = {}
            if nbShipsR > 0 then
                table.insert(msgs, nbShipsR .. " ship(s)")
            end
            if nbStaticsR > 0 then
                table.insert(msgs, nbStaticsR .. " structure(s)")
            end
            if nbVehiclesR > 0 then
                table.insert(msgs, nbVehiclesR .. " vehicle(s)")
            end
            if nbInfantryR > 0 then
                table.insert(msgs, nbInfantryR .. " soldier(s)")
            end
            message = message .. "ENEMIES: ".. table.concat(msgs, ",") .." remaining.\n"
            if self:isTraining() then
                local firstUnit = true
                for name, count in pairs(unitsByTypeR) do
                    local separator = ", "
                    if firstUnit then
                        separator = ""
                        firstUnit = false
                    end
                    message = message .. string.format("%s%d %s",separator, count, name)
                end
                message = message .. "\n"
            end
        end
        message = message .. "\n"

        if self:isShowZonePositionInfo() then
            -- add coordinates and position from bullseye
            local zoneCenter = self:getCenter()
            local lat, lon = coord.LOtoLL(zoneCenter)
            local mgrsString = mist.tostringMGRS(coord.LLtoMGRS(lat, lon), 3)
            local bullseye = mist.utils.makeVec3(mist.DBs.missionData.bullseye.blue, 0)
            local vec = {x = zoneCenter.x - bullseye.x, y = zoneCenter.y - bullseye.y, z = zoneCenter.z - bullseye.z}
            local dir = mist.utils.round(mist.utils.toDegree(mist.utils.getDir(vec, bullseye)), 0)
            local dist = mist.utils.get2DDist(zoneCenter, bullseye)
            local distMetric = mist.utils.round(dist/1000, 0)
            local distImperial = mist.utils.round(mist.utils.metersToNM(dist), 0)
            local fromBullseye = string.format('%03d', dir) .. ' for ' .. distMetric .. 'km /' .. distImperial .. 'nm'

            message = message .. "LAT LON (decimal): " .. mist.tostringLL(lat, lon, 2) .. ".\n"
            message = message .. "LAT LON (DMS)    : " .. mist.tostringLL(lat, lon, 0, true) .. ".\n"
            message = message .. "MGRS/UTM         : " .. mgrsString .. ".\n"
            message = message .. "FROM BULLSEYE    : " .. fromBullseye .. ".\n"
            message = message .. "\n"

            -- get altitude, qfe and wind information
            message = message .. veaf.weatherReport(zoneCenter, nil, true)
        end
    else
        message = message .. "zone is not yet active."
    end

    return message
end

function VeafCombatZone:spawnElement(zoneElement, now)
    veaf.loggers.get(veafCombatZone.Id):debug("VeafCombatZone[%s]:spawnElement([%s], [%s])",veaf.p(self:getFriendlyName()), veaf.p(zoneElement:getName()), veaf.p(now))
    veaf.loggers.get(veafCombatZone.Id):trace("zoneElement=%s", zoneElement)
    if not now and zoneElement:getSpawnDelay() and type(zoneElement:getSpawnDelay()) == "number" then
        -- self-schedule
        veaf.loggers.get(veafCombatZone.Id):trace("scheduling spawn of zoneElement=%s in %s seconds", zoneElement:getName(), zoneElement:getSpawnDelay())
        local id = mist.scheduleFunction(VeafCombatZone.spawnElement,{self, zoneElement, true}, timer.getTime()+zoneElement:getSpawnDelay())
        self:addDelayedSpawner(id)
    else
        -- spawn now
        veaf.loggers.get(veafCombatZone.Id):trace("spawning zoneElement=%s now", zoneElement:getName())
        local position = zoneElement:getPosition()
        if zoneElement:getSpawnRadius() > 0 then
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("position=[%s]",veaf.vecToString(position)))
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnRadius=[%s]",zoneElement:getSpawnRadius()))
            local mistP = mist.getRandPointInCircle(position, zoneElement:getSpawnRadius())
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("mistP=[%s]",veaf.vecToString(mistP)))
            position = {x = mistP.x, y = position.y, z = mistP.y}
        end
        if zoneElement:isDcsStatic() or zoneElement:isDcsGroup() then
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("respawning group [%s] at position [%s]",zoneElement:getName(), veaf.vecToString(position)))
            local vars = {}
            vars.gpName = zoneElement:getName()
            vars.name = zoneElement:getName()
            vars.newGroupName = veaf.getNameForSpawnedGroup(zoneElement:getCoalition(), zoneElement:getName(), self:getMissionEditorZoneName())
            vars.route = zoneElement:getRoute()
            vars.action = 'respawn'
            vars.point = position
            vars.renameUnitsSequentially = true
            local newGroup = mist.teleportToPoint(vars)
            if type(newGroup) == 'table' then
                veaf.loggers.get(veafCombatZone.Id):trace(string.format("[%s]:activate() - mist.teleportToPoint([%s])", self:getMissionEditorZoneName(), zoneElement:getName()))
                self:addSpawnedGroup(newGroup.name)
                veaf.readyForCombat(newGroup.name)
            else
                veaf.loggers.get(veafCombatZone.Id):trace(string.format("[%s]:activate() - mist.teleportToPoint([%s]) failed", self:getMissionEditorZoneName(), zoneElement:getName()))
            end
        elseif zoneElement:getVeafCommand() then
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("executing command [%s] at position [%s]",zoneElement:getVeafCommand(), veaf.vecToString(position)))
            local spawnedGroups = {}
            veafInterpreter.execute(zoneElement:getVeafCommand(), position, zoneElement:getCoalition(), nil, spawnedGroups)
            for _, newGroup in pairs(spawnedGroups) do
                veaf.loggers.get(veafCombatZone.Id):trace(string.format("[%s].addSpawnedGroup", zoneElement:getName()))
                self:addSpawnedGroup(newGroup)
                veaf.loggers.get(veafCombatZone.Id):trace(string.format("newGroup = [%s]", newGroup))
                local route = zoneElement:getRoute()
                veaf.loggers.get(veafCombatZone.Id):trace(string.format("got route"))
                local result = mist.goRoute(newGroup, route)
                veaf.loggers.get(veafCombatZone.Id):trace(string.format("sent group on its way"))
            end
        end
    end
end

-- activate the zone
function VeafCombatZone:activate()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:activate()",self:getMissionEditorZoneName()))
    self:setActive(true)

    for _, zoneElementGroup in pairs(self:getZoneElementsGroups()) do
        veaf.loggers.get(veafCombatZone.Id):trace(string.format("processing spawnGroup [%s]",zoneElementGroup.spawnGroup))
        local spawnCount = zoneElementGroup.spawnCount
        veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnCount = [%d]",spawnCount))
        local tries = 10
        local alreadySpawnedElements = {}
        local shuffledIndexes = {}
        for i=1,#zoneElementGroup.elements do
            local zoneElement = zoneElementGroup.elements[i]
            alreadySpawnedElements[zoneElement:getName()]=false
            table.insert(shuffledIndexes, i)
        end
        veaf.shuffle(shuffledIndexes)
        while spawnCount > 0 and tries > 0 do
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("tries = [%d]",tries))
            tries = tries - 1

            for i=1,#shuffledIndexes do
                local zoneElement = zoneElementGroup.elements[shuffledIndexes[i]]
                veaf.loggers.get(veafCombatZone.Id):trace(string.format("processing element [%s]",veaf.p(zoneElement)))
                if spawnCount > 0 then
                    if not alreadySpawnedElements[zoneElement:getName()] then
                        veaf.loggers.get(veafCombatZone.Id):trace(string.format("processing element [%s]",zoneElement:getName()))
                        local chance = math.random(0, 100)
                        if tries == 1 then chance = 0 end -- force chance if in the last try
                        veaf.loggers.get(veafCombatZone.Id):trace(string.format("chance = [%d]",chance))
                        veaf.loggers.get(veafCombatZone.Id):trace(string.format("spawnChance = [%d]",zoneElement:getSpawnChance()))
                        if chance <= zoneElement:getSpawnChance() then
                            veaf.loggers.get(veafCombatZone.Id):trace(string.format("chance hit (%d <= %d)",chance, zoneElement:getSpawnChance()))
                            spawnCount = spawnCount - 1
                            alreadySpawnedElements[zoneElement:getName()]=true
                            self:spawnElement(zoneElement)
                        else
                            veaf.loggers.get(veafCombatZone.Id):trace(string.format("chance missed (%d > %d)",chance, zoneElement:getSpawnChance()))
                        end
                    else
                        veaf.loggers.get(veafCombatZone.Id):trace(string.format("already spawned [%s]",zoneElement:getName()))
                    end
                end
            end
        end
    end

    -- start the completion watchdog
    self:scheduleWatchdogFunction()

    -- refresh the radio menu
    self:updateRadioMenu()

    return self
end

-- activate the next chained zone (if any)
function VeafCombatZone:activateNextChainedZone()
    local nextZoneName = self:getNextChainedCombatZone()
    local nextZone = veafCombatZone.GetZone(nextZoneName)
    if not nextZone then return self end
    local delay = self:getChainedCombatZonesDelay()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("activating the next chained zone ([%s]) in %s seconds)",veaf.p(nextZoneName), veaf.p(delay)))
    mist.scheduleFunction(VeafCombatZone.activate,{nextZone},timer.getTime()+delay)
    return self
end

-- desactivate the zone
function VeafCombatZone:desactivate()
    veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatZone[%s]:desactivate()",veaf.p(self.missionEditorZoneName)))
    self:setActive(false)
    self:unscheduleWatchdogFunction()

    for _, delayedSpawner in pairs(self:getDelayedSpawners()) do
        veaf.loggers.get(veafCombatZone.Id):trace("unscheduling delayed spawner %s", delayedSpawner)
        mist.removeFunction(delayedSpawner)
    end
    self:clearDelayedSpawners()

    for _, groupName in pairs(self:getSpawnedGroups()) do
        veaf.loggers.get(veafCombatZone.Id):trace(string.format("trying to destroy group [%s]",groupName))
        local group = Group.getByName(groupName)
        if not group then
            group = StaticObject.getByName(groupName)
            if group then
                veaf.loggers.get(veafCombatZone.Id):trace(string.format("found static [%s]",group:getName()))
            else
                veaf.loggers.get(veafCombatZone.Id):info(string.format("cannot find static [%s]",groupName))
            end
        end
        if group then
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("destroying group [%s]",group:getName()))
            group:destroy()
        end
    end
    self:clearSpawnedGroups()

    if self.enableJunkCleanup then
        -- remove the junk that the battle left behind
        veaf.loggers.get(veafCombatZone.Id):trace("removing the junk that the battle left behind")
        local zone = veaf.getTriggerZone(self.missionEditorZoneName)
        local volS = {
          id = world.VolumeType.SPHERE,
          params = {point = veaf.placePointOnLand(zone), radius = zone.radius}
        }
        veaf.loggers.get(veafCombatZone.Id):trace(string.format("volS=%s",veaf.p(volS)))
        local n=world.removeJunk(volS)
        veaf.loggers.get(veafCombatZone.Id):trace(string.format("world.removeJunk() returned %s",veaf.p(n)))
    end

    -- refresh the radio menu
    self:updateRadioMenu()

    return self
end

-- check if there are still units in zone
function VeafCombatZone:completionCheck()
    veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatZone[%s]:completionCheck()",veaf.p(self.missionEditorZoneName)))
    if not self:isCompletable() then
        return
    end
    local nbUnitsR = 0
    local nbUnitsB = 0

    for _, groupName in pairs(self:getSpawnedGroups()) do
        local group = Group.getByName(groupName)
        if group then
            for _, unit in pairs(group:getUnits()) do
                local coa = unit:getCoalition()
                if coa == 1 then
                    nbUnitsR = nbUnitsR + 1
                elseif coa == 2 then
                    nbUnitsB = nbUnitsB + 1
                end
            end
        else
            local static = StaticObject.getByName(groupName)
            if static then
                local coa = static:getCoalition()
                if coa == 1 then
                    nbUnitsR = nbUnitsR + 1
                elseif coa == 2 then
                    nbUnitsB = nbUnitsB + 1
                end
            end
        end
    end

    veaf.loggers.get(veafCombatZone.Id):trace(string.format("nbUnitsB=%d",nbUnitsB))
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("nbUnitsR=%d",nbUnitsR))

    if nbUnitsR == 0 then
        -- everyone is dead, let's end this mess
        if veafCombatZone.EventMessages.CombatZoneComplete then
            local message = string.format(veafCombatZone.EventMessages.CombatZoneComplete, self:getFriendlyName())
            trigger.action.outText(message, 15)
        end
        -- call the onCompleted hook
        if self.onCompletedHook then self.onCompletedHook(self) end
        -- desactivate the zone
        self:desactivate()
        -- activate the next chained zone if needed
        self:activateNextChainedZone()
    else
        -- reschedule
        self:scheduleWatchdogFunction()
    end
end


-- pop a smoke marker over the zone
function VeafCombatZone:popSmoke()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:popSmoke()",veaf.p(self.missionEditorZoneName)))
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("self:getCenter()=%s",veaf.vecToString(self:getCenter())))
    local smokePoint = self:getCenter()
    if self:isTraining() then
        -- compute the barycenter of all remaining units
        local totalPosition = {x = 0,y = 0,z = 0}
        local units, _ = veaf.safeUnpack(self:findUnitsInCombatZone())
        for count = 1,#units do
            if units[count] then
                totalPosition = mist.vec.add(totalPosition,Unit.getPosition(units[count]).p)
            end
        end
        if #units > 0 then
            smokePoint = mist.vec.scalar_mult(totalPosition,1/#units)
        end
    end
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("smokePoint=%s",veaf.vecToString(smokePoint)))
    veafSpawn.spawnSmoke(smokePoint, trigger.smokeColor.Red)
    self.smokeResetFunctionId = mist.scheduleFunction(veafCombatZone.SmokeReset,{self.missionEditorZoneName},timer.getTime()+veafCombatZone.SecondsBetweenSmokeRequests)
    trigger.action.outText(string.format(veafCombatZone.EventMessages.PopSmokeRequest, self:getFriendlyName()),5)
    self:updateRadioMenu()

    return self
end

-- pop an illumination  flare over a zone
function VeafCombatZone:popFlare()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatZone[%s]:popFlare()",veaf.p(self.missionEditorZoneName)))
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("self:getCenter()=%s",veaf.vecToString(self:getCenter())))

    veafSpawn.spawnIlluminationFlare(self:getCenter())
    self.flareResetFunctionId = mist.scheduleFunction(veafCombatZone.FlareReset,{self.missionEditorZoneName},timer.getTime()+veafCombatZone.SecondsBetweenFlareRequests)
    trigger.action.outText(string.format(veafCombatZone.EventMessages.UseFlareRequest, self:getFriendlyName()),5)
    self:updateRadioMenu()

    return self
end

-- updates the radio menu according to the zone state
function VeafCombatZone:updateRadioMenu(inBatch)
    veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatZone[%s]:updateRadioMenu(%s)",veaf.p(self.missionEditorZoneName), tostring(inBatch)))
    veaf.loggers.get(veafCombatZone.Id):debug("radioGroupName=%s", self.radioGroupName)

    -- do not update the radio menu if not yet initialized or if we don't want to
    if not self.radioParentPath or not self.enableRadioMenu then
        return self
    end

    local shouldAddSubMenu = self.enableUserActivation or self.active
    veaf.loggers.get(veafCombatZone.Id):debug("User activation enabled : %s, Zone active: %s, shouldAddSubMenu: %s", veaf.p(self.enableUserActivation), veaf.p(self.active), veaf.p(shouldAddSubMenu))

    -- reset the radio menu
    if self.radioRootPath then
        veaf.loggers.get(veafCombatZone.Id):debug("Remove the radio submenu %s", veaf.p(self:getRadioMenuName()))
        veafRadio.delSubmenu(self:getRadioMenuName(), self.radioParentPath)
        veafRadio.delSubmenu(self:getRadioMenuName(true), self.radioParentPath)
        self.radioRootPath = nil
    end
    if shouldAddSubMenu then
        veaf.loggers.get(veafCombatZone.Id):debug("add the radio submenu")
        self.radioRootPath = veafRadio.addSubMenu(self:getRadioMenuName(self:isActive()), self.radioParentPath)
    end

    if shouldAddSubMenu then
        -- populate the radio menu
        veaf.loggers.get(veafCombatZone.Id):debug("populate the radio menu")
        -- global commands
        veafRadio.addCommandToSubmenu("Get info", self.radioRootPath, veafCombatZone.GetInformationOnZone, self.missionEditorZoneName, veafRadio.USAGE_ForGroup)
        if self:isActive() then
            -- zone is active, set up accordingly (desactivate zone, get information, pop smoke, etc.)
            veaf.loggers.get(veafCombatZone.Id):debug("zone is active")
            if(self.enableUserActivation) then
                if self:isTraining() then
                    veafRadio.addCommandToSubmenu('Desactivate zone', self.radioRootPath, veafCombatZone.DesactivateZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll)
                else
                    veafRadio.addSecuredCommandToSubmenu('Desactivate zone', self.radioRootPath, veafCombatZone.DesactivateZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll)
                end
            end
            if self.enableSmokeAndFlare then
                if self.smokeResetFunctionId then
                    veafRadio.addCommandToSubmenu('Smoke not available', self.radioRootPath, veaf.emptyFunction, nil, veafRadio.USAGE_ForAll)
                else
                    veafRadio.addCommandToSubmenu('Request RED smoke on target', self.radioRootPath, veafCombatZone.SmokeZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll)
                end
                if self.flareResetFunctionId then
                    veafRadio.addCommandToSubmenu('Flare not available', self.radioRootPath, veaf.emptyFunction, nil, veafRadio.USAGE_ForAll)
                else
                    veafRadio.addCommandToSubmenu('Request illumination flare on target', self.radioRootPath, veafCombatZone.LightUpZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll)
                end
            end
        else
            -- zone is not active, set up accordingly (activate zone)
            veaf.loggers.get(veafCombatZone.Id):debug("zone is not active")
            if self.enableUserActivation then
                if self:isTraining() then
                    veafRadio.addCommandToSubmenu('Activate zone', self.radioRootPath, veafCombatZone.ActivateZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll)
                else
                    veafRadio.addSecuredCommandToSubmenu('Activate zone', self.radioRootPath, veafCombatZone.ActivateZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll)
                end
            end
        end
    end

    if not inBatch then veafRadio.refreshRadioMenu() end
    return self
end

---
--- lists all units and statics (and their groups names) in a combat zone that also match the combat zone name
---
function VeafCombatZone:findUnitsInCombatZone()
    local unitsNames = veaf.getUnitsNamesOfCoalition(true, nil) -- include statics, all coalitions
    local units = {}
    local resultUnits = {}
    local groupNames = {}
    local alreadyAddedGroups = {}
    local triggerZone = self:getTriggerZone()
    local upperTriggerzoneName = self:getMissionEditorZoneName():upper()

    if not triggerZone then
        return self
    end

    veaf.loggers.get(veafCombatZone.Id):trace("#unitsNames=%s", veaf.p(#unitsNames))

    veaf.loggers.get(veafCombatZone.Id):trace("triggerZone.type=%s", veaf.p(triggerZone.type))
    if triggerZone.type == 0 then -- circular
        units = mist.getUnitsInZones(unitsNames, {self:getMissionEditorZoneName()})
    elseif triggerZone.type == 2 then -- quad point
        units = mist.getUnitsInPolygon(unitsNames, triggerZone.verticies)
    end

    veaf.loggers.get(veafCombatZone.Id):trace("#units=%s", veaf.p(#units))

    for _, unit in pairs(units) do
        local unitName = unit:getName()
        local objectCategory = Object.getCategory(unit)
        local groupName = nil
        veaf.loggers.get(veafCombatZone.Id):trace(string.format("processing unit [%s]", unitName))
        veaf.loggers.get(veafCombatZone.Id):trace(string.format("objectCategory = [%d]", objectCategory))
        if objectCategory == 3 or objectCategory == 6 then -- 3 is static objects, 6 is cargo (a kind of static object)
            groupName = unitName -- default for static objects = groups themselves
        else
            groupName = unit:getGroup():getName()
        end
        veaf.loggers.get(veafCombatZone.Id):trace(string.format("groupName = %s", groupName))
        if string.sub(groupName:upper(),1,string.len(upperTriggerzoneName))==upperTriggerzoneName then
            resultUnits[#resultUnits + 1] = unit
            if not alreadyAddedGroups[groupName] then
                alreadyAddedGroups[groupName] = groupName
                groupNames[#groupNames + 1] = groupName
            end
        end
    end

    veaf.loggers.get(veafCombatZone.Id):trace(string.format("found %d units (%d groups) in zone", #resultUnits, #groupNames))
    return {resultUnits, groupNames}
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- VeafCombatOperationTaskingOrder object
-------------------------------------------------------------------------------------------------------------------------------------------------------------

VeafCombatOperationTaskingOrder = {
    -- combat zone of the tasking order
    zone = nil,
    -- what tasking orders needs to be completed before starting this one
    requiredCompleteNames = {}
}
VeafCombatOperationTaskingOrder.__index = VeafCombatOperationTaskingOrder

function VeafCombatOperationTaskingOrder:new(zone)
    local self = setmetatable({}, VeafCombatOperationTaskingOrder)
    self.zone = zone
    self.requiredCompleteNames = {}

    return self
end

function VeafCombatOperationTaskingOrder:setRequiredComplete(requiredCompleteNames)
    self.requiredCompleteNames = requiredCompleteNames
    return self
end

function VeafCombatOperationTaskingOrder:getZone()
    return self.zone
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- VeafCombatOperation object
-------------------------------------------------------------------------------------------------------------------------------------------------------------

VeafCombatOperation = VeafCombatZone:new()

function VeafCombatOperation:new(objectToCopy)
    local objectToCreate = objectToCopy or {} -- create object if user does not provide one
    setmetatable(objectToCreate, self)
    self.__index = self

    -- init the new object

    -- operation name (human-friendly)
    objectToCreate.friendlyName = nil
    -- technical operation name (named missionEditorZoneName not to break all zone stuffs)
    objectToCreate.missionEditorZoneName = nil
    -- mission briefing
    objectToCreate.briefing = nil
    -- operation is active
    objectToCreate.active = false
    -- list of zones used as tasking order
    objectToCreate.taskingOrderList = {}
    -- dictionnary of zones used as tasking order
    objectToCreate.taskingOrderDict = {}
    -- combat zone that we want to be completed before continuing operation
    objectToCreate.primaryTaskingOrders = {}
    -- the watchdog function checks for zone objectives completion
    objectToCreate.watchdogFunctionId = nil
    -- function to call when combat zone is over. The function is passed self combat zone
    objectToCreate.onCompletedHook = nil
    -- how many tasks were complete so far
    objectToCreate.currentCompletedTaskingOrderCount = 0

    return objectToCreate
end

---
--- setters and getters
---
function VeafCombatOperation:setOnCompletedHook(onCompletedFunction)
    self.onCompletedHook = onCompletedFunction
    return self
end

function VeafCombatOperation:getRadioMenuName()
    return self:getFriendlyName()
end

function VeafCombatOperation:getInformation()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:getInformation()",veaf.p(self.missionEditorZoneName) ))
    local message = "OPERATION "..self:getFriendlyName().." \n\n"
    if (self:getBriefing()) then
        message = message .. messageSeparator
        message = message .. self:getBriefing()
        message = message .. "\n\n"
    end

    if self:isActive() then
        message = message .. messageSeparator .. "Air Tasking Orders: \n"
        for _, primaryTaskingOrder in pairs(self.primaryTaskingOrders) do
            if primaryTaskingOrder.zone:isActive() then
                message = message .. primaryTaskingOrder:getZone():getFriendlyName() .. "\n"
            end
        end
    else
        message = message .. string.format(veafCombatZone.EventMessages.CombatOperationComplete, self:getFriendlyName())
    end

    return message
end

function VeafCombatOperation:addTaskingOrder(zone, requiredComplete)
    -- add requiredComplete in log
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:addTaskingOrder(%s)",veaf.p(self.missionEditorZoneName), veaf.p(zone.missionEditorZoneName)))
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("Adding combat zone %s to operation %s", zone.missionEditorZoneName, veaf.p(self.missionEditorZoneName)))
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("Tasks required before activation: %s", veaf.p(requiredComplete)))

    for _, mandatoryZoneName in pairs(requiredComplete or {}) do
        if(not self.taskingOrderDict[mandatoryZoneName]) then
            veaf.loggers.get(veafCombatZone.Id):error(string.format("Cannot add mandatory zone %s as it is not in known zones", veaf.p(mandatoryZoneName)))
            return self
        end
    end

    veaf.loggers.get(veafCombatZone.Id):trace("remove task order from combat zone radio menu")
    zone:disableRadioMenu()

    -- adds tasking order to the zone lists to make it accessible
    veafCombatZone.AddZone(zone)

    local newTaskingOrder = VeafCombatOperationTaskingOrder:new(zone)
        :setRequiredComplete(requiredComplete or {})

    table.insert(self.taskingOrderList, newTaskingOrder)
    self.taskingOrderDict[zone.missionEditorZoneName] = newTaskingOrder

    return self
end

-------------------
--- Other methods
-------------------
function VeafCombatOperation:scheduleWatchdogFunction()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:scheduleWatchdogFunction()", veaf.p(self.missionEditorZoneName)))
    self.watchdogFunctionId = mist.scheduleFunction(veafCombatZone.CompletionCheck,{self.missionEditorZoneName},timer.getTime()+veafCombatZone.SecondsBetweenWatchdogChecks)
    return self
end

function VeafCombatOperation:unscheduleWatchdogFunction()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:unscheduleWatchdogFunction()",veaf.p(self.missionEditorZoneName)))
    if self.watchdogFunctionId then
        mist.removeFunction(self.watchdogFunctionId)
    end
    self.watchdogFunctionId = nil
    return self
end

function VeafCombatOperation:updatePrimaryTasks()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:updatePrimaryTasks()",veaf.p(self.missionEditorZoneName)))

    veaf.loggers.get(veafCombatZone.Id):trace("Clear primary tasks")
    self.primaryTaskingOrders = {}

    veaf.loggers.get(veafCombatZone.Id):trace("Look for next tasks")
    local newPrimaryTasks = {}
    for _, candidateTaskingOrder in pairs(self.taskingOrderDict) do
        -- filter tasks that are not completed yet
        if candidateTaskingOrder:getZone():isActive() then
            local requirementFulfilled = true
            for _, requiredCombatZoneName in pairs(candidateTaskingOrder.requiredCompleteNames) do
                local requiredCombatZone = veafCombatZone.GetZone(requiredCombatZoneName)

                -- if any of required tasking order is active, then tasking order is not eligible
                if requiredCombatZone:isActive() then
                    requirementFulfilled = false
                    break
                end
            end

            if requirementFulfilled then
                table.insert(newPrimaryTasks, candidateTaskingOrder)
            end
        end
    end

    -- No task left, operation complete !
    if veaf.length(newPrimaryTasks) == 0 then
        veaf.loggers.get(veafCombatZone.Id):trace("No tasks left")
        self:desactivate()


        if veafCombatZone.EventMessages.CombatOperationComplete then
            trigger.action.outText(string.format(veafCombatZone.EventMessages.CombatOperationComplete, self.friendlyName), 10)
        end
        return self
    end


    veaf.loggers.get(veafCombatZone.Id):trace("Setting new primary tasks")
    self.primaryTaskingOrders = newPrimaryTasks
end

-- checks if primary tasks are completed to unlock next
function VeafCombatOperation:completionCheck()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:completionCheck()",veaf.p(self.missionEditorZoneName)))

    local completedTaskingOrderCount = 0
    -- if any of primary tasks is still active, then check is done
    for _, primaryTask in pairs(self.primaryTaskingOrders) do
        if not primaryTask:getZone():isActive() then
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("Primary task %s is completed",primaryTask:getZone():getFriendlyName()))
            completedTaskingOrderCount = completedTaskingOrderCount + 1
        end
    end

    veaf.loggers.get(veafCombatZone.Id):trace(string.format("%s completed out of %s, previous was %s",completedTaskingOrderCount, #self.primaryTaskingOrders, self.currentCompletedTaskingOrderCount))
    if completedTaskingOrderCount == #self.primaryTaskingOrders then
        veaf.loggers.get(veafCombatZone.Id):trace("Primary tasks complete")
        self:updatePrimaryTasks()
        self:updateRadioMenu()
        completedTaskingOrderCount = 0
        if not self:isActive() then
            return self
        end
    end

    veaf.loggers.get(veafCombatZone.Id):trace("Still got work to do.")

    if completedTaskingOrderCount ~= self.currentCompletedTaskingOrderCount then
        veaf.loggers.get(veafCombatZone.Id):trace("New tasking order completed. Update radio.")
        self:updatePrimaryTasks()
        self:updateRadioMenu()
    end
    self.currentCompletedTaskingOrderCount = completedTaskingOrderCount

    -- reschedule
    self:scheduleWatchdogFunction()


    return self
end

function VeafCombatOperation:initialize()
    veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatOperation[%s]:initialize()",veaf.p(self.missionEditorZoneName)))

    -- check parameters
    if not self.missionEditorZoneName then
        return self
    end
    if not self.friendlyName then
        self:setFriendlyName(self.missionEditorZoneName)
    end

    -- initializes  member combat zones and sets starting primary tasks
    for _, taskingOrder in pairs(self.taskingOrderDict) do
        taskingOrder:getZone():initialize()
    end


    -- deactivate the zone
    veaf.loggers.get(veafCombatZone.Id):trace("desactivate the operation")
    self:desactivate()

    return self
end

-- activate the operation
function VeafCombatOperation:activate()
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("VeafCombatOperation[%s]:activate()", veaf.p(self.missionEditorZoneName)))
    self:setActive(true)

    local primaryTasks = {}
    -- activates member combat zones and sets starting primary tasks
    veaf.loggers.get(veafCombatZone.Id):trace("activate the operation's zones")
    for _, taskingOrder in pairs(self.taskingOrderDict) do
        taskingOrder:getZone():activate()

        -- selects combat zones with no requiredComplete combat zones
        if veaf.length(taskingOrder.requiredCompleteNames) == 0 then table.insert(primaryTasks, taskingOrder) end
    end

    veaf.loggers.get(veafCombatZone.Id):trace("set primary task")
    self.primaryTaskingOrders = primaryTasks

    -- schedule the watchdog function
    self:scheduleWatchdogFunction()

    -- refresh the radio menu
    self:updateRadioMenu()

    return self
end

-- desactivate the operation
function VeafCombatOperation:desactivate()
    veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatOperation[%s]:desactivate()",veaf.p(self.missionEditorZoneName)))
    self:setActive(false)

    -- unscheduel watchdog function
    self:unscheduleWatchdogFunction()

    -- refresh the radio menu
    self:updateRadioMenu()

    return self
end

-- updates the radio menu according to the zone state
function VeafCombatOperation:updateRadioMenu(inBatch)
    veaf.loggers.get(veafCombatZone.Id):debug(string.format("VeafCombatOperation[%s]:updateRadioMenu(%s)",veaf.p(self.missionEditorZoneName), veaf.p(inBatch)))

    -- do not update the radio menu if not yet initialized
    if not veafCombatZone.rootPath then
        return self
    end

    local menuToFill = veafCombatZone.rootPath
    if(veafCombatZone.operationRootPath) then
        menuToFill = veafCombatZone.operationRootPath
    end

    -- reset the radio menu
    if self.radioRootPath then
        veaf.loggers.get(veafCombatZone.Id):trace("reset the radio submenu")
        veafRadio.clearSubmenu(self.radioRootPath)
    else
        veaf.loggers.get(veafCombatZone.Id):trace("add the radio submenu")
        self.radioRootPath = veafRadio.addSubMenu(self:getRadioMenuName(), menuToFill)
    end

    -- populate the radio menu
    veaf.loggers.get(veafCombatZone.Id):trace("populate the radio menu")
    -- global commands
    veafRadio.addCommandToSubmenu("Get info", self.radioRootPath, veafCombatZone.GetInformationOnZone, self.missionEditorZoneName, veafRadio.USAGE_ForGroup)
    for _, taskingOrder in pairs(self.primaryTaskingOrders) do
        if taskingOrder.zone:isActive() then
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("Add briefing for %s, %s", taskingOrder.zone:getFriendlyName(), taskingOrder.zone:getMissionEditorZoneName()))
            veafRadio.addCommandToSubmenu("Briefing " .. taskingOrder.zone:getFriendlyName(), self.radioRootPath, veafCombatZone.GetInformationOnZone, taskingOrder.zone:getMissionEditorZoneName(), veafRadio.USAGE_ForGroup)
        else
            veaf.loggers.get(veafCombatZone.Id):trace(string.format("Skip briefing for %s, %s as it is not active", taskingOrder.zone:getFriendlyName(), taskingOrder.zone:getMissionEditorZoneName()))
        end

    end

    if self:isActive() then
        -- zone is active, set up accordingly (desactivate zone, get information, pop smoke, etc.)
        veaf.loggers.get(veafCombatZone.Id):trace("zone is active")

        -- veafRadio.addSecuredCommandToSubmenu('Desactivate zone', self.radioRootPath, veafCombatZone.DesactivateZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll)

        -- if self.smokeResetFunctionId then 
        --     veafRadio.addCommandToSubmenu('Smoke not available', self.radioRootPath, veaf.emptyFunction, nil, veafRadio.USAGE_ForAll)
        -- else
        --     veafRadio.addCommandToSubmenu('Request RED smoke on target', self.radioRootPath, veafCombatZone.SmokeZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll)
        -- end
        -- if self.flareResetFunctionId then 
        --     veafRadio.addCommandToSubmenu('Flare not available', self.radioRootPath, veaf.emptyFunction, nil, veafRadio.USAGE_ForAll)
        -- else
        --     veafRadio.addCommandToSubmenu('Request illumination flare on target', self.radioRootPath, veafCombatZone.LightUpZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll)
        -- end
    else
        -- zone is not active, set up accordingly (activate zone)
        veaf.loggers.get(veafCombatZone.Id):trace("zone is not active")

        -- veafRadio.addSecuredCommandToSubmenu('Activate zone', self.radioRootPath, veafCombatZone.ActivateZone, self.missionEditorZoneName, veafRadio.USAGE_ForAll)
    end

    if not inBatch then veafRadio.refreshRadioMenu() end
    return self
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- global functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--------------------------------------------------------------------------------------------------------------
--- GLOBAL INTERFACE, working for both zones and operations
--------------------------------------------------------------------------------------------------------------

function veafCombatZone.GetZone(zoneName)
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.GetZone([%s])", veaf.p(zoneName)))
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("Searching for zone with name [%s]", veaf.p(zoneName)))
    if zoneName then
        local zone = veafCombatZone.zonesDict[zoneName:lower()]
        if not zone then
            local message = string.format("VeafCombatZone [%s] was not found !",zoneName)
            veaf.loggers.get(veafCombatZone.Id):error(message)
            trigger.action.outText(message,5)
        end
        return zone
    else
        return nil
    end
end

-- add a zone
function veafCombatZone.AddZone(zone)
    veaf.loggers.get(veafCombatZone.Id):debug(string.format("veafCombatZone.AddZone([%s])",veaf.p(veaf.ifnns(zone, "missionEditorZoneName"))))
    if zone then
        zone:initialize()
        table.insert(veafCombatZone.zonesList, zone)
        veafCombatZone.zonesDict[zone.missionEditorZoneName:lower()] = zone
        return zone
    else
        return nil
    end
end

-- activate a zone by number
function veafCombatZone.ActivateZoneNumber(number, silent)
    veaf.loggers.get(veafCombatZone.Id):debug(string.format("veafCombatZone.ActivateZoneNumber([%s])",veaf.p(number)))
    local zone = veafCombatZone.zonesList[number]
    if zone then
        veafCombatZone.ActivateZone(zone:getMissionEditorZoneName(), silent)
    end
end

-- activate a zone
function veafCombatZone.ActivateZone(zoneName, silent)
    veaf.loggers.get(veafCombatZone.Id):debug(string.format("veafCombatZone.ActivateZone([%s])", veaf.p(zoneName)))
    local zone = veafCombatZone.GetZone(zoneName)
    if zone then
        if zone:isActive() then
            if not silent then
                trigger.action.outText("VeafCombatZone "..zone:getFriendlyName().." is already active.", 10)
            end
            return
        end
        mist.scheduleFunction(zone.activate,{zone},timer.getTime()+1)
        if not silent then
            trigger.action.outText("VeafCombatZone "..zone:getFriendlyName().." has been activated.", 10)
            mist.scheduleFunction(veafCombatZone.GetInformationOnZone,{{zoneName}},timer.getTime()+2)
        end
        return zone
    else
        return nil
    end
end

-- desactivate a zone by number
function veafCombatZone.DesactivateZoneNumber(number, silent)
    veaf.loggers.get(veafCombatZone.Id):debug(string.format("veafCombatZone.DesactivateZoneNumber([%s])",veaf.p(number)))
    local zone = veafCombatZone.zonesList[number]
    if zone then
        veafCombatZone.DesactivateZone(zone:getMissionEditorZoneName(), silent)
    end
end

-- desactivate a zone by name
function veafCombatZone.DesactivateZone(zoneName, silent)
    veaf.loggers.get(veafCombatZone.Id):debug(string.format("veafCombatZone.DesactivateZone([%s])", veaf.p(zoneName)))
    local zone = veafCombatZone.GetZone(zoneName)
    if zone then
        if not(zone:isActive()) then
            if not silent then
                trigger.action.outText("VeafCombatZone "..zone:getFriendlyName().." is not active.", 10)
            end
            return
        end
        zone:desactivate()
        if not silent then
            trigger.action.outText("VeafCombatZone "..zone:getFriendlyName().." has been desactivated.", 10)
        end
        return zone
    else
        return nil
    end
end

-- print information about a zone
function veafCombatZone.GetInformationOnZone(parameters)
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.GetInformationOnZone([%s])",veaf.p(parameters)))
    local zoneName, unitName = veaf.safeUnpack(parameters)
    local zone = veafCombatZone.GetZone(zoneName)
    if zone then
        local text = zone:getInformation()
        if unitName then
            veaf.outTextForGroup(unitName, text, 30)
        else
            trigger.action.outText(text, 30)
        end
        return zone
    else
        return nil
    end
end

-- pop a smoke over a zone
function veafCombatZone.SmokeZone(zoneName)
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.SmokeZone([%s])", veaf.p(zoneName)))
    local zone = veafCombatZone.GetZone(zoneName)
    if zone then
        zone:popSmoke()
        return zone
    else
        return nil
    end
end

-- pop an illumination  flare over a zone
function veafCombatZone.LightUpZone(zoneName)
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.LightUpZone([%s])", veaf.p(zoneName)))
    local zone = veafCombatZone.GetZone(zoneName)
    if zone then
        zone:popFlare()
        return zone
    else
        return nil
    end
end

-- reset the "pop smoke" menus
function veafCombatZone.SmokeReset(zoneName)
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.SmokeReset([%s])", veaf.p(zoneName)))
    local zone = veafCombatZone.GetZone(zoneName)
    if zone then
        zone.smokeResetFunctionId = nil
        zone:updateRadioMenu()
        return zone
    else
        return nil
    end
end

-- reset the "pop flare" menus
function veafCombatZone.FlareReset(zoneName)
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.FlareReset([%s])", veaf.p(zoneName)))
    local zone = veafCombatZone.GetZone(zoneName)
    if zone then
        zone.flareResetFunctionId = nil
        zone:updateRadioMenu()
        return zone
    else
        return nil
    end
end

-- call the completion watchdog methods
function veafCombatZone.CompletionCheck(zoneName)
    veaf.loggers.get(veafCombatZone.Id):trace(string.format("veafCombatZone.CompletionCheck([%s])", veaf.p(zoneName)))
    local zone = veafCombatZone.GetZone(zoneName)
    if zone then
        zone:completionCheck()
        return zone
    else
        return nil
    end
end

--------------------------------------------------------------------------------------------------------------
--- END OF GLOBAL INTERFACE
--------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Radio menu and help
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Build the initial radio menu
function veafCombatZone.buildRadioMenu()
    veaf.loggers.get(veafCombatZone.Id):debug("buildRadioMenu()")

    -- don't create an empty menu
    if veaf.length(veafCombatZone.zonesDict) == 0 then
        return
    end

    veafCombatZone.rootPath = veafRadio.addMenu(veafCombatZone.RadioMenuName)
    veafCombatZone.combatZoneRootPath = veafCombatZone.rootPath

    if not(veafRadio.skipHelpMenus) then
        veafRadio.addCommandToSubmenu("HELP", veafCombatZone.rootPath, veafCombatZone.help, nil, veafRadio.USAGE_ForGroup)
    end

    if(veafCombatZone.CombatZoneRadioMenuName) then
        veafCombatZone.combatZoneRootPath = veafRadio.addSubMenu(veafCombatZone.CombatZoneRadioMenuName, veafCombatZone.rootPath)
    end

    if(veafCombatZone.OperationRadioMenuName) then
        veafCombatZone.operationRootPath = veafRadio.addSubMenu(veafCombatZone.OperationRadioMenuName, veafCombatZone.rootPath)
    end

    -- sort the zones alphabetically
    local names = {}
    local sortedZones = {}
    for _, zone in pairs(veafCombatZone.zonesDict) do
        table.insert(sortedZones, {name=zone:getMissionEditorZoneName(), sort=zone:getFriendlyName()})
    end
    local function compare(a,b)
		if not(a) then
			a = {}
		end
		if not(a["sort"]) then
			a["sort"] = 0
		end
		if not(b) then
			b = {}
		end
		if not(b["sort"]) then
			b["sort"] = 0
		end
        return a["sort"] < b["sort"]
    end
    table.sort(sortedZones, compare)
    for i = 1, #sortedZones do
        table.insert(names, sortedZones[i].name)
    end

    veaf.loggers.get(veafCombatZone.Id):trace("veafCombatZone.buildRadioMenu() - dumping names")
    for i = 1, #names do
        veaf.loggers.get(veafCombatZone.Id):trace("veafCombatZone.buildRadioMenu().names -> " .. names[i])
    end

    for _, zoneName in pairs(names) do
        local zone = veafCombatZone.GetZone(zoneName)
        if zone then
            if zone:getRadioGroupName() then
                local radioGroup = veafCombatZone.radioGroupsDict[zone:getRadioGroupName()]
                if not radioGroup then
                    -- create the radio group menu
                    radioGroup = veafRadio.addSubMenu(zone:getRadioGroupName(), veafCombatZone.combatZoneRootPath)
                    veaf.loggers.get(veafCombatZone.Id):debug("created radio group %s", zone:getRadioGroupName())
                    veafCombatZone.radioGroupsDict[zone:getRadioGroupName()] = radioGroup
                end
                zone:setRadioParentPath(radioGroup)
            else
                zone:setRadioParentPath(veafCombatZone.combatZoneRootPath)
            end
            zone:updateRadioMenu(true)
        end
    end

    veafRadio.refreshRadioMenu()
end

function veafCombatZone.help(unitName)
    local text =
        'Combat zones are defined by the mission maker\n' ..
        'You can activate and desactivate them at will,\n' ..
        'as well as ask for information, JTAC laser and smoke. \n\n' ..
        'Combat operations are defined by the mission maker\n' ..
        'A combat operation is a series of combat zones to complete,\n' ..
        'You can ask information to get briefing and intel for current tasking orders.'
        veaf.outTextForGroup(unitName, text, 30)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- initialisation
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function veafCombatZone.initialize()
    veaf.loggers.get(veafCombatZone.Id):info("Initializing module")
    veafCombatZone.buildRadioMenu()
end

veaf.loggers.get(veafCombatZone.Id):info(string.format("Loading version %s", veafCombatZone.Version))
