------------------------------------------------------------------
-- VEAF move units for DCS World
-- By mitch (2018)
--
-- Features:
-- ---------
-- * Listen to marker change events and execute move commands, with optional parameters
-- * Possibilities : 
-- *    - move a specific group to a marker point, at a specific speed
-- *    - create a new tanker flightplan, moving a specific tanker group
--
-- See the documentation : https://veaf.github.io/documentation/
------------------------------------------------------------------

--- veafMove Table.
veafMove = {}

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Global settings. Stores the script constants
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Identifier. All output in DCS.log will start with this.
veafMove.Id = "MOVE"

--- Version.
veafMove.Version = "1.10.0"

-- trace level, specific to this module
--veafMove.LogLevel = "trace"

veaf.loggers.new(veafMove.Id, veafMove.LogLevel)

--- Key phrase to look for in the mark text which triggers the command.
veafMove.Keyphrase = "_move"

veafMove.RadioMenuName = "MOVE"

veafMove.tankerMissionParameters = {
    ["A-10C"] = {speed=250, alt=12000},
    ["A-10C_2"] = {speed=250, alt=12000},
    ["AV8BNA"] = {speed=350, alt=18000},
    ["F-14A"] = {speed=400, alt=22000},
    ["F-14A-135-GR"] = {speed=400, alt=22000},
    ["F-14B"] = {speed=400, alt=22000},
    ["F-15C"] = {speed=400, alt=22000},
    ["F-15E"] = {speed=400, alt=22000},
    ["F-16A"] = {speed=400, alt=22000},
    ["F-16A MLU"] = {speed=400, alt=22000},
    ["F-16C bl.50"] = {speed=400, alt=22000},
    ["F-16C bl.52d"] = {speed=400, alt=22000},
    ["F-16C_50"] = {speed=400, alt=22000},
    ["F-4E"] = {speed=300, alt=18000},
    ["F/A-18A"] = {speed=400, alt=22000},
    ["F/A-18C"] = {speed=400, alt=22000},
    ["FA-18C_hornet"] = {speed=400, alt=22000},
    ["JF-17"] = {speed=400, alt=22000},
    ["M-2000C"] = {speed=400, alt=22000},
    ["MiG-29K"] = {speed=400, alt=22000},
    ["MiG-31"] = {speed=400, alt=22000},
    ["Mirage 2000-5"] = {speed=400, alt=22000},
    ["Su-24M"] = {speed=400, alt=22000},
    ["Su-24MR"] = {speed=400, alt=22000},
    ["Su-33"] = {speed=400, alt=22000},
    ["Su-34"] = {speed=400, alt=22000},
    ["Tornado GR4"] = {speed=400, alt=22000},
}
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Do not change anything below unless you know what you are doing!
-------------------------------------------------------------------------------------------------------------------------------------------------------------

veafMove.rootPath = nil

--- Initial Marker id.
veafMove.markid = 20000

traceMarkerId = 6548
debugMarkers = {}

veafMove.Tankers = {}
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Utility methods
-------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event handler functions.
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function executed when a mark has changed. This happens when text is entered or changed.
function veafMove.onEventMarkChange(eventPos, event)
    if veafMove.executeCommand(eventPos, event.text) then

        -- Delete old mark.
        veaf.loggers.get(veafMove.Id):trace(string.format("Removing mark # %d.", event.idx))
        trigger.action.removeMark(event.idx)

    end
end

