------------------------------------------------------------------
-- VEAF airbases information
-- By Flogas (2024)
--
-- Features:
-- ---------
-- * Extraction and normalization of airbase an runway data from DCS APIs
------------------------------------------------------------------
veafAirbases = {}

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Global module settings
-------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Identifier. All output in DCS.log will start with this.
veafAirbases.Id = "AIRBASES"

--- Version.
veafAirbases.Version = "1.1.0"

-- trace level, specific to this module
--veafAirbases.LogLevel = "trace"
veaf.loggers.new(veafAirbases.Id, veafAirbases.LogLevel)

veafAirbases.Airbases = nil

-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Local constants
-------------------------------------------------------------------------------------------------------------------------------------------------------------
local _manualRunwayNumberCorrections =
{
    -- Syria --
    ["Beirut-Rafic Hariri"] = { [34] = 3, [168] = 16 }, -- Correct RWYs: 17/35, 03/21, 16/34 ; API gives: RWY17 for 178° (ok), RWY35 for 034°, RWY03 for 168°
    ["Khalkhalah"] = { [151] = 15, [76] = 7 }, -- Correct RWYs: 15/33, 07/25 ; API gives: RWY03 for 151°, RWY03 for 076° ; note: runways have no painted numbers, 15/33 in DCS F10 airport window
    ["Minakh"] = { [42] = -1}, -- Correct RWY: 10/28 ; API gives: RWY10 for 101° (ok), RWY28 for 042° ; note: runway oriented to 042° is closed
    ["Ramat David"] = { [110] = 11, [88] = 9 }, -- Correct RWYs: 15/33, 11/29, 09/27 ; API gives: RWY15 for 146° (ok), RWY33 for 110°, RWY11 for 088°
    ["Nicosia"] = { [91] = 9 }, -- Correct RWYs: 14/32, 09/27 ; API gives: RWY32 for 328° (ok), RWY14 for 091°
    ["H3"] = { [64] = 6 }, -- Correct RWYs: 11/29, 06/24 ; API gives: RWY11 for 111° (ok), RWY29 for 064°
    ["Muwaffaq Salti"] = { [312] = 31 }, -- Correct RWYs: 08/26, 13/31 ; API gives: RWY26 for 261° (ok), RWY08 for 312°
    ["Tel Nof"] = { [184] = 18, [332] = 33 }, -- Correct RWYs: 15/33(2), 18/36 ; API gives: RWY33 for 332° (ok), RWY15 for 184°, RWY18 for 332°
    ["Ben Gurion"] = { [210] = 21, [262] = 26 }, -- Correct RWYs: 12/30, 03/21, 08/26 ; API gives: RWY12 for 123° (ok), RWY30 for 210°, RWY21 for 262°
    ["Hatzor"] = { [113] = 11 }, -- Correct RWYs: 05/23, 11/29(2) ; API gives: RWY23 for 236° (ok), RWY05 for 113°, RWY11 for 293° (ok)
    -- Nevada --
    ["Creech"] = { [145] = 13 }, -- Correct RWYs: 08/26, 13/31 ; API gives: RWY08 for 091° (ok), RWY26 for 145°
    ["Boulder City"] = { [278] = 27 }, -- Correct RWYs: 15/33, 09/27 ; API gives: RWY33 for 344° (ok), RWY15 for 278°
    ["North Las Vegas"] = { [313] = 30 }, -- Correct RWYs: 07/25, 12/30(2) ; API gives: RWY25 for 267° (ok), RWY07 for 313°, RWY30 for 133° (ok)
    ["Tonopah"] = { [345] = 33 }, -- Correct RWYs: 11/29, 15/33 ; API gives: RWY29 for 305° (ok), RWY11 for 345°
}

-------------------------------------------------------------------------------------------------------------------------------------------------------------
---  Static methods
-------------------------------------------------------------------------------------------------------------------------------------------------------------
function veafAirbases.initialize(bReset)
    bReset = bReset or false
    
    if (bReset) then
        veafAirbases.Airbases = nil
    end

    if (veafAirbases.Airbases) then
        return
    end

    veafAirbases.Airbases = {}

    local dcsAirBases = world.getAirbases()
    for i = 1, #dcsAirBases do
        local dcsAirbase = dcsAirBases[i]
