-- ====================================================================================
-- DCSEX.DCS - FUNCTIONS HANDLING DCS WORLD'S GAME RULES AND TABLES
-- ====================================================================================
-- DCSEx.dcs.getBRAA(point, refPoint, showAltitude, metricSystem, casualFormat)
-- DCSEx.dcs.getCJTFForCoalition(coalitionID)
-- DCSEx.dcs.getCoalitionAsString(coalitionID)
-- DCSEx.dcs.getCoalitionColor(coalitionID, alpha)
-- DCSEx.dcs.getFirstUnitCallsign(group)
-- DCSEx.dcs.getGroupCenterPoint(group)
-- DCSEx.dcs.getGroupIDAsNumber(group)
-- DCSEx.dcs.getNearestObject(refPoint, objectTable)
-- DCSEx.dcs.getNearestObjects(refPoint, objectTable, maxCount)
-- DCSEx.dcs.getNearestPoints(refPoint, pointsTable, maxCount)
-- DCSEx.dcs.getObjectIDAsNumber(obj)
-- DCSEx.dcs.getOppositeCoalition(coalitionID)
-- DCSEx.dcs.getPlayerUnitsInGroup(group)
-- DCSEx.dcs.getPlayerUnitsInGroupByID(groupID)
-- DCSEx.dcs.getRadioModulationName(modulationID)
-- DCSEx.dcs.getUnitCategoryFromFamily(unitFamily)
-- DCSEx.dcs.loadMission(fileName)
-- DCSEx.dcs.outPicture(fileName, durationSeconds, clearView, startDelay, horizontalAlign, verticalAlign, size, sizeUnits)
-- ====================================================================================

DCSEx.dcs = { }

-------------------------------------
-- Gets a BRAA (bearing, range, altitude, aspect) string about a point
-- Format is "[bearing to unit] for [distance] at [altitude]"
-------------------------------------
-- @param unit A unit
-- @param refPoint Reference point for the bearing and distance
-- @param showAltitude Should altitude be displayed?
-- @param metricSystem Should metric system units be used?
-- @param casualFormat Should the format be casual ("32nm northeast" instead of "45 for 32")?
-- @return BRAA, as a string
-------------------------------------
function DCSEx.dcs.getBRAA(point, refPoint, showAltitude, metricSystem, casualFormat)
    casualFormat = casualFormat or false
    metricSystem = metricSystem or false
    showAltitude = showAltitude or false

    if not point.z then point = DCSEx.math.vec2ToVec3(point, "land") end
    if not refPoint.z then refPoint = DCSEx.math.vec2ToVec3(refPoint, "land") end

    local braa = nil

    braa = tostring(DCSEx.math.getBearing(point, refPoint, casualFormat))

    local distance = DCSEx.math.getDistance2D(point, refPoint)
    if metricSystem then
        distance = distance / 1000.0
    else
        distance = DCSEx.converter.metersToNM(distance)
    end

    if casualFormat then 
        if metricSystem then
            braa = math.ceil(distance).."km "..braa
        else
            braa = math.ceil(distance).."nm "..braa
        end
    else
        braa = braa .. " for " .. tostring(math.ceil(distance))
    end

    if showAltitude then
        local altitude = point.y
        if metricSystem then
            altitude = math.floor(point.y / 100) * 100
        else
            altitude = math.floor(DCSEx.converter.metersToFeet(point.y) / 1000) * 1000
        end
        braa = braa .. " at " .. tostring(altitude)
        if casualFormat then
            if metricSystem then
                braa = braa.."m"
            else
                braa = braa.."ft"
            end
        end
    end

    return braa
end

-------------------------------------
-- Returns the CJTF country for a given coalition
-------------------------------------
-- @param A coalition ID
-- @return A country ID (country.id.CJTF_BLUE or country.id.CJTF_RED)
-------------------------------------
function DCSEx.dcs.getCJTFForCoalition(coalitionID)
    if coalitionID == coalition.side.RED then return country.id.CJTF_RED end

    return country.id.CJTF_BLUE
end

-------------------------------------
-- Returns the name of a coalition, as a string ("blue", "red" or "neutral")
-------------------------------------
-- @param A coalition ID
-- @return A string
-------------------------------------
function DCSEx.dcs.getCoalitionAsString(coalitionID)
    if coalitionID == coalition.side.NEUTRAL then return "neutral" end
    if coalitionID == coalition.side.RED then return "red" end
    if coalitionID == coalition.side.BLUE then return "blue" end
    return nil
end

