local util   = require 'utility'
local define = require 'proto.define'
local timer  = require 'timer'
local scope  = require 'workspace.scope'

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

---@class config.unit
---@field caller function
local mt = {}
mt.__index = mt

function mt:__call(...)
    self:caller(...)
    return self
end

function mt:__shr(default)
    self.default = default
    return self
end

local units = {}

local function register(name, default, checker, loader, caller)
    units[name] = {
        default = default,
        checker = checker,
        loader  = loader,
        caller  = caller,
    }
end

local Type = setmetatable({}, { __index = function (_, name)
    local unit = {}
    for k, v in pairs(units[name]) do
        unit[k] = v
    end
    return setmetatable(unit, mt)
end })

register('Boolean', false, function (self, v)
    return type(v) == 'boolean'
end, function (self, v)
    return v
end)

register('Integer', 0, function (self, v)
    return type(v) == 'number'
end, function (self, v)
    return math.floor(v)
end)

register('String', '', function (self, v)
    return type(v) == 'string'
end, function (self, v)
    return tostring(v)
end)

register('Nil', nil, function (self, v)
    return type(v) == 'nil'
end, function (self, v)
    return nil
end)

register('Array', {}, function (self, value)
    return type(value) == 'table'
end, function (self, value)
    local t = {}
    for _, v in ipairs(value) do
        if self.sub:checker(v) then
            t[#t+1] = self.sub:loader(v)
        end
    end
    return t
end, function (self, sub)
    self.sub = sub
end)

register('Hash', {}, function (self, value)
    if type(value) == 'table' then
        if #value == 0 then
            for k, v in pairs(value) do
                if not self.subkey:checker(k)
                or not self.subvalue:checker(v) then
                    return false
                end
            end
        else
            if not self.subvalue:checker(true) then
                return false
            end
            for _, v in ipairs(value) do
                if not self.subkey:checker(v) then
                    return false
                end
            end
        end
        return true
    end
    if type(value) == 'string' then
        return  self.subkey:checker('')
            and self.subvalue:checker(true)
    end
end, function (self, value)
    if type(value) == 'table' then
        local t = {}
        if #value == 0 then
            for k, v in pairs(value) do
                t[k] = v
            end
        else
            for _, k in pairs(value) do
                t[k] = true
            end
        end
        return t
    end
    if type(value) == 'string' then
        local t = {}
        for s in value:gmatch('[^' .. self.sep .. ']+') do
            t[s] = true
        end
        return t
    end
end, function (self, subkey, subvalue, sep)
    self.subkey   = subkey
    self.subvalue = subvalue
    self.sep      = sep
end)

register('Or', nil, function (self, value)
    for _, sub in ipairs(self.subs) do
        if sub:checker(value) then
            return true
        end
    end
    return false
end, function (self, value)
    for _, sub in ipairs(self.subs) do
        if sub:checker(value) then
            return sub:loader(value)
        end
    end
end, function (self, ...)
    self.subs = { ... }
end)

local Template = {
    ['Lua.runtime.version']                 = Type.String >> 'Lua 5.4',
    ['Lua.runtime.path']                    = Type.Array(Type.String) >> {
                                                "?.lua",
                                                "?/init.lua",
                                            },
    ['Lua.runtime.pathStrict']              = Type.Boolean >> false,
    ['Lua.runtime.special']                 = Type.Hash(Type.String, Type.String),
    ['Lua.runtime.meta']                    = Type.String >> '${version} ${language} ${encoding}',
    ['Lua.runtime.unicodeName']             = Type.Boolean,
    ['Lua.runtime.nonstandardSymbol']       = Type.Hash(Type.String, Type.Boolean, ';'),
    ['Lua.runtime.plugin']                  = Type.String,
    ['Lua.runtime.fileEncoding']            = Type.String >> 'utf8',
    ['Lua.runtime.builtin']                 = Type.Hash(Type.String, Type.String),
    ['Lua.diagnostics.enable']              = Type.Boolean >> true,
    ['Lua.diagnostics.globals']             = Type.Hash(Type.String, Type.Boolean, ';'),
    ['Lua.diagnostics.disable']             = Type.Hash(Type.String, Type.Boolean, ';'),
    ['Lua.diagnostics.severity']            = Type.Hash(Type.String, Type.String)
                                            >> util.deepCopy(define.DiagnosticDefaultSeverity),
    ['Lua.diagnostics.neededFileStatus']    = Type.Hash(Type.String, Type.String)
                                            >> util.deepCopy(define.DiagnosticDefaultNeededFileStatus),
    ['Lua.diagnostics.workspaceDelay']      = Type.Integer >> 5,
    ['Lua.diagnostics.workspaceRate']       = Type.Integer >> 100,
    ['Lua.diagnostics.libraryFiles']        = Type.String  >> 'Opened',
    ['Lua.diagnostics.ignoredFiles']        = Type.String  >> 'Opened',
    ['Lua.workspace.ignoreDir']             = Type.Array(Type.String),
    ['Lua.workspace.ignoreSubmodules']      = Type.Boolean >> true,
    ['Lua.workspace.useGitIgnore']          = Type.Boolean >> true,
    ['Lua.workspace.maxPreload']            = Type.Integer >> 3000,
    ['Lua.workspace.preloadFileSize']       = Type.Integer >> 500,
    ['Lua.workspace.library']               = Type.Hash(Type.String, Type.Boolean, ';'),
    ['Lua.workspace.checkThirdParty']       = Type.Boolean >> true,
    ['Lua.workspace.userThirdParty']        = Type.Array(Type.String),
    ['Lua.completion.enable']               = Type.Boolean >> true,
    ['Lua.completion.callSnippet']          = Type.String  >> 'Disable',
    ['Lua.completion.keywordSnippet']       = Type.String  >> 'Replace',
    ['Lua.completion.displayContext']       = Type.Integer >> 0,
    ['Lua.completion.workspaceWord']        = Type.Boolean >> true,
    ['Lua.completion.showWord']             = Type.String  >> 'Fallback',
    ['Lua.completion.autoRequire']          = Type.Boolean >> true,
    ['Lua.completion.showParams']           = Type.Boolean >> true,
    ['Lua.completion.requireSeparator']     = Type.String  >> '.',
    ['Lua.completion.postfix']              = Type.String  >> '@',
    ['Lua.signatureHelp.enable']            = Type.Boolean >> true,
    ['Lua.hover.enable']                    = Type.Boolean >> true,
    ['Lua.hover.viewString']                = Type.Boolean >> true,
    ['Lua.hover.viewStringMax']             = Type.Integer >> 1000,
    ['Lua.hover.viewNumber']                = Type.Boolean >> true,
    ['Lua.hover.previewFields']             = Type.Integer >> 20,
    ['Lua.hover.enumsLimit']                = Type.Integer >> 5,
    ['Lua.hover.expandAlias']               = Type.Boolean >> true,
    ['Lua.semantic.enable']                 = Type.Boolean >> true,
    ['Lua.semantic.variable']               = Type.Boolean >> true,
    ['Lua.semantic.annotation']             = Type.Boolean >> true,
    ['Lua.semantic.keyword']                = Type.Boolean >> false,
    ['Lua.hint.enable']                     = Type.Boolean >> false,
    ['Lua.hint.paramType']                  = Type.Boolean >> true,
    ['Lua.hint.setType']                    = Type.Boolean >> false,
    ['Lua.hint.paramName']                  = Type.String  >> 'All',
    ['Lua.hint.await']                      = Type.Boolean >> true,
    ['Lua.hint.arrayIndex']                 = Type.Boolean >> 'Auto',
    ['Lua.window.statusBar']                = Type.Boolean >> true,
    ['Lua.window.progressBar']              = Type.Boolean >> true,
    ['Lua.format.enable']                   = Type.Boolean >> true,
    ['Lua.format.defaultConfig']            = Type.Hash(Type.String, Type.String)
                                            >> {},
    ['Lua.telemetry.enable']                = Type.Or(Type.Boolean >> false, Type.Nil) >> nil,
    ['files.associations']                  = Type.Hash(Type.String, Type.String),
    ['files.exclude']                       = Type.Hash(Type.String, Type.Boolean),
    ['editor.semanticHighlighting.enabled'] = Type.Or(Type.Boolean, Type.String),
    ['editor.acceptSuggestionOnEnter']      = Type.String  >> 'on',
}

---@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 news[i] 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
