local setmetatable = setmetatable
local tostring = tostring
local pairs = pairs
local type = type
local ipairs = ipairs
local find = string.find
local insert = table.insert
local remove = table.remove
local concat = table.concat
local next = next
local tokenizer = require("aspect.tokenizer")
local tags = require("aspect.tags")
local tests = require("aspect.tests")
local err = require("aspect.error")
local quote_string = require("aspect.utils").quote_string
local get_keys = require("aspect.utils").keys
local strcount = require("aspect.utils").strcount
local compiler_error = err.compiler_error
local sub = string.sub
local strlen = string.len
local pcall = pcall
local config = require("aspect.config")
local import_type = config.macro.import_type
local loop_keys = config.loop.keys
local tag_type = config.compiler.tag_type
local strip_pattern = config.compiler.strip
local func = require("aspect.funcs")
local filters = require("aspect.filters")
local ast = require("aspect.ast")
local utils = require("aspect.utils")
local rstrip = utils.rtrim
local lstrip = utils.ltrim
local special = config.compiler.special

--- @class aspect.tag
--- @field id number unique ID of the tag in compilation
--- @field name string tag name
--- @field line number where the tag opened
--- @field ctx aspect.compiler.context|nil
--- @field append_text fun(tpl:aspect.compiler, text:string)
--- @field append_expr fun(tpl:aspect.compiler, expr:string)
--- @field append_code fun(tpl:aspect.compiler, code:string)

--- @class aspect.compiler.var_ref
--- @field name string
--- @field line number first use
--- @field keys table<string,number> key-name and line number of first usage

--- @class aspect.compiler.block
--- @field ctx aspect.compiler.context
--- @field parent boolean has parent function inside
--- @field desc string the description
--- @field start_line number
--- @field end_line number
--- @field start_pos number
--- @field end_pos number

--- @class aspect.compiler.macro
--- @field ctx aspect.compiler.context
--- @field args aspect.compiler.macro.arg[] list of used variables
--- @field desc string the description
--- @field start_line number of line there block started
--- @field end_line number of line there block ended

--- @class aspect.compiler.macro.arg
--- @field name string
--- @field default any

--- @class aspect.compiler.context
--- @field name string
--- @field code string[]
--- @field tpl_refs table<string, table>
--- @field var_refs table<string, aspect.compiler.var_ref>
--- @field vars table local variables

--- @class aspect.compiler
--- @field ctx_stack aspect.compiler.context[]
--- @field ctx aspect.compiler.context current context
--- @field aspect aspect.template
--- @field name string
--- @field blocks table<string, aspect.compiler.block> table of blocks
--- @field macros table<string, aspect.compiler.macro> table of macros with their code (witch array)
--- @field extends string|boolean if true then we has dynamic extend
--- @field import table
--- @field line number
--- @field prev_line number
--- @field tag_name string the current tag
--- @field ignore string|nil
--- @field tags aspect.tag[]
--- @field stats boolean
local compiler = {
    version = 1,
}

local mt = { __index = compiler }

--- @param aspect aspect.template
--- @param name string
--- @return aspect.compiler
function compiler.new(aspect, name, stats)
    return setmetatable({
        aspect = aspect,
        ctx = nil,
        ctx_stack = {},
        name = name,
        line = 1,
        prev_line = 1,
        macros = {},
        extends = nil,
        blocks = {},
        uses = {},
        tags = {},
        idx  = 0,
        tag_type = nil,
        import = {},
        ignore = nil,
        stats = stats or true,
    }, mt)
end

--- Start compiler
--- @param source string source code of the template
--- @return boolean ok or not ok
--- @return aspect.error if not ok returns error
function compiler:run(source)
    local ok, e = pcall(self.parse, self, source)
    if ok then
        return true
    else
        return false, err.new(e):set_name(self.name, self.line)
    end
end

