---------------------------------------------------------------
-- COBRA COUNTER BATTERY RADAR  v1 
-- by Oneeyed Simulations/Don Rudi
--
-- Trigger: FIRST shot of a group → COBRA-Delay → Report.
-- Further shots of the group will be ignored, as long as the
-- position has not moved more than SCOOT_THRESHOLD meters.
-- Shoot & Scoot: new position > SCOOT_THRESHOLD → new report.
---------------------------------------------------------------

-- ============================================================
-- CONFIGURATION
-- ============================================================

local CBR = {}

-- Position format:
--   true  = Bullseye (Bearing in degrees / distance in NM)
--   false = MGRS
CBR.USE_BULLSEYE = false

-- Simulated radar data processing delay after the first shot.
-- Real COBRA/Firefinder: 15-30s.
CBR.COBRA_DELAY = 15

-- Message display time in seconds
CBR.MESSAGE_DURATION = 90

-- Accuracy of the detected position for MGRS output in meters.
-- Set 1 / 10 / 100 / 1000
CBR.ACCURACY = 100

-- Minimum movement in meters before a position change is
-- detected (shoot & scoot detection).
CBR.SCOOT_THRESHOLD = 250

-- F10 map marker on each detection
CBR.MAP_MARKER         = true   -- true = place marker, false = disable
CBR.MAP_MARKER_TIMEOUT = 300    -- seconds until marker is removed, 0 = permanent

-- Radar mode:
--   "magic"      = unlimited range, no unit needed
--   "firefinder" = unit with RADAR_KEYWORD required, range limited
--   "auto"       = firefinder mode if unit found, otherwise magic
CBR.RADAR_MODE = "auto"

-- Keyword searched in unit names to identify the Firefinder unit.
-- Unit must be on the friendly side.
-- Recommended DCS unit type: HAWK Sr (used as proxy).
CBR.RADAR_KEYWORD = "FIREFINDER"

-- Detection range in meters when in firefinder mode.
-- AN/TPQ-36: ~18km, AN/TPQ-37: ~30km, COBRA: ~40km
CBR.RADAR_RANGE = 20000

-- Behaviour when Firefinder unit has been destroyed:
--   "silent" = no more reports until unit is replaced/respawned
--   "magic"  = fall back to unlimited range
CBR.RADAR_IF_DESTROYED = "silent"

-- How often (seconds) to re-scan for the Firefinder unit.
-- Handles late-spawned or destroyed/replaced units.
CBR.RADAR_SCAN_INTERVAL = 30

-- Coalitions
-- Set own coalition to "blue" or "red"
CBR.OWN_COALITION = "blue"

if CBR.OWN_COALITION == "blue" then
    CBR.ENEMY      = coalition.side.RED
    CBR.FRIENDLIES = coalition.side.BLUE
else
    CBR.ENEMY      = coalition.side.BLUE
    CBR.FRIENDLIES = coalition.side.RED
end

-- ============================================================
-- >>> NO USER EDITS BELOW THIS LINE <<<
-- ============================================================

-- ============================================================
-- STATE TABLES
--
-- CBR.pending[gName]  = {lat, lon, dcsPos, unitType}
--     Group has fired first shot, timer is running.
--     Further shots in the vicinity will be ignored.
--
-- CBR.reported[gName] = {lat, lon, dcsPos}
--     Message already displayed. Further shots will be ignored
--     until the group moves > SCOOT_THRESHOLD.
--
-- CBR.radar.units  = list of active Firefinder unit objects
-- CBR.radar.status = "magic" | "active" | "destroyed" | "absent"
-- ============================================================

CBR.pending  = {}
CBR.reported = {}
CBR.radar    = { units = {}, status = "magic" }
CBR.markId   = 1000  -- incrementing unique ID for F10 map markers

-- ============================================================
-- HELP FUNCTIONS: GEOMETRY
-- ============================================================

