--
-- fs51 - Compatibility layer for Minetest formspecs
--
-- Copyright © 2021 by luk3yx.
--

local fixers = ...
local get_player_information, type = minetest.get_player_information, type
local function remove_hypertext(text)
    -- If the text doesn't contain backslashes use gsub for performance
    if not text:find('\\', 1, true) then
        return text:gsub('<[^>]+>', '')
    end

    -- Otherwise iterate over it
    local res = ''
    local escaping, ignoring
    for i = 1, #text do
        local char = text:sub(i, i)
        if ignoring then
            ignoring = char ~= '>'
        elseif escaping then
            res = res .. char
            escaping = false
        elseif char == '<' then
            ignoring = true
        elseif char == '\\' then
            escaping = true
        else
            res = res .. char
        end
    end
    return res
end

-- Backport index_event by modifying the dropdown items so that they all start
-- with \x1b(fs51@idx_<N>). This is then parsed out here if the player has been
-- shown any formspec with a dropdown that has index_event. The extra check is
-- done to prevent trying to parse every single field for every single player.
local dropdown_hack_enabled = {}
minetest.after(0, minetest.register_on_player_receive_fields,
        function(player, _, fields)
    if dropdown_hack_enabled[player:get_player_name()] then
        local to_update = {}
        for field, raw_value in pairs(fields) do
            if field:sub(1, 6) == "\1fs51\1" then
                to_update[field] = raw_value:match("^\27%(fs51@idx_([0-9]+)%)")
            end
        end

        for field, value in pairs(to_update) do
            fields[field] = nil
            fields[field:sub(7)] = value
        end
    end
end)

minetest.register_on_leaveplayer(function(player)
    dropdown_hack_enabled[player:get_player_name()] = nil
end)

local function backport_for(name, formspec)
    local info = get_player_information(name)
    local formspec_version = info and info.formspec_version or 1
    if formspec_version >= 4 then return formspec end

    local tree, err = formspec_ast.parse(formspec)
    if not tree then
        minetest.log('warning', '[fs51] Error parsing formspec (in ' ..
            'monkey_patching.lua): ' .. tostring(err))
        return formspec
    end

    -- Add some placeholders
    local modified
    for node in formspec_ast.walk(tree) do
        local node_type = node.type
        if node_type == "dropdown" and node.index_event and node.items then
            -- Enable the dropdown hack for this player
            dropdown_hack_enabled[name] = true

            modified = true
            node.name = "\1fs51\1" .. node.name
            for i, item in ipairs(node.items) do
                node.items[i] = "\27(fs51@idx_" .. i .. ")" .. item
            end
            node.index_event = nil
        elseif formspec_version == 3 then  -- luacheck: ignore 542
            -- Don't do anything else
        elseif formspec_version == 1 and node_type == 'background9' then
            -- No need to set modified here
            node.type = 'background'
            node.middle_x, node.middle_y = nil, nil
            node.middle_x2, node.middle_y2 = nil, nil
        elseif node_type == 'animated_image' then
            modified = true
            node.type = 'image'
            local frame_start = node.frame_start or 1
            node.texture_name = ('(%s)^[verticalframe:%d:%d'):format(
                node.texture_name, node.frame_count, frame_start - 1)
        elseif node_type == 'model' and node.textures[1] then
            modified = true
            node.type = 'image'
            node.texture_name = node.textures[1]
        elseif node_type == 'hypertext' then
            -- Convert hypertext elements to regular textareas
            modified = true
            node.type = 'textarea'
            node.name = ''
            node.label = ''
            node.default = remove_hypertext(node.text)
            node.text = nil
        elseif node_type == 'scroll_container' then
            modified = true
            node.type = 'container'
            -- Scroll containers are always going to be broken on older clients
            for i = #node, 1, -1 do
                local inner_node = node[i]
                if inner_node.x and inner_node.y and
                        (inner_node.x >= node.w or inner_node.y >= node.h) then
                    table.remove(node, i)
                end
            end
        elseif formspec_version == 1 and node_type == 'tabheader' then
            node.w, node.h = nil, nil
        elseif formspec_version == 2 and node_type == 'bgcolor' then
            modified = true
            fixers.bgcolor(node)
        end
    end

    if formspec_version == 1 then
        modified = true
        tree = fs51.backport(tree)
    end

    if modified then
        return assert(formspec_ast.unparse(tree))
    end
    return formspec
end

-- Patch minetest.show_formspec()
local show_formspec = minetest.show_formspec
function minetest.show_formspec(pname, formname, formspec)
    return show_formspec(pname, formname, backport_for(pname, formspec))
end

-- Patch player:set_inventory_formspec()
local old_set_inventory_formspec
local function new_set_inventory_formspec(self, formspec, ...)
    return old_set_inventory_formspec(self,
        backport_for(self:get_player_name(), formspec), ...)
end

minetest.register_on_joinplayer(function(player)
    if old_set_inventory_formspec == nil then
        assert(type(player) == 'userdata', 'Fake player object?')
        local cls = getmetatable(player)
        old_set_inventory_formspec = cls.set_inventory_formspec
        cls.set_inventory_formspec = new_set_inventory_formspec

        -- In case the inventory formspec has been set in the meantime
        player:set_inventory_formspec(player:get_inventory_formspec())
    end
end)

if minetest.settings:get_bool('fs51.disable_meta_override') then
    return
end

-- Patch minetest.get_meta()
-- Inspired by https://gitlab.com/sztest/nodecore/-/blob/master/mods/nc_api
local old_nodemeta_set_string
local function new_nodemeta_set_string(self, k, v)
    if k == 'formspec' and type(v) == 'string' then
        v = fs51.backport_string(v) or v
    end
    return old_nodemeta_set_string(self, k, v)
end

local get_meta = minetest.get_meta
function minetest.get_meta(...)
    local meta = get_meta(...)
    if old_nodemeta_set_string == nil and type(meta) == 'userdata' then
        minetest.get_meta = get_meta
        local cls = getmetatable(meta)
        old_nodemeta_set_string = cls.set_string
        cls.set_string = new_nodemeta_set_string
    end
    return meta
end