function veafMove.executeCommand(eventPos, eventText, bypassSecurity)

    -- Check if marker has a text and the veafMove.keyphrase keyphrase.
    if eventText ~= nil and eventText:lower():find(veafMove.Keyphrase) then

        -- Analyse the mark point text and extract the keywords.
        local options = veafMove.markTextAnalysis(eventText)
        local result = false

        if options then
            -- Check options commands
            if options.moveGroup then
                result = veafMove.moveGroup(eventPos, options.groupName, options.speed, options.altitude)
            elseif options.moveTanker then
                result = veafMove.moveTanker(eventPos, options.groupName, options.speed, options.altitude, options.hdg, options.distance, options.teleport, options.silent)
            elseif options.changeTanker then
                result = veafMove.changeTanker(eventPos, options.speed, options.altitude)
            elseif options.moveAfac then
                result = veafMove.moveAfac(eventPos, options.groupName, options.speed, options.altitude, options.hdg, options.immortal)
            end
        else
            -- None of the keywords matched.
            return false
        end

        return result
    end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Analyse the mark text and extract keywords.
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Extract keywords from mark text.
function veafMove.markTextAnalysis(text)

    -- Option parameters extracted from the mark text.
    local switch = {}
    switch.moveGroup = false
    switch.moveTanker = false
    switch.changeTanker = false
    switch.moveAfac = false

    -- the name of the group to move ; mandatory
    switch.groupName = ""

    -- speed in knots
    switch.speed = -1 -- defaults to original speed

    -- tanker refuel leg altitude in feet
    switch.altitude = -1 -- defaults to tanker original altitude

    -- tanker refuel leg heading in degrees
    switch.hdg = nil -- defaults to original heading

    -- option to set AFAC to immortal
    switch.immortal = false

    -- tanker refuel leg distance in degrees
    switch.distance = nil -- defaults to original distance

    -- if true, teleport the tanker instead of simply making it move
    switch.teleport = false

    -- if false, Named Points will be created when moving the tankers
    switch.silent = false

    -- Check for correct keywords.
    if text:lower():find(veafMove.Keyphrase .. " group") then
        switch.moveGroup = true
        switch.speed = 20
    elseif text:lower():find(veafMove.Keyphrase .. " tankermission") then
        switch.changeTanker = true
        switch.speed = -1
        switch.altitude = -1
    elseif text:lower():find(veafMove.Keyphrase .. " tanker") then
        switch.moveTanker = true
        switch.speed = -1
        switch.altitude = -1
    elseif text:lower():find(veafMove.Keyphrase .. " afac") then
        switch.moveAfac = true
        switch.speed = 150
        switch.altitude = 15000
    else
        return nil
    end

    -- keywords are split by ","
    local keywords = veaf.split(text, ",")

    for _, keyphrase in pairs(keywords) do
        -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma.
        local str = veaf.breakString(veaf.trim(keyphrase), " ")
        local key = str[1]
        local val = str[2]

        if key:lower() == "name" then
            -- Set group name
            veaf.loggers.get(veafMove.Id):debug(string.format("Keyword name = %s", val))
            switch.groupName = val
        end

        if key:lower() == "speed" or key:lower() == "spd" then
            -- Set speed.
            veaf.loggers.get(veafMove.Id):debug(string.format("Keyword speed = %d", val))
            local nVal = tonumber(val)
            switch.speed = nVal
        end

        if key:lower() == "heading" or key:lower() == "hdg" then
            -- Set heading.
            veaf.loggers.get(veafMove.Id):debug(string.format("Keyword hdg = %d", val))
            local nVal = tonumber(val)
            switch.hdg = nVal
        end

        if key:lower() == "distance" or key:lower() == "dist" then
            -- Set distance.
            veaf.loggers.get(veafMove.Id):debug(string.format("Keyword distance = %d", val))
            local nVal = tonumber(val)
            switch.distance = nVal
        end

        if key:lower() == "alt" or key:lower() == "altitude" then
            -- Set altitude.
            veaf.loggers.get(veafMove.Id):debug(string.format("Keyword alt = %d", val))
            local nVal = tonumber(val)
            switch.altitude = nVal
        end

        if key:lower() == "teleport" then
            veaf.loggers.get(veafMove.Id):trace("Keyword teleport found")
            switch.teleport = true
        end

        if key:lower() == "silent" then
            veaf.loggers.get(veafMove.Id):trace("Keyword silent found")
            switch.silent = true
        end

        if key:lower() == "immortal" then
            veaf.loggers.get(veafMove.Id):trace("Keyword immortal found")
            switch.immortal = true
        end

    end

    -- check mandatory parameter "group"
    if not(switch.groupName) then return nil end
    return switch
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Group move command
-------------------------------------------------------------------------------------------------------------------------------------------------------------

------------------------------------------------------------------------------
-- veafMove.moveGroup
-- @param point eventPos
-- @param string groupName the group name to move on
-- @param float speed in knots
------------------------------------------------------------------------------
function veafMove.moveGroup(eventPos, groupName, speed, altitude)
    veaf.loggers.get(veafMove.Id):debug("veafMove.moveGroup(groupName = " .. groupName .. ", speed = " .. speed .. ", altitude=".. altitude)
    veaf.loggers.get(veafMove.Id):debug(string.format("veafMove.moveGroup: eventPos  x=%.1f z=%.1f", eventPos.x, eventPos.z))

    local result = veaf.moveGroupTo(groupName, eventPos, speed, altitude)
    if not(result) then
        trigger.action.outText(groupName .. ' not found for move group command' , 10)
    end
    return result
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Change tanker mission parameters
-------------------------------------------------------------------------------------------------------------------------------------------------------------

