local util     = require 'utility'
local timer    = require 'timer'
local scope    = require 'workspace.scope'
local template = require 'config.template'

---@alias config.source '"client"'|'"path"'|'"local"'

---@class config.api
local m = {}
m.watchList = {}

m.NULL = {}

m.nullSymbols = {
    [m.NULL] = true,
}

---@param scp      scope
---@param key      string
---@param nowValue any
---@param rawValue any
local function update(scp, key, nowValue, rawValue)
    local now = m.getNowTable(scp)
    local raw = m.getRawTable(scp)

    now[key] = nowValue
    raw[key] = rawValue
end

---@param uri uri
---@param key? string
---@return scope
local function getScope(uri, key)
    local raw = m.getRawTable(scope.override)
    if raw then
        if not key or raw[key] ~= nil then
            return scope.override
        end
    end
    if uri then
        ---@type scope
        local scp = scope.getFolder(uri) or scope.getLinkedScope(uri)
        if scp then
            if not key
            or m.getRawTable(scp)[key] ~= nil then
                return scp
            end
        end
    end
    return scope.fallback
end

---@param scp   scope
---@param key   string
---@param value any
function m.setByScope(scp, key, value)
    local unit = template[key]
    if not unit then
        return false
    end
    local raw = m.getRawTable(scp)
    if util.equal(raw[key], value) then
        return false
    end
    if unit:checker(value) then
        update(scp, key, unit:loader(value), value)
    else
        update(scp, key, unit.default, unit.default)
    end
    return true
end

---@param uri   uri
---@param key   string
---@param value any
function m.set(uri, key, value)
    local scp = getScope(uri)
    local oldValue = m.get(uri, key)
    m.setByScope(scp, key, value)
    local newValue = m.get(uri, key)
    if not util.equal(oldValue, newValue) then
        m.event(uri, key, newValue, oldValue)
        return true
    end
    return false
end

function m.add(uri, key, value)
    local unit = template[key]
    if not unit then
        return false
    end
    local list = m.getRaw(uri, key)
    if type(list) ~= 'table' then
        return false
    end
    local copyed = {}
    for i, v in ipairs(list) do
        if util.equal(v, value) then
            return false
        end
        copyed[i] = v
    end
    copyed[#copyed+1] = value
    local oldValue = m.get(uri, key)
    m.set(uri, key, copyed)
    local newValue = m.get(uri, key)
    if not util.equal(oldValue, newValue) then
        m.event(uri, key, newValue, oldValue)
        return true
    end
    return false
end

function m.prop(uri, key, prop, value)
    local unit = template[key]
    if not unit then
        return false
    end
    local map = m.getRaw(uri, key)
    if type(map) ~= 'table' then
        return false
    end
    if util.equal(map[prop], value) then
        return false
    end
    local copyed = {}
    for k, v in pairs(map) do
        copyed[k] = v
    end
    copyed[prop] = value
    local oldValue = m.get(uri, key)
    m.set(uri, key, copyed)
    local newValue = m.get(uri, key)
    if not util.equal(oldValue, newValue) then
        m.event(uri, key, newValue, oldValue)
        return true
    end
    return false
end

---@param uri uri
---@param key string
---@return any
function m.get(uri, key)
    local scp = getScope(uri, key)
    local value = m.getNowTable(scp)[key]
    if value == nil then
        value = template[key].default
    end
    if value == m.NULL then
        value = nil
    end
    return value
end

---@param uri uri
---@param key string
---@return any
function m.getRaw(uri, key)
    local scp = getScope(uri, key)
    local value = m.getRawTable(scp)[key]
    if value == nil then
        value = template[key].default
    end
    if value == m.NULL then
        value = nil
    end
    return value
end

---@param scp  scope
function m.getNowTable(scp)
    return scp:get 'config.now'
        or scp:set('config.now', {})
end

---@param scp  scope
function m.getRawTable(scp)
    return scp:get 'config.raw'
        or scp:set('config.raw', {})
end

---@param scp  scope
---@param ...  table
function m.update(scp, ...)
    local oldConfig = m.getNowTable(scp)
    local newConfig = {}
    scp:set('config.now', newConfig)
    scp:set('config.raw', {})

    local function expand(t, left)
        for key, value in pairs(t) do
            local fullKey = key
            if left then
                fullKey = left .. '.' .. key
            end
            if m.nullSymbols[value] then
                value = m.NULL
            end
            if     template[fullKey] then
                m.setByScope(scp, fullKey, value)
            elseif template['Lua.' .. fullKey] then
                m.setByScope(scp, 'Lua.' .. fullKey, value)
            elseif type(value) == 'table' then
                expand(value, fullKey)
            end
        end
    end

    local news = table.pack(...)
    for i = 1, news.n do
        if type(news[i]) == 'table' then
            expand(news[i])
        end
    end

    -- compare then fire event
    if oldConfig then
        for key, oldValue in pairs(oldConfig) do
            local newValue = newConfig[key]
            if not util.equal(oldValue, newValue) then
                m.event(scp.uri, key, newValue, oldValue)
            end
        end
    end

    m.event(scp.uri, '')
end

---@param callback fun(uri: uri, key: string, value: any, oldValue: any)
function m.watch(callback)
    m.watchList[#m.watchList+1] = callback
end

function m.event(uri, key, value, oldValue)
    if not m.changes then
        m.changes = {}
        timer.wait(0, function ()
            local delay = m.changes
            m.changes = nil
            for _, info in ipairs(delay) do
                for _, callback in ipairs(m.watchList) do
                    callback(info.uri, info.key, info.value, info.oldValue)
                end
            end
        end)
    end
    m.changes[#m.changes+1] = {
        uri      = uri,
        key      = key,
        value    = value,
        oldValue = oldValue,
    }
end

function m.addNullSymbol(null)
    m.nullSymbols[null] = true
end

return m
