local guide      = require 'parser.guide'
local util       = require 'utility'
local config     = require 'config'
local rpath      = require 'workspace.require-path'
local files      = require 'files'
---@class vm
local vm         = require 'vm.vm'

---@class parser.object
---@field _compiledNodes  boolean
---@field _node           vm.node
---@field _globalBase     table

-- 该函数有副作用,会给source绑定node!
local function bindDocs(source)
    local isParam = source.parent.type == 'funcargs'
                 or (source.parent.type == 'in' and source.finish <= source.parent.keys.finish)
    local docs = source.bindDocs
    for i = #docs, 1, -1 do
        local doc = docs[i]
        if doc.type == 'doc.type' then
            if not isParam then
                vm.setNode(source, vm.compileNode(doc))
                return true
            end
        end
        if doc.type == 'doc.class' then
            if (source.type == 'local' and not isParam)
            or (source._globalNode and guide.isSet(source))
            or source.type == 'tablefield'
            or source.type == 'tableindex' then
                vm.setNode(source, vm.compileNode(doc))
                return true
            end
        end
        if doc.type == 'doc.param' then
            if isParam and source[1] == doc.param[1] then
                local node = vm.compileNode(doc)
                if doc.optional then
                    node:addOptional()
                end
                vm.setNode(source, node)
                return true
            end
        end
        if doc.type == 'doc.module' then
            local name = doc.module
            local uri = rpath.findUrisByRequirePath(guide.getUri(source), name)[1]
            if not uri then
                return true
            end
            local state = files.getState(uri)
            local ast   = state and state.ast
            if not ast then
                return true
            end
            vm.setNode(source, vm.compileNode(ast))
            return true
        end
        if doc.type == 'doc.overload' then
            if not isParam then
                vm.setNode(source, vm.compileNode(doc))
            end
        end
    end
    return false
end

local searchFieldSwitch = util.switch()
    : case 'table'
    : call(function (suri, source, key, ref, pushResult)
        local hasFiled = false
        for _, field in ipairs(source) do
            if field.type == 'tablefield'
            or field.type == 'tableindex' then
                local fieldKey = guide.getKeyName(field)
                if key == nil
                or key == fieldKey then
                    hasFiled = true
                    pushResult(field)
                end
            end
            if field.type == 'tableexp' then
                if key == nil
                or key == field.tindex then
                    hasFiled = true
                    pushResult(field)
                end
            end
            if field.type == 'varargs' then
                if not hasFiled
                and type(key) == 'number'
                and key >= 1
                and math.tointeger(key) then
                    hasFiled = true
                    pushResult(field)
                end
                if key == nil then
                    pushResult(field)
                end
            end
        end
    end)
    : case 'string'
    : case 'doc.type.string'
    : call(function (suri, source, key, ref, pushResult)
        -- change to `string: stringlib` ?
        local stringlib = vm.getGlobal('type', 'stringlib')
        if stringlib then
            vm.getClassFields(suri, stringlib, key, ref, pushResult)
        end
    end)
    : case 'local'
    : case 'self'
    : call(function (suri, node, key, ref, pushResult)
        local fields
        if key then
            fields = vm.getLocalSourcesSets(node, key)
        else
            fields = vm.getLocalFields(node, false)
        end
        if not fields then
            return
        end
        local hasMarkDoc = {}
        for _, src in ipairs(fields) do
            if src.bindDocs then
                if bindDocs(src) then
                    local skey = guide.getKeyName(src)
                    if skey then
                        hasMarkDoc[skey] = true
                    end
                    pushResult(src, true)
                end
            end
        end
        for _, src in ipairs(fields) do
            local skey = guide.getKeyName(src)
            if not hasMarkDoc[skey] then
                pushResult(src)
            end
        end
    end)
    : case 'doc.type.array'
    : call(function (suri, source, key, ref, pushResult)
        if type(key) == 'number' then
            if key < 1
            or not math.tointeger(key) then
                return
            end
            pushResult(source.node)
        end
        if type(key) == 'table' then
            if vm.isSubType(suri, key, 'integer') then
                pushResult(source.node)
            end
        end
    end)
    : case 'doc.type.table'
    : call(function (suri, source, key, ref, pushResult)
        for _, field in ipairs(source.fields) do
            local fieldKey = field.name
            if fieldKey.type == 'doc.type' then
                local fieldNode = vm.compileNode(fieldKey)
                for fn in fieldNode:eachObject() do
                    if fn.type == 'global' and fn.cate == 'type' then
                        if key == nil
                        or fn.name == 'any'
                        or (fn.name == 'boolean' and type(key) == 'boolean')
                        or (fn.name == 'number'  and type(key) == 'number')
                        or (fn.name == 'integer' and math.tointeger(key))
                        or (fn.name == 'string'  and type(key) == 'string') then
                            pushResult(field)
                        end
                    end
                end
            end
            if fieldKey.type == 'doc.field.name' then
                if key == nil or fieldKey[1] == key then
                    pushResult(field)
                end
            end
        end
    end)
    : case 'global'
    : call(function (suri, node, key, ref, pushResult)
        if node.cate == 'variable' then
            if key then
                if type(key) ~= 'string' then
                    return
                end
                local global = vm.getGlobal('variable', node.name, key)
                if global then
                    for _, set in ipairs(global:getSets(suri)) do
                        pushResult(set)
                    end
                    if ref then
                        for _, get in ipairs(global:getGets(suri)) do
                            pushResult(get)
                        end
                    end
                end
            else
                local globals = vm.getGlobalFields('variable', node.name)
                for _, global in ipairs(globals) do
                    for _, set in ipairs(global:getSets(suri)) do
                        pushResult(set)
                    end
                    if ref then
                        for _, get in ipairs(global:getGets(suri)) do
                            pushResult(get)
                        end
                    end
                end
            end
        end
        if node.cate == 'type' then
            vm.getClassFields(suri, node, key, ref, pushResult)
        end
    end)
    : default(function (suri, source, key, ref, pushResult)
        local node = source._globalNode
        if not node then
            return
        end
        if node.cate == 'variable' then
            if key then
                if type(key) ~= 'string' then
                    return
                end
                local global = vm.getGlobal('variable', node.name, key)
                if global then
                    for _, set in ipairs(global:getSets(suri)) do
                        pushResult(set)
                    end
                    for _, get in ipairs(global:getGets(suri)) do
                        pushResult(get)
                    end
                end
            else
                local globals = vm.getGlobalFields('variable', node.name)
                for _, global in ipairs(globals) do
                    for _, set in ipairs(global:getSets(suri)) do
                        pushResult(set)
                    end
                    for _, get in ipairs(global:getGets(suri)) do
                        pushResult(get)
                    end
                end
            end
        end
        if node.cate == 'type' then
            vm.getClassFields(suri, node, key, ref, pushResult)
        end
    end)

