local util  = require 'utility'
local scope = require 'workspace.scope'
local guide = require 'parser.guide'
local config = require 'config'
---@class vm
local vm    = require 'vm.vm'

---@type table<string, vm.global>
local allGlobals = {}
---@type table<uri, table<string, boolean>>
local globalSubs = util.multiTable(2)

---@class parser.object
---@field package _globalBase parser.object
---@field package _globalBaseMap table<string, parser.object>
---@field global vm.global

---@class vm.global.link
---@field sets parser.object[]
---@field gets parser.object[]

---@class vm.global
---@field links table<uri, vm.global.link>
---@field setsCache? table<uri, parser.object[]>
---@field cate vm.global.cate
---@field uri string
local mt = {}
mt.__index = mt
mt.type = 'global'
mt.name = ''

---@param uri    uri
---@param source parser.object
function mt:addSet(uri, source)
    local link = self.links[uri]
    link.sets[#link.sets+1] = source
    self.setsCache = nil
end

---@param uri    uri
---@param source parser.object
function mt:addGet(uri, source)
    local link = self.links[uri]
    link.gets[#link.gets+1] = source
end

---@param suri uri
---@return parser.object[]
function mt:getSets(suri)
    if not self.setsCache then
        self.setsCache = {}
    end
    local scp = scope.getScope(suri)
    local cacheUri = scp.uri or '<callback>'
    if self.setsCache[cacheUri] then
        return self.setsCache[cacheUri]
    end
    local clock = os.clock()
    self.setsCache[cacheUri] = {}
    local cache = self.setsCache[cacheUri]
    for uri, link in pairs(self.links) do
        if link.sets then
            if scp:isVisible(uri) then
                for _, source in ipairs(link.sets) do
                    cache[#cache+1] = source
                end
            end
        end
    end
    local cost = os.clock() - clock
    if cost > 0.1 then
        log.warn('global-manager getSets costs', cost, self.name)
    end
    return cache
end

---@return parser.object[]
function mt:getAllSets()
    if not self.setsCache then
        self.setsCache = {}
    end
    local cache = self.setsCache['*']
    if cache then
        return cache
    end
    cache = {}
    self.setsCache['*'] = cache
    for _, link in pairs(self.links) do
        if link.sets then
            for _, source in ipairs(link.sets) do
                cache[#cache+1] = source
            end
        end
    end
    return cache
end

---@param uri uri
function mt:dropUri(uri)
    self.links[uri] = nil
    self.setsCache = nil
end

---@return string
function mt:getName()
    return self.name
end

---@return string
function mt:getCodeName()
    return (self.name:gsub(vm.ID_SPLITE, '.'))
end

---@return string
function mt:asKeyName()
    return self.cate .. '|' .. self.name
end

---@return string
function mt:getKeyName()
    return self.name:match('[^' .. vm.ID_SPLITE .. ']+$')
end

---@return string?
function mt:getFieldName()
    return self.name:match(vm.ID_SPLITE .. '(.-)$')
end

---@return boolean
function mt:isAlive()
    return next(self.links) ~= nil
end

---@param uri uri
---@return parser.object?
function mt:getParentBase(uri)
    local parentID = self.name:match('^(.-)' .. vm.ID_SPLITE)
    if not parentID then
        return nil
    end
    local parentName = self.cate .. '|' .. parentID
    local globalVar = allGlobals[parentName]
    if not globalVar then
        return nil
    end
    local link = globalVar.links[uri]
    if not link then
        return nil
    end
    local luckyBoy = link.sets[1] or link.gets[1]
    if not luckyBoy then
        return nil
    end
    return vm.getGlobalBase(luckyBoy)
end

---@param cate vm.global.cate
---@return vm.global
local function createGlobal(name, cate, uri)
    return setmetatable({
        name  = name,
        cate  = cate,
        uri   = uri,
        links = util.multiTable(2, function ()
            return {
                sets = {},
                gets = {},
            }
        end),
    }, mt)
end

---@class parser.object
---@field package _globalNode vm.global|false
---@field package _enums?     parser.object[]

local compileObject
local compilerGlobalSwitch = util.switch()
    : case 'local'
    : call(function (source)
        if source.special ~= '_G' then
            return
        end
        if source.ref then
            for _, ref in ipairs(source.ref) do
                compileObject(ref)
            end
        end
    end)
    : case 'getlocal'
    : call(function (source)
        if source.special ~= '_G' then
            return
        end
        if not source.next then
            return
        end
        compileObject(source.next)
    end)
    : case 'setglobal'
    : call(function (source)
        local uri    = guide.getUri(source)
        local name   = guide.getKeyName(source)
        if not name then
            return
        end
        local globalVar = vm.declareGlobal('variable', name, uri)
        globalVar:addSet(uri, source)
        source._globalNode = globalVar
    end)
    : case 'getglobal'
    : call(function (source)
        local uri    = guide.getUri(source)
        local name   = guide.getKeyName(source)
        if not name then
            return
        end
        local globalVar = vm.declareGlobal('variable', name, uri)
        globalVar:addGet(uri, source)
        source._globalNode = globalVar

        local nxt = source.next
        if nxt then
            compileObject(nxt)
        end
    end)
    : case 'setfield'
    : case 'setmethod'
    : case 'setindex'
    ---@param source parser.object
    : call(function (source)
        local name
        local keyName = guide.getKeyName(source)
        if not keyName then
            return
        end
        if source.node._globalNode then
            local parentName = source.node._globalNode:getName()
            if parentName == '_G' then
                name = keyName
            else
                name = ('%s%s%s'):format(parentName, vm.ID_SPLITE, keyName)
            end
        elseif source.node.special == '_G' then
            name = keyName
        end
        if not name then
            return
        end
        local uri    = guide.getUri(source)
        local globalVar = vm.declareGlobal('variable', name, uri)
        globalVar:addSet(uri, source)
        source._globalNode = globalVar
    end)
    : case 'getfield'
    : case 'getmethod'
    : case 'getindex'
    ---@param source parser.object
    : call(function (source)
        local name
        local keyName = guide.getKeyName(source)
        if not keyName then
            return
        end
        if source.node._globalNode then
            local parentName = source.node._globalNode:getName()
            if parentName == '_G' then
                name = keyName
            else
                name = ('%s%s%s'):format(parentName, vm.ID_SPLITE, keyName)
            end
        elseif source.node.special == '_G' then
            name = keyName
        end
        local uri    = guide.getUri(source)
        local globalVar = vm.declareGlobal('variable', name, uri)
        globalVar:addGet(uri, source)
        source._globalNode = globalVar

        local nxt = source.next
        if nxt then
            compileObject(nxt)
        end
    end)
    : case 'call'
    : call(function (source)
        if source.node.special == 'rawset'
        or source.node.special == 'rawget' then
            if not source.args then
                return
            end
            local g     = source.args[1]
            local key   = source.args[2]
            if g and key and g.special == '_G' then
                local name = guide.getKeyName(key)
                if name then
                    local uri    = guide.getUri(source)
                    local globalVar = vm.declareGlobal('variable', name, uri)
                    if source.node.special == 'rawset' then
                        globalVar:addSet(uri, source)
                        source.value = source.args[3]
                    else
                        globalVar:addGet(uri, source)
                    end
                    source._globalNode = globalVar

                    local nxt = source.next
                    if nxt then
                        compileObject(nxt)
                    end
                end
            end
        end
    end)
    : case 'doc.class'
    ---@param source parser.object
    : call(function (source)
        local uri  = guide.getUri(source)
        local name = guide.getKeyName(source)
        if not name then
            return
        end
        local class = vm.declareGlobal('type', name, uri)
        class:addSet(uri, source)
        source._globalNode = class

        if source.signs then
            local sign = vm.createSign()
            vm.setSign(source, sign)
            for _, obj in ipairs(source.signs) do
                sign:addSign(vm.compileNode(obj))
            end
            if source.extends then
                for _, ext in ipairs(source.extends) do
                    if ext.type == 'doc.type.table' then
                        vm.setGeneric(ext, vm.createGeneric(ext, sign))
                    end
                end
            end
        end
    end)
    : case 'doc.alias'
    : call(function (source)
        local uri  = guide.getUri(source)
        local name = guide.getKeyName(source)
        if not name then
            return
        end
        local alias = vm.declareGlobal('type', name, uri)
        alias:addSet(uri, source)
        source._globalNode = alias

        if source.signs then
            source._sign = vm.createSign()
            for _, sign in ipairs(source.signs) do
                source._sign:addSign(vm.compileNode(sign))
            end
            source.extends._generic = vm.createGeneric(source.extends, source._sign)
        end
    end)
    : case 'doc.enum'
    : call(function (source)
        local uri  = guide.getUri(source)
        local name = guide.getKeyName(source)
        if not name then
            return
        end
        local enum = vm.declareGlobal('type', name, uri)
        enum:addSet(uri, source)
        source._globalNode = enum

        local tbl = source.bindSource
        if not tbl then
            return
        end
        source._enums = {}
        if vm.docHasAttr(source, 'key') then
            for _, field in ipairs(tbl) do
                if     field.type == 'tablefield' then
                    source._enums[#source._enums+1] = {
                        type   = 'doc.type.string',
                        start  = field.field.start,
                        finish = field.field.finish,
                        [1]    = field.field[1],
                    }
                elseif field.type == 'tableindex' then
                    if field.index then
                        source._enums[#source._enums+1] = {
                            type   = 'doc.type.string',
                            start  = field.index.start,
                            finish = field.index.finish,
                            [1]    = field.index[1],
                        }
                    end
                end
            end
        else
            for _, field in ipairs(tbl) do
                if     field.type == 'tablefield' then
                    source._enums[#source._enums+1] = field
                    local subType = vm.declareGlobal('type', name .. '.' .. field.field[1], uri)
                    subType:addSet(uri, field)
                elseif field.type == 'tableindex' then
                    source._enums[#source._enums+1] = field
                    if field.index.type == 'string' then
                        local subType = vm.declareGlobal('type', name .. '.' .. field.index[1], uri)
                        subType:addSet(uri, field)
                    end
                end
            end
        end
    end)
    : case 'doc.type.name'
    : call(function (source)
        local uri  = guide.getUri(source)
        local name = source[1]
        if name == '_' then
            return
        end
        if name == 'self' then
            return
        end
        local type = vm.declareGlobal('type', name, uri)
        type:addGet(uri, source)
        source._globalNode = type
    end)
    : case 'doc.extends.name'
    : call(function (source)
        local uri  = guide.getUri(source)
        local name = source[1]
        local class = vm.declareGlobal('type', name, uri)
        class:addGet(uri, source)
        source._globalNode = class
    end)


---@alias vm.global.cate '"variable"' | '"type"'

---@param cate vm.global.cate
---@param name string
---@param uri? uri
---@return vm.global
function vm.declareGlobal(cate, name, uri)
    local key = cate .. '|' .. name
    if uri then
        globalSubs[uri][key] = true
    end
    if not allGlobals[key] then
        allGlobals[key] = createGlobal(name, cate, uri)
    end
    return allGlobals[key]
end

---@param cate   vm.global.cate
---@param name   string
---@param field? string
---@return vm.global?
function vm.getGlobal(cate, name, field)
    local key = cate .. '|' .. name
    if field then
        key = key .. vm.ID_SPLITE .. field
    end
    return allGlobals[key]
end

---@param cate   vm.global.cate
---@param name   string
---@return vm.global[]
function vm.getGlobalFields(cate, name)
    local globals = {}
    local key = cate .. '|' .. name

    local clock = os.clock()
    for gid, globalVar in pairs(allGlobals) do
        if  gid ~= key
        and util.stringStartWith(gid, key)
        and gid:sub(#key + 1, #key + 1) == vm.ID_SPLITE
        and not gid:find(vm.ID_SPLITE, #key + 2) then
            globals[#globals+1] = globalVar
        end
    end
    local cost = os.clock() - clock
    if cost > 0.1 then
        log.warn('global-manager getFields costs', cost)
    end

    return globals
end

---@param cate   vm.global.cate
---@return vm.global[]
function vm.getGlobals(cate)
    local globals = {}

    local clock = os.clock()
    for gid, globalVar in pairs(allGlobals) do
        if  util.stringStartWith(gid, cate)
        and not gid:find(vm.ID_SPLITE) then
            globals[#globals+1] = globalVar
        end
    end
    local cost = os.clock() - clock
    if cost > 0.1 then
        log.warn('global-manager getGlobals costs', cost)
    end

    return globals
end

---@return table<string, vm.global>
function vm.getAllGlobals()
    return allGlobals
end

---@return table<string, vm.global>
function vm.getExportableGlobals()
    local exportableGlobals = {}
    for key, globalVar in pairs(allGlobals) do
        --If the source uri for the global matches the global variable METAPATH
        --then the global is a builtin Lua language feature and should not be exported
        if globalVar.uri and not string.find(globalVar.uri, METAPATH, 1, true) then
            exportableGlobals[key] = globalVar
        end
    end
    return exportableGlobals
end

---@param suri uri
---@param cate   vm.global.cate
---@return parser.object[]
function vm.getGlobalSets(suri, cate)
    local globals = vm.getGlobals(cate)
    local result = {}
    for _, globalVar in ipairs(globals) do
        local sets = globalVar:getSets(suri)
        for _, set in ipairs(sets) do
            result[#result+1] = set
        end
    end
    return result
end

---@param suri uri
---@param cate vm.global.cate
---@param name string
---@return boolean
function vm.hasGlobalSets(suri, cate, name)
    local globalVar = vm.getGlobal(cate, name)
    if not globalVar then
        return false
    end
    local sets = globalVar:getSets(suri)
    if #sets == 0 then
        return false
    end
    return true
end

---@param uri uri
---@param key string
---@return boolean
local function checkIsGlobalRegex(uri, key)
    local dglobalsregex = config.get(uri, 'Lua.diagnostics.globalsRegex')
    if not dglobalsregex then
        return false
    end

    for _, pattern in ipairs(dglobalsregex) do
        if key:match(pattern) then
            return true
        end
    end

    return false
end

---@param src parser.object
local function checkIsUndefinedGlobal(src)
    if src.type ~= 'getglobal' then
        return false
    end

    local key = src[1]
    if not key then
        return false
    end

    local node = src.node
    if not node then
        return false
    end
    if node.tag ~= '_ENV' then
        return false
    end

    local uri = guide.getUri(src)
    local rspecial = config.get(uri, 'Lua.runtime.special')
    if rspecial[key] then
        return false
    end

    if vm.hasGlobalSets(uri, 'variable', key) then
        return false
    end

    local dglobals = config.get(uri, 'Lua.diagnostics.globals')
    if util.arrayHas(dglobals, key) then
        return false
    end

    if checkIsGlobalRegex(uri, key) then
        return false
    end

    return true
end

---@param src parser.object
---@return boolean
function vm.isUndefinedGlobal(src)
    local node = vm.compileNode(src)
    if node.undefinedGlobal == nil then
        node.undefinedGlobal = checkIsUndefinedGlobal(src)
    end
    return node.undefinedGlobal
end

---@param source parser.object
function compileObject(source)
    if source._globalNode ~= nil then
        return
    end
    source._globalNode = false
    compilerGlobalSwitch(source.type, source)
end

---@param source parser.object
---@return vm.global?
function vm.getGlobalNode(source)
    return source._globalNode or nil
end

---@param source parser.object
---@return parser.object[]?
function vm.getEnums(source)
    return source._enums
end

---@param source parser.object
---@return boolean
function vm.compileByGlobal(source)
    local globalVar = vm.getGlobalNode(source)
    if not globalVar then
        return false
    end
    vm.setNode(source, globalVar)
    if globalVar.cate == 'variable' then
        if guide.isAssign(source) then
            if vm.bindDocs(source) then
                return true
            end
            if source.value and source.value.type ~= 'nil' then
                vm.setNode(source, vm.compileNode(source.value))
                return true
            end
        else
            if vm.bindAs(source) then
                return true
            end
            local node = vm.traceNode(source)
            if node then
                vm.setNode(source, node, true)
                return true
            end
        end
    end
    local globalBase = vm.getGlobalBase(source)
    if not globalBase then
        return false
    end
    local globalNode = vm.compileNode(globalBase)
    vm.setNode(source, globalNode, true)
    return true
end

---@param source parser.object
---@return parser.object?
function vm.getGlobalBase(source)
    if source._globalBase then
        return source._globalBase
    end
    local globalVar = vm.getGlobalNode(source)
    if not globalVar then
        return nil
    end
    ---@cast source parser.object
    local root = guide.getRoot(source)
    if not root._globalBaseMap then
        root._globalBaseMap = {}
    end
    local name = globalVar:asKeyName()
    if not root._globalBaseMap[name] then
        ---@diagnostic disable-next-line: missing-fields
        root._globalBaseMap[name] = {
            type   = 'globalbase',
            parent = root,
            ['global'] = globalVar,
            start  = 0,
            finish = 0,
        }
    end
    source._globalBase = root._globalBaseMap[name]
    return source._globalBase
end

---@param source parser.object
local function compileAst(source)
    local env = guide.getENV(source)
    if not env then
        return
    end
    compileObject(env)
    guide.eachSpecialOf(source, 'rawset', function (src)
        compileObject(src.parent)
    end)
    guide.eachSpecialOf(source, 'rawget', function (src)
        compileObject(src.parent)
    end)
    guide.eachSourceTypes(source.docs, {
        'doc.class',
        'doc.alias',
        'doc.type.name',
        'doc.extends.name',
        'doc.enum',
    }, function (src)
        compileObject(src)
    end)
end

---@param uri uri
local function dropUri(uri)
    local globalSub = globalSubs[uri]
    globalSubs[uri] = nil
    for key in pairs(globalSub) do
        local globalVar = allGlobals[key]
        if globalVar then
            globalVar:dropUri(uri)
            if not globalVar:isAlive() then
                allGlobals[key] = nil
            end
        end
    end
end

return {
    compileAst = compileAst,
    dropUri    = dropUri,
}