-------------------------------------
-- Returns the RGBA color table for the given coalition, accoding to NATO symbology colors
-------------------------------------
-- @param coalitionID A coalition side
-- @param alpha (optional) Alpha. Default is 1.0
-- @return A RGBA color table
-------------------------------------
function DCSEx.dcs.getCoalitionColor(coalitionID, alpha)
    alpha = alpha or 1.0

    if coalitionID == coalition.side.RED then return {0.97, 0.52, 0.51, alpha}
    elseif coalitionID == coalition.side.BLUE then return {0.52, 0.87, 0.99, alpha}
    else
        return {0.5, 0.5, 0.5, alpha}
    end
end

-------------------------------------
-- Returns the callsign table for the first unit of the group
-------------------------------------
-- @param group A group
-- @return A callsign table, or nil if no units or no group
-------------------------------------
function DCSEx.dcs.getFirstUnitCallsign(group)
    if not group then return nil end

    local units = group:getUnits()
    if not units then return nil end

    local unit0 = units[1]
    if not unit0 then return nil end
    return unit0:getCallsign()
end

-------------------------------------
-- Returns a vec3 point at the center of all units of a group
-------------------------------------
-- @param group A group object
-- @return A vec3
-------------------------------------
function DCSEx.dcs.getGroupCenterPoint(group)
    if not group then return nil end
    local units = group:getUnits()
    if not units or #units == 0 then return nil end

    local centerPoint = { x = 0, y = 0, z = 0}
    for _,u in pairs(units) do
        local uPoint = u:getPoint()
        centerPoint.x = centerPoint.x + uPoint.x
        centerPoint.y = centerPoint.y + uPoint.y
        centerPoint.z = centerPoint.z + uPoint.z
    end

    centerPoint.x = centerPoint.x / #units
    centerPoint.y = centerPoint.y / #units
    centerPoint.z = centerPoint.z / #units

    return centerPoint
end

-------------------------------------
-- Returns the ID of a group as a number (here to fix a bug where sometimes ID is returned as a string)
-------------------------------------
-- @param group A group table
-- @return An ID (as an number) or nil if group is nil or has no ID
-------------------------------------
function DCSEx.dcs.getGroupIDAsNumber(group)
    if not group then
        return nil
    end

    return tonumber(group:getID())
end

-------------------------------------
-- Returns the object nearest (in a 2D plane) from a given point
-------------------------------------
-- @param refPoint A reference point, as a vec2 or vec3
-- @param objectTable A table of DCS objects
-- @return The object nearest from the point, or nil if no object was found
-------------------------------------
function DCSEx.dcs.getNearestObject(refPoint, objectTable)
    local nearestObjects = DCSEx.dcs.getNearestObjects(refPoint, objectTable, 1)
    if #nearestObjects == 0 then return nil end
    return nearestObjects[1]
end

-------------------------------------
-- Returns the nearest objects (in a 2D plane) from a given point
-------------------------------------
-- @param refPoint A reference point, as a vec2 or vec3
-- @param objectTable A table of DCS objects
-- @param maxCount (optional) Maximum number of objects to return
-- @return A table of objects, sorted by distance
-------------------------------------
function DCSEx.dcs.getNearestObjects(refPoint, objectTable, maxCount)
    if not refPoint then return DCSEx.table.deepCopy(objectTable) end

    refPoint = DCSEx.math.vec3ToVec2(refPoint)
    local sortedObjects = {}
    local sortedKeys = {}

    for i,o in pairs(objectTable) do
        local distance = DCSEx.math.getDistance2D(refPoint, o:getPoint())
        sortedObjects[distance] = i
        table.insert(sortedKeys, distance)
    end

    table.sort(sortedKeys)

    local sortedList = {}
    for _,v in ipairs(sortedKeys) do
        if maxCount and #sortedList >= maxCount then break end
        table.insert(sortedList, objectTable[sortedObjects[v]])
    end

    return sortedList
end

-------------------------------------
-- Returns the nearest points (in a 2D plane) from a given point
-------------------------------------
-- @param refPoint A reference point, as a vec2 or vec3
-- @param objectTable A table of points (vec2 or vec3)
-- @param maxCount (optional) Maximum number of points to return
-- @return A table of points, sorted by distance
-------------------------------------
function DCSEx.dcs.getNearestPoints(refPoint, pointsTable, maxCount)
    if not refPoint then return DCSEx.table.deepCopy(pointsTable) end

    refPoint = DCSEx.math.vec3ToVec2(refPoint)
    local sortedPoints = {}
    local sortedKeys = {}

    for i,pt in pairs(pointsTable) do
        local distance = DCSEx.math.getDistance2D(refPoint, pt)
        sortedPoints[distance] = i
        table.insert(sortedKeys, distance)
    end

    table.sort(sortedKeys)

    local sortedList = {}
    for _,v in ipairs(sortedKeys) do
        if maxCount and #sortedList >= maxCount then break end
        table.insert(sortedList, pointsTable[sortedPoints[v]])
    end

    return sortedList