--[[
        local s = string.format("%s  %s  %s  %s",
        veaf.p(dcsAirbase:getCallsign()), veaf.p(dcsAirbase:getUnit()), veaf.p(dcsAirbase:getID()), veaf.p(dcsAirbase:getCategoryEx())
        )
        veaf.loggers.get(veafAirbases.Id):trace(s)
        veaf.loggers.get(veafAirbases.Id):trace(veaf.p(dcsAirbase:getDesc()))
]]
        local veafAirbase = veafAirbase:create(dcsAirbase)
        if (veafAirbase) then
            table.insert(veafAirbases.Airbases, veafAirbase)
        end
    end
    veaf.loggers.get(veafAirbases.Id):trace("Airbases and runways initialized for theater " .. env.mission.theatre)
end

function veafAirbases.getAirbaseByName(sAirbaseName)
    veafAirbases.initialize()
    
    for _, veafAirbase in pairs(veafAirbases.Airbases) do
        if (veafAirbase.Name == sAirbaseName) then
            return veafAirbase
        end
    end

    return nil
end

function veafAirbases.getAirbaseFromDcsAirbase(dcsAirbase)
    if (dcsAirbase == nil) then
        return nil
    end

    return veafAirbases.getAirbaseByName(dcsAirbase:getName())
end

function veafAirbases.getNearestAirbaseList(dcsUnit, iCount)
    veafAirbases.initialize()
    
    iCount = iCount or 1
    local vec3Unit = dcsUnit:getPoint()
    local iMinDistance = nil
    local nearestList = {}

    local function Sort(a, b)
        if (a == nil and b == nil) then
            return false
        elseif (a == nil) then
            return false
        elseif (b == nil) then
            return true
        else
            return a[2] < b[2]
        end
    end

    for _, veafAirbase in pairs(veafAirbases.Airbases) do
        local vec3Airbase = veafAirbase.DcsAirbase:getPoint()
        local iDistance = mist.utils.get2DDist(vec3Unit, vec3Airbase)
        local bAdded = false

        -- first fill all the nil positions
        for i = 1, iCount, 1 do
            if (nearestList[i] == nil) then
                nearestList[i] = { veafAirbase, iDistance }
                bAdded = true
                break
            end
        end

        if (not bAdded) then
            -- then, replace the farthest one if the current one is closer
            for i = iCount, 1, -1 do
                if (iDistance < nearestList[i][2]) then
                    nearestList[i] = { veafAirbase, iDistance}
                    bAdded = true
                    break
                end
            end
        end

        if (bAdded) then
            table.sort(nearestList, Sort)
        end
        
    end

    return nearestList
end