---@param suri uri
---@param object vm.global
---@param key string|vm.global
---@param ref boolean
---@param pushResult fun(field: vm.object, isMark?: boolean)
function vm.getClassFields(suri, object, key, ref, pushResult)
    local mark = {}

    local function searchClass(class, searchedFields)
        local name = class.name
        if mark[name] then
            return
        end
        mark[name] = true
        searchedFields = searchedFields or {}
        for _, set in ipairs(class:getSets(suri)) do
            if set.type == 'doc.class' then
                -- check ---@field
                local hasFounded = {}
                for _, field in ipairs(set.fields) do
                    local fieldKey = guide.getKeyName(field)
                    if fieldKey then
                        -- ---@field x boolean -> class.x
                        if key == nil
                        or fieldKey == key then
                            if not searchedFields[fieldKey] then
                                pushResult(field, true)
                                hasFounded[fieldKey] = true
                            end
                        end
                    end
                    if not hasFounded[fieldKey] then
                        local keyType = type(key)
                        if keyType == 'table' then
                            -- ---@field [integer] boolean -> class[integer]
                            local fieldNode = vm.compileNode(field.field)
                            if vm.isSubType(suri, key.name, fieldNode) then
                                local nkey = '|' .. key.name
                                if not searchedFields[nkey] then
                                    pushResult(field, true)
                                    hasFounded[nkey] = true
                                end
                            end
                        else
                            local typeName
                            if keyType == 'number' then
                                if math.tointeger(key) then
                                    typeName = 'integer'
                                else
                                    typeName = 'number'
                                end
                            elseif keyType == 'boolean'
                            or     keyType == 'string' then
                                typeName = keyType
                            end
                            if typeName and field.field.type ~= 'doc.field.name' then
                                -- ---@field [integer] boolean -> class[1]
                                local fieldNode = vm.compileNode(field.field)
                                if vm.isSubType(suri, typeName, fieldNode) then
                                    local nkey = '|' .. typeName
                                    if not searchedFields[nkey] then
                                        pushResult(field, true)
                                        hasFounded[nkey] = true
                                    end
                                end
                            end
                        end
                    end
                end
                -- check local field and global field
                if not hasFounded[key] and set.bindSources then
                    for _, src in ipairs(set.bindSources) do
                        local skipSetLocal
                        if src.value and src.value.type == 'table' then
                            searchFieldSwitch('table', suri, src.value, key, ref, function (field)
                                local fieldKey = guide.getKeyName(field)
                                if fieldKey then
                                    if  not searchedFields[fieldKey]
                                    and guide.isSet(field) then
                                        hasFounded[fieldKey] = true
                                        pushResult(field, true)
                                        if src.type == 'local' then
                                            skipSetLocal = true
                                        end
                                    end
                                end
                            end)
                        end
                        if not skipSetLocal then
                            searchFieldSwitch(src.type, suri, src, key, ref, function (field)
                                local fieldKey = guide.getKeyName(field)
                                if fieldKey then
                                    if  not searchedFields[fieldKey]
                                    and guide.isSet(field) then
                                        hasFounded[fieldKey] = true
                                        pushResult(field, true)
                                    end
                                end
                            end)
                        end
                    end
                end
                -- look into extends(if field not found)
                if not hasFounded[key] and set.extends then
                    for fieldKey in pairs(hasFounded) do
                        searchedFields[fieldKey] = true
                    end
                    for _, extend in ipairs(set.extends) do
                        if extend.type == 'doc.extends.name' then
                            local extendType = vm.getGlobal('type', extend[1])
                            if extendType then
                                searchClass(extendType, searchedFields)
                            end
                        end
                    end
                end
            end
        end
    end

    local function searchGlobal(class)
        if class.cate == 'type' and class.name == '_G' then
            if key == nil then
                local sets = vm.getGlobalSets(suri, 'variable')
                for _, set in ipairs(sets) do
                    pushResult(set)
                end
            elseif type(key) == 'string' then
                local global = vm.getGlobal('variable', key)
                if global then
                    for _, set in ipairs(global:getSets(suri)) do
                        pushResult(set)
                    end
                end
            end
        end
    end

    searchClass(object)
    searchGlobal(object)
end

---@class parser.object
---@field _sign vm.sign|false

---@param source parser.object
---@return vm.sign?
local function getObjectSign(source)
    if source._sign ~= nil then
        return source._sign
    end
    source._sign = false
    if source.type == 'function' then
        if not source.bindDocs then
            return false
        end
        for _, doc in ipairs(source.bindDocs) do
            if doc.type == 'doc.generic' then
                if not source._sign then
                    source._sign = vm.createSign()
                    break
                end
            end
        end
        if not source._sign then
            return false
        end
        if source.args then
            for _, arg in ipairs(source.args) do
                local argNode = vm.compileNode(arg)
                if arg.optional then
                    argNode:addOptional()
                end
                source._sign:addSign(argNode)
            end
        end
    end
    if source.type == 'doc.type.function'
    or source.type == 'doc.type.table'
    or source.type == 'doc.type.array' then
        local hasGeneric
        guide.eachSourceType(source, 'doc.generic.name', function ()
            hasGeneric = true
        end)
        if not hasGeneric then
            return false
        end
        source._sign = vm.createSign()
        if source.type == 'doc.type.function' then
            for _, arg in ipairs(source.args) do
                if arg.extends then
                    local argNode = vm.compileNode(arg.extends)
                    if arg.optional then
                        argNode:addOptional()
                    end
                    source._sign:addSign(argNode)
                else
                    source._sign:addSign(vm.createNode())
                end
            end
        end
    end
    return source._sign
end

