-- From lua-resty-template (modified to remove external dependencies)
--[[
Copyright (c) 2014 - 2020 Aapo Talvensaari
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice, this
  list of conditions and the following disclaimer in the documentation and/or
  other materials provided with the distribution.

* Neither the name of the {organization} nor the names of its
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]]--

local setmetatable = setmetatable
local loadstring = loadstring
local tostring = tostring
local setfenv = setfenv
local require = require
local concat = table.concat
local assert = assert
local write = io.write
local pcall = pcall
local phase
local open = io.open
local load = load
local type = type
local dump = string.dump
local find = string.find
local gsub = string.gsub
local byte = string.byte
local null
local sub = string.sub
local var

local _VERSION = _VERSION
local _ENV = _ENV -- luacheck: globals _ENV
local _G = _G

local HTML_ENTITIES = {
    ["&"] = "&amp;",
    ["<"] = "&lt;",
    [">"] = "&gt;",
    ['"'] = "&quot;",
    ["'"] = "&#39;",
    ["/"] = "&#47;"
}

local CODE_ENTITIES = {
    ["{"] = "&#123;",
    ["}"] = "&#125;",
    ["&"] = "&amp;",
    ["<"] = "&lt;",
    [">"] = "&gt;",
    ['"'] = "&quot;",
    ["'"] = "&#39;",
    ["/"] = "&#47;"
}

local VAR_PHASES

local ESC    = byte("\27")
local NUL    = byte("\0")
local HT     = byte("\t")
local VT     = byte("\v")
local LF     = byte("\n")
local SOL    = byte("/")
local BSOL   = byte("\\")
local SP     = byte(" ")
local AST    = byte("*")
local NUM    = byte("#")
local LPAR   = byte("(")
local LSQB   = byte("[")
local LCUB   = byte("{")
local MINUS  = byte("-")
local PERCNT = byte("%")

local EMPTY  = ""

local VIEW_ENV
if _VERSION == "Lua 5.1" then
    VIEW_ENV = { __index = function(t, k)
        return t.context[k] or t.template[k] or _G[k]
    end }
else
    VIEW_ENV = { __index = function(t, k)
        return t.context[k] or t.template[k] or _ENV[k]
    end }
end

local newtab
do
    local ok
    ok, newtab = pcall(require, "table.new")
    if not ok then newtab = function() return {} end end
end

local function enabled(val)
    if val == nil then return true end
    return val == true or (val == "1" or val == "true" or val == "on")
end

local function trim(s)
    return gsub(gsub(s, "^%s+", EMPTY), "%s+$", EMPTY)
end

local function rpos(view, s)
    while s > 0 do
        local c = byte(view, s, s)
        if c == SP or c == HT or c == VT or c == NUL then
            s = s - 1
        else
            break
        end
    end
    return s
end

local function escaped(view, s)
    if s > 1 and byte(view, s - 1, s - 1) == BSOL then
        if s > 2 and byte(view, s - 2, s - 2) == BSOL then
            return false, 1
        else
            return true, 1
        end
    end
    return false, 0
end

local function read_file(path)
    local file, err = open(path, "rb")
    if not file then return nil, err end
    local content
    content, err = file:read "*a"
    file:close()
    return content, err
end

local function load_view(template)
    return function(view, plain)
	if plain == true then return view end
	local path, root = view, template.root
	if root and root ~= EMPTY then
	    if byte(root, -1) == SOL then root = sub(root, 1, -2) end
	    if byte(view,  1) == SOL then path = sub(view, 2) end
	    path = root .. "/" .. path
	end
	return plain == false and assert(read_file(path)) or read_file(path) or view
    end
end

local function load_file(func)
    return function(view) return func(view, false) end
end

local function load_string(func)
    return function(view) return func(view, true) end
end

local function loader(template)
    return function(view)
	return assert(load(view, nil, nil, setmetatable({ template = template }, VIEW_ENV)))
    end
end

local function visit(visitors, content, tag, name)
    if not visitors then
        return content
    end

    for i = 1, visitors.n do
        content = visitors[i](content, tag, name)
    end

    return content
end

