-- ============================================================
-- Asylum Compagnon — GearCompassJson.lua
-- Encodeur JSON local, compact et déterministe. Aucune exécution dynamique.
-- Les marqueurs explicites survivent aux SavedVariables et permettent de
-- distinguer [] de {} ainsi que JSON null de l'absence d'une clé Lua.
-- ============================================================

local _, NS = ...

local JSON = {}
NS.GearCompassJSON = JSON

local ARRAY_MARKER = "__gearcompass_json_array"
local NULL_MARKER = "__gearcompass_json_null"

JSON.NULL = { [NULL_MARKER] = true }

function JSON.Array(values)
    values = type(values) == "table" and values or {}
    values[ARRAY_MARKER] = true
    return values
end

function JSON.IsNull(value)
    return value == JSON.NULL or (type(value) == "table" and value[NULL_MARKER] == true)
end

local ESCAPES = {
    ['"'] = '\\"',
    ['\\'] = '\\\\',
    -- Neutralise les séquences de lien/couleur WoW dans l'EditBox. JSON.parse
    -- restitue exactement le caractère `|` après la vérification du checksum.
    ['|'] = '\\u007c',
    ['\b'] = '\\b',
    ['\f'] = '\\f',
    ['\n'] = '\\n',
    ['\r'] = '\\r',
    ['\t'] = '\\t',
}

function JSON.EscapeString(value)
    return (value:gsub('[%z\1-\31\\"|]', function(char)
        return ESCAPES[char] or string.format("\\u%04x", string.byte(char))
    end))
end

local function IsArray(value)
    if value[ARRAY_MARKER] == true then return true end
    local n = #value
    if n == 0 then return false end
    for key in pairs(value) do
        if type(key) ~= "number" or key < 1 or key > n or key % 1 ~= 0 then
            return false
        end
    end
    return true
end

local function EncodeValue(value, stack)
    if JSON.IsNull(value) or value == nil then return "null" end

    local kind = type(value)
    if kind == "string" then
        return '"' .. JSON.EscapeString(value) .. '"'
    elseif kind == "number" then
        if value ~= value or value == math.huge or value == -math.huge then return "null" end
        if value == math.floor(value) then return tostring(value) end
        return string.format("%.17g", value)
    elseif kind == "boolean" then
        return value and "true" or "false"
    elseif kind ~= "table" then
        error("GearCompass JSON: type Lua non pris en charge: " .. kind)
    end

    if stack[value] then error("GearCompass JSON: référence cyclique") end
    stack[value] = true

    local out = {}
    if IsArray(value) then
        for index = 1, #value do
            out[index] = EncodeValue(value[index], stack)
        end
        stack[value] = nil
        return "[" .. table.concat(out, ",") .. "]"
    end

    local keys = {}
    for key in pairs(value) do
        if key ~= ARRAY_MARKER and key ~= NULL_MARKER then
            if type(key) ~= "string" then
                stack[value] = nil
                error("GearCompass JSON: une clé d'objet n'est pas une chaîne")
            end
            keys[#keys + 1] = key
        end
    end
    table.sort(keys)
    for index, key in ipairs(keys) do
        out[index] = '"' .. JSON.EscapeString(key) .. '":' .. EncodeValue(value[key], stack)
    end
    stack[value] = nil
    return "{" .. table.concat(out, ",") .. "}"
end

function JSON.Encode(value)
    local ok, result = pcall(EncodeValue, value, {})
    if ok then return result end
    return nil, result
end

-- Test manuel isolé : /asylum gearcompass selftest (mode debug).
function JSON.SelfTest()
    local sample = {
        a = JSON.NULL,
        b = JSON.Array({ true, false, "x\n\t\\\"" }),
        c = 12,
        d = "|cnIQ4:|Hitem:123:0|h[Test]|h|r",
    }
    local encoded, err = JSON.Encode(sample)
    local expected = '{"a":null,"b":[true,false,"x\\n\\t\\\\\\\""]'
        .. ',"c":12,"d":"\\u007ccnIQ4:\\u007cHitem:123:0\\u007ch[Test]'
        .. '\\u007ch\\u007cr"}'
    return encoded == expected, encoded or err, expected
end
