--[[ MARK: - Template Engine
    
Builds a formspec from the contents of a game-provided main menu file.

=================================================================================
                            Menu File Documentation
=================================================================================

Menu files are essentially just formspecs, but with special semantics and extended syntax.

First, menu files are made up of a number of dialog definitions. A dialog definition is
basically an HTML tag, where the tag name is the name of the dialog. For example this:
```
<main>
  label[2,2;A label]
</main>
<other>
  button[1,1;4,1;button;This is a button]
</other>
```
will define a dialog named 'main', with a label in it, and a dialog named 'other',
with a button in it. Every menu file must have a dialog named 'main', which serves
as the entry point of the game's menu.

Note that to prevent dialog definitions from conflicting with the contents of
hypertext[] elements, they must not have leading whitespace before the opening
delimiter.

Being able to define these other dialogs would be pretty pointless if they couldn't
be used for anything. Accordingly, you can use standard formspec actions, e.g. buttons,
to segue to a different dialog. To do this, set the action name to
'.show_dialog_<name>', where <name> is the name of the dialog you want to open.
You can also use '.overlay_dialog_<name>' to draw the target dialog on top of the
current dialog, and '.unoverlay_dialog' to hide it again.

There are several other action patterns that invoke special behavior:
  - 
  - '.play': Start the game.
  - '.play_with_world_named_<name>': Start the game on the specified world. If the
    world in question does not exist, it will be created.

It is also possible to define metadata for the main menu. To do this, create a dialog
section named 'meta'. The contents of this section will then be parsed in the same way
as minetest.conf instead of being treated as a dialog.

Available metadata options are:
  - 'enable_clouds': true/false (defaults to false)


Now, all this is good when we know exactly what content we want on the main menu.
But what if we want to render e.g. a list of worlds, or even a list of game modes?
This can be done using the @foreach construct. @foreach takes in a list of values,
the name of a JSON file, or a reference to a menu-exposed list.

The major advantage of a foreach loop, however, is interpolation. Inside a foreach
loop, the pattern '${<expression>}' will be replaces with the result of <expression>.

Inside an expression, you can reference variables as '@<name>'. Which variables are
available depends on the iterated list. In the case of a list literal, the current
item is exposed as a variable named @item, while for a JSON file, each key in the
file's top-level object will correspond to a variable. (Note that variable names may
only contain letters or numbers, even if mapped from a JSON file.)

The menu-provided lists are:
  - @WORLDS: The list of worlds for this game. Exposes @name (the world name),
    @path (the world's full absolute path), @gameid (the ID of the current game),
    and @selected (whether the world is currently selected).
  - @MODS: The list of installed mods that are not incompatible with this game.
    Exposes @name, @title, @description, @author, @path, @depends, and @optional_depends.
  - @WORLDMODS: The list of mods installed on the current world. No-op if no world
    is selected.

Expressions also support rudimentary mathematical operations, namely addition (+),
'subtraction' (+-), multiplication (*), division (/), and exponentiation (^). Trying
to perform math on a non-tonumber()-able variable will treat the variable as 0.

As an example, this:
```
label[1,1;Worlds:]
@foreach:$WORLDS:name
  label[1,${@i + 1};World named ${@name}]
@endforeach:name

@foreach:[one,two,three]:name2
  label[4,${@i + 1};Item ${@i} is named: ${@item}]
@endforeach:name2
```
will:
  - Create a "Worlds" label at 1,1;
  - Create a label for every world with that world's name and a Y coordinate that
    corresponds to the world's position in the list, plus 1 so these labels start
    below the existing label.
  - Create labels for "one", "two", and "three" that display the item's value and
    its position in the list, with similarly increasing Y-coordinates.

Besides foreach loops, menu files also support conditionals. Conditionals are
written as '@if:<condition>:<name> ... @else:<name> ... @endif:<name>', where
<condition> is any expression. The conditional will be replaced with the contents
of its first block if the condition evaluates to non-zero, and the contents of
its second block otherwise. Note that the second block may not be omitted due to
how the parser works.

Example:
```
@if: @name = test :<name>
  <formspec 1>
@else:<name>
  <formspec 2>
@endif:<name>
```
    
Notes:
  - The only uniqueness requirements for the name of a block is that it not be
  the name of a statment of the same type contained in the body of that block.
  This is so that the parser knows which `end` belongs to which block without
  having to manage state.

Note: Because of the way the main menu works, image paths must be specified in full.
To make this non-painful, when referencing images use '$ASSET_PATH/<image name>'
instead of just the image name. $ASSET_PATH will be replaced with the actual path
to the game's menu/ directory, so images used by the main menu should be stored
there. You can use $ASSET_PATH in any context. Additionally, $DEFAULT_ASSET_PATH is
the path to the builtin assets folder, and $NO_IMAGE is the path to blank.png, in
case you need to stylistically unset a background image defined by a global style.
--]]

local zone = function() end