function compiler:get_code()
    local code = {
        "local _self = {",
        "\tv = " .. self.version .. ",",
        "\tname = " .. quote_string(self.name) .. ",",
        "\tblocks = {},",
        "\tmacros = {},",
    }
    if self.extends then
        if self.extends.static then
            insert(code,"\textends = " .. self.extends.value .. ",")
        else
            insert(code,"\textends = true")
        end
    end
    if self.uses and #self.uses > 0 then
        insert(code,"\tuses = {")
        for _, use in ipairs(self.uses) do
            insert(code,"\t\t{")
            insert(code,"\t\t\tname = " .. use.name .. ", ")
            insert(code,"\t\t\tline = " .. use.line .. ", ")
            if use.with then
                insert(code,"\t\t\twith = { " .. utils.implode_hashes(use.with) .. " }, ")
            end
            insert(code,"\t\t}")
        end
        insert(code,"\t}")
    end
    insert(code, "}\n")

    insert(code, "function _self.body(__, _context)")
    if self.extends then
        insert(code, "\treturn " .. self.extends.value)
    elseif #self.ctx.code then
        insert(code, "\t__:push_state(_self, 1)")
        for _, v in ipairs(self.ctx.code) do
            insert(code, "\t" .. v)
        end
        insert(code, "\t__:pop_state()")
    end
    insert(code, "end\n")

    if self.blocks then
        for n, b in pairs(self.blocks) do
            insert(code, "function _self.blocks." .. n .. "(__, _context)")
            for _, v in ipairs(b.ctx.code) do
                insert(code, "\t" .. v)
            end
            insert(code, "end\n")
        end
    end

    if self.macros then
        for n, m in pairs(self.macros) do
            insert(code, "function _self.macros." .. n .. "(__, _context)")
            for _, v in ipairs(m.ctx.code) do
                insert(code, "\t" .. v)
            end
            insert(code, "end\n")
        end
    end

    insert(code, "return _self")
    return concat(code, "\n")
end