-- Euclidean 2D distance using DCS coordinates (x/z = horizontal).
local function distanceM(p1, p2)
    local dx = p1.x - p2.x
    local dz = p1.z - p2.z
    return math.sqrt(dx * dx + dz * dz)
end

-- True Bearing from A to B (degrees, 0-360)
local function trueBearing(lat1, lon1, lat2, lon2)
    local dL   = math.rad(lon2 - lon1)
    local phi1 = math.rad(lat1)
    local phi2 = math.rad(lat2)
    local x = math.sin(dL) * math.cos(phi2)
    local y = math.cos(phi1) * math.sin(phi2)
          - math.sin(phi1) * math.cos(phi2) * math.cos(dL)
    return (math.deg(math.atan2(x, y)) + 360) % 360
end

-- Haversine distance in nautical miles
local function distanceNM(lat1, lon1, lat2, lon2)
    local R    = 6371000
    local dphi = math.rad(lat2 - lat1)
    local dlam = math.rad(lon2 - lon1)
    local a = math.sin(dphi / 2)^2
            + math.cos(math.rad(lat1)) * math.cos(math.rad(lat2))
            * math.sin(dlam / 2)^2
    return (R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))) / 1852
end

-- ============================================================
-- HELP FUNCTIONS: POSITION FORMATS
-- ============================================================

local function formatBullseye(lat, lon)
    local bVec = coalition.getMainRefPoint(CBR.FRIENDLIES)
    if not bVec then return "(Bullseye not set)" end
    local bLat, bLon = coord.LOtoLL(bVec)
    local brg  = math.floor(trueBearing(bLat, bLon, lat, lon) + 0.5) % 360
    local dist = math.floor(distanceNM(bLat, bLon, lat, lon) * 10 + 0.5) / 10
    return string.format("Bullseye %03d / %.1f NM", brg, dist)
end

local function formatMGRS(lat, lon)
    local mgrs = coord.LLtoMGRS(lat, lon)
    if not mgrs then return "(MGRS N/A)" end
    local e      = math.floor(mgrs.Easting  / CBR.ACCURACY)
    local n      = math.floor(mgrs.Northing / CBR.ACCURACY)
    local digits = 5 - math.floor(math.log(CBR.ACCURACY) / math.log(10) + 0.5)
    local fmt    = string.format("%%s %%s %%0%dd %%0%dd", digits, digits)
    return string.format(fmt, mgrs.UTMZone, mgrs.MGRSDigraph, e, n)
end

local function formatPosition(lat, lon)
    if CBR.USE_BULLSEYE then
        return formatBullseye(lat, lon)
    else
        return formatMGRS(lat, lon)
    end
end

-- ============================================================
-- HELP FUNCTIONS: ARTILLERY CHECK & TYPE
-- ============================================================

-- Returns type string or nil if unit is not an artillery type.
local function getArtilleryType(unit)
    if not unit or not unit:isExist() then return nil end

    if unit:hasAttribute("MLRS") then
        return "MLRS"
    end

    if unit:hasAttribute("Artillery") then
        return "Artillery"
    end

    return nil
end

local function isArtillery(unit)
    return getArtilleryType(unit) ~= nil
end

-- ============================================================
-- RADAR: FIREFINDER UNIT SEARCH & STATUS
-- ============================================================

