---@class vm
local vm        = require 'vm.vm'
local guide     = require 'parser.guide'

---@class vm.runner
---@field loc       parser.object
---@field mainBlock parser.object
---@field blocks    table<parser.object, true>
---@field steps     vm.runner.step[]
local mt = {}
mt.__index = mt
mt.index = 1

---@class parser.object
---@field _casts parser.object[]

---@class vm.runner.step
---@field type    'truthy' | 'falsy' | 'as' | 'add' | 'remove' | 'object' | 'save' | 'push' | 'merge' | 'cast'
---@field pos     integer
---@field order?  integer
---@field node?   vm.node
---@field object? parser.object
---@field name?   string
---@field cast?   parser.object
---@field tag?    string
---@field copy?   boolean
---@field new?    boolean
---@field ref1?   vm.runner.step
---@field ref2?   vm.runner.step

---@param filter    parser.object
---@param outStep   vm.runner.step
---@param blockStep vm.runner.step
function mt:_compileNarrowByFilter(filter, outStep, blockStep)
    if not filter then
        return
    end
    if filter.type == 'paren' then
        if filter.exp then
            self:_compileNarrowByFilter(filter.exp, outStep, blockStep)
        end
        return
    end
    if filter.type == 'unary' then
        if not filter.op
        or not filter[1] then
            return
        end
        if filter.op.type == 'not' then
            local exp = filter[1]
            if exp.type == 'getlocal' and exp.node == self.loc then
                self.steps[#self.steps+1] = {
                    type  = 'falsy',
                    pos   = filter.finish,
                    new   = true,
                }
                self.steps[#self.steps+1] = {
                    type  = 'truthy',
                    pos   = filter.finish,
                    ref1  = outStep,
                }
            end
        end
    elseif filter.type == 'binary' then
        if not filter.op
        or not filter[1]
        or not filter[2] then
            return
        end
        if filter.op.type == 'and' then
            local dummyStep = {
                type  = 'save',
                copy  = true,
                ref1  = outStep,
                pos   = filter.start - 1,
            }
            self.steps[#self.steps+1] = dummyStep
            self:_compileNarrowByFilter(filter[1], dummyStep, blockStep)
            self:_compileNarrowByFilter(filter[2], dummyStep, blockStep)
        end
        if filter.op.type == 'or' then
            self:_compileNarrowByFilter(filter[1], outStep, blockStep)
            local dummyStep = {
                type  = 'push',
                copy  = true,
                ref1  = outStep,
                pos   = filter.op.finish,
            }
            self.steps[#self.steps+1] = dummyStep
            self:_compileNarrowByFilter(filter[2], outStep, dummyStep)
            self.steps[#self.steps+1] = {
                type  = 'push',
                tag   = 'or reset',
                ref1  = blockStep,
                pos   = filter.finish,
            }
        end
        if filter.op.type == '=='
        or filter.op.type == '~=' then
            local loc, exp
            for i = 1, 2 do
                loc = filter[i]
                if loc.type == 'getlocal' and loc.node == self.loc then
                    exp = filter[i % 2 + 1]
                    break
                end
            end
            if not loc or not exp then
                return
            end
            if guide.isLiteral(exp) then
                if filter.op.type == '==' then
                    self.steps[#self.steps+1] = {
                        type  = 'remove',
                        name  = exp.type,
                        pos   = filter.finish,
                        ref1  = outStep,
                    }
                    self.steps[#self.steps+1] = {
                        type  = 'as',
                        name  = exp.type,
                        pos   = filter.finish,
                        new   = true,
                    }
                end
                if filter.op.type == '~=' then
                    self.steps[#self.steps+1] = {
                        type  = 'as',
                        name  = exp.type,
                        pos   = filter.finish,
                        ref1  = outStep,
                    }
                    self.steps[#self.steps+1] = {
                        type  = 'remove',
                        name  = exp.type,
                        pos   = filter.finish,
                        new   = true,
                    }
                end
            end
        end
    else
        if filter.type == 'getlocal' and filter.node == self.loc then
            self.steps[#self.steps+1] = {
                type  = 'truthy',
                pos   = filter.finish,
                new   = true,
            }
            self.steps[#self.steps+1] = {
                type  = 'falsy',
                pos   = filter.finish,
                ref1  = outStep,
            }
        end
    end
end

---@param block parser.object
function mt:_compileBlock(block)
    if self.blocks[block] then
        return
    end
    self.blocks[block] = true
    if block == self.mainBlock then
        return
    end

    local parentBlock = guide.getParentBlock(block)
    self:_compileBlock(parentBlock)

    if block.type == 'if' then
        ---@type vm.runner.step[]
        local finals = {}
        for i, childBlock in ipairs(block) do
            local blockStep = {
                type  = 'save',
                tag   = 'block',
                copy  = true,
                pos   = childBlock.start,
            }
            local outStep = {
                type  = 'save',
                tag   = 'out',
                copy  = true,
                pos   = childBlock.start,
            }
            self.steps[#self.steps+1] = blockStep
            self.steps[#self.steps+1] = outStep
            self.steps[#self.steps+1] = {
                type  = 'push',
                ref1  = blockStep,
                pos   = childBlock.start,
            }
            self:_compileNarrowByFilter(childBlock.filter, outStep, blockStep)
            if  not childBlock.hasReturn
            and not childBlock.hasGoTo
            and not childBlock.hasBreak then
                local finalStep = {
                    type = 'save',
                    pos  = childBlock.finish,
                    tag  = 'final #' .. i,
                }
                finals[#finals+1] = finalStep
                self.steps[#self.steps+1] = finalStep
            end
            self.steps[#self.steps+1] = {
                type   = 'push',
                tag    = 'reset child',
                ref1   = outStep,
                pos    = childBlock.finish,
            }
        end
        self.steps[#self.steps+1] = {
            type = 'push',
            tag  = 'reset if',
            pos  = block.finish,
            copy = true,
        }
        for _, final in ipairs(finals) do
            self.steps[#self.steps+1] = {
                type = 'merge',
                ref2 = final,
                pos  = block.finish,
            }
        end
    end

    if block.type == 'function'
    or block.type == 'while'
    or block.type == 'loop'
    or block.type == 'in'
    or block.type == 'repeat'
    or block.type == 'for' then
        local savePoint = {
            type = 'save',
            copy = true,
            pos  = block.start,
        }
        self.steps[#self.steps+1] = {
            type  = 'push',
            copy  = true,
            pos   = block.start,
        }
        self.steps[#self.steps+1] = savePoint
        self.steps[#self.steps+1] = {
            type = 'push',
            pos  = block.finish,
            ref1 = savePoint,
        }
    end
end

---@return parser.object[]
function mt:_getCasts()
    local root = guide.getRoot(self.loc)
    if not root._casts then
        root._casts = {}
        local docs = root.docs
        for _, doc in ipairs(docs) do
            if doc.type == 'doc.cast' and doc.loc then
                root._casts[#root._casts+1] = doc
            end
        end
    end
    return root._casts
end

function mt:_preCompile()
    local startPos  = self.loc.start
    local finishPos = 0

    for _, ref in ipairs(self.loc.ref) do
        self.steps[#self.steps+1] = {
            type   = 'object',
            object = ref,
            pos    = ref.range or ref.start,
        }
        if ref.start > finishPos then
            finishPos = ref.start
        end
        local block = guide.getParentBlock(ref)
        self:_compileBlock(block)
    end

    for i, step in ipairs(self.steps) do
        if step.type ~= 'object' then
            step.order = i
        end
    end

    local casts = self:_getCasts()
    for _, cast in ipairs(casts) do
        if  cast.loc[1] == self.loc[1]
        and cast.start > startPos
        and cast.finish < finishPos
        and guide.getLocal(self.loc, self.loc[1], cast.start) == self.loc then
            self.steps[#self.steps+1] = {
                type  = 'cast',
                cast  = cast,
                pos   = cast.start,
            }
        end
    end

    table.sort(self.steps, function (a, b)
        if a.pos == b.pos then
            return (a.order or 0) < (b.order or 0)
        else
            return a.pos < b.pos
        end
    end)
end

---@param loc  parser.object
---@param node vm.node
---@return vm.node
local function checkAssert(loc, node)
    local parent = loc.parent
    if parent.type == 'binary' then
        if parent.op and (parent.op.type == '~=' or parent.op.type == '==') then
            local exp
            for i = 1, 2 do
                if parent[i] == loc then
                    exp = parent[i % 2 + 1]
                end
            end
            if exp and guide.isLiteral(exp) then
                local callargs = parent.parent
                if  callargs.type == 'callargs'
                and callargs.parent.node.special == 'assert'
                and callargs[1] == parent then
                    if parent.op.type == '~=' then
                        node:remove(exp.type)
                    end
                    if parent.op.type == '==' then
                        node = vm.compileNode(exp)
                    end
                end
            end
        end
    end
    if  parent.type == 'callargs'
    and parent.parent.node.special == 'assert'
    and parent[1] == loc then
        node:setTruthy()
    end
    return node
end

---@param callback    fun(src: parser.object, node: vm.node)
function mt:launch(callback)
    local topNode = vm.getNode(self.loc):copy()
    for _, step in ipairs(self.steps) do
        local node = step.ref1 and step.ref1.node or topNode
        if     step.type == 'truthy' then
            if step.new then
                node = node:copy()
                topNode = node
            end
            node:setTruthy()
        elseif step.type == 'falsy' then
            if step.new then
                node = node:copy()
                topNode = node
            end
            node:setFalsy()
        elseif step.type == 'as' then
            if step.new then
                topNode = vm.createNode(vm.getGlobal('type', step.name))
            else
                node:clear()
                node:merge(vm.getGlobal('type', step.name))
            end
        elseif step.type == 'add' then
            if step.new then
                node = node:copy()
                topNode = node
            end
            node:merge(vm.getGlobal('type', step.name))
        elseif step.type == 'remove' then
            if step.new then
                node = node:copy()
                topNode = node
            end
            node:remove(step.name)
        elseif step.type == 'object' then
            topNode = callback(step.object, node) or node
            if step.object.type == 'getlocal' then
                topNode = checkAssert(step.object, node)
            end
        elseif step.type == 'save' then
            if step.copy then
                node = node:copy()
            end
            step.node = node
        elseif step.type == 'push' then
            if step.copy then
                node = node:copy()
            end
            topNode = node
        elseif step.type == 'merge' then
            node:merge(step.ref2.node)
        elseif step.type == 'cast' then
            topNode = node:copy()
            for _, cast in ipairs(step.cast.casts) do
                if     cast.mode == '+' then
                    if cast.optional then
                        topNode:addOptional()
                    end
                    if cast.extends then
                        topNode:merge(vm.compileNode(cast.extends))
                    end
                elseif cast.mode == '-' then
                    if cast.optional then
                        topNode:removeOptional()
                    end
                    if cast.extends then
                        topNode:removeNode(vm.compileNode(cast.extends))
                    end
                else
                    if cast.extends then
                        topNode:clear()
                        topNode:merge(vm.compileNode(cast.extends))
                    end
                end
            end
        end
    end
end

---@param loc parser.object
---@return vm.runner
function vm.createRunner(loc)
    local self = setmetatable({
        loc       = loc,
        mainBlock = guide.getParentBlock(loc),
        blocks    = {},
        steps     = {},
    }, mt)

    self:_preCompile()

    return self
end