function veafAirbases.getNearestAirbase(dcsUnit)
    local nearestList = veafAirbases.getNearestAirbaseList(dcsUnit, 1)
    if (nearestList and #nearestList >= 1) then
        local veafAirbase = nearestList[1][1]
        veaf.loggers.get(veafAirbases.Id):trace(string.format("Nearest airbase for [ %s ]: [ %s ] at %dm", dcsUnit:getName(), veafAirbase:toString(), nearestList[1][2]))
        return nearestList[1][1]
    else
        veaf.loggers.get(veafAirbases.Id):trace(string.format("No near airbase for [ %s ]", dcsUnit:getName()))
        return nil
    end
end

---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
---  Airbase descriptor class
---  Holds normalized information describing a DCS airbase
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
veafAirbase = {}
veafAirbase.__index = veafAirbase

---------------------------------------------------------------------------------------------------
---  CTOR
function veafAirbase:create(dcsAirbase)
    if (dcsAirbase == nil) then
        return nil
    end
    
    local sDisplayName = dcsAirbase:getName()
    local iCategory = dcsAirbase:getCategoryEx()
    local sTypeName = dcsAirbase:getTypeName()
    if (iCategory == Airbase.Category.SHIP and string.find(sTypeName, "FARP")) then
        iCategory = Airbase.Category.HELIPAD -- remediation for "FARP_SINGLE_01" or "VAP FARP" which are miscategorized
    end

    if (iCategory == Airbase.Category.SHIP) then
        local sShipType = sTypeName
        local dcsUnit = dcsAirbase:getUnit()
        if (dcsUnit) then
            local dcsUnitDesc = dcsUnit:getDesc()
            local sDcsUnitType = dcsUnit:getTypeName()

            if (dcsUnitDesc and not veaf.isNullOrEmpty(dcsUnitDesc.displayName)) then
                sShipType = dcsUnitDesc.displayName
            elseif (not veaf.isNullOrEmpty(sDcsUnitType)) then
                sShipType = sDcsUnitType
            end
        end    
        
        sDisplayName = string.format("%s(%s)", sDisplayName, sShipType)
        --[[       
        if (dcsUnit) then
            local dcsUnitName = dcsUnit:getName()
            local dcsUnitType = dcsUnit:getTypeName()
            veaf.loggers.get(veafAirbases.Id):trace(string.format(">>>>>>> unit - %s - type=%s", dcsUnitName, dcsUnitType))
            veaf.loggers.get(veafAirbases.Id):trace(veaf.p(dcsUnit:getDesc()))
            local desc = dcsUnit:getDesc()
            veaf.loggers.get(veafAirbases.Id):trace(desc.Kmax)
            veaf.loggers.get(veafAirbases.Id):trace(desc.displayName)
        end
        ]]        
    elseif (iCategory == Airbase.Category.HELIPAD) then
        sDisplayName = sDisplayName .. "(FARP)"
    end
    
    veaf.loggers.get(veafAirbases.Id):trace(string.format(">> Airbase - %s created with display name  %s", dcsAirbase:getName(), sDisplayName))


    local veafRunways = {}
    if (iCategory == Airbase.Category.AIRDROME) then
        local dcsRunways = dcsAirbase:getRunways()
        for iRunwayReportOrder, dcsRunway in pairs(dcsRunways) do
            local veafRunway = veafAirbaseRunway:create(dcsAirbase, dcsRunway, iRunwayReportOrder)
            if (veafRunway) then
                table.insert(veafRunways, veafRunway)
            end
        end
    end

    local this =
    {
        Name = dcsAirbase:getName(),
        DisplayName = sDisplayName,
        Category = iCategory,
        DcsAirbase = dcsAirbase,
        Runways = veafRunways
    }

    setmetatable(this, veafAirbase)

    return this
end

---------------------------------------------------------------------------------------------------
---  Methods
function veafAirbase:getRunwayInService(iWindDirectionTrue)
    local function _getHeadwind(iWindDirection, nRunwayHeading)
        local nAngle = math.abs(iWindDirection - nRunwayHeading)
    
        if (nAngle > 180) then
            nAngle = 360 - nAngle
        end
    
        return math.cos(math.rad(nAngle))
    end

    local bestRunwayEnd = nil
    local nBestHeadwind = -math.huge -- Start with lowest possible value
    
    for i, veafRunway in ipairs(self.Runways) do
        for _, veafRunwayEnd in ipairs(veafRunway) do
            local nHeadwind = _getHeadwind(iWindDirectionTrue, veafRunwayEnd.Heading)
            
            if nHeadwind > nBestHeadwind then
                nBestHeadwind = nHeadwind
                bestRunwayEnd = veafRunwayEnd
            end
        end
    end

    local sLog = string.format("Runway in service for [ %s ] with wind from [ %03dT ] -->", self:toString(), iWindDirectionTrue)
    if (bestRunwayEnd) then
        sLog = sLog .. string.format(" [ %02d ]", bestRunwayEnd.Number)
    else
        sLog = sLog .. " none identified"
    end

    veaf.loggers.get(veafAirbases.Id):trace(sLog)
    return bestRunwayEnd
end

function veafAirbase:getRunwayInServiceString(iWindDirectionTrue)
    local veafRunwayEnd = self:getRunwayInService(iWindDirectionTrue)
    if (veafRunwayEnd) then
        return string.format("%02d", veafRunwayEnd.Number)
    else
        return nil
    end
end


function veafAirbase:toString()
    local s = self.Name
    
    for _, veafRunway in pairs(self.Runways) do
        s = s .. string.format(" | %s", veafRunway:toString())
    end

    return s
end

---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
---  Runway descriptor class
---  Holds normalized information describing a DCS airbase runway
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
veafAirbaseRunway = {}
veafAirbaseRunway.__index = veafAirbaseRunway

---------------------------------------------------------------------------------------------------
---  CTOR
function veafAirbaseRunway:create(dcsAirbase, dcsRunway, iReportOrder)
    if (dcsAirbase == nil or dcsRunway == nil) then
        return nil
    end

    local function _normalizeHeading(nHeading)
        nHeading = nHeading % 360
        if (nHeading == 0) then nHeading = 360 end
        return nHeading
    end

    local function _normalizeNumber(iNumber)
        iNumber = iNumber % 36
        if (iNumber == 0) then iNumber = 36 end
        return iNumber
    end

    local function _numberFromHeading(nHeading)
        return mist.utils.round(nHeading / 10)
    end

    local function _numbersOffest(iOffset1, iOffest2)
        -- Calculate the absolute difference between angles
        local iOffset = math.abs(iOffset1 - iOffest2)
        local iOffsetOpposite = math.abs(36 - iOffset)
        return math.min(iOffset, iOffsetOpposite)
    end

    local sAirbaseName = dcsAirbase:getName()
    local iDcsNumber = tonumber(dcsRunway.Name)
    local nDcsHeading = _normalizeHeading(math.deg(-dcsRunway.course))

    -- correction for special misreported runways
    -- first reported runway is assumed always ok
    -- and we need to assume that since sometimes it has the same reported heading as another incorrect one (Tel Nof in Syria for ex)
    if (iReportOrder and iReportOrder > 1) then
        local manualAirbaseCorrections = _manualRunwayNumberCorrections[sAirbaseName]
        if (manualAirbaseCorrections) then
            local iCorrectedNumber = manualAirbaseCorrections[math.floor(nDcsHeading)]
            if (iCorrectedNumber and iCorrectedNumber > 0) then
                iDcsNumber = iCorrectedNumber
            elseif (iCorrectedNumber and iCorrectedNumber <= 0) then
                return nil -- for closed runways
            end
        end
    end

    --[[
    if (sAirbaseName == "Pahute Mesa") then
        veaf.loggers.get(veafAirbases.Id):trace("=======")
        veaf.loggers.get(veafAirbases.Id):trace(string.format("%d, %.2f", iDcsNumber, nDcsHeading))
        veaf.loggers.get(veafAirbases.Id):trace(veaf.p(dcsRunway))
    end
    ]]

    if (iDcsNumber == nil) then
        iDcsNumber = _numberFromHeading(nDcsHeading - veaf.getMagneticDeclination())
    end

    -- Calculate standard runway number (first two digits of heading)
    local iNumberFromHeading = _numberFromHeading(nDcsHeading)

    -- Calculate the opposite heading
    local nOppositeHeading = _normalizeHeading(nDcsHeading + 180)
    local iOppositeNumberFromHeading =  _numberFromHeading(nOppositeHeading)
    
    local iPrimaryNumber = iDcsNumber
    local iSecondaryNumber = _normalizeNumber(iDcsNumber + 18)
    local nPrimaryHeading, nSecondaryHeading

    local iFlipThreshold = 4
    if (_numbersOffest(iDcsNumber, iOppositeNumberFromHeading) <= iFlipThreshold or _numbersOffest(iDcsNumber, iNumberFromHeading) > iFlipThreshold) then
        -- If DCS number if close to computed opposite heading, or far from the DCS heading, flip the heading
        nPrimaryHeading = nOppositeHeading
        nSecondaryHeading = nDcsHeading
    else
        nPrimaryHeading = nDcsHeading
        nSecondaryHeading = nOppositeHeading
    end

    -- order the runway in ascending numbers
    local iNumber1 = iPrimaryNumber
    local nHeading1 = nPrimaryHeading
    local iNumber2 = iSecondaryNumber
    local nHeading2 = nSecondaryHeading

    if (iPrimaryNumber > iSecondaryNumber) then
        iNumber1 = iSecondaryNumber
        nHeading1 = nSecondaryHeading
        iNumber2 = iPrimaryNumber
        nHeading2 = nPrimaryHeading        
    end

    local this =
    {
        [1] = { Number = iNumber1, Heading = nHeading1 },
        [2] = { Number = iNumber2, Heading = nHeading2 },
    }

    setmetatable(this, veafAirbaseRunway)

    return this
end

---------------------------------------------------------------------------------------------------
---  Methods
function veafAirbaseRunway:toString()
    return string.format("RWY %02d(%.2fT) / %02d(%.2fT)", self[1].Number, self[1].Heading, self[2].Number, self[2].Heading)
end

---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
---  MODULE TESTS
---------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
--[[
veafAirbases.initialize()
veaf.loggers.get(veafAirbases.Id):trace("Airbases and runways initialized for theater " .. env.mission.theatre)
for _, veafAirbase in pairs(veafAirbases.Airbases) do
    veaf.loggers.get(veafAirbases.Id):trace(veafAirbase:toString())
end
]]