GuildHelper = GuildHelper or {}

local addon = GuildHelper
local json = {}

function json:kindOf(value)
    if type(value) ~= "table" then
        return type(value)
    end

    local index = 1

    for _ in pairs(value) do
        if value[index] ~= nil then
            index = index + 1
        else
            return "table"
        end
    end

    if index == 1 then
        return "table"
    end

    return "array"
end

function json:escapeString(value)
    local source = { "\\", '"', "/", "\b", "\f", "\n", "\r", "\t" }
    local target = { "\\", '"', "/", "b", "f", "n", "r", "t" }

    for index, char in ipairs(source) do
        value = value:gsub(char, "\\" .. target[index])
    end

    return value
end

function json:skipDelimiter(source, position, delimiter, required)
    position = position + #source:match("^%s*", position)

    if source:sub(position, position) ~= delimiter then
        if required then
            error("Expected " .. delimiter .. " near position " .. position)
        end

        return position, false
    end

    return position + 1, true
end

function json:parseString(source, position, value)
    value = value or ""

    if position > #source then
        error("Unexpected end of input while parsing string.")
    end

    local current = source:sub(position, position)

    if current == '"' then
        return value, position + 1
    end

    if current ~= "\\" then
        return self:parseString(source, position + 1, value .. current)
    end

    local escapes = {
        b = "\b",
        f = "\f",
        n = "\n",
        r = "\r",
        t = "\t",
    }
    local nextChar = source:sub(position + 1, position + 1)

    if not nextChar then
        error("Unexpected end of input while parsing escape sequence.")
    end

    return self:parseString(source, position + 2, value .. (escapes[nextChar] or nextChar))
end

function json:parseNumber(source, position)
    local numberText = source:match("^-?%d+%.?%d*[eE]?[+-]?%d*", position)
    local value = tonumber(numberText)

    if not value then
        error("Invalid number at position " .. position)
    end

    return value, position + #numberText
end

function json:Encode(value, asKey)
    local kind = self:kindOf(value)
    local parts = {}

    if kind == "array" then
        if asKey then
            error("Arrays cannot be encoded as keys.")
        end

        parts[#parts + 1] = "["
        for index, nestedValue in ipairs(value) do
            if index > 1 then
                parts[#parts + 1] = ","
            end
            parts[#parts + 1] = self:Encode(nestedValue)
        end
        parts[#parts + 1] = "]"
        return table.concat(parts)
    end

    if kind == "table" then
        if asKey then
            error("Tables cannot be encoded as keys.")
        end

        parts[#parts + 1] = "{"
        local first = true

        for key, nestedValue in pairs(value) do
            if not first then
                parts[#parts + 1] = ","
            end
            first = false
            parts[#parts + 1] = self:Encode(key, true)
            parts[#parts + 1] = ":"
            parts[#parts + 1] = self:Encode(nestedValue)
        end

        parts[#parts + 1] = "}"
        return table.concat(parts)
    end

    if kind == "string" then
        return '"' .. self:escapeString(value) .. '"'
    end

    if kind == "number" or kind == "boolean" then
        return tostring(value)
    end

    if kind == "nil" then
        return "null"
    end

    error("Unsupported json type: " .. kind)
end

function json:Decode(source, position, endDelimiter)
    position = position or 1

    if position > #source then
        error("Unexpected end of input.")
    end

    position = position + #source:match("^%s*", position)
    local first = source:sub(position, position)

    if first == "{" then
        local object = {}
        local key = true
        local delimiterFound = true
        position = position + 1

        while true do
            key, position = self:Decode(source, position, "}")
            if key == nil then
                return object, position
            end
            if not delimiterFound then
                error("Missing comma between object items.")
            end
            position = self:skipDelimiter(source, position, ":", true)
            object[key], position = self:Decode(source, position)
            position, delimiterFound = self:skipDelimiter(source, position, ",")
        end
    end

    if first == "[" then
        local array = {}
        local value = true
        local delimiterFound = true
        position = position + 1

        while true do
            value, position = self:Decode(source, position, "]")
            if value == nil then
                return array, position
            end
            if not delimiterFound then
                error("Missing comma between array items.")
            end
            array[#array + 1] = value
            position, delimiterFound = self:skipDelimiter(source, position, ",")
        end
    end

    if first == '"' then
        return self:parseString(source, position + 1)
    end

    if first == "-" or first:match("%d") then
        return self:parseNumber(source, position)
    end

    if first == endDelimiter then
        return nil, position + 1
    end

    local literals = {
        ["true"] = true,
        ["false"] = false,
        ["null"] = nil,
    }

    for literal, value in pairs(literals) do
        local endPosition = position + #literal - 1
        if source:sub(position, endPosition) == literal then
            return value, endPosition + 1
        end
    end

    error("Invalid json at position " .. position)
end

addon.Json = json
