-- ============================================================
--  ForgeComm.lua  |  v1.1.0
--  ForgeDMC Toolkit
--
--  Replaces : AceComm-3.0
--  Author   : ForgeDMC  |  2026-03-21  |  MIT
--
--  FIXES v1.1.0
--  ------------
--  * Renamed NativeComm → ForgeComm throughout (was inconsistent with
--    every other module in the toolkit)
--  * Added SendChunked() — splits payloads into 220-byte packets with
--    a FGC|total|seq|payload header; receiver reassembles transparently
--  * Send() now asserts message ≤ 255 bytes and directs larger payloads
--    to SendChunked with a clear error message
--  * Added warning log when RegisterAddonMessagePrefix hits Blizzard's
--    ~64 prefix session cap
--  * Cleaned up inline assembly buffer on successful reassembly
--
--  USAGE
--  -----
--  -- Register a handler (safe to call before PLAYER_LOGIN):
--  ForgeComm:Register("MYADDON", function(prefix, msg, channel, sender)
--      print("Received:", msg, "from", sender)
--  end)
--
--  -- Send a short message (< 255 bytes):
--  ForgeComm:Send("MYADDON", "ping:1234", "PARTY")
--  ForgeComm:Send("MYADDON", "hello",     "RAID")
--  ForgeComm:Send("MYADDON", "data",      "GUILD")
--  ForgeComm:Send("MYADDON", "msg",       "WHISPER", "PlayerName-Realm")
--
--  -- Send a large payload (any length — auto chunked):
--  ForgeComm:SendChunked("MYADDON", bigString, "RAID")
--  -- The receiver's callback fires once with the fully reassembled string.
--
--  -- Cleanup:
--  ForgeComm:Unregister("MYADDON")
--  ForgeComm:IsRegistered("MYADDON")  -- true / false
--
--  LIMITS
--  ------
--  * Prefix  : 1–16 characters (Blizzard hard limit)
--  * Chunk   : 220 bytes per packet (safe margin under the 255-byte cap)
--  * Sending : only valid after PLAYER_LOGIN (queued prefixes register then)
-- ============================================================

ForgeComm = ForgeComm or {}

-- ── Chunking constants ────────────────────────────────────────

-- BUG FIX: was 250, but chunked packets include a header:
--   "FGC\031<total>\031<seq>\031<payload>"
-- worst-case header overhead = 3+1+4+1+4+1 = 14 bytes → 250+14 = 264 > 255 limit.
-- 220-byte payload gives worst-case packet of 234 bytes, safely under 255.
local CHUNK_SIZE   = 220        -- max payload bytes per packet
local CHUNK_MAGIC  = "FGC"      -- 3-byte header magic
local SEP          = "\031"     -- ASCII Unit Separator (unlikely in addon data)

-- ── State ─────────────────────────────────────────────────────