end

-------------------------------------
-- Returns the coalition opposed to the provided coalition (coalition.side.NEUTRAL still returns NEUTRAL)
-------------------------------------
-- @param group A coalition
-- @return Another coalition
-------------------------------------
function DCSEx.dcs.getOppositeCoalition(coalitionID)
    if coalitionID == coalition.side.RED then return coalition.side.BLUE end
    if coalitionID == coalition.side.BLUE then return coalition.side.RED end
    return coalition.side.NEUTRAL
end

-------------------------------------
-- Returns all player-controlled units in a group
-------------------------------------
-- @param group A group object
-- @return A table of unit objects
-------------------------------------
function DCSEx.dcs.getPlayerUnitsInGroup(group)
    if not group then return nil end
    local units = group:getUnits()
    if not units then return { } end

    local players = {}
    for _,u in pairs(units) do
        if u:getPlayerName() then
            table.insert(players, u)
        end
    end

    return players
end

-------------------------------------
-- Returns all player-controlled units in the group with the given ID
-------------------------------------
-- @param groupID A group ID
-- @return A table of unit objects
-------------------------------------
function DCSEx.dcs.getPlayerUnitsInGroupByID(groupID)
    return DCSEx.dcs.getPlayerUnitsInGroup(DCSEx.world.getGroupByID(groupID))
end

-------------------------------------
-- Returns a radio modulation type as a string
-------------------------------------
-- @param modulationID A modulation ID (from radio.modulation enum)
-- @return A string
-------------------------------------
function DCSEx.dcs.getRadioModulationName(modulationID)
    if modulationID == radio.modulation.FM then return "FM" end
    return "AM"
end

-------------------------------------
-- Returns the ID of an object as a number (here to fix a bug where sometimes ID is returned as a string)
-------------------------------------
-- @param obj An object (unit, static object...)
-- @return An ID (as an number) or nil if unit is nil or has no ID
-------------------------------------
function DCSEx.dcs.getObjectIDAsNumber(obj)
    if not obj then return nil end
    return tonumber(obj:getID())
end

-------------------------------------
-- Returns a the unit category (Unit.Category enum) an unit family (DCSEx.enums.unitFamily) belongs to
-------------------------------------
-- @param unitFamily A value from the Unit.Category enum
-- @return A value from the DCSEx.enums.unitFamily enum
-------------------------------------
function DCSEx.dcs.getUnitCategoryFromFamily(unitFamily)
    return math.floor(unitFamily / 100)
end

-------------------------------------
-- Loads another DCS World mission
-------------------------------------
-- @param fileName Filename of the mission
-------------------------------------
function DCSEx.dcs.loadMission(fileName)
    net.dostring_in("mission", string.format("a_load_mission(\"%s\")", fileName))
end

-------------------------------------
-- Displays a picture on the screen of ALL players
-------------------------------------
-- @param fileName Filename/ResourceName of the image in the mission resources
-- @param durationSeconds Duration (in seconds) during which the image should be displayed
-- @param startDelay After how many seconds should the image be displayed? (default: 0)
-- @param horizontalAlign Horizontal alignment of the image (0/1/2=left/center/right) (default: 1)
-- @param verticalAlign Vertical alignment of the image (0/1/2=top/center/bottom) (default: 1)
-- @param size Size of the image, in pixels or % of the screen (see sizeUnits) (default: 100)
-- @param sizeUnits If 0, the size parameter is in pixels. If 1, it's in % of screen size (default: 0)
-------------------------------------
function DCSEx.dcs.outPicture(fileName, durationSeconds, clearView, startDelay, horizontalAlign, verticalAlign, size, sizeUnits)
    clearView = clearView or false
    startDelay = startDelay or 0
    horizontalAlign = horizontalAlign or 1
    verticalAlign = verticalAlign or 1
    size = size or 100
    sizeUnits = sizeUnits or 0

    if clearView then clearView = "true" else clearView = "false" end

    net.dostring_in("mission", string.format("a_out_picture(getValueResourceByKey(\"%s\"), %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", fileName, durationSeconds, clearView, startDelay, horizontalAlign, verticalAlign, size, sizeUnits))
end