---@param func  parser.object
---@param index integer
---@return (parser.object|vm.generic)?
function vm.getReturnOfFunction(func, index)
    if func.type == 'function' then
        if not func._returns then
            func._returns = {}
        end
        if not func._returns[index] then
            func._returns[index] = {
                type   = 'function.return',
                parent = func,
                index  = index,
            }
        end
        return func._returns[index]
    end
    if func.type == 'doc.type.function' then
        local rtn = func.returns[index]
        if not rtn then
            return nil
        end
        local sign = getObjectSign(func)
        if not sign then
            return rtn
        end
        return vm.createGeneric(rtn, sign)
    end
end

---@return vm.node
local function getReturnOfSetMetaTable(args)
    local tbl  = args[1]
    local mt   = args[2]
    local node = vm.createNode()
    if tbl then
        node:merge(vm.compileNode(tbl))
    end
    if mt then
        vm.compileByParentNode(mt, '__index', false, function (src)
            for n in vm.compileNode(src):eachObject() do
                if n.type == 'global'
                or n.type == 'local'
                or n.type == 'table'
                or n.type == 'doc.type.table' then
                    node:merge(n)
                end
            end
        end)
    end
    return node
end

---@param source parser.object
local function matchCall(source)
    local call = source.parent
    if not call
    or call.type ~= 'call'
    or call.node ~= source then
        return
    end
    local funcs = vm.getMatchedFunctions(source, call.args)
    local myNode = vm.getNode(source)
    if not myNode then
        return
    end
    local needRemove
    for n in myNode:eachObject() do
        if n.type == 'function'
        or n.type == 'doc.type.function' then
            if not util.arrayHas(funcs, n) then
                if not needRemove then
                    needRemove = vm.createNode()
                end
                needRemove:merge(n)
            end
        end
    end
    if needRemove then
        local newNode = myNode:copy()
        newNode:removeNode(needRemove)
        vm.setNode(source, newNode, true)
    end
end