local _handlers   = {}   -- [prefix] = callback function  (string messages)
local _objHandlers = {}  -- [prefix] = callback function  (deserialized tables)
local _registered = {}   -- [prefix] = true  (ack'd by C_ChatInfo)
local _incoming   = {}   -- [prefix][sender] = { total, chunks={} }

-- Magic prefix that identifies SendObject payloads (STX + "FO").
-- Must not appear in normal ForgeComm string messages.
local OBJ_MAGIC = "\x02FO"

-- ── Listener frame ────────────────────────────────────────────

local _frame = CreateFrame("Frame")
_frame:RegisterEvent("PLAYER_LOGIN")
_frame:RegisterEvent("CHAT_MSG_ADDON")

_frame:SetScript("OnEvent", function(_, event, prefix, message, channel, sender)

    -- ── PLAYER_LOGIN: flush queued prefix registrations ────────
    if event == "PLAYER_LOGIN" then
        for p in pairs(_handlers) do
            if not _registered[p] then
                if C_ChatInfo.RegisterAddonMessagePrefix(p) then
                    _registered[p] = true
                else
                    DEFAULT_CHAT_FRAME:AddMessage(
                        "|cffff4444ForgeComm|r: Failed to register prefix '"
                        .. p .. "' — Blizzard session cap reached?")
                end
            end
        end
        return
    end

    -- ── CHAT_MSG_ADDON: dispatch to registered handler ─────────
    if event ~= "CHAT_MSG_ADDON" then return end

    local handler    = _handlers[prefix]
    local objHandler = _objHandlers[prefix]
    if not handler and not objHandler then return end

    local shortSender = (sender and sender:match("^([^%-]+)")) or sender

    -- Detect chunked packet: "FGC\031<total>\031<seq>\031<payload>"
    local total, seq, payload = message:match(
        "^" .. CHUNK_MAGIC .. SEP .. "(%d+)" .. SEP .. "(%d+)" .. SEP .. "(.*)")

    -- Inner helper: dispatch a fully-assembled (or single-packet) message.
    local function dispatch(msg)
        if objHandler and msg:sub(1, 3) == OBJ_MAGIC then
            -- Object message: strip the 3-byte magic, deserialize, fire object handler.
            local ok, tbl = pcall(ForgePacker.UnpackCompressed, msg:sub(4))
            if ok and type(tbl) == "table" then
                pcall(objHandler, prefix, tbl, channel, sender, shortSender)
            end
        elseif handler then
            -- Plain string message: fire normal handler.
            pcall(handler, prefix, msg, channel, sender, shortSender)
        end
    end

    if total then
        -- ── Reassembly path ────────────────────────────────────
        total = tonumber(total)
        seq   = tonumber(seq)

        _incoming[prefix]         = _incoming[prefix]         or {}
        _incoming[prefix][sender] = _incoming[prefix][sender] or { total = total, chunks = {} }

        local bag = _incoming[prefix][sender]
        bag.chunks[seq] = payload

        -- Count received chunks.
        local received = 0
        for _ in pairs(bag.chunks) do received = received + 1 end

        if received == total then
            -- All chunks present — assemble in order and fire.
            local parts = {}
            for i = 1, total do
                if not bag.chunks[i] then return end  -- gap (shouldn't happen)
                parts[i] = bag.chunks[i]
            end
            _incoming[prefix][sender] = nil
            dispatch(table.concat(parts))
        end
    else
        -- ── Single-packet path ─────────────────────────────────
        dispatch(message)
    end
end)

-- ── Public API ────────────────────────────────────────────────

--- Register a prefix and callback. Safe to call before PLAYER_LOGIN.
-- @param prefix    string    1–16 characters
-- @param callback  function  (prefix, message, channel, sender, shortSender)
function ForgeComm:Register(prefix, callback)
    assert(type(prefix) == "string" and #prefix >= 1 and #prefix <= 16,
        "ForgeComm:Register - prefix must be 1–16 characters, got: " .. tostring(prefix))
    assert(type(callback) == "function",
        "ForgeComm:Register - callback must be a function")

    _handlers[prefix] = callback

    if IsLoggedIn() and not _registered[prefix] then
        if C_ChatInfo.RegisterAddonMessagePrefix(prefix) then
            _registered[prefix] = true
        else
            DEFAULT_CHAT_FRAME:AddMessage(
                "|cffff4444ForgeComm|r: Failed to register prefix '"
                .. prefix .. "' — Blizzard session cap reached?")
        end
    end
end

--- Register a prefix and receive tables serialized by SendObject.
-- The callback receives (prefix, table, channel, sender, shortSender).
-- A single prefix can have both a Register and RegisterObject handler;
-- plain string messages go to Register, object messages go to RegisterObject.
function ForgeComm:RegisterObject(prefix, callback)
    assert(type(prefix) == "string" and #prefix >= 1 and #prefix <= 16,
        "ForgeComm:RegisterObject - prefix must be 1–16 characters")
    assert(type(callback) == "function",
        "ForgeComm:RegisterObject - callback must be a function")

    _objHandlers[prefix] = callback

    if IsLoggedIn() and not _registered[prefix] then
        if C_ChatInfo.RegisterAddonMessagePrefix(prefix) then
            _registered[prefix] = true
        end
    end
end

--- Silence callbacks for a prefix. The prefix remains registered with Blizzard.
function ForgeComm:Unregister(prefix)
    _handlers[prefix]    = nil
    _objHandlers[prefix] = nil
    _incoming[prefix]    = nil
end

--- Send a single message. Payload must be ≤ 255 bytes.
-- For larger payloads use SendChunked().
-- @param prefix   string
-- @param message  string   ≤ 255 bytes
-- @param channel  string   "PARTY"|"RAID"|"GUILD"|"OFFICER"|"INSTANCE_CHAT"|"WHISPER"
-- @param target   string   Required for WHISPER ("PlayerName-Realm")
function ForgeComm:Send(prefix, message, channel, target)
    assert(_registered[prefix],
        "ForgeComm:Send - prefix not registered (call Register first): " .. tostring(prefix))
    assert(type(message) == "string",
        "ForgeComm:Send - message must be a string")
    assert(#message <= 255,
        "ForgeComm:Send - message is " .. #message .. " bytes; use SendChunked() for payloads over 255 bytes")

    channel = channel or "PARTY"

    if channel == "WHISPER" then
        assert(type(target) == "string" and target ~= "",
            "ForgeComm:Send - WHISPER requires a target string ('PlayerName-Realm')")
        C_ChatInfo.SendAddonMessage(prefix, message, channel, target)
    else
        C_ChatInfo.SendAddonMessage(prefix, message, channel)
    end
end

--- Send a message of arbitrary length, splitting into CHUNK_SIZE-byte packets.
-- The receiver's registered callback is called once with the fully reassembled string.
-- @param prefix   string
-- @param message  string   Any length
-- @param channel  string
-- @param target   string   (WHISPER only)
function ForgeComm:SendChunked(prefix, message, channel, target)
    assert(_registered[prefix],
        "ForgeComm:SendChunked - prefix not registered: " .. tostring(prefix))
    assert(type(message) == "string",
        "ForgeComm:SendChunked - message must be a string")

    channel = channel or "PARTY"

    -- Split into chunks.
    local chunks = {}
    for i = 1, #message, CHUNK_SIZE do
        chunks[#chunks + 1] = message:sub(i, i + CHUNK_SIZE - 1)
    end

    -- If the whole message fits in one packet, send plain (no chunking overhead).
    if #chunks == 1 and #message <= 255 then
        return self:Send(prefix, message, channel, target)
    end

    local total = #chunks
    for seq, chunk in ipairs(chunks) do
        local packet = CHUNK_MAGIC .. SEP .. total .. SEP .. seq .. SEP .. chunk
        if channel == "WHISPER" then
            C_ChatInfo.SendAddonMessage(prefix, packet, channel, target)
        else
            C_ChatInfo.SendAddonMessage(prefix, packet, channel)
        end
    end
end

--- Serialize a table with ForgePacker (Pack + Compress) and send it chunked.
-- The receiver must have called RegisterObject() for the same prefix.
-- @param prefix   string
-- @param obj      table
-- @param channel  string
-- @param target   string  (WHISPER only)
function ForgeComm:SendObject(prefix, obj, channel, target)
    assert(_registered[prefix],
        "ForgeComm:SendObject - prefix not registered: " .. tostring(prefix))
    assert(type(obj) == "table",
        "ForgeComm:SendObject - obj must be a table")
    assert(ForgePacker, "ForgeComm:SendObject requires ForgePacker to be loaded")
    local payload = OBJ_MAGIC .. ForgePacker.PackCompressed(obj)
    return self:SendChunked(prefix, payload, channel, target)
end

--- Returns true if a Lua handler is registered for this prefix.
function ForgeComm:IsRegistered(prefix)
    return _handlers[prefix] ~= nil or _objHandlers[prefix] ~= nil
end