function veafMove.changeTanker(eventPos, speed, alt)
    veaf.loggers.get(veafMove.Id):debug(string.format("veafMove.changeTanker(speed=%s, alt=%s)", tostring(speed), tostring(alt)))
    veaf.loggers.get(veafMove.Id):trace(string.format("eventPos=%s",veaf.p(eventPos)))
    veaf.loggers.get(veafMove.Id):cleanupMarkers(debugMarkers)

    local tankerUnit = nil
    local units = veaf.findUnitsInCircle(eventPos, 2000, false)
    veaf.loggers.get(veafMove.Id):trace(string.format("units=%s", veaf.p(units)))
    if units then
        for name, _ in pairs(units) do
            -- try and find a tanker unit
            local unit = Unit.getByName(name)
            if unit and unit:getDesc()["attributes"]["Tankers"] then
                tankerUnit = unit
                break
            end
        end
    end

    if not tankerUnit then
        veaf.loggers.get(veafMove.Id):warn("Cannot find tanker unit around marker")
		trigger.action.outText("Cannot find tanker unit around marker" , 10)
        return false
    end

    local tankerGroup = tankerUnit:getGroup()
    local tankerGroupName = tankerGroup:getName()

    local tankerData = veaf.getGroupData(tankerGroupName)
    if not(tankerData) then
        local text = "Cannot move tanker " .. tankerGroupName .. " ; cannot find group data"
        veaf.loggers.get(veafMove.Id):info(text)
        trigger.action.outText(text)
        return
    end

    local route = veaf.findInTable(tankerData, "route")
    local points = veaf.findInTable(route, "points")
    if points then
        veaf.loggers.get(veafMove.Id):trace("found a " .. #points .. "-points route for tanker " .. tankerGroupName)
        -- modify the last 3 points
        local idxPoint1 = #points-2
        local idxPoint2 = #points-1
        local idxPoint3 = #points

        -- point1 is the point where the tanker mission starts ; we'll change the speed and altitude
        local point1 = points[idxPoint1]
        veaf.loggers.get(veafMove.Id):trace("found point1")
        -- set speed
        if speed > -1 then
            point1.speed = speed/1.94384  -- in m/s
        else
            speed = point1.speed*1.94384  -- in knots 
        end
        -- set altitude
        if alt > -1 then
            point1.alt = alt * 0.3048 -- in meters
        else
            alt = point1.alt / 0.3048 -- in feet
        end
        veaf.loggers.get(veafMove.Id):trace(string.format("newPoint1=%s",veaf.p(point1)))

        -- point 2 is the start of the tanking Orbit ; we'll change the speed and altitude
        local point2 = points[idxPoint2]
        veaf.loggers.get(veafMove.Id):trace("found point2")
        local foundOrbit = false
        local task1 = veaf.findInTable(point2, "task")
        if task1 then
            local tasks = task1.params.tasks
            if (tasks) then
                veaf.loggers.get(veafMove.Id):trace("found %s tasks", veaf.p(#tasks))
                for j, task in pairs(tasks) do
                    veaf.loggers.get(veafMove.Id):trace("found task #%s", veaf.p(j))
                    if task.params then
                        veaf.loggers.get(veafMove.Id):trace("has .params")
                        if task.id and task.id == "Orbit" then
                            veaf.loggers.get(veafMove.Id):debug("Found a ORBIT task for tanker " .. tankerGroupName)
                            foundOrbit = true
                            if speed > -1 then
                                task.params.speed = speed/1.94384  -- in m/s
                                point2.speed = speed/1.94384  -- in m/s
                            end
                            if alt > -1 then
                                task.params.altitude = alt * 0.3048 -- in meters
                                point2.alt = alt * 0.3048 -- in meters
                            end
                        end
                    end
                end
            end
        end
        if not foundOrbit then
            local text = "Cannot set tanker " .. tankerGroupName .. " parameters because it has no ORBIT task defined"
            veaf.loggers.get(veafMove.Id):info(text)
            trigger.action.outText(text)
            return
        end

        -- point 3 is the end of the tanking Orbit ; we'll change the speed and altitude
        local point3 = points[idxPoint3]
        veaf.loggers.get(veafMove.Id):trace("found point3")
        -- change speed
        if speed > -1 then
            point3.speed = speed/1.94384  -- in m/s
        end
        -- change altitude
        if alt > -1 then
            point3.alt = alt * 0.3048 -- in meters
        end
        veaf.loggers.get(veafMove.Id):trace("newpoint3="..veaf.p(point3))

        -- replace whole mission
        veaf.loggers.get(veafMove.Id):debug("Resetting changed tanker mission")
        -- replace the mission
        local mission = {
            id = 'Mission',
            params = tankerData
        }
        local controller = tankerGroup:getController()
        controller:setTask(mission)

        local msg = string.format("Set tanker %s to %d kn (ground) at %d ft", tankerGroupName, speed, alt)
        veaf.loggers.get(veafMove.Id):info(msg)
		trigger.action.outText(msg , 10)
        return true
    else
        return false
    end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Tanker move command
-------------------------------------------------------------------------------------------------------------------------------------------------------------

function veafMove.moveTanker(eventPos, groupName, speed, alt, hdg, distance, teleport, silent)
    veaf.loggers.get(veafMove.Id):debug(string.format("veafMove.moveTanker(groupName=%s, speed=%s, alt=%s, hdg=%s, distance=%s)",tostring(groupName), tostring(speed), tostring(alt), tostring(hdg), tostring(distance)))
    veaf.loggers.get(veafMove.Id):cleanupMarkers(debugMarkers)

    veaf.loggers.get(veafMove.Id):trace(string.format("eventPos=%s",veaf.p(eventPos)))

    local FIRSTPOINT_DISTANCE_SECONDS = 60 -- seconds to fly to WP1

    local unitGroup = Group.getByName(groupName)
	if unitGroup == nil then
        veaf.loggers.get(veafMove.Id):info(groupName .. ' not found for move tanker command')
		trigger.action.outText(groupName .. ' not found for move tanker command' , 10)
		return false
    end

    local tankerData = veaf.getGroupData(groupName)
    if not(tankerData) then
        local text = "Cannot move tanker " .. groupName .. " ; no group data"
        veaf.loggers.get(veafMove.Id):info(text)
        trigger.action.outText(text)
        return false
    end
    veaf.loggers.get(veafMove.Id):trace("tankerData : %s", veaf.p(tankerData))

    local route = veaf.findInTable(tankerData, "route")
    local points = veaf.findInTable(route, "points")
    if points then
        veaf.loggers.get(veafMove.Id):trace("found a " .. #points .. "-points route for tanker " .. groupName)
        -- modify the last 3 points
        local idxPoint1 = #points-2
        local idxPoint2 = #points-1
        local idxPoint3 = #points

        local point1 = points[idxPoint1]
        veaf.loggers.get(veafMove.Id):trace(string.format("point1=%s",veaf.p(point1)))

        local point2 = points[idxPoint2]
        veaf.loggers.get(veafMove.Id):trace(string.format("point2=%s",veaf.p(point2)))

        local point3 = points[idxPoint3]
        veaf.loggers.get(veafMove.Id):trace(string.format("point3=%s",veaf.p(point3)))

        -- if distance is not set, compute distance between point2 and point3
        local distance = distance
        if distance == nil then
            distance = math.sqrt((point3.x - point2.x)^2+(point3.y - point2.y)^2)
        else
            -- convert distance to meters
            distance = distance * 1852 -- meters
        end

        -- if hdg is not set, compute heading between point2 and point3
        local hdg = hdg
        if hdg == nil then
            hdg = veaf.headingBetweenPoints(point2, point3)
        else
            hdg = hdg * math.pi/180
        end

        -- if speed is not set, use point2 speed
        local speed = speed
        if speed == nil or speed < 0 then
            speed = point2.speed
        else
            -- convert speed to m/s
            speed = speed/1.94384
        end

        -- if alt is not set, use point2 altitude
        local alt = alt
        if alt == nil or alt < 0 then
            alt = point2.alt
        else
            -- convert altitude to meters
            alt = alt * 0.3048 -- meters
        end

        veaf.loggers.get(veafMove.Id):trace(string.format("distance=%s",veaf.p(distance)))
        veaf.loggers.get(veafMove.Id):trace(string.format("hdg=%s",veaf.p(hdg)))
        veaf.loggers.get(veafMove.Id):trace(string.format("speed=%s",veaf.p(speed)))
        veaf.loggers.get(veafMove.Id):trace(string.format("alt=%s",veaf.p(alt)))

        -- the first point in the refuel leg is based on the marker position
        local startLegPoint= { x=eventPos.x, y=eventPos.z, alt=alt, speed=speed }
        veaf.loggers.get(veafMove.Id):trace(string.format("startLegPoint=%s",veaf.p(startLegPoint)))
        if veafNamedPoints and not silent then
            veafNamedPoints.namePoint({x=startLegPoint.x, y=startLegPoint.alt, z=startLegPoint.y}, groupName .. " refuel start", unitGroup:getCoalition(), true)
        end

        -- compute the second point in the refuel leg based on desired heading and distance
        local endLegPoint= { x=startLegPoint.x, y=startLegPoint.y, alt=alt, speed=speed }
        veaf.loggers.get(veafMove.Id):trace(string.format("distance=%s",veaf.p(distance)))
        veaf.loggers.get(veafMove.Id):trace(string.format("hdg=%s",veaf.p(hdg)))
        endLegPoint.x = startLegPoint.x + distance * math.cos(hdg)
        endLegPoint.y = startLegPoint.y + distance * math.sin(hdg)
        veaf.loggers.get(veafMove.Id):trace(string.format("endLegPoint=%s",veaf.p(endLegPoint)))
        if veafNamedPoints and not silent then
            veafNamedPoints.namePoint({x=endLegPoint.x, y=endLegPoint.alt, z=endLegPoint.y}, groupName .. " refuel end", unitGroup:getCoalition(), true)
        end

        -- compute the point where the tanker should move in the opposite direction from the desired heading, at a standard distance
        local movePoint= { x=startLegPoint.x, y=startLegPoint.y, alt=alt, speed=speed }
        local teleportPoint= { x=startLegPoint.x, y=startLegPoint.y, alt=alt, speed=speed }
        local reverseHdg = hdg - math.pi
        if reverseHdg < 0 then
            reverseHdg = reverseHdg + math.pi*2
        end
        veaf.loggers.get(veafMove.Id):trace(string.format("reverseHdg=%s",veaf.p(reverseHdg)))
        movePoint.x = startLegPoint.x + 1.5 * speed * FIRSTPOINT_DISTANCE_SECONDS * math.cos(reverseHdg)
        movePoint.y = startLegPoint.y + 1.5 * speed * FIRSTPOINT_DISTANCE_SECONDS * math.sin(reverseHdg)
        teleportPoint.x = startLegPoint.x + 3 * speed * FIRSTPOINT_DISTANCE_SECONDS * math.cos(reverseHdg)
        teleportPoint.y = startLegPoint.y + 3 * speed * FIRSTPOINT_DISTANCE_SECONDS * math.sin(reverseHdg)
        veaf.loggers.get(veafMove.Id):trace(string.format("movePoint=%s",veaf.p(movePoint)))

        -- set point1 to the computed movePoint
        point1.x = movePoint.x
        point1.y = movePoint.y
        point1.alt = movePoint.alt
        point1.speed = movePoint.speed
        veaf.loggers.get(veafMove.Id):trace(string.format("newPoint1=%s",veaf.p(point1)))

        -- set point2 to the start of the tanking Orbit (startLegPoint)
        local foundOrbit = false
        local task1 = veaf.findInTable(point2, "task")
        if task1 then
            local tasks = task1.params.tasks
            if (tasks) then
                veaf.loggers.get(veafMove.Id):trace("found " .. #tasks .. " tasks")
                for j, task in pairs(tasks) do
                    veaf.loggers.get(veafMove.Id):trace(string.format("found task #%s", veaf.p(j)))
                    if task.params then
                        veaf.loggers.get(veafMove.Id):trace("has .params")
                        if task.id and task.id == "Orbit" then
                            veaf.loggers.get(veafMove.Id):debug("Found a ORBIT task for tanker " .. groupName)
                            foundOrbit = true
                            task.params.speed = speed
                            task.params.altitude = alt
                        end
                    end
                end
            end
        end
        if not foundOrbit then
            local text = "Cannot move tanker " .. groupName .. " because it has no ORBIT task defined"
            veaf.loggers.get(veafMove.Id):info(text)
            trigger.action.outText(text, 10)
            return false
        end
        point2.x = startLegPoint.x
        point2.y = startLegPoint.y
        point2.alt = startLegPoint.alt
        point2.speed = startLegPoint.speed
        veaf.loggers.get(veafMove.Id):trace(string.format("newPoint2=%s",veaf.p(point2)))

        -- set point2 to the end of the tanking Orbit (endLegPoint)
        point3.x = endLegPoint.x
        point3.y = endLegPoint.y
        point3.alt = endLegPoint.alt
        point3.speed = endLegPoint.speed
        veaf.loggers.get(veafMove.Id):trace("newpoint3="..veaf.p(point3))

        --actually move the group
        local delay = 0

        -- teleport if the option is set
        if teleport then
            veaf.loggers.get(veafMove.Id):debug("Teleport the tanker")
            local vars = { groupName = groupName, point = teleportPoint, action = "teleport"}
            local grp = mist.teleportToPoint(vars)
            unitGroup = Group.getByName(groupName)

            veafMove.teleportEscort(groupName, movePoint, teleportPoint)

            delay = 1
        end

        veaf.loggers.get(veafMove.Id):debug(string.format("Resetting moved tanker mission in %d seconds", delay))
        veafMove.replaceMission(unitGroup, tankerData, delay)

        return true
    else
        return false
    end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Escort move method, only called internally
-- @param escorted_groupName, string, corresponds to the groupname of the aicraft being escorted
-- @param movePoint, vec3 + speed, corresponds to the first waypoint that the escorted aircraft will take after it was moved
-- @param teleportPoint, vec3 + speed, corresponds to the waypoint on which the escorted aircraft is teleported to, this is required
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function veafMove.teleportEscort(escorted_groupName, movePoint, teleportPoint)

    --verify existence of the escorted aircraft
    local unitGroup = Group.getByName(escorted_groupName)

    if not unitGroup then
        veaf.loggers.get(veafMove.Id):info("Cannot move the escort of " .. escorted_groupName .. " ; this groupName does not correspond to any aircraft")
        return false
    end

    --verify the existence of the escort and proper configuration
    local escortedId = Group.getID(unitGroup) --this and only this serves as a groupID, what is given in EscortData does not correspond on the DCS side
    local groupName_escort = escorted_groupName .. " escort" --standardized escort groupName
    local unitGroup_escort = Group.getByName(groupName_escort)
    local escort_flag = false --indicates the go ahead for teleport/replaceMission calls
    local route_escort = {}
    local points_escort = {}
    local idxPoint1_escort = nil
    local idxPoint2_escort = nil
    local point1_escort = {}
    local point2_escort = {}
    local task2_escort = {}
    local tasks_escort = {}
    local task_escort = {}
    local EscortData = {}
    if unitGroup_escort ~= nil then
        EscortData = veaf.getGroupData(groupName_escort)
        if not(EscortData) then
            local text = "Cannot move Escort " .. groupName_escort .. " ; no group data"
            veaf.loggers.get(veafMove.Id):info(text)
        else
            veaf.loggers.get(veafMove.Id):trace("EscortData : %s", veaf.p(EscortData))
            route_escort = veaf.findInTable(EscortData, "route")
            points_escort = veaf.findInTable(route_escort, "points")

            if points_escort then
                veaf.loggers.get(veafMove.Id):debug("Escort has WP")
                idxPoint1_escort = #points_escort-1 --second to last waypoint
                idxPoint2_escort = #points_escort --last waypoint where the escort has to be set up in the editor
                point1_escort = points_escort[idxPoint1_escort]
                point2_escort = points_escort[idxPoint2_escort]
                task2_escort = veaf.findInTable(point2_escort, "task")
                if task2_escort.params.tasks then
                    veaf.loggers.get(veafMove.Id):debug("Last escort WP has tasks")
                    tasks_escort = task2_escort.params.tasks
                    for k, task in pairs(tasks_escort) do
                        --if task.enabled and task.id and task.id == "Escort" and task.params and task.params.groupId == unitGroup_Id then --this line should be used to verify proper configuration of the escort but as it turns out the groupId stored in params has nothing to do with the groupId DCS needs for the escort task, use Group.getID(groupClass) instead to get the correct ID required for DCS, but no way to derive it from EscortData 
                        if task.enabled and task.id and task.id == "Escort" and task.params then
                            veaf.loggers.get(veafMove.Id):trace("Found correct escort Tasking ! Extracted Escorted ID : %s", task.params.groupId)
                            veaf.loggers.get(veafMove.Id):trace("Required escort ID : %s", escortedId)
                            escort_flag = true
                            task_escort=task --recover the escort task table to insert the "new" escorted ID, even though it's the same but it seems DCS destroys it after the escorted group respawns
                        end
                    end
                end
            end
        end
    else
        veaf.loggers.get(veafMove.Id):info(groupName_escort .. ' not found for move tanker escort command')
    end

    if not escort_flag then
        return false
    end

    --distances by which the escort is offseted from the escorted group in the map's referential, task_escort provides relative spacing
    local escort_offset = {}
    local hdg = veaf.headingBetweenPoints(teleportPoint, movePoint)
    escort_offset.x = (task_escort.params.pos.x*math.cos(hdg) - task_escort.params.pos.z*math.sin(hdg))
    escort_offset.z = (task_escort.params.pos.x*math.sin(hdg) + task_escort.params.pos.z*math.cos(hdg))

    local teleportPoint_escort = {}
    teleportPoint_escort.x = teleportPoint.x + escort_offset.x
    teleportPoint_escort.y = teleportPoint.y + escort_offset.z
    teleportPoint_escort.alt = teleportPoint.alt + task_escort.params.pos.y
    teleportPoint_escort.speed = teleportPoint.speed

    --Effectively waypoint 0, the AI will have to fly over it and in the editor it never poses a problem but in scripting the AI will do orbits to try and reach it
    --so it has to be offseted
    point1_escort.x = (teleportPoint.x + movePoint.x)/2 + escort_offset.x
    point1_escort.y = (teleportPoint.y + movePoint.y)/2 + escort_offset.z
    point1_escort.alt = movePoint.alt + task_escort.params.pos.y
    point1_escort.speed = movePoint.speed

    --Waypoint 1 where the escort tasking will come into play
    point2_escort.x = 2*point1_escort.x - teleportPoint.x - escort_offset.x
    point2_escort.y =  2*point1_escort.y - teleportPoint.y - escort_offset.z
    point2_escort.alt = movePoint.alt + task_escort.params.pos.y
    point2_escort.speed = movePoint.speed

    task_escort.params.groupId = escortedId --assign the new groupID within the old escort mission, only necessary after teleporting as the tanker's ID will have changed

    veaf.loggers.get(veafMove.Id):debug("Teleport the escort")
    local vars_escort = { groupName = groupName_escort, point = teleportPoint_escort, action = "teleport"}
    local grp_escort = mist.teleportToPoint(vars_escort)
    unitGroup_escort = Group.getByName(groupName_escort)

    veafMove.replaceMission(unitGroup_escort, EscortData)
    --this method appears to not work very well, the escort just doesn't defend the group

    --mist.goRoute(groupName_escort, route_escort)
    --works even worse, sends them to X=0, Z=0

    return true
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- support method to replace the mission of moved aircraft (default delay of 1s for teleported aircraft)
-- @param unitGroup, data returned by the Group.getByName(groupName) command
-- @param missionData, data returned by the veaf.getGroupData(groupName) command
-- @optional param delay, integer, delay to apply before replacing the mission, useful when teleporting, recommended 1s for such a scenario (which is the default value)
-- @optional param immortal, boolean, sets the group which is seeing it's mission replaced to immortal and invisible
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function veafMove.replaceMission(unitGroup, missionData, delay, immortal)

    local delay = delay or 1

    local actualReplaceMission = function(unitGroup, missionData, immortal)

        local freq = missionData.frequency or 243 --set frequency or guard channel
        local mod = missionData.modulation or 0 --set modulation or AM (=0)

        veaf.loggers.get(veafMove.Id):debug(string.format("Resetting %s mission", unitGroup:getName()))
        veaf.loggers.get(veafMove.Id):debug(string.format("replaceMissionData=%s", veaf.p(missionData)))
        --... for the escort, necessary to re assign the mission wether the tanker was teleported (== changed ID) or not because DCS

        local mission = {
            id = 'Mission',
            params = missionData
        }
        local controller = unitGroup:getController()
        controller:setTask(mission)

        if immortal then
            -- JTAC needs to be invisible and immortal
            veaf.loggers.get(veafMove.Id):trace("Group immortalized")
            local _setImmortal = {
                id = 'SetImmortal',
                params = {
                    value = true
                }
            }
            -- invisible to AI, Shagrat
            local _setInvisible = {
                id = 'SetInvisible',
                params = {
                    value = true
                }
            }

            Controller.setCommand(controller, _setImmortal)
            Controller.setCommand(controller, _setInvisible)
        end

        --have to set the frequency again as setTask seems to ignore missionData.frequency and switch the unit to 124AM
        local _setFrequency = {
            id = 'SetFrequency',
            params = {
                frequency = freq * 1000000,
                modulation = mod
            }
        }

        Controller.setCommand(controller, _setFrequency)
    end

    mist.scheduleFunction(actualReplaceMission, {unitGroup, missionData, immortal}, timer.getTime()+delay)
end

------------------------------------------------------------------------------
-- veafMove.moveAfac
-- @param point eventPos
-- @param string groupName 
-- @param float speed in knots
-- @param float alt in feet
-- @param float hdg in degrees
-- @param boolean immortal
------------------------------------------------------------------------------
function veafMove.moveAfac(eventPos, groupName, speed, alt, heading, immortal)
    if not speed then
        speed = 150
    end
    if not alt then
        alt = 20000
    end
    veaf.loggers.get(veafMove.Id):debug("veafMove.moveAfac(groupName = " .. groupName .. ", speed = " .. speed .. ", alt = " .. alt)
    veaf.loggers.get(veafMove.Id):debug(string.format("veafMove.moveAfac: eventPos  x=%.1f z=%.1f", eventPos.x, eventPos.z))

    local distanceFromTeleport = 3000 --distance between the orbit point and the teleport point in meters

	local unitGroup = Group.getByName(groupName)
	if unitGroup == nil then
        veaf.loggers.get(veafMove.Id):info(groupName .. ' not found for move afac command')
		trigger.action.outText(groupName .. ' not found for move afac command' , 10)
		return false
	end

    local coalition = unitGroup:getCoalition()

    local afacData = veaf.getGroupData(groupName)
    local isDynamicallySpawned = false
    if not afacData then
        for number, dynAFACcallsign in pairs(veafSpawn.AFAC.callsigns[coalition]) do
            if groupName:find(dynAFACcallsign.name) then
                veaf.loggers.get(veafMove.Id):trace("AFAC is dynamically spawned")
                afacData = veafSpawn.AFAC.missionData[coalition][number]
                isDynamicallySpawned = true
            end
        end
    end
    veaf.loggers.get(veafMove.Id):trace("Found AFAC named " .. groupName .. " for move command")
    veaf.loggers.get(veafMove.Id):debug(string.format("AFAC mission data is : %s", veaf.p(afacData)))

    local route_afac = veaf.findInTable(afacData, "route")
    local points_afac = veaf.findInTable(route_afac, "points")
    if points_afac then

        veaf.loggers.get(veafMove.Id):trace("Found AFAC waypoints")
        local idxPoint1_afac = #points_afac-1 --second to last waypoint
        local idxPoint2_afac = #points_afac --last waypoint
        local point1_afac = points_afac[idxPoint1_afac]
        local point2_afac = points_afac[idxPoint2_afac]
        local FACflag = false
        local OrbitFlag = false

        -- if hdg is not set, compute heading between point1 and point2
        local hdg = heading
        if hdg == nil then
            hdg = veaf.headingBetweenPoints(point1_afac, point2_afac)
        else
            hdg = heading * math.pi/180
        end

        -- teleport position
        local teleportPosition = {
            ["x"] = eventPos.x - distanceFromTeleport*math.cos(hdg), --teleport 3km south of orbit point
            ["y"] = eventPos.z - distanceFromTeleport*math.sin(hdg),
            ["alt"] = alt * 0.3048 -- in meters
        }

        -- orbit position
        local fromPosition = {
            ["x"] = eventPos.x,
            ["y"] = eventPos.z
        }

        --check valid configuration of the AFAC
        if point1_afac and point2_afac then
            veaf.loggers.get(veafMove.Id):debug("AFAC has at least the two waypoints required")
            local tasks1_afac=point1_afac.task.params.tasks
            local tasks2_afac=point2_afac.task.params.tasks
            if tasks1_afac and tasks2_afac then
                for _, task in pairs(tasks1_afac) do
                    if task.id == "FAC" then
                        veaf.loggers.get(veafMove.Id):trace("FAC configuration valid on second to last WP")
                        FACflag = true
                    end
                end

                for _, task in pairs(tasks2_afac) do
                    if task.id == "Orbit" then
                        veaf.loggers.get(veafMove.Id):trace("AFAC Orbit configuration valid on last WP")
                        OrbitFlag = true
                    end
                end
            end
        end

        if FACflag == false or OrbitFlag == false then
            veaf.loggers.get(veafMove.Id):info(groupName .. ' has an invalid FAC/Orbit configuration')
            trigger.action.outText(groupName .. ' has an invalid FAC/Orbit configuration' , 10)
            return false
        end

        --edit the last two waypoints of the AFAC's flight plan with the new requested position,speed and alt info
        point1_afac.speed=speed
        point1_afac.alt=teleportPosition.alt
        point1_afac.x=teleportPosition.x + distanceFromTeleport*math.cos(hdg)/2
        point1_afac.y=teleportPosition.y + distanceFromTeleport*math.sin(hdg)/2

        point2_afac.speed=speed
        point2_afac.alt=teleportPosition.alt
        point2_afac.x=eventPos.x
        point2_afac.y=eventPos.z

        --teleport the group south of the requested location
        veaf.loggers.get(veafMove.Id):trace("AFAC ".. groupName .. " teleported")
        local vars = { groupName = groupName, point = teleportPosition, action = "teleport" }
        if isDynamicallySpawned then
            vars = { groupName = groupName, groupData = afacData, anyTerrain = true, point = teleportPosition, action = "teleport" }
        end
        local grp = mist.teleportToPoint(vars)
        unitGroup = Group.getByName(groupName) --refresh group class after respawn, not necessary but safer considering at least the groupId changes

        --necessary delay for the following code to not be ignored
        local delay=1

        -- replace whole mission
        veafMove.replaceMission(unitGroup, afacData, delay, immortal)

        return true
    else
        return false
    end

end

-- prepare tanker units
function veafMove.findAllTankers()
    local TankerTypeNames = {"KC130", "KC-135", "KC135MPRS", "KJ-2000", "IL-78M"}
    veaf.loggers.get(veafMove.Id):trace(string.format("findAllTankers()"))
    local result = {}
    local units = mist.DBs.unitsByName -- local copy for faster execution
    for name, unit in pairs(units) do
        veaf.loggers.get(veafMove.Id):trace(string.format("name=%s, unit.type=%s", veaf.p(name), veaf.p(unit.type)))
        --veaf.loggers.get(veafMove.Id):trace(string.format("unit=%s", veaf.p(unit)))
        --local unit = Unit.getByName(name)
        if unit then
            for _, tankerTypeName in pairs(TankerTypeNames) do
                if tankerTypeName:lower() == unit.type:lower() then
                    table.insert(result, unit.groupName)
                end
            end
        end
    end
    return result
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Radio menu and help
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Build a radio menu to move or teleport a tanker
function veafMove.moveTankerToMe(parameters)
    local subParameters, unitName = veaf.safeUnpack(parameters)
    local tankerName, direction = veaf.safeUnpack(subParameters)
    veaf.loggers.get(veafMove.Id):debug(string.format("veafMove.moveTankerToMe(tankerName=%s, unitName=%s, direction=%d)", tankerName, unitName, direction))
    local unit = Unit.getByName(unitName)
    if unit then
        local unitType = unit:getDesc()["typeName"]
        veaf.loggers.get(veafMove.Id):trace(string.format("checking unit %s of type %s", tostring(unitName), tostring(unitType)))
        local tankerMissionParameters = veafMove.tankerMissionParameters[unitType]
        if not tankerMissionParameters then
            tankerMissionParameters = { speed = -1, alt = -1}  -- -1 means to use the currently defined speed and altitude
        end
        veafMove.moveTanker(unit:getPosition().p, tankerName, tankerMissionParameters.speed, tankerMissionParameters.alt, direction, nil, true, false)
        veaf.outTextForUnit(unitName, string.format("%s - Moving to your position right away !", tankerName), 15)
    end
end

--- Build the initial radio menu
function veafMove.buildRadioMenu()
    veaf.loggers.get(veafMove.Id):debug(string.format("veafMove.buildRadioMenu()"))
    veafMove.rootPath = veafRadio.addSubMenu(veafMove.RadioMenuName)
    if not(veafRadio.skipHelpMenus) then
        veafRadio.addCommandToSubmenu("HELP", veafMove.rootPath, veafMove.help, nil, veafRadio.USAGE_ForGroup)
    end
    for _, tankerUnitName in pairs(veafMove.Tankers) do
        local tankerName = tankerUnitName
        if veafAssets then
            veaf.loggers.get(veafMove.Id):trace(string.format("searching for asset name %s", tankerUnitName))
            local asset = veafAssets.get(tankerUnitName)
            if asset then
                tankerName = asset.description
                veaf.loggers.get(veafMove.Id):trace(string.format("found asset name : %s", tankerName))
            end
        end
        -- Move tanker to me
        local menuName = string.format("%s - WEST", tankerName)
        local moveTankerPath = veafRadio.addSubMenu(menuName, veafMove.rootPath)
        veafRadio.addCommandToSubmenu(menuName, moveTankerPath, veafMove.moveTankerToMe, {tankerUnitName, 270}, veafRadio.USAGE_ForGroup)

        menuName = string.format("%s - EAST", tankerName)
        moveTankerPath = veafRadio.addSubMenu(menuName, veafMove.rootPath)
        veafRadio.addCommandToSubmenu(menuName, moveTankerPath, veafMove.moveTankerToMe, {tankerUnitName, 90}, veafRadio.USAGE_ForGroup)
    end
end

function veafMove.help(unitName)
    local text =
        'Create a marker and type "_move <group|tanker|afac>, name <groupname> " in the text\n' ..
        'This will issue a move command to the specified group in the DCS world\n' ..
        'Type "_move group, name [groupname]" to move the specified group to the marker point\n' ..
        '     add ", speed [speed]" to make the group move and at the specified speed (in knots)\n' ..
        'Type "_move tanker, name [groupname]" to create a new tanker flight plan and move the specified tanker.\n' ..
        '     add ", speed [speed]" to make the tanker move and execute its refuel mission at the specified speed (in knots)\n' ..
        '     add ", alt [altitude]" to specify the refuel leg altitude (in feet)\n' ..
        'Type "_move afac, name [groupname]" to create a new JTAC flight plan and move the specified afac drone.\n' ..
        '     add ", speed [speed]" to make the tanker move and execute its mission at the specified speed (in knots)\n' ..
        '     add ", alt [altitude]" to specify the altitude at which the drone will circle (in feet)'
    veaf.outTextForGroup(unitName, text, 30)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- initialisation
-------------------------------------------------------------------------------------------------------------------------------------------------------------

function veafMove.initialize()
    if #veafMove.Tankers == 0 then
        -- find all existing Tankers
        veafMove.Tankers = veafMove.findAllTankers()
    end
    veafMove.buildRadioMenu()
    veafMarkers.registerEventHandler(veafMarkers.MarkerChange, veafMove.onEventMarkChange)
end

veaf.loggers.get(veafMove.Id):info(string.format("Loading version %s", veafMove.Version))
