-- Tommy's Radio System Configuration (ESX Example)
-- Documentation: https://docs.timmygstudios.com/docs/tommys-radio
-- Config Version 5.7
--
-- Most radio settings are now managed via the in-game admin panel
-- (requires tlib.admin ACE permission). Settings are stored in data.json.
-- This file only contains items that require Lua code or a server restart.

-- ESX Integration
ESX = exports['es_extended']:getSharedObject()

Config = {

    -- ================================================================
    --  NETWORK & AUTHENTICATION
    -- ================================================================

    -- Final connection string sent to clients. Include protocol and/or port
    -- if using a proxy (e.g., "https://proxy.example.com"). When empty, the
    -- server's auto-detected IP address and serverPort are used instead.
    serverAddress          = "",
    -- Port for the radio voice server and dispatch panel.
    serverPort             = 7777,

    -- Secure token for radio authentication. Change this to a long, random
    -- string -- it protects the voice server and dispatch panel API.
    authToken              = "changeme",

    -- NAC ID / password for the dispatch channel.
    dispatchNacId          = "141",

    -- ================================================================
    --  GENERAL
    -- ================================================================

    checkForUpdates        = true,
    healthCheck            = true,
    logLevel               = 3, -- 0 = Error, 1 = Warnings, 2 = Minimal, 3 = Normal, 4 = Debug, 5 = Verbose
    useDiscordAuth         = false,

    -- ================================================================
    --  KEYBINDS
    -- ================================================================

    -- Empty string = action is registered but unbound; players can assign a key
    -- via the FiveM keybind menu (Settings → Key Bindings → FiveM). Volume keys
    -- ship with sensible defaults because there is no on-radio equivalent for
    -- those actions; the rest default to the radio model's on-screen buttons.
    controls               = {
        talkRadioKey       = "B",
        toggleRadioKey     = "F6",
        closeRadioKey      = "", -- leave empty = handled by radio model config
        powerBtnKey        = "",
        channelUpKey       = "",
        channelDownKey     = "",
        zoneUpKey          = "",
        zoneDownKey        = "",
        menuUpKey          = "",
        menuDownKey        = "",
        menuRightKey       = "",
        menuLeftKey        = "",
        menuHomeKey        = "",
        menuBtn1Key        = "",
        menuBtn2Key        = "",
        menuBtn3Key        = "",
        emergencyBtnKey    = "",
        styleUpKey         = "",
        styleDownKey       = "",
        voiceVolumeUpKey   = "EQUALS",
        voiceVolumeDownKey = "MINUS",
        sfxVolumeUpKey     = "RBRACKET",
        sfxVolumeDownKey   = "LBRACKET",
        volume3DUpKey      = "",
        volume3DDownKey    = "",
    },

    -- ================================================================
    --  GPS & SIGNAL
    -- ================================================================

    signalTowerCoordinates = {
        { x = 1860.0,  y = 3677.0,  z = 33.0 },
        { x = 449.0,   y = -992.0,  z = 30.0 },
        { x = -979.0,  y = -2632.0, z = 23.0 },
        { x = -2364.0, y = 3229.0,  z = 45.0 },
        { x = -449.0,  y = 6025.0,  z = 35.0 },
        { x = 1529.0,  y = 820.0,   z = 79.0 },
        { x = -573.0,  y = -146.0,  z = 38.0 },
        { x = -3123.0, y = 1334.0,  z = 25.0 },
        { x = 5266.79, y = -5427.7, z = 139.7 },
    },

    -- ================================================================
    --  BATTERY
    -- ================================================================

    batteryTick            = function(currentBattery, deltaTime) -- [user hook]
        local playerPed = PlayerPedId()
        local vehicle = GetVehiclePedIsIn(playerPed, false)

        if vehicle ~= 0 then
            local chargeRate = 0.5
            return math.min(100.0, currentBattery + (chargeRate * deltaTime))
        else
            local dischargeRate = 0.1
            return math.max(0.0, currentBattery - (dischargeRate * deltaTime))
        end
    end,

    -- ================================================================
    --  ALERTS
    -- ================================================================
    -- Alert definitions used by the SGN button and dispatch panel.
    -- The first entry ([1]) is the default alert triggered by the in-game
    -- SGN button. Tone names correspond to entries in
    -- client/radios/default/tones.json.
    --
    -- See shared/old-config.lua for full documentation on tone configuration,
    -- repeat behaviour, deactivation banners, and other fields.

    alerts                 = {
        [1] = {
            name             = "SIGNAL 100",
            color            = "#a38718",
            isPersistent     = true,
            tones            = {
                ["activate"]   = "PRIORITY",
                ["repeat"]     = "PRIORITY_REPEAT",
                ["deactivate"] = "ALERT_C",
            },
            repeatInterval   = 15000,
            repeatShowBanner = false,
            deactivateLabel  = "RESUME",
            deactivateColor  = "#1a8a38",
        },
        [2] = {
            name = "SIGNAL 3",
            color = "#1852a3",
            isPersistent = true,
            tone = "ALERT_SIGNAL3",
        },
        [3] = {
            name = "Ping",
            color = "#1852a3",
            tone = "ALERT_B",
        },
        [4] = {
            name = "Bonk",
            color = "#1c4ba3",
            toneOnly = true,
            tone = "BONK",
        },
    },

    -- ================================================================
    --  ZONES & CHANNELS
    -- ================================================================

    zones                  = {
        [1] = {
            name = "Statewide",
            nacIds = { "141", "110" },
            Channels = {
                [1] = {
                    name = "DISP",
                    type = "conventional",
                    frequency = 154.755,
                    allowedNacs = { "141" },
                    scanAllowedNacs = { "110", "200" },
                    gps = { color = 54, visibleToNacs = { "141" } },
                },
                [2] = {
                    name = "C2C",
                    type = "trunked",
                    frequency = 856.1125,
                    frequencyRange = { 856.000, 859.000 },
                    coverage = 500,
                    allowedNacs = { "141" },
                    scanAllowedNacs = { "110", "200" },
                    gps = { color = 25, visibleToNacs = { "141" } },
                },
                [3] = {
                    name = "10-1",
                    type = "conventional",
                    frequency = 154.785,
                    allowedNacs = { "141" },
                    scanAllowedNacs = { "110", "200" },
                    gps = { color = 47, visibleToNacs = { "141" } },
                },
                [4] = {
                    name = "OPS-1",
                    type = "conventional",
                    frequency = 154.815,
                    allowedNacs = { "141" },
                    scanAllowedNacs = { "110", "200" },
                    gps = { color = 40, visibleToNacs = { "141" } },
                },
            },
        },
        [2] = {
            name = "Los Santos",
            nacIds = { "141" },
            Channels = {
                [1] = {
                    name = "DISP",
                    type = "conventional",
                    frequency = 460.250,
                    allowedNacs = { "141" },
                    scanAllowedNacs = { "110", "200" },
                    gps = { visibleToNacs = { "141" } },
                },
                [2] = {
                    name = "C2C",
                    type = "trunked",
                    frequency = 460.325,
                    frequencyRange = { 460.325, 462.325 },
                    coverage = 250,
                    allowedNacs = { "141" },
                    scanAllowedNacs = { "110", "200" },
                    gps = { color = 25, visibleToNacs = { "141" } },
                },
                [3] = {
                    name = "10-1",
                    type = "conventional",
                    frequency = 460.275,
                    allowedNacs = { "141" },
                    scanAllowedNacs = { "110", "200" },
                    gps = { color = 47, visibleToNacs = { "141" } },
                },
                [4] = {
                    name = "OPS-1",
                    type = "conventional",
                    frequency = 462.450,
                    allowedNacs = { "50" },
                    scanAllowedNacs = { "141" },
                    gps = { color = 40, visibleToNacs = { "141" } },
                },
            },
        },
        [3] = {
            name = "Blaine County",
            nacIds = { "141" },
            Channels = {
                [1] = {
                    name = "DISP",
                    type = "conventional",
                    frequency = 155.070,
                    allowedNacs = { "141" },
                    scanAllowedNacs = { "110", "200" },
                    gps = { color = 52, visibleToNacs = { "141" } },
                },
                [2] = {
                    name = "C2C",
                    type = "trunked",
                    frequency = 155.220,
                    frequencyRange = { 155.220, 157.220 },
                    coverage = 250,
                    allowedNacs = { "141" },
                    scanAllowedNacs = { "110", "200" },
                    gps = { color = 25, visibleToNacs = { "141" } },
                },
                [3] = {
                    name = "10-1",
                    type = "conventional",
                    frequency = 155.100,
                    allowedNacs = { "141" },
                    scanAllowedNacs = { "110", "200" },
                    gps = { color = 47, visibleToNacs = { "141" } },
                },
                [4] = {
                    name = "OPS-1",
                    type = "conventional",
                    frequency = 157.350,
                    allowedNacs = { "141" },
                    scanAllowedNacs = { "110", "200" },
                    gps = { color = 40, visibleToNacs = { "141" } },
                },
            },
        },
    },

    -- ================================================================
    --  CLIENT CALLBACKS
    -- ================================================================

    talkCheck              = function() -- [user hook]
        if IsPlayerDead(PlayerId()) then
            return false
        end

        if IsPedSwimming(PlayerPedId()) then
            return false
        end

        return true
    end,

    -- Optional per-provider tone → filename map. Drop the WAV files into
    -- client/radios/default/sounds/ and list them here. Any unmapped tone
    -- (or an omitted provider / nil sirenToneFiles) falls back to the
    -- default bgSiren.wav, so this table is entirely optional.
    sirenToneFiles         = {
        -- tELS main/aux tone index (1-based, matches SIREN_TONES):
        --   1 = Wail, 2 = Yelp, 3 = Priority, 4 = Powercall, 5 = Custom
        tels = {
            -- [1] = "wail.wav",
            -- [2] = "yelp.wav",
        },
        -- LVC state_lxsiren tone index:
        --   1 = Airhorn, 2 = Wail, 3 = Yelp, 4 = Priority,
        --   5-10 = Custom A-F, 11 = Powercall, 12-14 = Fire tones
        lvc  = {
            -- [2] = "wail.wav",
            -- [3] = "yelp.wav",
        },
        -- JLS exposes only a bool (no tone info) → always uses default bgSiren.wav.
        -- Native GTA sirens also fall through to default bgSiren.wav.
    },

    -- Return values:
    --   false / nil  → siren is off
    --   true         → siren is on, plays default bgSiren.wav
    --   "filename"   → siren is on, plays a custom file from client/radios/default/sounds/
    --                   (e.g. return "wail.wav" to play sounds/wail.wav instead of bgSiren.wav)
    -- The default hook below walks tELS → LVC → JLS → native in order. Returning
    -- `true` at any stage is fine; tone-specific WAVs are opt-in via sirenToneFiles.
    --
    -- Custom ELS integrators can force an immediate re-poll (instead of waiting
    -- up to 500ms) by calling:
    --   exports['tRadio']:NotifySirenChanged()
    -- Useful when your ELS changes siren state outside of LVC's `lvc:UpdateThirdParty`
    -- event — call it right after flipping the backing flag.
    bgSirenCheck           = function(lvcSirenState) -- [user hook]
        local playerPed = PlayerPedId()
        if not playerPed or playerPed == 0 then return false end

        local vehicle = GetVehiclePedIsIn(playerPed, false)
        if not vehicle or vehicle == 0 then return false end

        local toneFiles = Config.sirenToneFiles or {}

        -- tELS: nil = not a tELS vehicle (fall through); bool = authoritative
        local ok, telsActive = pcall(function()
            return exports['tELS']:IsSirenActive(vehicle)
        end)
        if ok and telsActive ~= nil then
            if not telsActive then return false end
            -- Entity(...) can throw if the handle invalidated between the export
            -- return and here (vehicle despawn, player stream-out). pcall the
            -- state-bag read and fall back to the default WAV.
            local bagOk, tone = pcall(function()
                local bag = Entity(vehicle).state
                return (bag and bag['tels:sirenOn'] and bag['tels:sirenTone'])
                    or (bag and bag['tels:auxSirenOn'] and bag['tels:auxSirenTone'])
            end)
            local mapped = (bagOk and tone and toneFiles.tels and toneFiles.tels[tone]) or nil
            return mapped or true
        end

        -- LVC (tone id from lvc:UpdateThirdParty → state_lxsiren, cached in cl_bridge.lua)
        if lvcSirenState and lvcSirenState > 0 then
            return (toneFiles.lvc and toneFiles.lvc[lvcSirenState]) or true
        end

        -- JLS: symmetric with tELS — nil = not JLS-equipped (fall through);
        -- bool = authoritative. Returning false here short-circuits the native
        -- fallback, which would otherwise report true for JLS vehicles that use
        -- the native siren flag as a backing store.
        local jlsOk, jlsActive = pcall(function()
            return exports['Jace3483_JLS']:IsSirenActive(vehicle)
        end)
        if jlsOk and jlsActive ~= nil then
            return jlsActive and true or false
        end

        -- native GTA siren fallback
        return IsVehicleSirenOn(vehicle)
    end,

    -- ================================================================
    --  SERVER CALLBACKS
    -- ================================================================
    -- These functions run on the SERVER and are called automatically
    -- by the radio system. Below are ESX-specific implementations.

    radioAccessCheck = function(playerId)
        if not playerId or playerId <= 0 then
            log("Invalid playerId in radioAccessCheck: " .. tostring(playerId), 0)
            return false
        end

        local xPlayer = ESX.GetPlayerFromId(playerId)
        if not xPlayer then
            return false
        end

        -- Check if player has radio item in inventory
        local radioItem = xPlayer.getInventoryItem('radio')
        return radioItem and radioItem.count > 0 or false
    end,

    getUserNacId = function(serverId)
        if not serverId or serverId <= 0 then
            return nil
        end

        local xPlayer = ESX.GetPlayerFromId(serverId)
        if not xPlayer then
            return "0"
        end

        local job = xPlayer.getJob()
        if not job then
            return "0"
        end

        -- Assign NAC IDs based on job
        if job.name == "police" then
            return "141"
        elseif job.name == "ambulance" then
            return "200"
        elseif job.name == "fire" or job.name == "firefighter" then
            return "300"
        end

        return "0"
    end,

    getPlayerName = function(serverId)
        if not serverId then return "DISPATCH" end
        if serverId <= 0 then return "DISPATCH" end

        local xPlayer = ESX.GetPlayerFromId(serverId)
        if not xPlayer then
            local name = GetPlayerName(serverId)
            if not name or name == "" then
                return "Player " .. serverId
            end
            return name
        end

        -- Check for callsign in metadata first
        local callsign = xPlayer.getMeta("callsign")
        if callsign and callsign ~= "" and callsign ~= "NO CALLSIGN" then
            return callsign
        end

        -- Try to get character name from getName (returns firstname + lastname with esx_identity)
        local characterName = xPlayer.getName()
        if characterName and characterName ~= "" then
            -- Extract last name if full name is provided
            local lastName = string.match(characterName, "%s+(%S+)$")
            if lastName then
                return lastName
            end
            return characterName
        end

        -- Fallback to FiveM player name
        local name = GetPlayerName(serverId)
        if not name or name == "" then
            return "Player " .. serverId
        end

        -- Extract callsign from name format "Name S. 2L-319"
        local extractedCallsign = string.match(name, "%s([%w%-]+%d+)$")
        if extractedCallsign then
            return extractedCallsign
        end

        return name
    end,
}

-- ================================================================
--  ESX USEABLE ITEM
-- ================================================================
-- Makes the radio a useable item in ESX. Players must have a "radio"
-- item in their inventory to open the radio UI.

if IsDuplicityVersion() then
    -- Server-side: Register useable item
    ESX.RegisterUsableItem('radio', function(playerId)
        TriggerClientEvent('radio:use', playerId)
    end)
else
    -- Client-side: Handle radio use event
    RegisterNetEvent('radio:use', function()
        exports['tRadio']:openRadio()
    end)
end