-- Scans all friendly groups for units whose name contains
-- CBR.RADAR_KEYWORD. Returns a list of all matching unit objects.
local function findAllFirefinderUnits()
    local found  = {}
    local groups = coalition.getGroups(CBR.FRIENDLIES)
    if not groups then return found end
    for _, grp in ipairs(groups) do
        local units = grp:getUnits()
        if units then
            for _, u in ipairs(units) do
                if u and u:isExist()
                and string.find(u:getName(), CBR.RADAR_KEYWORD, 1, true) then
                    found[#found + 1] = u
                end
            end
        end
    end
    return found
end

-- Updates CBR.radar state. Called at startup and periodically.
local function updateRadarStatus()
    if CBR.RADAR_MODE == "magic" then
        CBR.radar.units  = {}
        CBR.radar.status = "magic"
        return "magic (unlimited)"
    end

    local found = findAllFirefinderUnits()

    if #found > 0 then
        CBR.radar.units  = found
        CBR.radar.status = "active"
        local names = {}
        for _, u in ipairs(found) do
            names[#names + 1] = u:getName()
        end
        return string.format("active – %d unit(s): %s (range %.0fkm)",
            #found, table.concat(names, ", "), CBR.RADAR_RANGE / 1000)
    end

    -- Nothing found
    if CBR.radar.status == "active" then
        -- Was active before → all units destroyed/despawned
        CBR.radar.units  = {}
        CBR.radar.status = "destroyed"
        env.info("[COBRA] WARNING: all Firefinder units lost")
        if CBR.RADAR_IF_DESTROYED == "magic" then
            CBR.radar.status = "magic"
            return "destroyed – falling back to magic (unlimited)"
        end
        return "destroyed – radar SILENT"
    end

    if CBR.radar.status == "destroyed" then
        return "destroyed – radar SILENT"
    end

    if CBR.RADAR_MODE == "auto" then
        CBR.radar.units  = {}
        CBR.radar.status = "magic"
        return "auto – no FIREFINDER found, using magic (unlimited)"
    end

    -- RADAR_MODE == "firefinder", no unit found
    CBR.radar.units  = {}
    CBR.radar.status = "absent"
    return "absent – no FIREFINDER unit, radar SILENT"
end

-- Returns true if at least one active Firefinder unit covers dcsPos.
-- Also prunes dead unit references on the fly.
local function radarCovers(dcsPos)
    local s = CBR.radar.status

    if s == "magic"   then return true  end
    if s == "absent"
    or s == "destroyed" then return false end

    -- status == "active": check each unit
    local alive = {}
    local covers = false
    for _, u in ipairs(CBR.radar.units) do
        if u and u:isExist() then
            alive[#alive + 1] = u
            if distanceM(dcsPos, u:getPoint()) <= CBR.RADAR_RANGE then
                covers = true
            end
        end
    end

    -- Update live list (removes destroyed units without waiting for next scan)
    CBR.radar.units = alive

    if #alive == 0 then
        -- All units gone since last scan
        CBR.radar.status = "destroyed"
        env.info("[COBRA] WARNING: all Firefinder units lost mid-mission")
        if CBR.RADAR_IF_DESTROYED == "magic" then
            CBR.radar.status = "magic"
            return true
        end
        return false
    end

    return covers
end

-- Periodic re-scan loop
local function radarScanLoop(_, t)
    local statusStr = updateRadarStatus()
    env.info("[COBRA] Radar scan: " .. statusStr)
    return t + CBR.RADAR_SCAN_INTERVAL
end

-- ============================================================
-- MESSAGE OUTPUT
-- ============================================================

local function sendReport(gName, unitType, lat, lon)
    local posStr = formatPosition(lat, lon)
    local msg = string.format(
        "[ COBRA HOSTILE FIRE DETECTED ]\n"
     .. "-------------------------------\n"
     .. "Type:      %s\n"
     .. "Position:  %s\n"
     .. "-------------------------------",
        unitType, posStr
    )
    trigger.action.outTextForCoalition(
        CBR.FRIENDLIES, msg, CBR.MESSAGE_DURATION)

    -- F10 map marker
    if CBR.MAP_MARKER then
        local vec3      = coord.LLtoLO(lat, lon, 0)
        local markerTxt = string.format("COBRA: %s\n%s", unitType, posStr)
        local mid       = CBR.markId
        CBR.markId      = CBR.markId + 1
        -- signature: markToCoalition(id, text, point, coalition, readonly)
        trigger.action.markToCoalition(mid, markerTxt, vec3, CBR.FRIENDLIES, true)
        if CBR.MAP_MARKER_TIMEOUT > 0 then
            timer.scheduleFunction(function()
                trigger.action.removeMark(mid)
            end, nil, timer.getTime() + CBR.MAP_MARKER_TIMEOUT)
        end
    end

    env.info(string.format("[COBRA] Report: %s @ %s", gName, posStr))
end

-- ============================================================
-- EVENT HANDLER
-- ============================================================

local cobraHandler = {}

function cobraHandler:onEvent(event)
    if event.id ~= world.event.S_EVENT_SHOT then return end

    local unit = event.initiator
    if not unit or not unit:isExist() then return end
    if unit:getCoalition() ~= CBR.ENEMY then return end
    if not isArtillery(unit) then return end

    local grp = unit:getGroup()
    if not grp then return end
    local gName = grp:getName()

    local dcsPos   = unit:getPoint()
    local lat, lon = coord.LOtoLL(dcsPos)
    local now      = timer.getTime()
    local unitType = getArtilleryType(unit)

    -- --------------------------------------------------------
    -- Radar coverage check
    -- --------------------------------------------------------
    if not radarCovers(dcsPos) then
        env.info(string.format(
            "[COBRA] Shot by %s outside radar coverage – ignored", gName))
        return
    end

    -- --------------------------------------------------------
    -- Message already displayed? → Check shoot & scoot
    -- --------------------------------------------------------
    if CBR.reported[gName] then
        if distanceM(dcsPos, CBR.reported[gName].dcsPos) < CBR.SCOOT_THRESHOLD then
            return  -- same position, no further report
        end
        env.info(string.format(
            "[COBRA] Shoot & Scoot detected: %s – new position", gName))
        CBR.reported[gName] = nil
    end

    -- --------------------------------------------------------
    -- Timer already running? → ignore same position
    -- --------------------------------------------------------
    if CBR.pending[gName] then
        if distanceM(dcsPos, CBR.pending[gName].dcsPos) < CBR.SCOOT_THRESHOLD then
            return  -- timer already running, nothing to do
        end
        env.info(string.format(
            "[COBRA] Position change before timer end: %s", gName))
        CBR.pending[gName] = nil
    end

    -- --------------------------------------------------------
    -- First shot (or new position after shoot & scoot):
    -- Schedule COBRA timer
    -- --------------------------------------------------------
    CBR.pending[gName] = { lat = lat, lon = lon, dcsPos = dcsPos, unitType = unitType }

    env.info(string.format(
        "[COBRA] First shot: %s (%s) – report in %ds", gName, unitType, CBR.COBRA_DELAY))

    local snap = { gName = gName, lat = lat, lon = lon, dcsPos = dcsPos, unitType = unitType }

    timer.scheduleFunction(function()
        local p = CBR.pending[snap.gName]
        if not p then return end  -- overwritten by scoot
        if distanceM(p.dcsPos, snap.dcsPos) >= CBR.SCOOT_THRESHOLD then
            return  -- a newer timer is responsible
        end

        sendReport(snap.gName, snap.unitType, snap.lat, snap.lon)

        CBR.pending[snap.gName]  = nil
        CBR.reported[snap.gName] = {
            lat    = snap.lat,
            lon    = snap.lon,
            dcsPos = snap.dcsPos,
        }
    end, nil, now + CBR.COBRA_DELAY)
end

world.addEventHandler(cobraHandler)

-- ============================================================
-- STARTUP
-- ============================================================

local initialStatus = updateRadarStatus()
timer.scheduleFunction(radarScanLoop, nil,
    timer.getTime() + CBR.RADAR_SCAN_INTERVAL)

local modeStr = CBR.USE_BULLSEYE and "Bullseye (Bearing/NM)"
    or string.format("MGRS (%dm)", CBR.ACCURACY)

env.info(string.format(
    "[COBRA] Counter Battery Radar v3 | Coalition: %s | Mode: %s | Delay: %ds | Scoot: %dm | Radar: %s",
    CBR.OWN_COALITION, modeStr, CBR.COBRA_DELAY, CBR.SCOOT_THRESHOLD, initialStatus))