local function new(template, safe)
    template = template or newtab(0, 26)

    template._VERSION    = "2.0"
    template.cache       = {}
    template.load        = load_view(template)
    template.load_file   = load_file(template.load)
    template.load_string = load_string(template.load)
    template.print       = write

    local load_chunk = loader(template)

    local caching
    if VAR_PHASES and VAR_PHASES[phase()] then
        caching = enabled(var.template_cache)
    else
        caching = true
    end

    local visitors
    function template.visit(func)
        if not visitors then
            visitors = { func, n = 1 }
            return
        end
        visitors.n = visitors.n + 1
        visitors[visitors.n] = func
    end

    function template.caching(enable)
        if enable ~= nil then caching = enable == true end
        return caching
    end

    function template.output(s)
        if s == nil or s == null then return EMPTY end
        if type(s) == "function" then return template.output(s()) end
        return tostring(s)
    end

    function template.escape(s, c)
        if type(s) == "string" then
            if c then return gsub(s, "[}{\">/<'&]", CODE_ENTITIES) end
            return gsub(s, "[\">/<'&]", HTML_ENTITIES)
        end
        return template.output(s)
    end

    function template.new(view, layout)
        local vt = type(view)

        if vt == "boolean" then return new(nil,  view) end
        if vt == "table"   then return new(view, safe) end
        if vt == "nil"     then return new(nil,  safe) end

        local render
        local process
        if layout then
            if type(layout) == "table" then
                render = function(self, context)
                    context = context or self
                    context.blocks = context.blocks or {}
                    context.view = template.process(view, context)
                    layout.blocks = context.blocks or {}
                    layout.view = context.view or EMPTY
                    layout:render()
                end
                process = function(self, context)
                    context = context or self
                    context.blocks = context.blocks or {}
                    context.view = template.process(view, context)
                    layout.blocks = context.blocks or {}
                    layout.view = context.view
                    return tostring(layout)
                end
            else
                render = function(self, context)
                    context = context or self
                    context.blocks = context.blocks or {}
                    context.view = template.process(view, context)
                    template.render(layout, context)
                end
                process = function(self, context)
                    context = context or self
                    context.blocks = context.blocks or {}
                    context.view = template.process(view, context)
                    return template.process(layout, context)
                end
            end
        else
            render = function(self, context)
                return template.render(view, context or self)
            end
            process = function(self, context)
                return template.process(view, context or self)
            end
        end

        if safe then
            return setmetatable({
                render = function(...)
                    local ok, err = pcall(render, ...)
                    if not ok then
                        return nil, err
                    end
                end,
                process = function(...)
                    local ok, output = pcall(process, ...)
                    if not ok then
                        return nil, output
                    end
                    return output
                end,
             }, {
                __tostring = function(...)
                    local ok, output = pcall(process, ...)
                    if not ok then
                        return ""
                    end
                    return output
            end })
        end

        return setmetatable({
            render = render,
            process = process
        }, {
            __tostring = process
        })
    end

    function template.precompile(view, path, strip, plain)
        local chunk = dump(template.compile(view, nil, plain), strip ~= false)
        if path then
            local file = open(path, "wb")
            file:write(chunk)
            file:close()
        end
        return chunk
    end

    function template.precompile_string(view, path, strip)
        return template.precompile(view, path, strip, true)
    end

    function template.precompile_file(view, path, strip)
        return template.precompile(view, path, strip, false)
    end

    function template.compile(view, cache_key, plain)
        assert(view, "view was not provided for template.compile(view, cache_key, plain)")
        if cache_key == "no-cache" then
            return load_chunk(template.parse(view, plain)), false
        end
        cache_key = cache_key or view
        local cache = template.cache
        if cache[cache_key] then return cache[cache_key], true end
        local func = load_chunk(template.parse(view, plain))
        if caching then cache[cache_key] = func end
        return func, false
    end

    function template.compile_file(view, cache_key)
        return template.compile(view, cache_key, false)
    end

    function template.compile_string(view, cache_key)
        return template.compile(view, cache_key, true)
    end

    function template.parse(view, plain)
        assert(view, "view was not provided for template.parse(view, plain)")
        if plain ~= true then
            view = template.load(view, plain)
            if byte(view, 1, 1) == ESC then return view end
        end
        local j = 2
        local c = {[[
context=... or {}
local ___,blocks,layout={},blocks or {}
local function include(v, c) return template.process(v, c or context) end
local function echo(...) for i=1,select("#", ...) do ___[#___+1] = tostring(select(i, ...)) end end
]] }
        local i, s = 1, find(view, "{", 1, true)
        while s do
            local t, p = byte(view, s + 1, s + 1), s + 2
            if t == LCUB then
                local e = find(view, "}}", p, true)
                if e then
                    local z, w = escaped(view, s)
                    if i < s - w then
                        c[j] = "___[#___+1]=[=[\n"
                        c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
                        c[j+2] = "]=]\n"
                        j=j+3
                    end
                    if z then
                        i = s
                    else
                        c[j] = "___[#___+1]=template.escape("
                        c[j+1] = visit(visitors, trim(sub(view, p, e - 1)), "{")
                        c[j+2] = ")\n"
                        j=j+3
                        s, i = e + 1, e + 2
                    end
                end
            elseif t == AST then
                local e = find(view, "*}", p, true)
                if e then
                    local z, w = escaped(view, s)
                    if i < s - w then
                        c[j] = "___[#___+1]=[=[\n"
                        c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
                        c[j+2] = "]=]\n"
                        j=j+3
                    end
                    if z then
                        i = s
                    else
                        c[j] = "___[#___+1]=template.output("
                        c[j+1] = visit(visitors, trim(sub(view, p, e - 1)), "*")
                        c[j+2] = ")\n"
                        j=j+3
                        s, i = e + 1, e + 2
                    end
                end
            elseif t == PERCNT then
                local e = find(view, "%}", p, true)
                if e then
                    local z, w = escaped(view, s)
                    if z then
                        if i < s - w then
                            c[j] = "___[#___+1]=[=[\n"
                            c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
                            c[j+2] = "]=]\n"
                            j=j+3
                        end
                        i = s
                    else
                        local n = e + 2
                        if byte(view, n, n) == LF then
                            n = n + 1
                        end
                        local r = rpos(view, s - 1)
                        if i <= r then
                            c[j] = "___[#___+1]=[=[\n"
                            c[j+1] = visit(visitors, sub(view, i, r))
                            c[j+2] = "]=]\n"
                            j=j+3
                        end
                        c[j] = visit(visitors, trim(sub(view, p, e - 1)), "%")
                        c[j+1] = "\n"
                        j=j+2
                        s, i = n - 1, n
                    end
                end
            elseif t == LPAR then
                local e = find(view, ")}", p, true)
                if e then
                    local z, w = escaped(view, s)
                    if i < s - w then
                        c[j] = "___[#___+1]=[=[\n"
                        c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
                        c[j+2] = "]=]\n"
                        j=j+3
                    end
                    if z then
                        i = s
                    else
                        local f = visit(visitors, sub(view, p, e - 1), "(")
                        local x = find(f, ",", 2, true)
                        if x then
                            c[j] = "___[#___+1]=include([=["
                            c[j+1] = trim(sub(f, 1, x - 1))
                            c[j+2] = "]=],"
                            c[j+3] = trim(sub(f, x + 1))
                            c[j+4] = ")\n"
                            j=j+5
                        else
                            c[j] = "___[#___+1]=include([=["
                            c[j+1] = trim(f)
                            c[j+2] = "]=])\n"
                            j=j+3
                        end
                        s, i = e + 1, e + 2
                    end
                end
            elseif t == LSQB then
                local e = find(view, "]}", p, true)
                if e then
                    local z, w = escaped(view, s)
                    if i < s - w then
                        c[j] = "___[#___+1]=[=[\n"
                        c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
                        c[j+2] = "]=]\n"
                        j=j+3
                    end
                    if z then
                        i = s
                    else
                        c[j] = "___[#___+1]=include("
                        c[j+1] = visit(visitors, trim(sub(view, p, e - 1)), "[")
                        c[j+2] = ")\n"
                        j=j+3
                        s, i = e + 1, e + 2
                    end
                end
            elseif t == MINUS then
                local e = find(view, "-}", p, true)
                if e then
                    local x, y = find(view, sub(view, s, e + 1), e + 2, true)
                    if x then
                        local z, w = escaped(view, s)
                        if z then
                            if i < s - w then
                                c[j] = "___[#___+1]=[=[\n"
                                c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
                                c[j+2] = "]=]\n"
                                j=j+3
                            end
                            i = s
                        else
                            y = y + 1
                            x = x - 1
                            if byte(view, y, y) == LF then
                                y = y + 1
                            end
                            local b = trim(sub(view, p, e - 1))
                            if b == "verbatim" or b == "raw" then
                                if i < s - w then
                                    c[j] = "___[#___+1]=[=[\n"
                                    c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
                                    c[j+2] = "]=]\n"
                                    j=j+3
                                end
                                c[j] = "___[#___+1]=[=["
                                c[j+1] = visit(visitors, sub(view, e + 2, x))
                                c[j+2] = "]=]\n"
                                j=j+3
                            else
                                if byte(view, x, x) == LF then
                                    x = x - 1
                                end
                                local r = rpos(view, s - 1)
                                if i <= r then
                                    c[j] = "___[#___+1]=[=[\n"
                                    c[j+1] = visit(visitors, sub(view, i, r))
                                    c[j+2] = "]=]\n"
                                    j=j+3
                                end
                                c[j] = 'blocks["'
                                c[j+1] = b
                                c[j+2] = '"]=include[=['
                                c[j+3] = visit(visitors, sub(view, e + 2, x), "-", b)
                                c[j+4] = "]=]\n"
                                j=j+5
                            end
                            s, i = y - 1, y
                        end
                    end
                end
            elseif t == NUM then
                local e = find(view, "#}", p, true)
                if e then
                    local z, w = escaped(view, s)
                    if i < s - w then
                        c[j] = "___[#___+1]=[=[\n"
                        c[j+1] = visit(visitors, sub(view, i, s - 1 - w))
                        c[j+2] = "]=]\n"
                        j=j+3
                    end
                    if z then
                        i = s
                    else
                        e = e + 2
                        if byte(view, e, e) == LF then
                            e = e + 1
                        end
                        s, i = e - 1, e
                    end
                end
            end
            s = find(view, "{", s + 1, true)
        end
        s = sub(view, i)
        if s and s ~= EMPTY then
            c[j] = "___[#___+1]=[=[\n"
            c[j+1] = visit(visitors, s)
            c[j+2] = "]=]\n"
            j=j+3
        end
        c[j] = "return layout and include(layout,setmetatable({view=table.concat(___),blocks=blocks},{__index=context})) or table.concat(___)" -- luacheck: ignore
        return concat(c)
    end

    function template.parse_file(view)
        return template.parse(view, false)
    end

    function template.parse_string(view)
        return template.parse(view, true)
    end

    function template.process(view, context, cache_key, plain)
        assert(view, "view was not provided for template.process(view, context, cache_key, plain)")
        return template.compile(view, cache_key, plain)(context)
    end

    function template.process_file(view, context, cache_key)
        assert(view, "view was not provided for template.process_file(view, context, cache_key)")
        return template.compile(view, cache_key, false)(context)
    end

    function template.process_string(view, context, cache_key)
        assert(view, "view was not provided for template.process_string(view, context, cache_key)")
        return template.compile(view, cache_key, true)(context)
    end

    function template.render(view, context, cache_key, plain)
        assert(view, "view was not provided for template.render(view, context, cache_key, plain)")
        template.print(template.process(view, context, cache_key, plain))
    end

    function template.render_file(view, context, cache_key)
        assert(view, "view was not provided for template.render_file(view, context, cache_key)")
        template.render(view, context, cache_key, false)
    end

    function template.render_string(view, context, cache_key)
        assert(view, "view was not provided for template.render_string(view, context, cache_key)")
        template.render(view, context, cache_key, true)
    end

    if safe then
        return setmetatable({}, {
            __index = function(_, k)
                if type(template[k]) == "function" then
                    return function(...)
                        local ok, a, b = pcall(template[k], ...)
                        if not ok then
                            return nil, a
                        end
                        return a, b
                    end
                end
                return template[k]
            end,
            __new_index = function(_, k, v)
                template[k] = v
            end,
        })
    end

    return template
end

return new()