if not minetest then
    zone = require("jit.zone")
    minetest = {
        formspec_escape = function() end,
        hypertext_escape = function() end,
        get_translator = function()
            return function() end, function() end
        end
    }
    local function basic_dump(o)
        local tp = type(o)
        if tp == "number" then
            local s = tostring(o)
            if tonumber(s) == o then
                return s
            end
            -- Prefer an exact representation over a compact representation.
            -- e.g. basic_dump(0.3) == "0.3",
            -- but basic_dump(0.1 + 0.2) == "0.30000000000000004"
            -- so the user can see that 0.1 + 0.2 ~= 0.3
            return string.format("%.17g", o)
        elseif tp == "string" then
            return string.format("%q", o)
        elseif tp == "boolean" then
            return tostring(o)
        elseif tp == "nil" then
            return "nil"
        elseif tp == "userdata" then
            return tostring(o)
        else
            return string.format("<%s>", tp)
        end
    end

    local keywords = {
        ["and"] = true,
        ["break"] = true,
        ["do"] = true,
        ["else"] = true,
        ["elseif"] = true,
        ["end"] = true,
        ["false"] = true,
        ["for"] = true,
        ["function"] = true,
        ["goto"] = true,  -- Lua 5.2
        ["if"] = true,
        ["in"] = true,
        ["local"] = true,
        ["nil"] = true,
        ["not"] = true,
        ["or"] = true,
        ["repeat"] = true,
        ["return"] = true,
        ["then"] = true,
        ["true"] = true,
        ["until"] = true,
        ["while"] = true,
    }
    local function is_valid_identifier(str)
        if not str:find("^[a-zA-Z_][a-zA-Z0-9_]*$") or keywords[str] then
            return false
        end
        return true
    end

    --------------------------------------------------------------------------------
    -- Dumps values in a line-per-value format.
    -- For example, {test = {"Testing..."}} becomes:
    --   _["test"] = {}
    --   _["test"][1] = "Testing..."
    -- This handles tables as keys and circular references properly.
    -- It also handles multiple references well, writing the table only once.
    -- The dumped argument is internal-only.

    function dump2(o, name, dumped)
        name = name or "_"
        -- "dumped" is used to keep track of serialized tables to handle
        -- multiple references and circular tables properly.
        -- It only contains tables as keys.  The value is the name that
        -- the table has in the dump, eg:
        -- {x = {"y"}} -> dumped[{"y"}] = '_["x"]'
        dumped = dumped or {}
        if type(o) ~= "table" then
            return string.format("%s = %s\n", name, basic_dump(o))
        end
        if dumped[o] then
            return string.format("%s = %s\n", name, dumped[o])
        end
        dumped[o] = name
        -- This contains a list of strings to be concatenated later (because
        -- Lua is slow at individual concatenation).
        local t = {}
        for k, v in pairs(o) do
            local keyStr
            if type(k) == "table" then
                if dumped[k] then
                    keyStr = dumped[k]
                else
                    -- Key tables don't have a name, so use one of
                    -- the form _G["table: 0xFFFFFFF"]
                    keyStr = string.format("_G[%q]", tostring(k))
                    -- Dump key table
                    t[#t + 1] = dump2(k, keyStr, dumped)
                end
            else
                keyStr = basic_dump(k)
            end
            local vname = string.format("%s[%s]", name, keyStr)
            t[#t + 1] = dump2(v, vname, dumped)
        end
        return string.format("%s = {}\n%s", name, table.concat(t))
    end


    -- This dumps values in a human-readable expression format.
    -- If possible, the resulting string should evaluate to an equivalent value if loaded and executed.
    -- For example, {test = {"Testing..."}} becomes:
    -- [[{
    --  test = {
    --      "Testing..."
    --  }
    -- }]]
    function dump(value, indent)
        indent = indent or "\t"
        local newline = indent == "" and "" or "\n"

        local rope = {}
        local write
        do
            -- Keeping the length of the table as a local variable is *much*
            -- faster than invoking the length operator.
            -- See https://gitspartv.github.io/LuaJIT-Benchmarks/#test12.
            local i = 0
            function write(str)
                i = i + 1
                rope[i] = str
            end
        end

        local n_refs = {}
        local function count_refs(val)
            if type(val) ~= "table" then
                return
            end
            local tbl = val
            if n_refs[tbl] then
                n_refs[tbl] = n_refs[tbl] + 1
                return
            end
            n_refs[tbl] = 1
            for k, v in pairs(tbl) do
                count_refs(k)
                count_refs(v)
            end
        end
        count_refs(value)

        local refs = {}
        local cur_ref = 1
        local function write_value(val, level)
            if type(val) ~= "table" then
                write(basic_dump(val))
                return
            end

            local tbl = val
            if refs[tbl] then
                write(refs[tbl])
                return
            end

            if n_refs[val] > 1 then
                refs[val] = ("getref(%d)"):format(cur_ref)
                write(("setref(%d)"):format(cur_ref))
                cur_ref = cur_ref + 1
            end
            write("{")
            if next(tbl) == nil then
                write("}")
                return
            end
            write(newline)

            local function write_entry(k, v)
                write(indent:rep(level))
                write("[")
                write_value(k, level + 1)
                write("] = ")
                write_value(v, level + 1)
                write(",")
                write(newline)
            end

            local keys = {string = {}, number = {}}
            for k in pairs(tbl) do
                local t = type(k)
                if keys[t] then
                    table.insert(keys[t], k)
                end
            end

            -- Write string-keyed entries
            table.sort(keys.string)
            for _, k in ipairs(keys.string) do
                local v = val[k]
                if is_valid_identifier(k) then
                    write(indent:rep(level))
                    write(k)
                    write(" = ")
                    write_value(v, level + 1)
                    write(",")
                    write(newline)
                else
                    write_entry(k, v)
                end
            end

            -- Write number-keyed entries
            local len = 0
            for i in ipairs(tbl) do
                len = i
            end
            if #keys.number == len then -- table is a list
                for _, v in ipairs(tbl) do
                    write(indent:rep(level))
                    write_value(v, level + 1)
                    write(",")
                    write(newline)
                end
            else -- table harbors arbitrary number keys
                table.sort(keys.number)
                for _, k in ipairs(keys.number) do
                    write_entry(k, tbl[k])
                end
            end

            -- Write all remaining entries
            for k, v in pairs(val) do
                if not keys[type(k)] then
                    write_entry(k, v)
                end
            end

            write(indent:rep(level - 1))
            write("}")
        end
        write_value(value, 1)
        return table.concat(rope)
    end
    function string.split(str, delim, include_empty, max_splits, sep_is_pattern)
        delim = delim or ","
        if delim == "" then
            error("string.split separator is empty", 2)
        end
        max_splits = max_splits or -2
        local items = {}
        local pos, len = 1, #str
        local plain = not sep_is_pattern
        max_splits = max_splits + 1
        repeat
            local np, npe = string.find(str, delim, pos, plain)
            np, npe = (np or (len+1)), (npe or (len+1))
            if (not np) or (max_splits == 1) then
                np = len + 1
                npe = np
            end
            local s = string.sub(str, pos, np - 1)
            if include_empty or (s ~= "") then
                max_splits = max_splits - 1
                items[#items + 1] = s
            end
            pos = npe + 1
        until (max_splits == 0) or (pos > (len + 1))
        return items
    end
end

local fe = minetest.formspec_escape
local hte = minetest.hypertext_escape

template = {}

local operators = {
    ["or"] = {prec = 0, assoc = "left", kind = "binary"},
    ["and"] = {prec = 1, assoc = "left", kind = "binary"},
    ["=="] = {prec = 2, assoc = "left", kind = "binary"},
    ["<"] = {prec = 2, assoc = "left", kind = "binary"},
    ["<="] = {prec = 2, assoc = "left", kind = "binary"},
    [">"] = {prec = 2, assoc = "left", kind = "binary"},
    [">="] = {prec = 2, assoc = "left", kind = "binary"},
    ["+"] = {prec = 3, assoc = "left", kind = "binary"},
    ["-"] = {prec = 3, assoc = "left", kind = "binary"},
    ["*"] = {prec = 4, assoc = "left", kind = "binary"},
    ["/"] = {prec = 4, assoc = "left", kind = "binary"},
    ["%"] = {prec = 5, assoc = "left", kind = "binary"},
    ["not"] = {prec = 9, kind = "unary"},
    ["^"] = {prec = 10, assoc = "right", kind = "binary"},
    -- Add more operators here, e.g. ["=="] = {prec = 0, assoc = "left"} for comparisons
}

function template.build_expr(expr)
    
    -- First pass: Collect tokens into a flat list
    local out = {}
    local pos = 1
    
    local len = #expr
    
    while pos <= len do
        local matched = false
        
        
        -- Skip whitespace
        local ws_start, ws_end = expr:find("^%s+", pos)
        if ws_start then
            pos = ws_end +1
            matched = true
        end
        
        
        -- Strings
        if not matched then
            local str_start, str_end, str = expr:find('^"([^"]+)"', pos)
            if not str_start then 
                str_start, str_end, str = expr:find("^'([^']+)'", pos)
            end
            if str_start then
                out[#out +1] = {type = "string", value = str}
                
                pos = str_end +1
                matched = true
            end
        end
        
        
        
        -- Numbers
        if not matched then
            local num_start, num_end, num = expr:find("^(%d+%.?%d*[eE]?[+-]?%d*)", pos)
            if num_start then
                out[#out +1] = {type = "number", value = tonumber(num)}
                
                pos = num_end +1
                matched = true
            end
        end
        
        
        
        -- Operators
        if not matched then
            for op in pairs(operators) do
                local op_start = pos
                local op_end = pos +op:len() -1
                if expr:sub(op_start, op_end) == op then
                    out[#out +1] = {type = "operator", op = op}
                    
                    pos = op_end +1
                    matched = true
                end
            end
        end
        
        
        
        -- Grouping operators
        if not matched then
            local op_start, op_end, op = expr:find("^(%p)", pos)
            if op_start then
                if op == "," then
                    out[#out +1] = {type = "comma"}
                elseif op == "." then
                    out[#out +1] = {type = "dot"}
                elseif op == "(" then
                    out[#out +1] = {type = "lparen"}
                elseif op == ")" then
                    out[#out +1] = {type = "rparen"}
                elseif op == "[" then
                    out[#out +1] = {type = "lbracket"}
                elseif op == "]" then
                    out[#out +1] = {type = "rbracket"}
                else
                    goto aaa
                end
                
                pos = op_end +1
                matched = true
                ::aaa::
            end
        end
        
        
        
        -- Keywords
        if not matched then
            if expr:find("^true") then
                out[#out +1] = {type = "bool", value = true}
                pos = pos +3
                matched = true
            elseif expr:find("^false") then
                out[#out +1] = {type = "bool", value = false}
                pos = pos +4
                matched = true
            end
        end
        
        
        
        -- Identifiers
        if not matched then
            local id_start, id_end, id = expr:find("^([%a_][%w_]*)", pos)
            if id_start then
                local item = {type = "identifier", name = id}
                
                pos = id_end +1
                out[#out +1] = item
                matched = true
            end
        end
        
        
        
        if not matched then
            pos = pos +1
        end
        
    end
    
    
    
    -- Second pass: Fold the flat list into an expression tree
    local tree = {}
    local cursor = 1
    
    local peek, consume, expect, parse_expr, parse_primary, parse_postfix
    
    function peek()
        return out[cursor]
    end
    function consume()
        cursor = cursor +1
        return out[cursor -1]
    end
    
    function expect(ty)
        local t = consume()
        if not t or t.type ~= ty then
            error("Expected " .. ty .. " but got " .. (t and t.type or "nil").."(in expression `"..expr.."`)")
        end
    end
    
    parse_expr = function(min_prec) 
        min_prec = min_prec or 0
        
        -- Parse left operand: handle unary prefix operators
        local left
        local token = peek()
        if token and token.type == "operator" then
            local op_info = operators[token.op]
            if op_info and op_info.kind == "unary" then
                -- For unary prefix, use a high binding power
                consume()
                -- Recurse with high precedence for operand
                local operand = parse_expr(op_info.prec)
                left = {type = "unary_operator", op = token.op, expr = operand}
            else
                left = parse_primary()
            end
        else
            left = parse_primary()
        end

        -- Apply postfix operators (index, call) - highest precedence
        left = parse_postfix(left)

        -- Handle binary operators (infix)
        while true do
            local token = peek()
            if not token or token.type ~= "operator" then break end
            local op_info = operators[token.op]
            if not op_info or op_info.kind ~= "binary" or op_info.prec < min_prec then break end

            consume()
            local next_min_prec = op_info.prec
            if op_info.assoc == "left" then
                next_min_prec = next_min_prec + 1
            end
            -- For right-assoc, use >= so same prec binds right
            local right = parse_expr(next_min_prec)
            left = {type = "binary_operator", op = token.op, lhs = left, rhs = right}
        end
        
        return left
    end
    
    -- Parse postfix operators (index, call)
    parse_postfix = function(left) 
        while peek() do
            local t = peek()
            if t.type == "lbracket" then
                -- Indexing has high precedence
                consume()
                local key = parse_expr(0)  -- Inside [] can have full expressions
                expect("rbracket")
                left = {type = "index", base = left, key = key}
            elseif t.type == "lparen" then
                -- Function call
                consume()
                local args = {}
                while peek().type ~= "rparen" do
                    local e = parse_expr(0)
                    args[#args +1] = e
                    if peek().type == "comma" then consume() end
                end
                expect("rparen")
                left = {type = "call", func = left, args = args}
            elseif t.type == "dot" then
                consume()
                if peek().type == "identifier" then
                    left = {type = "index", base = left, key = {type = "string", value = consume().name}}
                else -- Syntax error
                    
                end
            else
                break
            end
        end
        
        return left
    end
    
    parse_primary = function() 
        local t = peek()
        if t.type == "number" or t.type == "string" or t.type == "bool" or t.type == "list" then
            consume()
            return t
        elseif t.type == "identifier" then
            consume()
--            if peek() and peek().type == "lparen" then
--                consume()
--                local args = {}
--                while peek().type ~= "rparen" do
--                    local e = parse_expr(0)
--                    args[#args +1] = e
--                    if peek().type == "comma" then consume() end
--                end
--                
--                return {type = "call", func = t, args = args}
--            end
            return t
        elseif t.type == "lbracket" then
            consume()
            local list = {}
            while peek().type ~= "rbracket" do
                local e = parse_expr(0)
                list[#list +1] = e
                if peek().type == "comma" then consume() end
            end
            return {type = "list", value = list}
        elseif t.type == "lparen" then
            consume()
            local e = parse_expr(0)
            expect("rparen")
            return e
        else
            print("Unexpected token: "..consume().type)
        end
        
    end
    
    
    return parse_expr(0)
end

function template.build(menu)
    local _id = 0
    local function id()
        _id = _id +1
        return _id
    end
    
    local out = {["@root"] = {}}
    local view_name = "main"
    local block = {parent = {}, id="_root"}
    local item = {parent = {}, id="_root"}
    local offset = 1
    local pos = 1
    while true do
        local matched = false
        local stmt_start, stmt_end, stmt = menu:find("{%%%s*(.-)%s*%%}", pos)
        local expr_start, expr_end, expr = menu:find("{{(.-)}}", pos)
        local comment_start, comment_end, comment = menu:find("{#(.-)#}", pos)
        
        -- Statements
        if stmt_start and (not expr_start or stmt_start < expr_start) and (not comment_start or stmt_start < comment_start) then
            
            block[#block +1] = menu:sub(offset, stmt_start -1)
            local verb = stmt:match "^(%a+)"
            if verb == "if" then
                item = {
                    id = id(),
                    type = "if",
                    parent = block,
                    conditions = {{condition = template.build_expr(stmt:match "if%s*(.*)"), body = {}}}
                }
                block[#block +1] = item
                block = item.conditions[1].body
                block.parent = item

                if not block.parent then print("block.parent set to nil: "..menu:sub(stmt_start -50, stmt_end +50)) end
            elseif verb == "elseif" then
                local x = {
                    condition = template.build_expr(stmt:match "if%s*(.*)"),
                    body = {}
                }
                item.conditions[#item.conditions +1] = x
                block = x.body
                block.parent = item

                if not block.parent then print("block.parent set to nil: "..menu:sub(stmt_start -50, stmt_end +50)) end
            elseif verb == "else" then
                local x = {
                    condition = {type = "bool", value = true},
                    body = {}
                }
                item.conditions[#item.conditions +1] = x
                block = x.body
                block.parent = item

                if not block.parent then print("block.parent set to nil: "..menu:sub(stmt_start -50, stmt_end +50)) end
            elseif verb == "endif" then
                block.parent = nil
                block = item.parent
                item.parent = nil
                item = block.parent
            elseif verb == "for" then
                local var, iter = stmt:match "for%s*([%a_][%w_]*)%s*in%s*(.*)"
                item = {
                    id = id(),
                    type = "for",
                    parent = block,
                    varname = var,
                    iterate = template.build_expr(iter),
                    body = {}
                }
                block[#block +1] = item
                block = item.body
                block.parent = item
            elseif verb == "endfor" then
                block.parent = nil
                block = item.parent
                item.parent = nil
                item = block.parent
            elseif verb == "macro" then
                local name, params = stmt:match "macro%s*([%a_][%w_]*)%s*(.*)"
                item = {
                    type = "macro",
                    parent = block,
                    name = name,
                    params = params:match("^%((.*)%)$"):split("%s*,%s*", false, -1, true),
                    body = {}
                }
                block[#block +1] = item
                block = item.body
                block.parent = item
            elseif verb == "endmacro" then
                block.parent = nil
                block = item.parent
                item.parent = nil
                item = block.parent
            elseif verb == "set" then
                local name, expr = stmt:match "set %s*([%a_][%w_]*)%s*=?%s*(.*)"
                if expr and expr ~= "" then
                    block[#block +1] = {type = "set", name = name, value = template.build_expr(expr)}
                else
                    item = {
                        type = "set_block",
                        parent = block,
                        name = name,
                        value = {}
                    }
                    block[#block +1] = item
                    block = item.value
                    block.parent = item
                end
            elseif verb == "endset" then
                block = item.parent
                item.parent = nil
                item = block.parent
                block.parent = nil
            elseif verb == "include" then
                
            elseif verb == "view" then
                view_name = stmt:match "view%s*([%a_][%w_]*)%s*" or "main"
                table.insert_all(out["@root"], block)
                item = {id = "root"}
                block = {id = "root"}
            elseif verb == "endview" then
                out[view_name] = block
                block = {}
            end
            
            offset = stmt_end +1
            pos = stmt_end
            matched = true
            
        end
        
        -- Interpolations
        if expr_start and expr_start < (stmt_start or math.huge) and expr_start < (comment_start or math.huge) then
            block[#block +1] = menu:sub(offset, expr_start -1)
            
            block[#block +1] = template.build_expr(expr)
            
            offset = expr_end +1
            pos = expr_end
            matched = true
        end

        -- Comments
        if comment_start and comment_start < (stmt_start or math.huge) and comment_start < (expr_start or math.huge) then
            block[#block +1] = menu:sub(offset, comment_start -1)
            offset = comment_end +1
            pos = comment_end
            matched = true
        end
        
        
        if not matched then
            break
        end
    end
    
    block[#block +1] = menu:sub(offset)
    out[#out +1] = block
        
    -- Attach a reference to the root scope in each view.
    for k, v in pairs(out) do
        if k ~= "@root" then
            v.root = out["@root"]
        end
    end
    
    return out
end

function template.evaluate_expr(expr, vars)
    assert(expr, "No expression given!")
    if expr.type == "binary_operator" then
        assert(expr.lhs and expr.rhs, "Binary operator missing an operand")
        if expr.op == "+" then
            return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) + (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
        elseif expr.op == "*" then
            return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) * (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
        elseif expr.op == "-" then
            return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) - (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
        elseif expr.op == "/" then
            return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) / (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
        elseif expr.op == "%" then
            return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) % (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
        elseif expr.op == "or" then
            return template.evaluate_expr(expr.lhs, vars) or template.evaluate_expr(expr.rhs, vars)
        elseif expr.op == "and" then
            return template.evaluate_expr(expr.lhs, vars) and template.evaluate_expr(expr.rhs, vars)
        elseif expr.op == "==" then
            return template.evaluate_expr(expr.lhs, vars) == template.evaluate_expr(expr.rhs, vars)
        elseif expr.op == ">" then
            return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) > (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
        elseif expr.op == ">" then
            return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) >= (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
        elseif expr.op == "<" then
            return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) < (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
        elseif expr.op == "<=" then
            return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) <= (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
        else
            return template.evaluate_expr(expr.lhs, vars)
        end
    elseif expr.type == "unary_operator" then
        assert(expr.expr, "Invalid unary expression")
        if expr.op == "not" then
            return not template.evaluate_expr(expr.expr, vars)
        else
            return template.evaluate_expr(expr.expr, vars)
        end
    elseif expr.type == "identifier" then
        return vars[expr.name]
    elseif expr.type == "index" then
        assert(expr.base and expr.key, "Invalid indexing")
        return template.evaluate_expr(expr.base, vars)[template.evaluate_expr(expr.key, vars)]
    elseif expr.type == "call" then
        assert(expr.func, "Invalid function ref")
        local fn = template.evaluate_expr(expr.func, vars)
        if type(fn) == "function" then
            local args = {}
            for _, x in pairs(expr.args) do
                assert(x, "Invalid argument")
                args[#args +1] = template.evaluate_expr(x, vars)
            end
            return fn(unpack(args))
        end
    elseif expr.type == "list" then
        local out = {}
        for i, x in ipairs(expr.value) do
            assert(x, "Invalid list item")
            out[i] = template.evaluate_expr(x, vars)
        end
        return out
    else
        return expr.value or false
    end
end


local S, PS = minetest.get_translator()
local env = {
    S = S,
    translate = S,
    PS = S,
    translate_n = PS,
    fe = fe,
    formspec_escape = fe,
    hte = hte,
    hypertext_escape = hte,
    DEFAULT_ASSETS = assets,
    floor = math.floor,
    ceil = math.ceil,
    round = math.round,
    min = math.min,
    max = math.max,
    -- Convert arguments to a formspec-safe representation that can be evaluated in on_reveive_fields
    action = function(type, ...)
        local args = {...}
        if type == "set" then
            local name = args[1]
            return ".set_|"..fe(name).."|_to_"..fe(minetest.write_json(args[2]))
        end
        return out
    end,
    cycle = function(idx, ...)
        local options = {...}
        return options[((idx -1) %#options) +1]
    end,
    upper = function(str)
        return string.upper(str)
    end,
    len = function(x)
        return #x
    end,
    range = function(min, max, step)
        local out = {}
        if not step then step = 1 end
        for i = min, max, step do
            out[#out +1] = i
        end
        return out
    end
}

function template.evaluate(dialog, vars, depth)
    -- Ensure that variables from `env` remain accessible even when `vars` has its own metatable set.
    local out
    if not depth then
        local real_vars = vars
        vars = setmetatable({}, {
            __index = function(tbl, k)
                return real_vars[k] or env[k]
            end
        })
        depth = 0
        out = dialog.root and {template.evaluate(dialog.root, vars, depth +1)} or {}
    else
        out = {}
    end
    for _, item in ipairs(dialog) do
        if type(item) == "string" then
            out[#out +1] = item
        else
            if item.type == "if" then
                for _, cond in ipairs(item.conditions) do
                    if template.evaluate_expr(cond.condition, vars) then
                        out[#out +1] = template.evaluate(cond.body, vars, depth +1)
                        break
                    end
                end
            elseif item.type == "for" then
                local iterable = template.evaluate_expr(item.iterate, vars)
                if type(iterable) == "table" then
                    for i, entry in ipairs(iterable) do
                        local vars2 = {
                            index = i,
                            index0 = i -1,
                            length = #iterable,
                            first = i == 1,
                            depth = 1,
                            previtem = iterable[i -1],
                            nextitem = iterable[i +1],
                            cycle = function(...)
                                return env.cycle(i, ...)
                            end
                        }
                        vars2.last = i == vars2.length
                        vars2.revindex = vars2.length -vars2.index
                        vars2.revindex0 = vars2.length -vars2.index0
                        if vars.loop and vars.loop.depth then
                            vars.depth = vars.loop.depth +1
                        end
                        -- Save the original values of these variables, to simulate a closure.
                        local prev_var = vars[item.varname]
                        local prev_loop = vars.loop
                        vars[item.varname] = entry
                        vars.loop = vars2
                        out[#out +1] = template.evaluate(item.body, vars, depth +1)
                        -- Restore the variables we set to their original values.
                        vars[item.varname] = prev_var
                        vars.loop = prev_loop
                    end
                end
            elseif item.type == "set" then
                vars[item.name] = template.evaluate_expr(item.value, vars)
            elseif item.type == "set_block" then
                vars[item.name] = template.evaluate(item.value, vars, depth +1)
            elseif item.type == "macro" then
                vars[item.name] = function(...)
                    local argv = {...}
                    local args = {}
                    for i, x in ipairs(argv) do
                        args[item.params[i]] = x
                    end
                    return template.evaluate(item.body, setmetatable(args, {__index = vars}), depth +1)
                end
            else
                out[#out + 1] = tostring(template.evaluate_expr(item, vars))
            end
        end
    end
    return table.concat(out)
end


function template.test()
--    local profile = require("jit.profile")
    
--    local profile_data = {}
--    local function profile_callback(thread, samples, vmstate)
--        profile_data[#profile_data +1] = {profile.dumpstack(thread, "pF;", -100), vmstate,
--                        " ", samples, "\n"}
--    end

    -- Start profiling
--    profile.start("li0.1", profile_callback)
    
    local ast = template.build [[
            {% macro field(x, y, w, h, name, value) %}
                image[{{ x }},{{ y }};{{ w }},{{ h }};{{ DEFAULT_ASSETS }}btn_bg_2_light.png;8,8]
                field[{{ x + 0.1 }},{{ y }};{{ w - 0.2 }},{{ h }};{{ name }};;{{ value }}]
            {% endmacro %}
            {% view main %}
            
            {% if selected_world %}
                {% set list_width = WIDTH * 0.3 %}
            {% else %}
                {% set list_width = WIDTH * 0.8 %}
            {% endif %}
            
            {{ field(2, 2, 4, 0.75, 'test', '') }}
            
            {% set col = 12 % 3 %}
            {{ col > 0 and 'yes' or 'no' }}
            
            {{ action("set", "q", [1, 2, 3]) }}
            
            scroll_container[0,0;{{ foo }},{{ foo }};worldscroll;vertical;;1,0]
            
            {#
            image[{{ WIDTH * 0.1 - 0.1 }},{{ HEIGHT * 0.1 - 0.1}};{{ WIDTH * 0.8 + 0.2 }},{{ HEIGHT * 0.8 + 0.2 }};{{ DEFAULT_ASSETS }}bg_translucent.png;8,8]
            scroll_container[{{ WIDTH * 0.1 }},{{ HEIGHT * 0.1 }};{{ list_width }},{{ HEIGHT * 0.8 }};worldscroll;vertical;;0,0]
            {% for world in get_worlds() %}
                style[.select_world_{{ world.path }};bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
                style[.select_world_{{ world.path }}:hovered;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
                style[.select_world_{{ world.path }}:hovered+focused;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
                
                button[0,{{ loop.index0 * 0.5}};{{ list_width }},0.5;.select_world_{{ world.path }};{{ world.name }}]
            {% endfor %}
            scroll_container_end[]
            
            {% if selected_world %}
                box[{{ WIDTH * 0.4 - 0.05 }},{{ HEIGHT * 0.1 - 0.1 }};0.1,{{ HEIGHT * 0.8 + 0.2 }};#292d2fff]
                scroll_container[{{ WIDTH * 0.4 + 0.05 }},{{ HEIGHT * 0.1 }};{{ WIDTH * 0.8 - 0.05 }},{{ HEIGHT * 0.8 }};worlconfigscroll;vertical;;0,0]
                {% set j = 0 %}
                
                {% if setting_damage %}
                    {% if damage_enabled %}
                        image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_filled.png;.set_damage_enabled_to_false;]
                    {% else %}
                        image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_empty.png;.set_damage_enabled_to_true;]
                    {% endif %}
                    label[0.5,{{ j + 0.25 }};Damage]
                    {% set j = j + 0.5 %}
                {% endif %}
                
                {% if setting_creative %}
                    {% if creative_enabled %}
                        image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_filled.png;.set_creative_enabled_to_false;]
                    {% else %}
                        image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_empty.png;.set_creative_enabled_to_true;]
                    {% endif %}
                    label[0.5,{{ j + 0.25 }};Creative]
                    {% set j = j + 0.5 %}
                {% endif %}
                
                {% set play_str = "Play" %}
                
                {% if setting_server %}
                    {% if server_enabled %}
                        image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_filled.png;.set_server_enabled_to_false;]
                        {% set play_str = "Host server" %}
                    {% else %}
                        image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_empty.png;.set_server_enabled_to_true;]
                        {% set play_str = "Play" %}
                    {% endif %}

                    label[0.5,{{ j + 0.25 }};Server]
                    {% set j = j + 0.5 %}
                {% endif %}
                
                {% set j = j + 1 %}

                button[{{ WIDTH * 0.05 }},{{ j }};{{ WIDTH * 0.4 }},0.75;.overlay_view_modconfig;Configure mods]
                button[{{ WIDTH * 0.05 }},{{ j + 1 }};{{ WIDTH * 0.4 }},0.75;.play;{{ play_str }}]
                
                scroll_container_end[]
                scrollbar[-800,6;0,2;vertical;worldconfigscroll;]
            {% endif %}
            scrollbaroptions[arrows=hide]
            scrollbar[-800,6;0,2;vertical;worldscroll;]
            
            {% endview %}
            {% view modconfig %}
            
            image[{{ WIDTH * 0.05 - 0.1 }},{{ HEIGHT * 0.05 - 0.1 }};{{ WIDTH * 0.4 + 0.2 }},{{ HEIGHT * 0.9 - 0.75 }};{{ DEFAULT_ASSETS }}btn_bg.png;8,8]
            hypertext[{{ WIDTH * 0.05 }},{{ HEIGHT * 0.05 }};{{ WIDTH * 0.4 }},0.75;;<global valign=middle halign=center><b>Available mods</b>]
            scroll_container[{{ WIDTH * 0.05 }},{{ HEIGHT * 0.05 + 0.75 }};{{ WIDTH * 0.4 }},{{ HEIGHT * 0.9 - 1.7 }};modsscroll;vertical;;0,0]
            {% for mod in get_mods() %}
                style[.select_mod_{{ mod.name }};bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
                style[.select_mod_{{ mod.name }}:hovered;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
                style[.select_mod_{{ mod.name }}:hovered+focused;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
                button[0,{{ loop.index0 * 0.5 }};{{ WIDTH * 0.4 }},0.5;.select_mod_{{ mod.name }};{{ mod.name }}]
            {% endfor %}
            scroll_container_end[]
            scrollbar[-800,6;0,2;vertical;modsscroll;]
            
            image[{{ WIDTH * 0.55 - 0.1 }},{{ HEIGHT * 0.05 - 0.1 }};{{ WIDTH * 0.4 + 0.2 }},{{ HEIGHT * 0.9 - 0.75 }};{{ DEFAULT_ASSETS }}btn_bg.png;8,8]
            hypertext[{{ WIDTH * 0.55 }},{{ HEIGHT * 0.05 }};{{ WIDTH * 0.4 }},0.75;;<global valign=middle halign=center><b>Enabled mods</b>]
            scroll_container[{{ WIDTH * 0.55 }},{{ HEIGHT * 0.05 + 0.75 }};{{ WIDTH * 0.4 }},{{ HEIGHT * 0.9 - 1.7 }};worldmodsscroll;vertical;;0,0]
            {% for mod in get_world_mods() %}
                {% if not mod.game_provided %}
                    style[.select_mod_{{ mod.name }};bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
                    style[.select_mod_{{ mod.name }}:hovered;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
                    style[.select_mod_{{ mod.name }}:hovered+focused;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
                    button[0,{{ loop.index0 * 0.5 }};{{ WIDTH * 0.4 }},0.5;.select_mod_{{ mod.name }};{{ mod.name }}]
                {% endif %}
            {% endfor %}
            scroll_container_end[]
            scrollbar[-800,6;0,2;vertical;worldmodsscroll;]
            
            image_button[{{ WIDTH * 0.45 }},{{ HEIGHT * 0.5 - WIDTH * 0.1 }};{{ WIDTH * 0.1 }},{{ WIDTH * 0.1 }};{{ DEFAULT_ASSETS }}arrow_right.png;.add_mod_to_world;]
            image_button[{{ WIDTH * 0.45 }},{{ HEIGHT * 0.5 }};{{ WIDTH * 0.1 }},{{ WIDTH * 0.1 }};{{ DEFAULT_ASSETS }}arrow_left.png;.remove_mod_from_world;]
            tooltip[.add_mod_to_world;Add mod to world;#444;#aaa]
            tooltip[.remove_mod_from_world;Remove mod from world;#444;#aaa]
            #}
            
            button[{{ WIDTH * 0.1 }},{{ HEIGHT * 0.95 - 0.75 }};{{ WIDTH * 0.4 }},0.75;.unoverlay_view;Cancel]
            button[{{ WIDTH * 0.5 }},{{ HEIGHT * 0.95 - 0.75 }};{{ WIDTH * 0.4 }},0.75;.unoverlay_view;Confirm]
            
            {% endview %}
            {% view addworld %}
            
            {% endview %}
    ]]
    
--    print(dump(ast, "  "))
    
    print(template.evaluate(ast.main, setmetatable({z = false, bar = true, foo = 3, blah = "test"}, {__index = {q = "hello"}})))
    
--    profile.stop()
--    
--    print("Profiler data: "..dump(profile_data, "  "))
--    
--    -- Analyze results
--    for stack, samples in pairs(profile_data) do
--        print(string.format("%s: %d samples", stack, samples))
--    end
end

local time = os.clock()
--for _ = 1, 1000 do
    template.test()
--end
print("Execution finished in: "..(os.clock() -time))