---@return vm.node?
local function getReturn(func, index, args)
    if func.special == 'setmetatable' then
        if not args then
            return nil
        end
        return getReturnOfSetMetaTable(args)
    end
    if func.special == 'pcall' and index > 1 then
        if not args then
            return nil
        end
        local newArgs = {}
        for i = 2, #args do
            newArgs[#newArgs+1] = args[i]
        end
        return getReturn(args[1], index - 1, newArgs)
    end
    if func.special == 'xpcall' and index > 1 then
        if not args then
            return nil
        end
        local newArgs = {}
        for i = 3, #args do
            newArgs[#newArgs+1] = args[i]
        end
        return getReturn(args[1], index - 1, newArgs)
    end
    if func.special == 'require' then
        if not args then
            return nil
        end
        local nameArg = args[1]
        if not nameArg or nameArg.type ~= 'string' then
            return nil
        end
        local name = nameArg[1]
        if not name or type(name) ~= 'string' then
            return nil
        end
        local uri = rpath.findUrisByRequirePath(guide.getUri(func), name)[1]
        if not uri then
            return nil
        end
        local state = files.getState(uri)
        local ast   = state and state.ast
        if not ast then
            return nil
        end
        return vm.compileNode(ast)
    end
    local funcNode = vm.compileNode(func)
    ---@type vm.node?
    local result
    for mfunc in funcNode:eachObject() do
        if mfunc.type == 'function'
        or mfunc.type == 'doc.type.function' then
            ---@cast mfunc parser.object
            local returnObject = vm.getReturnOfFunction(mfunc, index)
            if returnObject then
                local returnNode = vm.compileNode(returnObject)
                for rnode in returnNode:eachObject() do
                    if rnode.type == 'generic' then
                        returnNode = rnode:resolve(guide.getUri(func), args)
                        break
                    end
                end
                if returnNode then
                    for rnode in returnNode:eachObject() do
                        -- TODO: narrow type
                        if rnode.type ~= 'doc.generic.name' then
                            result = result or vm.createNode()
                            result:merge(rnode)
                        end
                    end
                    if result and returnNode:isOptional() then
                        result:addOptional()
                    end
                end
            end
        end
    end
    return result
end

---@param source parser.object
---@return boolean
local function bindAs(source)
    local root = guide.getRoot(source)
    local docs = root.docs
    if not docs then
        return false
    end
    local ases = docs._asCache
    if not ases then
        ases = {}
        docs._asCache = ases
        for _, doc in ipairs(docs) do
            if doc.type == 'doc.as' and doc.as then
                ases[#ases+1] = doc
            end
        end
        table.sort(ases, function (a, b)
            return a.start < b.start
        end)
    end

    if #ases == 0 then
        return false
    end

    local max = #ases
    local index
    local left  = 1
    local right = max
    for _ = 1, 1000 do
        if left == right then
            index = left
            break
        end
        index = left + (right - left) // 2
        local doc = ases[index]
        if doc.originalComment.start < source.finish + 2 then
            left = index + 1
        else
            right = index
        end
    end

    local doc = ases[index]
    if doc and doc.originalComment.start == source.finish + 2 then
        vm.setNode(source, vm.compileNode(doc.as), true)
        return true
    end

    return false
end

---@param source parser.object
---@param key? string|vm.global
---@param pushResult fun(source: parser.object)
function vm.compileByParentNode(source, key, ref, pushResult)
    local parentNode = vm.compileNode(source)
    local docedResults = {}
    local commonResults = {}
    local suri = guide.getUri(source)
    local hasClass
    for node in parentNode:eachObject() do
        if  node.type == 'global'
        and node.cate == 'type'
        ---@cast node vm.global
        and not guide.isBasicType(node.name) then
            hasClass = true
            break
        end
    end
    for node in parentNode:eachObject() do
        if not hasClass
        or (
                node.type == 'global'
            and node.cate == 'type'
            ---@cast node vm.global
            and not guide.isBasicType(node.name)
        )
        or node.type == 'doc.type.string' then
            searchFieldSwitch(node.type, suri, node, key, ref, function (res, markDoc)
                if markDoc then
                    docedResults[#docedResults+1] = res
                else
                    commonResults[#commonResults+1] = res
                end
            end)
        end
    end
    if #docedResults > 0 then
        for _, res in ipairs(docedResults) do
            pushResult(res)
        end
    end
    if #docedResults == 0 or key == nil then
        for _, res in ipairs(commonResults) do
            pushResult(res)
        end
    end
end

---@return vm.node?
local function selectNode(source, list, index)
    if not list then
        return nil
    end
    local exp
    if list[index] then
        exp = list[index]
    else
        for i = index, 1, -1 do
            if list[i] then
                local last = list[i]
                if last.type == 'call'
                or last.type == 'varargs' then
                    index = index - i + 1
                    exp = last
                end
                break
            end
        end
    end
    if not exp then
        vm.setNode(source, vm.declareGlobal('type', 'nil'))
        return vm.getNode(source)
    end
    ---@type vm.node?
    local result
    if exp.type == 'call' then
        result = getReturn(exp.node, index, exp.args)
        if not result then
            vm.setNode(source, vm.declareGlobal('type', 'unknown'))
            return vm.getNode(source)
        end
    else
        ---@type vm.node
        result = vm.compileNode(exp)
        if result and exp.type == 'varargs' and result:isEmpty() then
            result:merge(vm.declareGlobal('type', 'unknown'))
        end
    end
    if source.type == 'function.return' then
        -- remove any for returns
        local rtnNode = vm.createNode()
        for n in result:eachObject() do
            if guide.isLiteral(n) then
                rtnNode:merge(n)
            end
            if n.type == 'global' and n.cate == 'type' then
                if  n.name ~= 'any' then
                    rtnNode:merge(n)
                end
            else
                rtnNode:merge(n)
            end
        end
        vm.setNode(source, rtnNode)
        return rtnNode
    end
    vm.setNode(source, result)
    return result
end

---@param source parser.object
---@param node   vm.node.object
---@return boolean
local function isValidCallArgNode(source, node)
    if source.type == 'function' then
        return node.type == 'doc.type.function'
    end
    if source.type == 'table' then
        return node.type == 'doc.type.table'
            or (    node.type == 'global'
                and node.cate == 'type'
                ---@cast node vm.global
                and not guide.isBasicType(node.name)
            )
    end
    if source.type == 'dummyarg' then
        return true
    end
    return false
end

---@param func parser.object
---@param index integer
---@return parser.object?
local function getFuncArg(func, index)
    local args = func.args
    if not args then
        return nil
    end
    if args[index] then
        return args[index]
    end
    local lastArg = args[#args]
    if lastArg and lastArg.type == '...' then
        return lastArg
    end
    return nil
end

---@param arg      parser.object
---@param call     parser.object
---@param callNode vm.node
---@param fixIndex integer
---@param myIndex  integer
local function compileCallArgNode(arg, call, callNode, fixIndex, myIndex)
    local eventIndex, eventMap
    if call.args then
        for i = 1, 2 do
            local eventArg = call.args[i + fixIndex]
            if not eventArg then
                break
            end
            eventMap = vm.getLiterals(eventArg)
            if eventMap then
                eventIndex = i
                break
            end
        end
    end

    for n in callNode:eachObject() do
        if n.type == 'function' then
            ---@cast n parser.object
            local sign = getObjectSign(n)
            local farg = getFuncArg(n, myIndex)
            if farg then
                for fn in vm.compileNode(farg):eachObject() do
                    if isValidCallArgNode(arg, fn) then
                        if fn.type == 'doc.type.function' then
                            ---@cast fn parser.object
                            if sign then
                                local generic = vm.createGeneric(fn, sign)
                                local args    = {}
                                for i = fixIndex + 1, myIndex - 1 do
                                    args[#args+1] = call.args[i]
                                end
                                fn = generic:resolve(guide.getUri(call), args)
                            end
                        end
                        vm.setNode(arg, fn)
                    end
                end
            end
        end
        if n.type == 'doc.type.function' then
            ---@cast n parser.object
            local myEvent
            if n.args[eventIndex] then
                local argNode = vm.compileNode(n.args[eventIndex])
                myEvent = argNode:get(1)
            end
            if not myEvent
            or not eventMap
            or myIndex <= eventIndex
            or myEvent.type ~= 'doc.type.string'
            or eventMap[myEvent[1]] then
                local farg = getFuncArg(n, myIndex)
                if farg then
                    for fn in vm.compileNode(farg):eachObject() do
                        if isValidCallArgNode(arg, fn) then
                            vm.setNode(arg, fn)
                        end
                    end
                end
            end
        end
    end
end

---@param arg parser.object
---@param call parser.object
---@param index? integer
---@return vm.node?
function vm.compileCallArg(arg, call, index)
    if not index then
        for i, carg in ipairs(call.args) do
            if carg == arg then
                index = i
                break
            end
        end
        if not index then
            return nil
        end
    end

    local callNode = vm.compileNode(call.node)
    compileCallArgNode(arg, call, callNode, 0, index)

    if call.node.special == 'pcall'
    or call.node.special == 'xpcall' then
        local fixIndex = call.node.special == 'pcall' and 1 or 2
        if call.args and call.args[1] then
            callNode = vm.compileNode(call.args[1])
            compileCallArgNode(arg, call, callNode, fixIndex, index - fixIndex)
        end
    end
    return vm.getNode(arg)
end

---@class parser.object
---@field _iterator? table
---@field _iterArgs? table
---@field _iterVars? table<parser.object, vm.node>

---@param source parser.object
local function compileForVars(source)
    if source._iterator then
        return
    end
        --  for k, v in pairs(t) do
        --> for k, v in iterator, status, initValue do
        --> local k, v = iterator(status, initValue)
    source._iterator = {
        type = 'dummyfunc',
        parent = source,
    }
    source._iterArgs = {{},{}}
    source._iterVars = {}
    -- iterator
    selectNode(source._iterator,    source.exps, 1)
    -- status
    selectNode(source._iterArgs[1], source.exps, 2)
    -- initValue
    selectNode(source._iterArgs[2], source.exps, 3)
    if source.keys then
        for i, loc in ipairs(source.keys) do
            local node = getReturn(source._iterator, i, source._iterArgs)
            if node then
                if i == 1 then
                    node:removeOptional()
                end
                source._iterVars[loc] = node
            end
        end
    end
end

---@param source parser.object
---@return vm.node
local function compileLocal(source)
    vm.setNode(source, source)

    local hasMarkDoc
    if source.bindDocs then
        hasMarkDoc = bindDocs(source)
    end
    local hasMarkParam
    if source.type == 'self' and not hasMarkDoc then
        hasMarkParam = true
        if source.parent.type == 'callargs' then
            -- obj:func(...)
            if source.parent.parent and source.parent.parent.node and source.parent.parent.node.node then
                vm.setNode(source, vm.compileNode(source.parent.parent.node.node))
            end
        else
            -- function obj:func(...)
            if source.parent.parent and source.parent.parent.parent and source.parent.parent.parent.node then
                vm.setNode(source, vm.compileNode(source.parent.parent.parent.node))
            end
        end
    end
    local hasMarkValue
    if source.value then
        if not hasMarkDoc or guide.isLiteral(source.value) then
            hasMarkValue = true
            if source.value.type == 'table' then
                vm.setNode(source, source.value)
            elseif source.value.type ~= 'nil' then
                vm.setNode(source, vm.compileNode(source.value))
            end
        end
    end
    -- function x.y(self, ...) --> function x:y(...)
    if  source[1] == 'self'
    and not hasMarkDoc
    and source.parent.type == 'funcargs'
    and source.parent[1] == source then
        local setfield = source.parent.parent.parent
        if setfield.type == 'setfield' then
            hasMarkParam = true
            vm.setNode(source, vm.compileNode(setfield.node))
        end
    end
    if source.parent.type == 'funcargs' and not hasMarkDoc and not hasMarkParam then
        local func = source.parent.parent
        local funcNode = vm.compileNode(func)
        local hasDocArg
        for n in funcNode:eachObject() do
            if n.type == 'doc.type.function' then
                for index, arg in ipairs(n.args) do
                    if func.args[index] == source then
                        local argNode = vm.compileNode(arg)
                        for an in argNode:eachObject() do
                            if an.type ~= 'doc.generic.name' then
                                vm.setNode(source, an)
                            end
                        end
                        hasDocArg = true
                    end
                end
            end
        end
        if not hasDocArg then
            vm.setNode(source, vm.declareGlobal('type', 'any'))
        end
    end
    -- for x in ... do
    if source.parent.type == 'in' then
        compileForVars(source.parent)
        local keyNode = source.parent._iterVars[source]
        if keyNode then
            vm.setNode(source, keyNode)
        end
    end

    -- for x = ... do
    if source.parent.type == 'loop' then
        if source.parent.loc == source then
            vm.setNode(source, vm.declareGlobal('type', 'integer'))
        end
    end

    vm.getNode(source):setData('hasDefined', hasMarkDoc or hasMarkParam or hasMarkValue)
end

local binarySwich = util.switch()
    : case 'and'
    : call(function (source)
        local node1 = vm.compileNode(source[1])
        local node2 = vm.compileNode(source[2])
        local r1 = vm.test(source[1])
        if r1 == true then
            vm.setNode(source, node2)
        elseif r1 == false then
            vm.setNode(source, node1)
        else
            vm.setNode(source, node2)
        end
    end)
    : case 'or'
    : call(function (source)
        local node1 = vm.compileNode(source[1])
        local node2 = vm.compileNode(source[2])
        local r1 = vm.test(source[1])
        if r1 == true then
            vm.setNode(source, node1)
        elseif r1 == false then
            vm.setNode(source, node2)
        else
            vm.getNode(source):merge(node1)
            vm.getNode(source):setTruthy()
            vm.getNode(source):merge(node2)
        end
    end)
    : case '=='
    : case '~='
    : call(function (source)
        local result = vm.equal(source[1], source[2])
        if result == nil then
            vm.setNode(source, vm.declareGlobal('type', 'boolean'))
        else
            if source.op.type == '~=' then
                result = not result
            end
            vm.setNode(source, {
                type   = 'boolean',
                start  = source.start,
                finish = source.finish,
                parent = source,
                [1]    = result,
            })
        end
    end)
    : case '<<'
    : case '>>'
    : case '&'
    : case '|'
    : case '~'
    : call(function (source)
        local a = vm.getInteger(source[1])
        local b = vm.getInteger(source[2])
        if a and b then
            local op = source.op.type
            local result = op.type == '<<' and a << b
                        or op.type == '>>' and a >> b
                        or op.type == '&'  and a &  b
                        or op.type == '|'  and a |  b
                        or op.type == '~'  and a ~  b
            vm.setNode(source, {
                type   = 'integer',
                start  = source.start,
                finish = source.finish,
                parent = source,
                [1]    = result,
            })
        else
            vm.setNode(source, vm.declareGlobal('type', 'integer'))
        end
    end)
    : case '+'
    : case '-'
    : case '*'
    : case '/'
    : case '%'
    : case '//'
    : case '^'
    : call(function (source)
        local a = vm.getNumber(source[1])
        local b = vm.getNumber(source[2])
        local op = source.op.type
        local zero = b == 0
                and (  op == '%'
                    or op == '/'
                    or op == '//'
                )
        if a and b and not zero then
            local result = op == '+'  and a +  b
                        or op == '-'  and a -  b
                        or op == '*'  and a *  b
                        or op == '/'  and a /  b
                        or op == '%'  and a %  b
                        or op == '//' and a // b
                        or op == '^'  and a ^  b
            vm.setNode(source, {
                type   = math.type(result) == 'integer' and 'integer' or 'number',
                start  = source.start,
                finish = source.finish,
                parent = source,
                [1]    = result,
            })
        else
            if op == '+'
            or op == '-'
            or op == '*'
            or op == '//'
            or op == '%' then
                local uri = guide.getUri(source)
                local infer1 = vm.getInfer(source[1])
                local infer2 = vm.getInfer(source[2])
                if infer1:hasType(uri, 'integer')
                or infer2:hasType(uri, 'integer') then
                    if  not infer1:hasType(uri, 'number')
                    and not infer2:hasType(uri, 'number') then
                        vm.setNode(source, vm.declareGlobal('type', 'integer'))
                        return
                    end
                end
            end
            vm.setNode(source, vm.declareGlobal('type', 'number'))
        end
    end)
    : case '..'
    : call(function (source)
        local a =  vm.getString(source[1])
                or vm.getNumber(source[1])
        local b =  vm.getString(source[2])
                or vm.getNumber(source[2])
        if a and b then
            if type(a) == 'number' or type(b) == 'number' then
                local uri     = guide.getUri(source)
                local version = config.get(uri, 'Lua.runtime.version')
                if math.tointeger(a) and math.type(a) == 'float' then
                    if version == 'Lua 5.3' or version == 'Lua 5.4' then
                        a = ('%.1f'):format(a)
                    else
                        a = ('%.0f'):format(a)
                    end
                end
                if math.tointeger(b) and math.type(b) == 'float' then
                    if version == 'Lua 5.3' or version == 'Lua 5.4' then
                        b = ('%.1f'):format(b)
                    else
                        b = ('%.0f'):format(b)
                    end
                end
            end
            vm.setNode(source, {
                type   = 'string',
                start  = source.start,
                finish = source.finish,
                parent = source,
                [1]    = a .. b,
            })
        else
            vm.setNode(source, vm.declareGlobal('type', 'string'))
        end
    end)
    : case '>'
    : case '<'
    : case '>='
    : case '<='
    : call(function (source)
        local a = vm.getNumber(source[1])
        local b = vm.getNumber(source[2])
        if a and b then
            local op = source.op.type
            local result = op.type == '>'  and a >  b
                        or op.type == '<'  and a <  b
                        or op.type == '>=' and a >= b
                        or op.type == '<=' and a <= b
            vm.setNode(source, {
                type   = 'boolean',
                start  = source.start,
                finish = source.finish,
                parent = source,
                [1]    =result,
            })
        else
            vm.setNode(source, vm.declareGlobal('type', 'boolean'))
        end
    end)

local compilerSwitch = util.switch()
    : case 'nil'
    : case 'boolean'
    : case 'integer'
    : case 'number'
    : case 'string'
    : case 'doc.type.function'
    : case 'doc.type.table'
    : case 'doc.type.array'
    : call(function (source)
        vm.setNode(source, source)
    end)
    : case 'table'
    : call(function (source)
        vm.setNode(source, source)

        if source.parent.type == 'callargs' then
            local call = source.parent.parent
            vm.compileCallArg(source, call)
        end

        if source.parent.type == 'setglobal'
        or source.parent.type == 'local'
        or source.parent.type == 'setlocal'
        or source.parent.type == 'tablefield'
        or source.parent.type == 'tableindex'
        or source.parent.type == 'setfield'
        or source.parent.type == 'setindex' then
            local parentNode = vm.compileNode(source.parent)
            for _, pn in ipairs(parentNode) do
                if  pn.type == 'global'
                and pn.cate == 'type' then
                    ---@cast pn vm.global
                    if not guide.isBasicType(pn.name) then
                        vm.setNode(source, pn)
                    end
                elseif pn.type == 'doc.type.table' then
                    vm.setNode(source, pn)
                end
            end
        end
    end)
    : case 'function'
    : call(function (source)
        vm.setNode(source, source)

        if source.bindDocs then
            for _, doc in ipairs(source.bindDocs) do
                if doc.type == 'doc.overload' then
                    vm.setNode(source, vm.compileNode(doc))
                end
            end
        end

        -- table.sort(string[], function (<?x?>) end)
        if source.parent.type == 'callargs' then
            local call = source.parent.parent
            vm.compileCallArg(source, call)
        end
    end)
    : case 'paren'
    : call(function (source)
        if bindAs(source) then
            return
        end
        if source.exp then
            vm.setNode(source, vm.compileNode(source.exp))
        end
    end)
    : case 'local'
    : case 'self'
    ---@param source parser.object
    : call(function (source)
        compileLocal(source)
        local refs = source.ref
        if not refs then
            return
        end

        local hasMark = vm.getNode(source):getData 'hasDefined'

        vm.launchRunner(source, function (src, node)
            if src.type == 'setlocal' then
                if src.bindDocs then
                    for _, doc in ipairs(src.bindDocs) do
                        if doc.type == 'doc.type' then
                            vm.setNode(src, vm.compileNode(doc), true)
                            return vm.getNode(src)
                        end
                    end
                end
                if src.value then
                    if src.value.type == 'table' then
                        vm.setNode(src, vm.createNode(src.value), true)
                    else
                        vm.setNode(src, vm.compileNode(src.value), true)
                    end
                elseif src.value
                and    src.value.type == 'binary'
                and    src.value.op and src.value.op.type == 'or'
                and    src.value[1] and src.value[1].type == 'getlocal' and src.value[1].node == source then
                    -- x = x or 1
                    vm.setNode(src, vm.compileNode(src.value))
                else
                    vm.setNode(src, node, true)
                end
                return vm.getNode(src)
            elseif src.type == 'getlocal' then
                if bindAs(src) then
                    return
                end
                vm.setNode(src, node, true)
                matchCall(src)
            end
        end)

        if not hasMark then
            local parentFunc = guide.getParentFunction(source)
            for _, ref in ipairs(source.ref) do
                if  ref.type == 'setlocal'
                and guide.getParentFunction(ref) == parentFunc then
                    local refNode = vm.getNode(ref)
                    if refNode then
                        vm.setNode(source, refNode)
                    end
                end
            end
        end
    end)
    : case 'setlocal'
    : call(function (source)
        vm.compileNode(source.node)
    end)
    : case 'getlocal'
    : call(function (source)
        if bindAs(source) then
            return
        end
        vm.compileNode(source.node)
    end)
    : case 'setfield'
    : case 'setmethod'
    : case 'setindex'
    : case 'getfield'
    : case 'getmethod'
    : case 'getindex'
    : call(function (source)
        if guide.isGet(source) and bindAs(source) then
            return
        end
        ---@type string|vm.node
        local key = guide.getKeyName(source)
        if key == nil and source.index then
            key = vm.compileNode(source.index)
        end
        if key == nil then
            return
        end
        if type(key) == 'table' then
            ---@cast key vm.node
            local uri = guide.getUri(source)
            local value = vm.getTableValue(uri, vm.compileNode(source.node), key)
            if value then
                vm.setNode(source, value)
            end
            for k in key:eachObject() do
                if k.type == 'global' and k.cate == 'type' then
                    ---@cast k vm.global
                    vm.compileByParentNode(source.node, k, false, function (src)
                        vm.setNode(source, vm.compileNode(src))
                        if src.value then
                            vm.setNode(source, vm.compileNode(src.value))
                        end
                    end)
                end
            end
        else
            ---@cast key string
            vm.compileByParentNode(source.node, key, false, function (src)
                vm.setNode(source, vm.compileNode(src))
                if src.value then
                    vm.setNode(source, vm.compileNode(src.value))
                end
            end)
        end
    end)
    : case 'setglobal'
    : call(function (source)
        if source.node[1] ~= '_ENV' then
            return
        end
        local key = guide.getKeyName(source)
        vm.compileByParentNode(source.node, key, false, function (src)
            if src.type == 'doc.type.field'
            or src.type == 'doc.field' then
                vm.setNode(source, vm.compileNode(src))
            end
        end)
    end)
    : case 'getglobal'
    : call(function (source)
        if bindAs(source) then
            return
        end
        if source.node[1] ~= '_ENV' then
            return
        end
        local key = guide.getKeyName(source)
        vm.compileByParentNode(source.node, key, false, function (src)
            vm.setNode(source, vm.compileNode(src))
        end)
    end)
    : case 'tablefield'
    : case 'tableindex'
    : call(function (source)
        local hasMarkDoc
        if source.bindDocs then
            hasMarkDoc = bindDocs(source)
        end

        if not hasMarkDoc then
            vm.compileByParentNode(source.node, guide.getKeyName(source), false, function (src)
                if src.type == 'doc.field'
                or src.type == 'doc.type.field' then
                    hasMarkDoc = true
                    vm.setNode(source, vm.compileNode(src))
                end
            end)
        end

        if not hasMarkDoc and source.value then
            vm.setNode(source, vm.compileNode(source.value))
        end

    end)
    : case 'field'
    : case 'method'
    : call(function (source)
        vm.setNode(source, vm.compileNode(source.parent))
    end)
    : case 'tableexp'
    : call(function (source)
        vm.setNode(source, vm.compileNode(source.value))
    end)
    : case 'function.return'
    : call(function (source)
        local func  = source.parent
        local index = source.index
        local hasMarkDoc
        if func.bindDocs then
            local sign = getObjectSign(func)
            local lastReturn
            for _, doc in ipairs(func.bindDocs) do
                if doc.type == 'doc.return' then
                    for _, rtn in ipairs(doc.returns) do
                        lastReturn = rtn
                        if rtn.returnIndex == index then
                            hasMarkDoc = true
                            local hasGeneric
                            if sign then
                                guide.eachSourceType(rtn, 'doc.generic.name', function (src)
                                    hasGeneric = true
                                end)
                            end
                            if hasGeneric then
                                ---@cast sign -?
                                vm.setNode(source, vm.createGeneric(rtn, sign))
                            else
                                vm.setNode(source, vm.compileNode(rtn))
                            end
                        end
                    end
                end
            end
            if lastReturn and not hasMarkDoc and lastReturn.types[1][1] == '...' then
                hasMarkDoc = true
                vm.setNode(source, vm.declareGlobal('type', 'unknown'))
            end
        end
        local hasReturn
        if func.returns and not hasMarkDoc then
            for _, rtn in ipairs(func.returns) do
                if selectNode(source, rtn, index) then
                    hasReturn = true
                end
            end
            if hasReturn then
                local hasKnownType
                local hasUnknownType
                for n in vm.getNode(source):eachObject() do
                    if guide.isLiteral(n) then
                        if n.type ~= 'nil' then
                            hasKnownType = true
                            break
                        end
                        goto CONTINUE
                    end
                    if n.type == 'global' and n.cate == 'type' then
                        if n.name ~= 'nil' then
                            hasKnownType = true
                            break
                        end
                        goto CONTINUE
                    end
                    hasUnknownType = true
                    ::CONTINUE::
                end
                if not hasKnownType and hasUnknownType then
                    vm.setNode(source, vm.declareGlobal('type', 'unknown'))
                end
            end
        end
        if not hasMarkDoc and not hasReturn then
            vm.setNode(source, vm.declareGlobal('type', 'nil'))
        end
    end)
    : case 'main'
    : call(function (source)
        if source.returns then
            for _, rtn in ipairs(source.returns) do
                if rtn[1] then
                    vm.setNode(source, vm.compileNode(rtn[1]))
                end
            end
        end
    end)
    : case 'select'
    : call(function (source)
        local vararg = source.vararg
        if vararg.type == 'call' then
            local node = getReturn(vararg.node, source.sindex, vararg.args)
            if not node then
                return
            end
            for n in node:eachObject() do
                if  n.type == 'global'
                and n.cate == 'type'
                and n.name == '...' then
                    return
                end
            end
            vm.setNode(source, node)
        end
        if vararg.type == 'varargs' then
            vm.setNode(source, vm.compileNode(vararg))
        end
    end)
    : case 'varargs'
    : call(function (source)
        if source.node then
            vm.setNode(source, vm.compileNode(source.node))
        end
    end)
    : case 'call'
    : call(function (source)
        local node = getReturn(source.node, 1, source.args)
        if not node then
            return
        end
        for n in node:eachObject() do
            if  n.type == 'global'
            and n.cate == 'type'
            and n.name == '...' then
                return
            end
        end
        vm.setNode(source, node)
    end)
    : case 'doc.type'
    : call(function (source)
        for _, typeUnit in ipairs(source.types) do
            vm.setNode(source, vm.compileNode(typeUnit))
        end
        if source.optional then
            vm.getNode(source):addOptional()
        end
    end)
    : case 'doc.type.integer'
    : case 'doc.type.string'
    : case 'doc.type.boolean'
    : case 'doc.type.code'
    : call(function (source)
        vm.setNode(source, source)
    end)
    : case 'doc.generic.name'
    : call(function (source)
        vm.setNode(source, source)
    end)
    : case 'doc.type.sign'
    : call(function (source)
        local uri = guide.getUri(source)
        vm.setNode(source, source)
        if not source.node[1] then
            return
        end
        local global = vm.getGlobal('type', source.node[1])
        if not global then
            return
        end
        for _, set in ipairs(global:getSets(uri)) do
            if set.type == 'doc.class' then
                if set.extends then
                    for _, ext in ipairs(set.extends) do
                        if ext.type == 'doc.type.table' then
                            if ext._generic then
                                local resolved = ext._generic:resolve(uri, source.signs)
                                vm.setNode(source, resolved)
                            end
                        end
                    end
                end
            end
            if set.type == 'doc.alias' then
                if set.extends._generic then
                    local resolved = set.extends._generic:resolve(uri, source.signs)
                    vm.setNode(source, resolved)
                end
            end
        end
    end)
    : case 'doc.class.name'
    : call(function (source)
        vm.setNode(source, vm.compileNode(source.parent))
    end)
    : case 'doc.field'
    : call(function (source)
        if not source.extends then
            return
        end
        local fieldNode = vm.compileNode(source.extends)
        if source.optional then
            fieldNode:addOptional()
        end
        vm.setNode(source, fieldNode)
    end)
    : case 'doc.type.field'
    : call(function (source)
        if not source.extends then
            return
        end
        local fieldNode = vm.compileNode(source.extends)
        if source.optional then
            fieldNode:addOptional()
        end
        vm.setNode(source, fieldNode)
    end)
    : case 'doc.param'
    : call(function (source)
        if not source.extends then
            return
        end
        vm.setNode(source, vm.compileNode(source.extends))
    end)
    : case 'doc.vararg'
    : call(function (source)
        if not source.vararg then
            return
        end
        vm.setNode(source, vm.compileNode(source.vararg))
    end)
    : case '...'
    : call(function (source)
        local func = source.parent.parent
        if func.type ~= 'function' then
            return
        end
        if not func.bindDocs then
            return
        end
        for _, doc in ipairs(func.bindDocs) do
            if doc.type == 'doc.vararg' then
                vm.setNode(source, vm.compileNode(doc))
            end
            if doc.type == 'doc.param' and doc.param[1] == '...' then
                vm.setNode(source, vm.compileNode(doc))
            end
        end
    end)
    : case 'doc.overload'
    : call(function (source)
        vm.setNode(source, vm.compileNode(source.overload))
    end)
    : case 'doc.see.name'
    : call(function (source)
        local type = vm.getGlobal('type', source[1])
        if type then
            vm.setNode(source, type)
        end
    end)
    : case 'doc.type.arg'
    : call(function (source)
        if source.extends then
            vm.setNode(source, vm.compileNode(source.extends))
        else
            vm.setNode(source, vm.declareGlobal('type', 'any'))
        end
        if source.optional then
            vm.getNode(source):addOptional()
        end
    end)
    : case 'unary'
    : call(function (source)
        if bindAs(source) then
            return
        end
        if not source[1] then
            return
        end
        if source.op.type == 'not' then
            local result = vm.test(source[1])
            if result == nil then
                vm.setNode(source, vm.declareGlobal('type', 'boolean'))
                return
            else
                vm.setNode(source, {
                    type   = 'boolean',
                    start  = source.start,
                    finish = source.finish,
                    parent = source,
                    [1]    = not result,
                })
                return
            end
        end
        if source.op.type == '#' then
            vm.setNode(source, vm.declareGlobal('type', 'integer'))
            return
        end
        if source.op.type == '-' then
            local v = vm.getNumber(source[1])
            if v == nil then
                local infer = vm.getInfer(source[1])
                if infer:hasType(guide.getUri(source), 'integer') then
                    vm.setNode(source, vm.declareGlobal('type', 'integer'))
                else
                    vm.setNode(source, vm.declareGlobal('type', 'number'))
                end
                return
            else
                vm.setNode(source, {
                    type   = 'number',
                    start  = source.start,
                    finish = source.finish,
                    parent = source,
                    [1]    = -v,
                })
                return
            end
        end
        if source.op.type == '~' then
            local v = vm.getInteger(source[1])
            if v == nil then
                vm.setNode(source, vm.declareGlobal('type', 'integer'))
                return
            else
                vm.setNode(source, {
                    type   = 'integer',
                    start  = source.start,
                    finish = source.finish,
                    parent = source,
                    [1]    = ~v,
                })
                return
            end
        end
    end)
    : case 'binary'
    : call(function (source)
        if bindAs(source) then
            return
        end
        if not source[1] or not source[2] then
            return
        end
        binarySwich(source.op.type, source)
    end)

---@param source parser.object
local function compileByNode(source)
    compilerSwitch(source.type, source)
end

---@param source parser.object
local function compileByGlobal(source)
    local global = source._globalNode
    if not global then
        return
    end
    ---@cast source parser.object
    local root = guide.getRoot(source)
    local uri = guide.getUri(source)
    if not root._globalBase then
        root._globalBase = {}
    end
    local name = global:asKeyName()
    if not root._globalBase[name] then
        root._globalBase[name] = {
            type   = 'globalbase',
            parent = root,
        }
    end
    local globalNode = vm.getNode(root._globalBase[name])
    if globalNode then
        vm.setNode(source, globalNode, true)
        return
    end
    ---@type vm.node
    globalNode = vm.createNode(global)
    vm.setNode(root._globalBase[name], globalNode, true)
    vm.setNode(source, globalNode, true)

    -- TODO:don't mix
    --local sets = global.links[uri].sets or {}
    --local gets = global.links[uri].gets or {}
    --for _, set in ipairs(sets) do
    --    vm.setNode(set, globalNode, true)
    --end
    --for _, get in ipairs(gets) do
    --    vm.setNode(get, globalNode, true)
    --end

    if global.cate == 'variable' then
        local hasMarkDoc
        for _, set in ipairs(global:getSets(uri)) do
            if set.bindDocs then
                if bindDocs(set) then
                    globalNode:merge(vm.compileNode(set))
                    hasMarkDoc = true
                end
            end
        end
        for _, set in ipairs(global:getSets(uri)) do
            if set.value then
                if not hasMarkDoc or guide.isLiteral(set.value) then
                    globalNode:merge(vm.compileNode(set.value))
                end
            end
            vm.setNode(set, globalNode)
        end
    end
    if global.cate == 'type' then
        for _, set in ipairs(global:getSets(uri)) do
            if set.type == 'doc.class' then
                if set.extends then
                    for _, ext in ipairs(set.extends) do
                        if ext.type == 'doc.type.table' then
                            if not ext._generic then
                                globalNode:merge(vm.compileNode(ext))
                            end
                        end
                    end
                end
            end
            if set.type == 'doc.alias' then
                if not set.extends._generic then
                    globalNode:merge(vm.compileNode(set.extends))
                end
            end
        end
    end
end

---@param source vm.object
---@return vm.node
function vm.compileNode(source)
    if not source then
        if TEST then
            error('Can not compile nil source')
        else
            log.error('Can not compile nil source')
        end
    end

    local cache = vm.getNode(source)
    if cache ~= nil then
        return cache
    end

    if source.type == 'generic' then
        vm.setNode(source, source)
        return vm.getNode(source)
    end

    ---@cast source parser.object
    vm.setNode(source, vm.createNode(), true)
    compileByGlobal(source)
    compileByNode(source)
    matchCall(source)

    local node = vm.getNode(source)

    return node
end