--- Main parse function
--- @param source string the template source code
function compiler:parse(source)
    local l = 1
    local tag_pos = find(source, "{", l, true)
    local strip = false
    -- found tag delimiter
    local delim = sub(source, tag_pos or 1, (tag_pos or 1) + 1)
    local open = {
        ["{{"] = tag_type.EXPRESSION,
        ["{%"] = tag_type.CONTROL,
        ["{#"] = tag_type.COMMENT,
    }
    self.ctx_stack = {}
    self.macros = {}
    self.blocks = {}
    -- create global context space
    self:start_context(true, true)
    while tag_pos do
        if open[delim] then
            if l <= tag_pos - 1 then -- cut text before tag
                local frag = sub(source, l, tag_pos - 1)
                self.line = self.line + strcount(frag, "\n")
                if strip then
                    frag = lstrip(frag, strip)
                    strip = nil
                end
                local wcp = strip_pattern[sub(source, tag_pos + 2, tag_pos + 2)]
                if wcp then -- checks if tag has space control
                    frag = rstrip(frag, wcp)
                end
                if frag ~= "" then
                    self:append_text(frag)
                end
            end
            local t, p = sub(source, tag_pos + 1, tag_pos + 1), tag_pos + 2
            if strip_pattern[sub(source, tag_pos + 2, tag_pos + 2)] then
                tag_pos = tag_pos + 1
            end
            if t == "{" then -- '{{'
                self.tag_type = tag_type.EXPRESSION
                local tok = tokenizer.new(source, tag_pos + 2)
                local info = {}
                local expr = self:parse_expression(tok, info)
                self:append_expr(expr, info.raw)
                if tok:is_valid() then
                    compiler_error(tok, "syntax", "expecting end of tag")
                end
                local path = tok:get_path_as_string()
                l = tag_pos + 2 + strlen(path) + strlen(tok.finish_token) -- start tag pos + '{{' +  tag length + '}}'
                self.line = self.line + strcount(path, "\n")
                strip = strip_pattern[sub(tok.finish_token, 1, 1)]
                tok = nil
            elseif t == "%" then -- '{%'
                self.tag_type = tag_type.CONTROL
                local tok = tokenizer.new(source, tag_pos + 2)
                self.tag_name = tok:get_token()
                local tag_name = 'tag_' .. tok:get_token()
                if tags[tag_name] then
                    self:append_code(tags[tag_name](self, tok:next())) -- call tags.tag_{{ name }}(compiler, tok)
                    if tok:is_valid() then
                        compiler_error(tok, "syntax", "expecting end of tag")
                    end
                else
                    compiler_error(nil, "syntax", "unknown tag '" .. tok:get_token() .. "'")
                end
                local path = tok:get_path_as_string()
                l = tag_pos + 2 + strlen(path) + strlen(tok.finish_token) -- start tag pos + '{%' + tag length + '%}'
                self.line = self.line + strcount(path, "\n")
                strip = strip_pattern[sub(tok.finish_token, 1, 1)]
                tok = nil
                self.tag_name = nil
            elseif t == "#" then -- '{#'
                local end_comment = find(source, "#}", p, true)
                self.line = self.line + strcount(sub(source, tag_pos, end_comment - 1), "\n")
                tag_pos = end_comment
                l = tag_pos + 2
            end
            self.tag_type = nil
        else
            tag_pos = tag_pos + 2
        end
        while true do
            tag_pos = find(source, "{", tag_pos + 2, true)
            if not tag_pos then
                break
            end
            delim = sub(source, tag_pos, tag_pos + 1)
            if open[delim] then
                break
            end
        end
        if self.ignore and tag_pos then
            while tag_pos do -- skip all tags until self.ignore tag
                if find(source, "^{%%[-~]?%s*" .. self.ignore, tag_pos) == tag_pos then
                    self.ignore = nil
                    break
                end
                tag_pos = find(source, "{", tag_pos + 1, true)
            end
        end
    end
    if #self.tags > 0  then
        local tag = self.tags[#self.tags]
        compiler_error(nil, "syntax", "Unexpected end of the template '" ..
            self.name .. "', expecting tag 'end" .. tag.name .. "' (opened on line " .. tag.line .. ")")
    end
    local frag = sub(source, l)
    if strip then
        frag = lstrip(frag, strip)
    end
    if frag ~= "" then
        self:append_text(frag)
    end
    if #self.ctx_stack > 1 then
        compiler_error(nil, "syntax", "Unexpected extra contexts (" .. #self.ctx_stack .. ")")
    end
    self.ctx = self:end_context(self.ctx) -- finish last context
end

--- @param tok aspect.tokenizer
--- @param opts table
function compiler:parse_var_name(tok, opts)
    opts = opts or {}
    opts.var_system = false
    if tok:is_word() then
        local var = tok:get_token()
        if var == "_context" or var == "__" or var == "_self" then
            opts.var_system = true
        else
            opts.var_system = false
        end
        tok:next()
        return var
    else
        compiler_error(tok, "syntax", "expecting variable name")
    end
end

--- Parse basic variable name like:
--- one
--- one.two
--- one["two"].three
--- @param tok aspect.tokenizer
function compiler:parse_variable(tok)
    if tok:is_word() then
        local var, keys
        if tok:is('loop') then -- magick variable name {{ loop }}
            var = {"loop"}
            local tag, pos = self:get_last_tag('for')
            while tag do
                if tag.has_loop ~= true then
                    tag.has_loop = tag.has_loop or {}
                    local loop_key = tok:next():require("."):next():get_token()
                    if not loop_keys[loop_key] then
                        compiler_error(tok, "syntax", "loop-variable expects one of [" .. concat(get_keys(loop_keys), ", ") .. "] keys")
                    end
                    var[#var + 1] = '"' .. loop_key .. '"'
                    if loop_key == "parent" then
                        tag.has_loop.parent = true
                        tag, pos = self:get_last_tag('for', pos - 1)
                    else
                        tag.has_loop[loop_key] = true
                        tag = false
                    end
                end
                tok:next()
                --else
                --    tok:next()
                --    var = {"loop"}
            end
        elseif tok:is("_context") then -- magick variable name {{ _context }}
            tok:next()
        --    var = {"_context"}
        end
        local var_info
        if not var then
            var = {self:parse_var_name(tok)}
            var_info = self:touch_var(var[1])
            if not self.ctx.vars[var[1]] then
                var[1] = "_context." .. var[1]
            end
        else
            var_info = self:touch_var(var[1])
        end
        while tok:is(".") or tok:is("[") do
            local mode = tok:get_token()
            tok:next()
            if mode == "." then -- a.b
                insert(var, '"' .. tok:require_type('word'):get_token() .. '"')
                if var_info then
                    var_info.keys[tok:get_token()] = self.line
                end
                tok:next()
            elseif mode == "[" then -- a[b]
                insert(var, self:parse_expression(tok))
                tok:require("]"):next()
            end
        end
        if #var == 1 then
            return var[1]
        else
            return "__.v(" .. concat(var, ", ") .. ")"
        end
    else
        compiler_error(tok, "syntax", "expecting variable name")
    end
end

--- @param tok aspect.tokenizer
--- @param args_info table
--- @param assoc boolean returns table or values or list
function compiler:parse_args(tok, args_info, assoc, fname)
    local args, i = {}, 1
    if tok:is(")") then
        return args
    end
    while tok:is_valid() do
        local key, value, info, arg_info = nil, nil, {}, nil
        if tok:is_word() and tok:is_next("=") then
            key = tok:get_token()
            tok:next():require("="):next()
        end
        value = self:parse_expression(tok, info)
        if key then
            for _, arg in ipairs(args_info) do
                if arg.name == key then
                    arg_info = arg
                    break
                end
            end
            if not arg_info then
                compiler_error(tok, "compile", (fname or "callable") .. " has no '" .. key .. "' argument")
            end
        else
            if args_info[i] then
                arg_info = args_info[i]
            else
                compiler_error(tok, "compile", (fname or "callable") .. " has no argument #" .. i)
            end
        end
        if assoc then
            args[arg_info.name] = utils.cast_lua(value, info.type, args_info.type)
        else
            args[i] = utils.cast_lua(value, info.type, args_info.type)
        end
        i = i + 1
        if tok:is(",") then
            tok:next()
        else
            break
        end
    end
    return args
end

--- parse coma separated arguments
--- @param tok aspect.tokenizer
function compiler:parse_function(tok)
    local name, args
    if tok:is_word() then
        name = tok:get_token()
    else
        compiler_error(tok, "syntax", "expecting function name")
    end
    if not func.fn[name] then
        compiler_error(tok, "compile", "function " .. name .. "() not found")
    end
    tok:next():require("("):next()
    if func.args[name] then
        args = self:parse_args(tok, func.args[name], true, "function " .. name)
        tok:require(")"):next()
        if func.parsers[name] then
            return func.parsers[name](self, args)
        else
            return "__.fn." .. name .. "(__, {" .. utils.implode_hashes(args) .. "})"
        end
    elseif func.parsers[name] then
        local code = func.parsers[name](self, nil, tok)
        tok:require(")"):next()
        return code
    else
        tok:require(")"):next()
        return "__.fn." .. name .. "(__, {})"
    end
end

--- @param tok aspect.tokenizer
function compiler:parse_macro(tok)
    if tok:is("_self") then
        local name = tok:next():require("."):next():get_token()
        if not self.macros[name] then
            compiler_error(tok, "syntax", "Macro " .. name .. " not defined in this (" .. self.name .. ") template")
        end
        return "_self.macros." .. name .."(__, " .. self:parse_macro_args(tok:next():require("(")) .. ")", false
    elseif tok:is_word() then
        if tok:is_next(".") then
            local var = tok:get_token()
            if self.import[var] ~= import_type.GROUP then
                compiler_error(tok, "syntax", "Macro " .. var .. " not imported")
            end
            local name = tok:next():require("."):next():get_token()
            return "(" .. var .. "." .. name .. " and "
                    .. var .. "." .. name .. "(__, " .. self:parse_macro_args(tok:next()) .. "))", false
        elseif tok:is_next("(") then
            local name = tok:get_token()
            if self.import[name] ~= import_type.SINGLE then
                compiler_error(tok, "syntax", "Macro " .. name .. " not imported")
            end
            tok:next():require("(")
            return name .."(__, " .. self:parse_macro_args(tok) .. ")", false
        end
    end
end

--- Parse arguments for macros
--- @param tok aspect.tokenizer
function compiler:parse_macro_args(tok)
    tok:require("("):next()
    local i, args = 1, {}
    while true do
        local key, value = nil, nil
        if tok:is_word() then
            if tok:is_next("=") then
                key = tok:get_token()
                tok:next():require("="):next()
                value = self:parse_expression(tok)
            else
                value = self:parse_expression(tok)
            end
        else
            value = self:parse_expression(tok)
        end
        if key then
            args[ key ] = value
        else
            args[ i ] = value
        end
        if tok:is(",") then
            tok:next()
        else
            break
        end
        i = i + 1
    end
    tok:require(")"):next()
    args = utils.implode_hashes(args)
    if args then
        return "{ " .. args .. " }"
    else
        return "{}"
    end

end

--- Parse array (list or hash)
--- [1,2,3]
--- {"one": 1, "two": 2}
--- @param tok aspect.tokenizer
--- @param plain boolean return table without brackets
function compiler:parse_array(tok, plain)
    local vals = {}
    if tok:is("[") then
        tok:next()
        if tok:is("]") then
            return "{}"
        end
        while true do
            insert(vals, self:parse_expression(tok))
            if tok:is(",") then
                tok:next()
            elseif tok:is(":") then
                compiler_error(tok, "syntax", "list can't have named keys, use hash instead")
            else
                break
            end
        end
        tok:require("]"):next()
    elseif tok:is("{") then
        tok:next()
        if tok:is("}") then
            tok:next()
            return "{}"
        end
        while true do
            local key
            if tok:is_word() then
                key = '"' .. tok:get_token() .. '"'
                tok:next()
            elseif tok:is_string() or tok:is_number() then
                key = tok:get_token()
                tok:next()
            elseif tok:is("(") then
                key = self:parse_expression(tok)
            end
            insert(vals, "[" .. key .. "] = " .. self:parse_expression(tok:require(":"):next()))
            if tok:is(",") then
                tok:next()
            else
                break
            end
        end
        tok:require("}"):next()
    else
        compiler_error(tok, "syntax", "expecting list or hash")
    end
    if plain then
        return concat(vals, ", ")
    else
        return "{" .. concat(vals, ", ") .. "}"
    end
end

--- @param tok aspect.tokenizer
--- @return table
function compiler:parse_hash(tok)
    local hash = {}
    tok:require('{'):next()
    if tok:is("}") then
        tok:next()
        return hash
    end
    while true do
        local key
        if tok:is_word() then
            key = tok:get_token()
            tok:next()
        else
            compiler_error(tok, "syntax", "expecting hash key")
        end

        hash[key] = self:parse_expression(tok:require(":"):next())
        if tok:is(",") then
            tok:next()
        else
            break
        end
    end
    tok:require("}"):next()
    return hash
end

--- @param tok aspect.tokenizer
--- @param info table
--- @return string
function compiler:parse_filters(tok, var, info)
    info = info or {}
    info.raw = false
    while tok:is("|") do -- parse pipeline filter
        if tok:next():is_word() then
            local filter = tok:get_token()
            if tok:next():is(".") then
                filter = filter .. "." .. tok:next():require_type("word"):get_token()
                tok:next()
            end
            if not filters.fn[filter] then
                compiler_error(tok, "compile", "unknown filter " .. filter)
            end
            if filter == "raw" then
                info.raw = true
            end
            local args

            if tok:is("(") then
                args = self:parse_args(tok:next(), filters.info[filter].args, false, "filter " .. filter)
                tok:require(")"):next()
            end
            if info.type then
                var = utils.cast_lua(var, info.type, filters.info[filter].input)
            end
            if args and #args > 0 then
                var = "__.f['" .. filter .. "'](" .. var .. ", " .. concat(args, ", ") .. ")"
            else
                var = "__.f['" .. filter .. "'](" .. var .. ")"
            end
            info.type = filters.info[filter].output
        else
            compiler_error(tok, "syntax", "expecting filter name")
        end
    end
    return var
end

--- Parse operand of the expression
--- @param tok aspect.tokenizer
--- @param info table
function compiler:parse_value(tok, info)
    local var
    info = info or {}
    info.type = nil
    info.raw = false
    if tok:is_word() then -- is variable name
        if special[tok:get_token()] then
            if tok:is("true") or tok:is("false") then -- is regular true/false/nil
                var = tok:get_token()
                tok:next()
                info.type = "boolean"
            elseif tok:is("null") or tok:is("nil") then -- is null
                var = 'nil'
                info.type = "nil"
                tok:next()
            else
                compiler_error(tok, "syntax", "unknown special token")
            end
        elseif tok:is_next("(") then
            if self.import[tok:get_token()] == import_type.SINGLE then
                var = self:parse_macro(tok)
                info.type = "nil"
            else
                var = self:parse_function(tok)
                info.type = "any"
            end
        elseif tok:is_seq{"word", ".", "word", "("} then
            var = self:parse_macro(tok)
            info.type = "nil"
        else
            var = self:parse_variable(tok)
            info.type = "any"
        end
    elseif tok:is_string() then -- is string or number
        var = tok:get_token()
        tok:next()
        info.type = "string"
    elseif tok:is_number() then
        var = tok:get_token()
        tok:next()
        info.type = "number"
    elseif tok:is("[") or tok:is("{") then -- is list or hash
        var = self:parse_array(tok)
        info.type = "table"
    elseif tok:is("(") then -- is expression
        local expr_info = {}
        var = self:parse_expression(tok:next(), expr_info)
        info.type = expr_info.type
        tok:require(")"):next()
    else
        compiler_error(tok, "syntax", "expecting any value")
    end
    if tok:is("|") then
        var = self:parse_filters(tok, var, info)
    end
    return var
end

--- Parse any expression
--- @param tok aspect.tokenizer
--- @param opts table
function compiler:parse_expression(tok, opts)
    opts = opts or {}
    local expr = ast.new():parse(self, tok):pack()
    opts.type = expr.type
    opts.bracket = expr.bracket
    opts.raw = expr.raw
    return expr.value
end

--- @param tok aspect.tokenizer
--- @return table test information
function compiler:parse_is(tok)
    local test = {}
    if tok:require("is"):next():is('not') then
        test["not"] = true
        tok:next()
    end
    test.name = tok:get_token()
    tok:next()
    if tests.args[test.name] then
        if type(tests.args[test.name]) == "string" then -- has function: is divisible by(expr)
            tok:require(tests.args[test.name]):next()
            test.name = test.name .. "_" .. tests.args[test.name]
        end
        test.expr = self:parse_expression(tok:require("("):next())
        tok:next()
    end
    if not tests.fn["is_" .. test.name] then
        compiler_error(tok, "syntax", "expecting valid test name")
    end
    return test
end

--- Append any text
--- @param text string
function compiler:append_text(text)
    local tag = self:get_last_tag()
    local line = self:get_checkpoint()
    local code = self.ctx.code
    if line then
        insert(code, line)
    end
    if tag and tag.append_text then
        insert(code, tag.append_text(tag, text))
    else
        insert(code, "__(" .. quote_string(text) .. ")")
    end
end

function compiler:append_expr(lua, raw)
    local tag = self:get_last_tag()
    local line = self:get_checkpoint()
    local code = self.ctx.code
    if line then
        insert(code, line)
    end
    if lua then
        if tag and tag.append_expr then
            insert(code, tag.append_expr(tag, lua, raw))
        elseif raw then
            insert(code, "__(" .. lua .. ")")
        else
            insert(code, "__:e(" .. lua .. ")")
        end
    end
end

--- Append any lua code
--- @param lua string
function compiler:append_code(lua)
    local tag = self:get_last_tag()
    local line = self:get_checkpoint()
    local code = self.ctx.code
    if line then
        insert(code, line)
    end
    if type(lua) == "table" then
        for _, l in ipairs(lua) do
            if tag and tag.append_code then
                insert(code, tag.append_code(tag, l))
            else
                insert(code, l)
            end
        end
    elseif lua then
        if tag and tag.append_code then
            insert(code, tag.append_code(tag, lua))
        else
            insert(code, lua)
        end
    end
end

function compiler:get_checkpoint()
    if self.prev_line ~= self.line then
        self.prev_line = self.line
        return "__.line = " .. self.line
    else
        return nil
    end
end

--- Add ref to another template
--- @param name string template name
function compiler:add_template_ref(name)
    if not self.tag_name then
        return
    end
    if not self.ctx.tpl_refs[name] then
        self.ctx.tpl_refs[name] = { [self.tag_name] = self.line }
    elseif not self.ctx.tpl_refs[name][self.tag_name] then
        self.ctx.tpl_refs[name][self.tag_name] = self.line
    end
end

--- Add local variable name to scope (used for includes, blocks and macros)
--- @param name string
function compiler:push_var(name)
    if #self.tags > 0 then
        local tag = self.tags[#self.tags]
        if not tag.vars then
            tag.vars = {[name] = true}
        else
            tag.vars[name] = true
        end
    end
    if not self.ctx.vars[name] then
        self.ctx.vars[name] = 1
    else
        self.ctx.vars[name] = self.ctx.vars[name] + 1
    end
end

--- Analyze and add variable ref if its global variable
--- @param name string template name
--- @return aspect.compiler.var_ref
function compiler:touch_var(name)
    if self.ctx.vars[name] then -- this is local variable
        return
    end
    if not self.ctx.var_refs[name] then
        self.ctx.var_refs[name] = {
            line = self.line,
            keys = {}
        }
    end
    return self.ctx.var_refs[name]
end

--- Returns all variable names defined in the scope (without global)
--- @return table|nil list of variables like ["variable"] = variable
function compiler:get_local_vars()
    local vars = {}
    for k, _ in pairs(self.ctx.vars) do
        vars[k] = k
    end
    if next(vars) then
        return vars
    else
        return nil
    end
end

--- Push the tag into scope stack
--- @param name string the tag name
--- @param create_context boolean|string if string, the string will be name of context
--- @param isolate_context boolean isolate variables and refs from previous context
--- @return aspect.tag
function compiler:push_tag(name, create_context, isolate_context)
    self.idx = self.idx + 1
    --- @type aspect.tag
    local tag = {
        id = self.idx,
        name = name,
        line = self.line,
        vars = {}
    }
    if create_context or isolate_context then
        tag.ctx = self:start_context(create_context, isolate_context)
    end
    local prev = self.tags[#self.tags]
    if prev then
        if prev.append_text then
            tag.append_text = prev.append_text
        end
        if prev.append_expr then
            tag.append_expr = prev.append_expr
        end
        if prev.append_code then
            tag.append_code = prev.append_code
        end
    end
    insert(self.tags, tag)
    return tag
end

--- Remove tag from stack
--- @param name string the tag name
--- @return aspect.tag
function compiler:pop_tag(name)
    if #self.tags then
        --- @type aspect.tag
        local tag = self.tags[#self.tags]
        if tag.name == name then
            if tag.vars then -- pop variables
                local vars = self.ctx.vars
                for var_name, _ in pairs(tag.vars) do
                    if vars[var_name] and vars[var_name] > 0 then
                        vars[var_name] = vars[var_name] - 1
                        if vars[var_name] == 0 then
                            vars[var_name] = nil
                        end
                    else
                        compiler_error(nil, "compiler", "broken variable scope")
                    end
                end
            end
            if tag.ctx then
                self:end_context(tag.ctx)
            end
            return remove(self.tags)
        else
            compiler_error(nil, "syntax",
               "unexpected tag 'end" .. name .. "'. Expecting tag 'end" .. tag.name .. "' (opened on line " .. tag.line .. ")")
        end
    else
        compiler_error(nil, "syntax", "unexpected tag 'end" .. name .. "'. Tag ".. name .. " never opened")
    end
end

--- Returns last tag from stack
--- @param name string if set then returns last tag with this name
--- @param from number|nil stack offset
--- @return aspect.tag
--- @return number|nil stack position
function compiler:get_last_tag(name, from)
    if name then
        if from and from < 1 then
            return nil
        end
        from = from or #self.tags
        if #self.tags > 0 then
            for i=from, 1, -1 do
                if self.tags[i].name == name then
                    return self.tags[i], i
                end
            end
        end
    elseif #self.tags then
        return self.tags[#self.tags], #self.tags
    end
    return nil
end

--- Crate new context space for code, variables, refs ...
--- @param name boolean|string
--- @param isolate boolean
--- @return aspect.compiler.context
function compiler:start_context(name, isolate)
    --- @type aspect.compiler.context
    local ctx = {}
    ctx.id = #self.ctx_stack + 1
    if type(name) == "string" then
        ctx.name = name
        ctx.code = {
            "__:push_state(_self, " .. self.line .. ", " .. quote_string(ctx.name) .. ")"
        }
    elseif not name and ctx.id > 1 then
        ctx.code = self.ctx_stack[#self.ctx_stack].code
    else
        ctx.code = {}
    end
    if isolate then
        ctx.vars = {}
        ctx.tpl_refs = {}
        ctx.var_refs = {}
    elseif self.ctx then
        ctx.vars = self.ctx.vars
        ctx.tpl_refs = self.ctx.tpl_refs
        ctx.var_refs = self.ctx.var_refs
    end

    self.ctx_stack[ctx.id] = ctx
    self.ctx = ctx
    return ctx
end

--- @return aspect.compiler.context
function compiler:end_context(ctx)
    if ctx.id ~= self.ctx.id then -- just assert
        compiler_error(nil, "compiler", "incorrect termination of context")
    end
    if #self.ctx_stack > 0 then
        --- @type aspect.compiler.context
        local prev_ctx = remove(self.ctx_stack)
        if prev_ctx.name then
            insert(prev_ctx.code, "__:pop_state()")
        end
        self.ctx = self.ctx_stack[#self.ctx_stack]
        return prev_ctx
    else
        compiler_error(nil, "compiler", "broken context stack")
    end
end

return compiler