-- arcfall_ping.lua
-- Custom RTT ping for Arcfall (OTCv8 + OTX2 backend).
print('[arcfall_ping] module file loaded')
-- OTCv8's native GameClientPing pipeline doesn't work for our 772 client
-- (sends one ping on login, never schedules next). We implement ping over
-- extended opcode 181 with a decimal-string timestamp payload (no \0 bytes,
-- since OTX2's popString is strlen-based and would truncate binary payloads).
--
-- HUD: own widget under topMenu, NOT the native pingLabel. The native one
-- gets hidden by client_topmenu.online() and overwritten with yellow "Ping: ??"
-- by onPingBack(-1) from the C++ engine. Owning a sibling widget eliminates
-- the race entirely.

ArcfallPing = {}
ArcfallPing.opcode      = 181
ArcfallPing.intervalMs  = 1000
ArcfallPing.timeoutMs   = 4000

local sendEvent      = nil
local timeoutEvent   = nil
local lastSendMillis = 0
local hudWidget      = nil

----------------------------------------------------------------------
-- Timestamp encoding as decimal string
----------------------------------------------------------------------
local function packTs(n)
    return tostring(math.floor(n))
end

local function unpackTs(s)
    if type(s) ~= 'string' then return nil end
    return tonumber(s)
end

----------------------------------------------------------------------
-- HUD: own widget anchored to topMenu (no native pingLabel collision)
----------------------------------------------------------------------
local function getHud()
    if hudWidget and not hudWidget:isDestroyed() then return hudWidget end

    local topMenuMod = modules.client_topmenu
    local topMenu = nil
    -- Try the public accessor first; fall back to a recursive lookup.
    if topMenuMod and topMenuMod.getTopMenu then
        topMenu = topMenuMod.getTopMenu()
    end
    if not topMenu then
        local root = g_ui.getRootWidget()
        topMenu = root and root:recursiveGetChildById('topMenu')
    end
    if not topMenu then return nil end

    -- Native pingLabel is removed in 20-topmenu.otui — no need to destroy it here.
    local w = g_ui.createWidget('TopMenuPingLabel', topMenu)
    w:setId('arcfallPingLabel')
    w:setTextAutoResize(true)

    -- Anchor where pingLabel was (under fpsLabel).
    local fps = topMenu:getChildById('fpsLabel')
    if fps then
        w:addAnchor(AnchorTop,  'fpsLabel', AnchorBottom)
        w:addAnchor(AnchorLeft, 'fpsLabel', AnchorLeft)
        print('[arcfall_ping] anchored to fpsLabel at top-left of topMenu')
    else
        w:addAnchor(AnchorTop,  'parent', AnchorTop)
        w:addAnchor(AnchorLeft, 'parent', AnchorLeft)
        print('[arcfall_ping] WARNING: no fpsLabel; anchored to topMenu parent')
    end

    -- Bring to front of widget z-order
    w:raise()

    hudWidget = w
    print(string.format('[arcfall_ping] HUD widget created (id=%s parent=%s)', w:getId(), topMenu:getId() or '?'))
    return hudWidget
end

local function colorFor(ms)
    if ms <= 80  then return '#00FF00' end  -- green
    if ms <= 150 then return '#FFFF00' end  -- yellow
    return '#FF0000'                         -- red
end

local function setHud(text, color)
    local w = getHud()
    if not w then return end
    w:setText(text)
    if color then w:setColor(color) end
    w:setVisible(true)
end

local function resetHud()
    if hudWidget and not hudWidget:isDestroyed() then
        hudWidget:setText('')
        hudWidget:setVisible(false)
    end
end

----------------------------------------------------------------------
-- Network
----------------------------------------------------------------------
local function sendPing()
    sendEvent = nil
    if not g_game.isOnline() then
        sendEvent = scheduleEvent(sendPing, ArcfallPing.intervalMs)
        return
    end

    local proto = g_game.getProtocolGame()
    if not proto then
        sendEvent = scheduleEvent(sendPing, ArcfallPing.intervalMs)
        return
    end

    lastSendMillis = g_clock.millis()
    local ok, err = pcall(function()
        proto:sendExtendedOpcode(ArcfallPing.opcode, packTs(lastSendMillis))
    end)
    if not ok then
        print('[arcfall_ping] send failed: ' .. tostring(err))
    end

    -- Re-arm timeout: if no echo before this fires, mark with distinctive text
    -- so we can tell our widget apart from any stale native one.
    if timeoutEvent then removeEvent(timeoutEvent) end
    timeoutEvent = scheduleEvent(function()
        timeoutEvent = nil
        if g_game.isOnline() then
            setHud('Ping: ??', '#FFFF00')
        end
    end, ArcfallPing.timeoutMs)

    sendEvent = scheduleEvent(sendPing, ArcfallPing.intervalMs)
end

local function onExtopcode(protocol, opcode, buffer)
    if opcode ~= ArcfallPing.opcode then return end
    local sentTs = unpackTs(buffer)
    if not sentTs then return end

    local now = g_clock.millis()
    local rtt = now - sentTs
    if rtt < 0 then rtt = 0 end
    if rtt > 99999 then rtt = 99999 end

    if timeoutEvent then
        removeEvent(timeoutEvent)
        timeoutEvent = nil
    end

    -- Expose RTT to other modules (e.g. game_stats) so they can mirror our value
    -- instead of g_game.getPing() which returns -1 when native GameClientPing is off.
    ArcfallPing.lastRtt = rtt
    ArcfallPing.lastRttTime = now

    local pingText = string.format('Ping: %d ms', rtt)
    local pingColor = colorFor(rtt)
    pcall(function() setHud(pingText, pingColor) end)
    pcall(function()
        local stats = modules.game_stats
        if stats and stats.ui and stats.ui.ping then
            stats.ui:setVisible(true)
            stats.ui.ping:setVisible(true)
            stats.ui.ping:setText(pingText)
            stats.ui.ping:setColor(pingColor)
        end
    end)
end

----------------------------------------------------------------------
-- Lifecycle
----------------------------------------------------------------------
function init()
    print('[arcfall_ping] init() — registering opcode 181')
    ProtocolGame.registerExtendedOpcode(ArcfallPing.opcode, onExtopcode)
    if sendEvent then removeEvent(sendEvent); sendEvent = nil end
    sendEvent = scheduleEvent(sendPing, 2000)
    print('[arcfall_ping] perpetual ping loop scheduled')
end

function terminate()
    ProtocolGame.unregisterExtendedOpcode(ArcfallPing.opcode)
    if sendEvent    then removeEvent(sendEvent);    sendEvent    = nil end
    if timeoutEvent then removeEvent(timeoutEvent); timeoutEvent = nil end
    if hudWidget and not hudWidget:isDestroyed() then
        hudWidget:destroy()
    end
    hudWidget = nil
end
