gamedata = {
    
}

local http = minetest.get_http_api()

-- Used for brevity when making fullscreen things
window = minetest.get_window_info()
size = window.max_formspec_size
dpi = {
    x = window.size.x /size.x,
    y = window.size.y /size.y
}

-- Default textures
default_textures = minetest.get_texturepath_share().."/base/pack/"

-- FIXME: Assets
assets = os.getenv("HOME").."/eclipse-workspace/mods/mtmenu/"

local version = minetest.get_version()

-- This is where all view-specific state information goes.
state = {}

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

function include(file)
    -- FIXME: Use get_builtin_path()
    dofile(os.getenv("HOME").."/eclipse-workspace/mods/mtmenu/"..file)
end

dofile(minetest.get_builtin_path().."common/settings/settingtypes.lua")


-- Theme

theme = {
    text_color = "#aaa",
    muted_text_color = "#777",
    code_color = "#da8",
    link_color = "#8ad",
    mod_label_color = "#888",
    game_label_color = "#888",
    texture_pack_label_color = "#888",
    
    bg = "",
    
    modal_bg = "#0008",
    
    syntax_keyword = "#d8d",
    syntax_keyword_value = "#79e",
    syntax_identifier = "#adf",
    syntax_operator = "#f66",
    syntax_string = "#da8",
    syntax_number = "#cdb",
    syntax_comment = "#777",
    syntax_placeholder = "#ccc",
}

--End theme


---[[

minetest.set_formspec_prepend("\
    style[*;textcolor=#aaa]\
    style_type[field;border=false]\
    style_type[pwdfield;border=false]\
    style_type[image_button;border=false]\
    style_type[button;border=false;bgimg="..assets.."btn_bg_2.png;bgimg_middle=8,8]\
    style_type[button:hovered;border=false;bgimg="..assets.."btn_bg_2_hover.png;bgimg_middle=8,8]\
    style[nobg,nobg:hovered,nobg:focused,nobg:hovered+focused;border=false;bgimg="..default_textures.."blank.png;bgimg_middle=0;content_offset=0,0]\
")

local meta_header = "formspec_version[8]\
        size["..size.x..","..size.y.."]\
        padding[0,0]\
        bgcolor[#000;true;#151618]\
        style_type[box;bordercolors=#124722;borderwidths=-5;colors=#151618]\
        image[0,"..(size.y *0.08)..");"..size.x..","..(size.x *(72/672))..";"..default_textures.."menu_header.png]\
        "
--        box[0,0;"..size.x..","..size.y..";]\

local game_header = "formspec_version[8]\
        size["..size.x..","..size.y.."]\
        padding[0,0]\
        bgcolor[#0000;true;#0000]\
        "
local servers_header = "formspec_version[8]\
        size["..size.x..","..size.y.."]\
        padding[0,0]\
        bgcolor[#0000;true;#151618]\
        "

local content_header = "formspec_version[8]\
        size["..size.x..","..size.y.."]\
        padding[0,0]\
        bgcolor[#0000;true;#151618]\
        "

local default_game_menu = [[
    {% 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 %}
    
    {% set worlds = get_worlds() %}
        
    {# Special layouts are applied when there are less than four worlds for a given game. #}
    {% if len(worlds) == 1 %}

        image[{{ WIDTH * 0.5 - 2 }},{{ HEIGHT * 0.5 - 2 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
        image_button[{{ WIDTH * 0.5 - 1.5 }},{{ HEIGHT * 0.5 - 1.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[1]) }};]
        button[{{ WIDTH * 0.5 - 2 }},{{ HEIGHT * 0.5 + 1 }};4,0.75;nobg;{{ worlds[1].name }}]
    
    {% elseif len(worlds) == 2 %}
    
        image[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 - 2 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
        image_button[{{ WIDTH * 0.5 - 4 }},{{ HEIGHT * 0.5 - 1.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[1]) }};]
        button[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 1 }};4,0.75;nobg;{{ worlds[1].name }}]
        
        image[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 - 2 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
        image_button[{{ WIDTH * 0.5 + 1 }},{{ HEIGHT * 0.5 - 1.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[2]) }};]
        button[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 1 }};4,0.75;nobg;{{ worlds[2].name }}]
    
    {% elseif len(worlds) == 3 %}
    
        image[{{ WIDTH * 0.5 - 2 }},{{ HEIGHT * 0.5 - 4.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
        image_button[{{ WIDTH * 0.5 - 1.5 }},{{ HEIGHT * 0.5 - 4.25 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[1]) }};]
        button[{{ WIDTH * 0.5 - 2 }},{{ HEIGHT * 0.5 - 1.5 }};4,0.75;nobg;{{ worlds[1].name }}]
    
        image[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 0.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
        image_button[{{ WIDTH * 0.5 - 4 }},{{ HEIGHT * 0.5 + 0.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[2]) }};]
        button[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 3.5 }};4,0.75;nobg;{{ worlds[2].name }}]
        
        image[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 0.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
        image_button[{{ WIDTH * 0.5 + 1 }},{{ HEIGHT * 0.5 + 0.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[3]) }};]
        button[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 3.5 }};4,0.75;nobg;{{ worlds[3].name }}]

    {% elseif len(worlds) == 4 %}

        image[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 - 4.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
    image_button[{{ WIDTH * 0.5 - 4 }},{{ HEIGHT * 0.5 - 4.25 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[1]) }};]
        button[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 - 1.5 }};4,0.75;nobg;{{ worlds[1].name }}]
        
        image[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 - 4.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
        image_button[{{ WIDTH * 0.5 + 1 }},{{ HEIGHT * 0.5 - 4.25 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[2]) }};]
        button[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 - 1.5 }};4,0.75;nobg;{{ worlds[2].name }}]

        image[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 0.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
        image_button[{{ WIDTH * 0.5 - 4 }},{{ HEIGHT * 0.5 + 0.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[3]) }};]
        button[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 3.5 }};4,0.75;nobg;{{ worlds[3].name }}]
        
        image[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 0.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
        image_button[{{ WIDTH * 0.5 + 1 }},{{ HEIGHT * 0.5 + 0.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[4]) }};]
        button[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 3.5 }};4,0.75;nobg;{{ worlds[4].name }}]

    {% else %}
        scroll_container[0,0;{{ WIDTH }},{{ HEIGHT }};worldscroll;vertical;;1,0]
        {% set cols = floor((WIDTH - 2) / 5) %}
        {% set trailing_cols = len(worlds) % cols %}
        {% set rows = ceil(len(worlds) / cols) %}
        {% set col = 0 %}
        {% set row = 0 %}
        {% set rx = cols * 2.5 %}
        {% set ry = min(rows * 2.5, (HEIGHT - 2) / 2) %}
        {% for world in worlds %}
            
            image[{{ WIDTH * 0.5 - rx + col * 5 }},{{ HEIGHT * 0.5 - ry + row * 5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
            image_button[{{ WIDTH * 0.5 - rx + col * 5 + 0.5 }},{{ HEIGHT * 0.5 - ry + row * 5 + 0.25 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', world) }};]
            button[{{ WIDTH * 0.5 - rx + col * 5 }},{{ HEIGHT * 0.5 - ry + row * 5 + 3 }};4,0.75;nobg;{{ fe(world.name) }}]
            
            {% set col = col + 1 %}
            {% if col > cols - 1 %}
                {% set col = 0 %}
                {% set row = row + 1 %}
                {% if row == rows - 1 and trailing_cols > 0 %}
                    {% set rx = min(trailing_cols, cols) * 2.5 %}
                {% endif %}
            {% endif %}
            
        {% endfor %}
        scroll_container_end[]
        scrollbar[-800,0;0,0;vertical;worldscroll;]
    {% endif %}
    
    {% if selected_world %}
        box[0,0;{{ WIDTH }},{{ HEIGHT }};#0003]
        image[{{ WIDTH * 0.05 }},{{ HEIGHT * 0.05 }};{{ WIDTH * 0.9 }},{{ HEIGHT * 0.9 }};{{ DEFAULT_ASSETS }}btn_bg.png;8,8]
        label[1,1;Selected world: {{ fe(selected_world.name) }}]
        button[1,1;3,1;{{ action('set', 'selected_world', nil) }};Back]
    {% endif %}
    
    {#
    
    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 %}
]]


function core.on_before_close()
    --minetest.settings:write()
end


minetest.async_jobs = {}

local function handle_job(jobid, serialized_retval)
    local retval = minetest.deserialize(serialized_retval)
    assert(type(minetest.async_jobs[jobid]) == "function")
    minetest.async_jobs[jobid](retval)
    minetest.async_jobs[jobid] = nil
end

minetest.async_event_handler = handle_job

function minetest.handle_async(func, parameter, callback)
    -- Serialize parameters
    local serialized_param = minetest.serialize(parameter)

    if serialized_param == nil then
        return false
    end

    local jobid = minetest.do_async_callback(func, serialized_param)

    minetest.async_jobs[jobid] = callback

    return true
end


-- MARK: - Helpers

local function readable_content_type(type)
    if type == "game" then
        return "Game"
    elseif type == "mod" then
        return "Mod"
    elseif type == "modpack" then
        return "Mod collection"
    elseif type == "txp" then
        return "Texture pack"
    else
        return "Unknown"
    end
end

-- Returns the contents of the file, or nil in case of failure.
local function read_file(path)
    local f = io.open(path)
    if f then
        local data = f:read("a")
        f:close()
        if data == "" then return end
        return data
    end
end

-- Check if a file exists.
local function file_exists(path)
    local f = io.open(path)
    if f then
        f:close()
        return true
    end
    return false
end

-- Returns the get_games() entry corresponding to a given game ID.
local function get_game_info(game)
    for _, x in ipairs(minetest.get_games()) do
        if x.id == game then
            return x
        end
    end
end

local function get_world_index(path)
    for i, x in ipairs(minetest.get_worlds()) do
        if x.path == path then
            return i
        end
    end
    return -1
end

function get_worlds_for_game(id)
    local out = {}
    for _, x in ipairs(minetest.get_worlds()) do
        if x.gameid == id then
            out[#out +1] = x
        end
    end
    table.sort(out, function(a, b)
        return a.name < b.name
    end)
    return out
end

function get_mods_for_game(id)
    local out = {}
    for _, x in ipairs(get_all_mods()) do
        local conf = Settings(x.path.."mod.conf")
        local unsupported_games = conf:get("unsupported_games")
        if unsupported_games then
            if table.indexof(unsupported_games:trim():split(","), id) == -1 then
                out[#out +1] = x
            end
        else
            local supported_games = conf:get("supported_games")
            if supported_games then
                if table.indexof(supported_games:trim():split(","), id) ~= -1 then
                    out[#out +1] = x
                end
            else
                out[#out +1] = x
            end
        end
    end
    return out
end

local function get_game_mods(id)
    local out = {}
    local base = get_game_info(id).path.."/mods/"
    for _, x in pairs(minetest.get_dir_list(base, true)) do
        out[#out +1] = {
            name = x,
            path = base..x
        }
    end
    return out
end

function get_mods_for_world(world)
    local conf = Settings(world.."/world.mt")
    local mods = {}
    local game
    local game_mods = {}
    for k, v in pairs(conf:to_table()) do
        if k == "gameid" then
            game = v
        elseif k:sub(1, 9) == "load_mod_" then
            if v ~= "false" and v ~= "nil" and v then
                mods[k:sub(10)] = minetest.get_user_path().."/"..v
            end
        end
    end
    if game then
        for _, x in ipairs(get_game_mods(game)) do
            game_mods[x.name] = x.path
        end
    end
    local config = minetest.check_mod_configuration(world, mods)
--    print(dump(config))
    for _, x in ipairs(config.unsatisfied_mods) do
        x.unsatisfied = true
        config.satisfied_mods[#config.satisfied_mods +1] = x
    end
    if game then
        for _, x in ipairs(config.satisfied_mods) do
            if game_mods[x.name] then
                x.game_provided = true
            end
        end
    end
    return config.satisfied_mods
end

-- Returns a list of content available for download.
function get_available_content()
    local version = minetest.get_version()
    local cdb = minetest.settings:get("contentdb_url")
    local url = cdb.."/api/packages/?type=mod&type=game&type=txp&protocol_version="..minetest.get_max_supp_proto().."&engine_version="..minetest.urlencode(version.string)
    
    for _, x in pairs(minetest.settings:get("contentdb_flag_blacklist"):split(",")) do
        x = x:trim()
        if x ~= "" then
            url = url.."&hide="..minetest.urlencode(x)
        end
    end

    local languages
    local lang = minetest.get_language()
    if lang ~= "" then
        languages = { lang, "en;q=0.8" }
    else
        languages = { "en" }
    end

    local http = minetest.get_http_api()
    local response = http.fetch_sync({
        url = url,
        extra_headers = {
            "Accept-Language: "..table.concat(languages, ", ")
        },
    })
    if not response.succeeded then
        return
    end
    local items = minetest.parse_json(response.data)
    local out = {}
    for _, x in pairs(items) do
        out[#out +1] = x
    end
    return out
end

-- Returns a list of all the installed mods.
function get_all_mods(dir)
    local all = false
    if not dir then
        if state.all_mods then return state.all_mods end
        dir = minetest.get_modpath()
        all = true
    end
    local out = {}
    for _, x in ipairs(minetest.get_dir_list(dir, true)) do
        local info = minetest.get_content_info(dir.."/"..x)
        if info.type == "mod" or info.type == "modpack" then
            out[#out +1] = info
        end
    end
    if all then
        state.all_mods = out
    end
    return out
end

-- Returns a list of all the installed games.
function get_all_games(dir)
    local all = false
    if not dir then
        if state.all_games then return state.all_games end
        dir = minetest.get_gamepath()
        all = true
    end
    local out = {}
    for _, x in ipairs(minetest.get_dir_list(dir, true)) do
        local info = minetest.get_content_info(dir.."/"..x)
        if info.type == "game" then
            out[#out +1] = info
        end
    end
    if all then
        state.all_games = out
    end
    return out
end

-- Returns a list of all the installed texture packs.
function get_all_texture_packs(dir)
    local all = false
    if not dir then
        if state.all_texture_packs then return state.all_texture_packs end
        dir = minetest.get_texturepath()
        all = true
    end
    local out = {}
    for _, x in ipairs(minetest.get_dir_list(dir, true)) do
        local info = minetest.get_content_info(dir.."/"..x)
        if info.type == "txp" then
            out[#out +1] = info
        end
    end
    if all then
        state.all_texture_packs = out
    end
    return out
end

-- Return a list of all content, as a conglomeration of the lists of mods, games, and texture packs.
function get_all_content()
    if not state.all_content then
        state.all_content = {}
        table.insert_all(state.all_content, get_all_mods())
        table.insert_all(state.all_content, get_all_games())
        table.insert_all(state.all_content, get_all_texture_packs())
        table.sort(state.all_content, function(a, b)
            if not a then
                return b ~= nil
            elseif not b then
                return a == nil
            end
            return a.name < b.name
        end)
    end
    return state.all_content
end

-- Parses text as .conf format.
function parse_conf_text(txt)
    local out = {}
    local multiline = false
    local key = ""
    local value = ""
    for line in (txt.."\n"):gmatch("(.*)\n") do
        if multiline then
            if line:find("\"\"\"") then
                line = line:gsub("\"\"\"", ""):trim()
                multiline = false
                out[key] = value.."\n"..line
                key = ""
                value = ""
            else
                value = value.."\n"..line:trim()
            end
        else
            local k, v = line:match("(.-)=(.*)")
            if not k then break end
            if v:find("\"\"\"") then
                key = k:trim()
                value = v:gsub("\"\"\"", ""):trim()
                multiline = true
            else
                out[k:trim()] = v:trim()
            end
        end
    end
    return out
end

local function markdown_to_hypertext_format(md)
    return md:gsub("\n%s*####%s*([^\n]-)\n", "\n<b>%1</b>\n")
        :gsub("\n%s*###%s*([^\n]-)\n", "\n<big>%1</big>\n")
        :gsub("([^\n]-)\n%-+\n", "\n<big>%1</big>\n")
        :gsub("\n%s*##%s*([^\n]-)\n", "\n<bigger>%1</bigger>\n")
        :gsub("\n%s*#%s*([^\n]-)\n", "\n<style size=48>%1</style>\n")
        :gsub("%*%*([^`\n]-)%*%*", "<b>%1</b>")
        :gsub("%*([^`\n]-)%*", "<i>%1</i>")
        :gsub("__([^`\n]-)__", "<b>%1</b>")
        :gsub("\n_([^`\n]-)_", "<i>%1</i>")
        :gsub("%*%s+(.-)\n", "• %1\n")
--        :gsub("```%a-\n(.-)```", "<mono><style color="..theme.code_color..">%1</style></mono>")
--        :gsub("`([^`]-)`", "<mono><style color="..theme.code_color..">%1</style></mono>")
        -- Since we can't (and shouldn't) display images over HTTPS at all, simply get rid of them.
        :gsub("%[?!%[[^%]]-%]%([^\n]+%)", "")
end


local keywords = {
    "and", "break", "do", "else", "elseif", "end", --[["false",]] "for", "function",
    "goto", "if", "in", "local", --[["nil",]] "not", "or", "repeat", "return",
    "then", --[["true",]] "until", "while"
}
local keyword_set = {}
for _, kw in ipairs(keywords) do
    keyword_set[kw] = true
end

local keyword_value_set = {
    ["true"] = true,
    ["false"] = true,
    ["nil"] = true,
    ["math.huge"] = true
}

local function colorize(str, color)
    return "<style color="..(theme["syntax_"..color] or "#fff")..">"..str.."</style>"
end

local function syntax_highlight(code)
    local out = ""
    local pos = 1
    while pos <= #code do
        local matched = false

        -- Whitespace
        local ws_start, ws_end = code:find("^%s+", pos)
        if ws_start then
            out = out..colorize(code:sub(ws_start, ws_end), "whitespace")
            pos = ws_end + 1
            matched = true
        end

        -- Single-line comment
        if not matched then
            local com_start, com_end = code:find("^%-%-[^\n]*", pos)
            if com_start then
                out = out..colorize(code:sub(com_start, com_end), "comment")
                pos = com_end + 1
                matched = true
            end
        end

        -- String (double-quoted, simple escape handling)
        if not matched then
            local str_start, str_end = code:find('^"[^"]*"', pos)
            if str_start then
                out = out..colorize(code:sub(str_start, str_end), "string")
                pos = str_end + 1
                matched = true
            end
        end

        -- String (single-quoted, similar)
        if not matched then
            local str_start, str_end = code:find("^'[^']*", pos)
            if str_start then
                out = out..colorize(code:sub(str_start, str_end), "string")
                pos = str_end + 1
                matched = true
            end
        end

        -- Documentation placeholders. These aren't in Lua, but that means I
        -- can highlight them without worrying about confusing alternative meanings.
        if not matched then
            local str_start, str_end = code:find('^\\?<[^>]*>', pos)
            if str_start then
                out = out..colorize(code:sub(str_start, str_end), "placeholder")
                pos = str_end + 1
                matched = true
            end
        end

        -- Number (basic integer/float)
        if not matched then
            local num_start, num_end = code:find("^%d+%.?%d*[eE]?[+-]?%d*", pos)
            if num_start then
                out = out..colorize(code:sub(num_start, num_end), "number")
                pos = num_end + 1
                matched = true
            end
        end

        -- Identifier/Keyword
        if not matched then
            local id_start, id_end, id_text = code:find("^([%a_][%w_]*)", pos)
            if id_start then
                if keyword_set[id_text] then
                    out = out..colorize(id_text, "keyword")
                elseif keyword_value_set[id_text] then
                    out = out..colorize(id_text, "keyword_value")
                else
                    out = out..colorize(id_text, "identifier")
                end
                pos = id_end + 1
                matched = true
            end
        end

        -- Operators/Punctuation (catch-all for non-alphanumeric)
        if not matched then
            local op_start, op_end = code:find("^[^%s%a%d_\"'{}(),%][]", pos)
            if op_start then
                local op = code:sub(op_start, op_end)
                out = out..colorize(op, op == "-" and code:sub(op_end +1, op_end +2):find "^%d" and "number" or "operator")
                pos = op_end + 1
                matched = true
            end
        end

        -- If nothing matched, advance to avoid infinite loop (e.g., invalid char)
        if not matched then
            out = out..colorize(code:sub(pos, pos), "generic")
            pos = pos + 1
        end
    end
    return out
end

local function markdown_to_hypertext(md)
    md = ("\n"..hte(md or "").."\n")
    local out = ""
    -- The following is necessary because code blocks should not have formatting applied to their contents.
    local offset = 1
    while true do
        local block_start, block_end, lang, block = md:find("```(%a*)\n(.-)```", offset)
        local code_start, code_end, code = md:find("[^`]`([^`\n]+)`", offset)
        if not block_start and not code_start then break end
        if block_start and block_start < (code_start or math.huge) then
            -- Don't apply syntax highlighting to blocks annotated as a language other than Lua.
            -- However, assume that blocks with no annotation are probably Lua.
            if lang == "lua" then
                block = syntax_highlight(block)
            end
            -- The block:gsub hack is used so that leading whitespace (indentation) is not trimmed.
            out = out..markdown_to_hypertext_format(md:sub(offset, block_start -1)).."<mono><style color="..theme.code_color..">"..block:gsub("\n", "\n\u{a0}").."</style></mono>"
            offset = block_end +1
        end
        if code_start and code_start < (block_start or math.huge) then
            out = out..markdown_to_hypertext_format(md:sub(offset, code_start)).."<mono><style color="..theme.code_color..">"..code.."</style></mono>"
            offset = code_end +1
        end
    end
    out = out..markdown_to_hypertext_format(md:sub(offset))
--    md = md
--        :gsub("\n%s*####%s*([^\n]-)\n", "\n<b>%1</b>\n")
--        :gsub("\n%s*###%s*([^\n]-)\n", "\n<big>%1</big>\n")
--        :gsub("([^\n]-)\n%-+\n", "\n<big>%1</big>\n")
--        :gsub("\n%s*##%s*([^\n]-)\n", "\n<bigger>%1</bigger>\n")
--        :gsub("\n%s*#%s*([^\n]-)\n", "\n<style size=48>%1</style>\n")
--        :gsub("%*%*([^`\n]-)%*%*", "<b>%1</b>")
--        :gsub("%*([^`\n]-)%*", "<i>%1</i>")
--        :gsub("__([^`\n]-)__", "<b>%1</b>")
--        :gsub("\n_([^`\n]-)_", "<i>%1</i>")
--        :gsub("%*%s+(.-)\n", "• %1\n")
--        -- Since we can't (and shouldn't) display images over HTTPS at all, simply get rid of them.
--        :gsub("%[?!%[[^%]]-%]%([^\n]+%)", "")
--    print(md)
    return fe(out)
end


include "templates.lua"

--[[ MARK: - Stylesheet Parser
    The question of custom themes being what it is, and in order to avoid an
    unreasonably convoluted settings page, I've opted to allow users to declare
    themes simply by editing a stylesheet.
    
    Stylesheets are defined in basic toml format, like so:
    ```
    [button]
    
    color = #faa
    
    [label]
    
    color = #444
    
    [.table_even]
    
    bgcolor = #111
    ```
    
    Each section begins with a header (denoted by brackets), which specifies the type of element to apply the style to. Prefixing a header name with a dot will cause that header to match elements which are given a certain class.
    
    Rules take the form of `<property> = <value>`. Rules specified prior to any header will apply to all elements.
--]]

local function build_stylesheet_rules(rules)
    local out = {}
    local offset = 1
    while true do
        local a, b, key, value = rules:find("(%w+)%s*=%s*([%w%p]+)", offset)
        if not a then break end
        out[key] = value
        offset = b
    end
    return out
end

function build_stylesheet(sheet)
    -- Strip commetns
    sheet = sheet:gsub("%-%-.*\n", "")
    local out = {}
    local last_header
    local offset = 1
    while true do
        local a, b, header = sheet:find("%[%s*([%a._-]+)%s*%]", offset)
        if not a then break end
        out[last_header or "*"] = build_stylesheet_rules(sheet:sub(offset, a -1))
        last_header = header
        offset = b +1
    end
    out[last_header or "*"] = build_stylesheet_rules(sheet:sub(offset))
    return out
end

print(dump(build_stylesheet([[
    [button]
    
    color = #faa
    
    [label]
    
    color = #444
]])))

function evaluate_stylesheet()
    
end

-- MARK: - Views

-- The meta menu, for choosing which game menu to enter or entering the servers/content views.
local last_game = minetest.settings:get("menu_last_game")
function show_meta_menu(v)
    state.loc = "games"
    local games = minetest.get_games()
    if #games < 1 then
        minetest.update_formspec(meta_header.."\
        hypertext[0,0;"..size.x..","..size.y..";ht;<global valign=middle halign=center><tag name=action color=#222 hovercolor=#444>\
            <style size=72 color=#222><b>No games installed.</b></style>\
            <style size=36 color=#222><action name=content>Click here to install a game.</action></style>]\
        ")
        return
    end
    local idx = 1
    for i, x in ipairs(games) do
        if x.id == last_game then idx = i end
    end
    if v then idx = v else v = idx end
    local fs = ""
    
    
    fs = fs.."\
    scroll_container[0,0;"..size.x..","..size.y..";carousel;horizontal;;]\
    box[0,0;"..(#games *5)..",1;#0000]\
    "

    
    local center = (size.x /2) +(v /10)
        
    for i, x in ipairs(games) do
        fs = fs.."\
        image_button["..(center +(i -(#games /2)) *0.25)..","..(size.y -0.5)..";0.25,0.25;"..assets..(i == idx and "circle_light" or "circle")..".png;jump"..i..";]\
        "
        
        local dist = i -idx
        local scale = 4 /(math.abs(dist) +1)
        if scale > 0.1 then
            --This looks neat, but is useless for UI purposes: center +10^(1 /math.abs(dist)) *math.sign(dist)
            local lc = center +math.abs(dist *10)^0.55 *math.sign(dist)
            if x.menuicon_path == "" then
                x.menuicon_path = assets.."games.png"
            end
            --TODO: Do these need some kind of background?
            fs = fs.."\
                image_button["..(lc -(scale /2))..","..(size.y /2 -(scale /2) +(4 -scale) /2 +2)..";"..scale..","..scale..";"..fe(x.menuicon_path)..";game"..fe(x.id)..";]\
                tooltip[game"..fe(x.id)..";"..fe(x.title)..";#444;#aaa]\
                "
            if dist == 0 then
                fs = fs.."\
                button["..(lc -(scale /2))..","..(size.y /2 +4)..";4,0.5;nobg;"..fe(x.title).."]\
                "
            end
        end
    end
    
    
    fs = fs.."\
    scroll_container_end[]\
    scrollbaroptions[min=1;max="..#games..";smallstep=1]\
    scrollbar[0,-800;"..size.x..",0;horizontal;carousel;"..(v or "").."]\
    "
    
    minetest.update_formspec(meta_header..fs.."\
        style[show_servers_view;border=false;bgimg="..fe(assets).."menu_servers.png;bgimg_middle=0]\
        style[show_servers_view:hovered;border=false;bgimg="..fe(assets).."menu_servers_hovered.png;bgimg_middle=0]\
        button["..(size.x *0.05)..","..(size.y *0.3)..";4,4;show_servers_view;]\
        button["..(size.x *0.05)..","..(size.y *0.3 +4)..";4,0.5;nobg;Servers]\
        style[show_content_view;border=false;bgimg="..fe(assets).."menu_content.png;bgimg_middle=0]\
        style[show_content_view:hovered;border=false;bgimg="..fe(assets).."menu_content_hovered.png;bgimg_middle=0]\
        button["..(size.x -(size.x *0.05) -4)..","..(size.y *0.3)..";4,4;show_content_view;]\
        button["..(size.x -(size.x *0.05) -4)..","..(size.y *0.3 +4)..";4,0.5;nobg;Content]\
        style[show_settings_view,show_about_view;bgimg="..assets.."menu_tab_bg.png]\
        style[show_settings_view:hovered,show_about_view:hovered;bgimg="..assets.."menu_tab_bg.png;bgcolor=#fffd]\
        button[1,0;2,0.5;show_about_view;About]\
        button["..(size.x -3)..",0;2,0.5;show_settings_view;Settings]\
        ")
end

-- Shows games' custom menus.
function show_game_menu(args)
    if not state.current_game then
        minetest.log("warning", "Main menu attempted to show a menu for a nonexistent game; aborting.")
        return
    end
    if not args then args = {} end
    local game = state.current_game
    if not game.menu then
        local file = read_file(game.path.."/menu/mainmenu.txt") or default_game_menu
        
        game.disabled_settings = {}
        local settings = Settings(game.path.."/game.conf")
        
        local disabled_settings = string.split(settings:get("disabled_settings") or "", ",")
        for _, x in ipairs(disabled_settings) do
            x = x:trim()
            if x == "enable_damage" then
                game.disabled_settings.damage = false
            elseif x == "!enable_damage" then
                game.disabled_settings.damage = true
            elseif x == "creative_mode" then
                game.disabled_settings.creative = false
            elseif x == "!creative_mode" then
                game.disabled_settings.creative = true
            elseif x == "enable_server" then
                game.disabled_settings.server = false
            end
        end
        
        local overlays = {}
        local backgrounds = {}
        local headers = {}
        local footers = {}
        local music = {}
        
        for _, x in ipairs(minetest.get_dir_list(game.path.."/menu", false)) do
            if x:sub(1, string.len("background")) == "background" then
                local a, b, id = x:find("background.(%d+).png")
                if a or x == "background.png" then
                    backgrounds[(tonumber(id) or 0) +1] = x
                end
            elseif x:sub(1, string.len("overlay")) == "overlay" then
                local a, b, id = x:find("overlay.(%d+).png")
                if a or x == "overlay.png" then
                    overlays[(tonumber(id) or 0) +1] = x
                end
            elseif x:sub(1, string.len("header")) == "header" then
                local a, b, id = x:find("header.(%d+).png")
                if a or x == "header.png" then
                    headers[(tonumber(id) or 0) +1] = x
                end
            elseif x:sub(1, string.len("footer")) == "footer" then
                local a, b, id = x:find("footer.(%d+).png")
                if a or x == "footer.png" then
                    footers[(tonumber(id) or 0) +1] = x
                end
            elseif x:sub(1, string.len("theme")) == "theme" then
                local a, b, id = x:find("theme.(%d+).ogg")
                if a or x == "theme.ogg" then
                    music[(tonumber(id) or 0) +1] = x
                end
            end
        end
        
        if #backgrounds > 0 then
            local path = game.path.."/menu/"..backgrounds[math.random(#backgrounds)]
            if minetest.set_background("background", path) then
                game.background = path
            end
        elseif #overlays > 0 then
            local path = game.path.."/menu/"..overlays[math.random(#overlays)]
            -- I don't know why, but using "overlay" here simply does nothing. Whatever
            -- the difference is, it isn't visual, so this effectively 'fixes' it.
            if minetest.set_background("background", path) then
                game.overlay = path
            end
        else
            minetest.set_background("background", "")
        end

        if #headers > 0 then
            local path = game.path.."/menu/"..headers[math.random(#headers)]
            if minetest.set_background("header", path) then
                game.header = path
            end
        end
        
        if #music > 0 then
            local path = game.path.."/menu/"..music[math.random(#music)]
            if minetest.set_background("header", path) then
                game.music = path
            end
        end
        
        game.menu = template.build(file)
        
        state.menu_vars = {
            get_worlds = function() return get_worlds_for_game(game.id) end,
            get_mods = function() return get_mods_for_game(game.id) end,
            get_world_mods = function(world)
                world = world or state.menu_vars.selected_world
                return world and get_mods_for_world(state.menu_vars.selected_world) or {}
            end
        }
        
        state.menu_vars.setting_damage = game.disabled_settings.damage == nil
        state.menu_vars.setting_creative = game.disabled_settings.creative == nil
        state.menu_vars.setting_server = game.disabled_settings.server == nil
    end
    
    if args.show_dialog then
        state.menu_current = {args.show_dialog}
    elseif args.overlay_dialog then
        state.menu_current[#state.menu_current +1] = args.overlay_dialog
    elseif args.unoverlay_dialog and #state.menu_current > 1 then
        state.menu_current[#state.menu_current] = nil
    end

    state.menu_vars.WIDTH = size.x
    state.menu_vars.HEIGHT = size.y
    
    local fs = ""
    
    if not game.background and not game.overlay then
        fs = "\
        bgcolor[#000;true;#151618]\
        "
    end
        
    for i, x in ipairs(state.menu_current) do
        if i > 1 then
            fs = fs.."\
            box[0,0;"..size.x..","..size.y..";#0009]\
            "
        end
        fs = fs..template.evaluate(game.menu[x], state.menu_vars)
    end
    
    minetest.update_formspec(game_header..fs.."\
    button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
    ")
end

function add_favorite_server(address, port)
    state.favorite_servers[#state.favorite_servers +1] = {address = address, port = port}
    local f = io.open(minetest.get_user_path().."/client/serverlist/favoriteservers.json", "w")
    f:write(minetest.write_json(state.favorite_servers))
    f:flush()
    f:close()
end

function remove_favorite_server(address, port)
    local idx
    for i, x in pairs(state.favorite_servers) do
        if x.address == address and x.port == port then
            idx = i
            break
        end
    end
    if idx then
        table.remove(state.favorite_servers, idx)
        local f = io.open(minetest.get_user_path().."/client/serverlist/favoriteservers.json", "w")
        f:write(minetest.write_json(state.favorite_servers))
        f:flush()
        f:close()
    end
end

function refresh_server_list()
    minetest.handle_async(
        function(param)
            local http = minetest.get_http_api()
            local url = ("%s/list?proto_version_min=%d&proto_version_max=%d"):format(
                "https://servers.luanti.org" or minetest.settings:get("serverlist_url"),
                minetest.get_min_supp_proto(),
                minetest.get_max_supp_proto()
            )

            local response = http.fetch_sync{url = url}
            if not response.succeeded then
                return {}
            end

            local retval = minetest.parse_json(response.data)
            return retval and retval.list or {}
        end,
        nil,
        function(result)
            local list = table.copy(state.favorite_servers)
            table.insert_all(list, result)
            state.serverlist = list
            if state.serverlist_filtered then
                state.serverlist_filtered = search_server_list(state.servers_filter)
            end
            if state.loc == "servers" then
                show_servers_menu()
            end
        end
    )
end

function search_server_list(input)
    if input:trim() == "" then
        return nil
    end
    
    local search_str = ""
    local words = {}
    local mods = {}
    local games = {}
    local players = {}
    
    input = input:lower()
    
    for x in input:gmatch("%S+") do
        if x:sub(1, 4) == "mod:" then
            mods[#mods +1] = x:sub(5)
        elseif x:sub(1, 7) == "player:" then
            players[#players +1] = x:sub(8)
        elseif x:sub(1, 5) == "game:" then
            games[#games +1] = x:sub(6)
        else
            words[#words +1] = x
        end
    end
    
--    print(dump({words = words, mods= mods, players =players, games = games}, "  "))
    
    local out = {}
    for _, x in ipairs(state.serverlist) do
        local passed = true
        
        for _, a in ipairs(words) do
            if not (x.description and x.description:lower():find(a, 1, true) or x.name and x.name:lower():find(a, 1, true)) then
                passed = false
            end
        end
        
        -- PUC Lua doesn't have `continue`, hence the indentation.
        if passed then
            if #games > 0 then
                passed = false
            end
            for _, a in ipairs(games) do
                if a == x.gameid then
                    passed = true
                end
            end
            
            if passed then
                if x.mods and #mods > 0 then
                    local found = 0
                    for _, a in ipairs(x.mods) do
                        for _, b in ipairs(mods) do
                            if a == b then
                                found = found +1
                            end
                        end
                    end
                    passed = found == #mods
                else
                    passed = not not x.mods
                end
                
                if passed then
                    if x.clients_list and #players > 0 then
                        local found = 0
                        for _, a in ipairs(x.clients_list) do
                            for _, b in ipairs(players) do
                                if a:lower() == b then
                                    found = found +1
                                end
                            end
                        end
                        
                        passed = found == #players
                    else
                        passed = not not x.clients_list
                    end
                    if passed then
                        out[#out +1] = x
                    end
                end
            end
        end
    end
    return out
end

-- Shows the server list.
function show_servers_menu()
    local fs = ""
    local loading
    if not state.serverlist then
        loading = true
        refresh_server_list()
        local favorite_servers = minetest.parse_json(read_file(minetest.get_user_path().."/client/serverlist/favoriteservers.json") or "{}")
        for _, x in ipairs(favorite_servers) do
            x.favorite = true
        end
        state.favorite_servers = favorite_servers
        state.serverlist = favorite_servers
    end

    -- Overlay dialogs don't occlude area tooltips, so such tooltips are disabled in that case.
    local showing_dialog = state.joining_server or state.connecting_to_server or state.showing_server_mods or state.showing_server_players
    
    local current_server
    local joining_server
    
    fs = "\
    style[servers_filter;border=false;textcolor=#aaa]\
    image["..(size.x *0.095)..","..(size.y *0.1 -0.85)..";"..(size.x *0.81 -5.65)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
    field["..(size.x *0.095 +0.1)..","..(size.y *0.1 -0.85)..";"..(size.x *0.81 -5.45)..",0.75;servers_filter;;"..(state.menu_vars.servers_filter or "").."]\
    style[servers_search,servers_refresh,servers_cancel;bgimg="..assets.."btn_bg_2.png;bgimg_middle=8,8]\
    image_button["..(size.x *0.91 -5.65)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."search.png;servers_search;]\
    tooltip[servers_search;Search server list;#444;#aaa]\
    image_button["..(size.x *0.91 -4.8)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."cancel.png;servers_cancel;]\
    tooltip[servers_cancel;Cancel search;#444;#aaa]\
    image_button["..(size.x *0.91 -3.95)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."refresh.png;servers_refresh;]\
    tooltip[servers_refresh;Refresh server list;#444;#aaa]\
    button["..(size.x *0.91 -3.1)..","..(size.y *0.1 -0.85)..";3,0.75;direct_connection;Direct Connection...]\
    image["..(size.x *0.095)..","..(size.y *0.1)..";"..(size.x *0.81)..","..(size.y *0.8)..";"..assets.."btn_bg.png;8,8]\
    scroll_container[0,"..(size.y *0.1 +0.1)..";"..size.x..","..(size.y *0.8 -0.2)..";serverscroll;vertical;;0,0]\
    "
    local listx = size.x *0.1
    local infox = state.current_server and size.x *0.6 or size.x *0.9
    local max = size.x *0.9
    local i = 0
    for idx, x in ipairs(state.serverlist_filtered or state.serverlist) do
        -- Skip incompatible servers. You can't join them, so showing them is rather pointless.
        if (x.proto_max or version.proto_min) >= version.proto_min then
            local ping_lvl = 0
            local lag = (x.lag or 0) * 1000 + (x.ping or 0) * 250
            if lag <= 125 then
                ping_lvl = 4
            elseif lag <= 175 then
                ping_lvl = 3
            elseif lag <= 250 then
                ping_lvl = 2
            elseif lag <= 400 then
                ping_lvl = 1
            end
            
            local name = x.name and x.name:trim() or ""
            if name == "" then
                name = minetest.colorize("#888", x.address..":"..x.port)
            end
            local label_offset = 0.1
            fs = fs.."\
            style[serverinfo"..idx..";border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#"..(i %2 == 1 and "373530" or "403e39").."ff]\
            style[serverinfo"..idx..":hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0]\
            button["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;serverinfo"..idx..";]\
            "
            if x.favorite then
                fs = fs.."\
                image["..(listx)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_favorite.png]\
                "..(showing_dialog and "" or "tooltip["..(listx)..","..(i *0.5)..";0.5,0.5;Favorite server;#444;#aaa]\
                ")
                label_offset = 0.6
            end
            local clients = ""
            local color = "#aaa"
            local icons_offset = (infox -0.6)
            if x.clients then
                icons_offset = icons_offset -1
                clients = x.clients..(x.clients_max and "/"..x.clients_max or "")
                if x.clients > 0 and x.clients_max then
                    local percent = x.clients /x.clients_max
                    if percent < 0.75 then
                        color = "#638b67"
                    elseif percent < 1 then
                        color = "#a69174"
                    else
                        color = "#9d5b5b"
                    end
                end
                fs = fs.."\
                hypertext["..icons_offset..","..(i *0.5)..";1,0.5;;<global valign=middle halign=center color="..color..">"..fe(clients).."]\
                "
            end
            if x.pvp == false then
                icons_offset = icons_offset -0.5
                fs = fs.."\
                image["..(icons_offset)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_peaceful.png]\
                "..(showing_dialog and "" or "tooltip["..(icons_offset)..","..(i *0.5)..";0.5,0.5;Peaceful;#444;#aaa]\
                ")
            end
            if x.creative then
                icons_offset = icons_offset -0.5
                fs = fs.."\
                image["..(icons_offset)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_creative.png]\
                "..(showing_dialog and "" or "tooltip["..(icons_offset)..","..(i *0.5)..";0.5,0.5;Creative;#444;#aaa]\
                ")
            end
            fs = fs.."\
            label["..(listx +label_offset)..","..(i *0.5 +0.25)..";"..fe(name).."]\
            "..((x.ping or x.lag) and "image["..(infox -0.6)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_icon_ping_"..ping_lvl..".png]\
            "..(showing_dialog and "" or "tooltip["..(infox -0.6)..","..(i *0.5)..";0.5,0.5;Ping: "..math.floor(lag)..";#444;#aaa]\
            ") or "")
            i = i +1
        end
    end
    if loading then
        fs = fs.."\
        hypertext["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;;<global valign=middle halign=center color=#777>Loading...]\
        "
    end
    if i < 1 then
        fs = fs.."\
        hypertext["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;;<global valign=middle halign=center color=#777>No servers found.]\
        "
    end
    fs = fs.."\
    scroll_container_end[]\
    scrollbar[-800,0;1,1;vertical;serverscroll;"..(state.menu_vars.serverscroll and tonumber(state.menu_vars.serverscroll:sub(5)) or 0).."]\
    "
    -- Server detail view.
    if state.current_server then
        fs = fs.."box["..(infox -0.05)..","..(size.y *0.1 +0.05)..";0.1,"..(size.y *0.8 -0.1)..";#292d2fff]\
        container["..infox..","..(size.y *0.1).."]\
        hypertext[0,0;"..(max -infox)..",0.75;;<global valign=middle halign=center color=#aaa><b>"..fe(hte(state.current_server.name or state.current_server.address..":"..state.current_server.port)).."</b>]\
        image[0.1,0.75;"..(max -infox -0.2)..",0.5;"..assets.."btn_bg_2_dark.png;8,8]\
        box["..((max -infox) *0.7 -0.05)..",0.75;0.1,0.5;#292d2fff]\
        hypertext[0.2,0.8;"..((max -infox) *0.7 -0.2)..",0.5;server_address;<global valign=middle color=#aaa>"..fe(hte(state.current_server.address)).."]\
        hypertext["..((max -infox) *0.7 +0.1)..",0.8;"..((max -infox) *0.3 -0.2)..",0.5;server_port;<global valign=middle color=#aaa>"..fe(hte(tostring(state.current_server.port))).."]\
        "
        local buttons_offset = 0.1
        if state.current_server.mods then
            fs = fs.."\
            image_button["..buttons_offset..",1.5;0.5,0.5;"..assets.."menu_servers_mods.png;show_server_mods;]\
            tooltip[show_server_mods;Show mod list;#444;#aaa]\
            "
            buttons_offset = buttons_offset +0.5
        end
        if state.current_server.clients_list then
            fs = fs.."\
            image_button["..buttons_offset..",1.5;0.5,0.5;"..assets.."menu_servers_players.png;show_server_players;]\
            tooltip[show_server_players;Show player list;#444;#aaa]\
            "
            buttons_offset = buttons_offset +0.5
        end
        if state.current_server.favorite then
            fs = fs.."\
            image_button["..buttons_offset..",1.5;0.5,0.5;"..assets.."menu_servers_unfavorite.png;unfavorite_server;]\
            tooltip[unfavorite_server;Remove from favorites;#444;#aaa]\
            "
            buttons_offset = buttons_offset +0.5
        end
        local y = buttons_offset > 0.1 and 2.25 or 1.75
        fs = fs.."\
        hypertext[0.1,"..y..";"..(max -infox -0.2)..","..((size.y *0.8 -1.5 -1.05))..";server_desc;<global color=#aaa>"..fe(hte(state.current_server.description or "")).."]\
        button[0.1,"..(size.y *0.8 -1)..";"..(max -infox -0.2)..",0.75;join_server;Join Server]\
        container_end[]\
        "
    end
    
    fs = fs.."\
    button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
    "
    
    -- Mod list dialog.
    if state.showing_server_mods then
        fs = fs.."\
        box[0,0;"..size.x..","..size.y..";#0008]\
        style[_even,_even:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#403e39ff]\
        style[_odd,_odd:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#373530ff]\
        image["..(size.x /4 -0.1)..","..(size.y /4 -0.1)..";"..(size.x /2 +0.2)..","..(size.y /2 +0.2)..";"..assets.."btn_bg.png;8,8]\
        scroll_container["..(size.x /4)..","..(size.y /4)..";"..(size.x /2)..","..(size.y /2 -1)..";modsscroll;vertical;;0,0]\
        "
        for i, x in ipairs(state.current_server.mods) do
            fs = fs.."\
            button[0,"..(i *0.5 -0.5)..";"..(infox -listx)..",0.5;"..(i %2 == 1 and "_odd" or "_even")..";]\
            label[0.1,"..(i *0.5 -0.25)..";"..fe(x).."]\
            "
        end
        fs = fs.."\
        scroll_container_end[]\
        scrollbar[-800,0;1,1;vertical;modsscroll;]\
        button["..(size.x /4)..","..(size.y *0.75 -0.875)..";"..(size.x /2)..",0.75;close_dialog;Back]\
        "
    end
    
    -- Player list dialog.
    if state.showing_server_players then
        fs = fs.."\
        box[0,0;"..size.x..","..size.y..";#0008]\
        style[_even,_even:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#403e39ff]\
        style[_odd,_odd:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#373530ff]\
        image["..(size.x /4 -0.1)..","..(size.y /4 -0.1)..";"..(size.x /2 +0.2)..","..(size.y /2 +0.2)..";"..assets.."btn_bg.png;8,8]\
        scroll_container["..(size.x /4)..","..(size.y /4)..";"..(size.x /2)..","..(size.y /2 -1)..";modsscroll;vertical;;0,0]\
        "
        for i, x in ipairs(state.current_server.clients_list) do
            fs = fs.."\
            button[0,"..(i *0.5 -0.5)..";"..(infox -listx)..",0.5;"..(i %2 == 1 and "_odd" or "_even")..";]\
            label[0.1,"..(i *0.5 -0.25)..";"..fe(x).."]\
            "
        end
        fs = fs.."\
        scroll_container_end[]\
        scrollbar[-800,0;1,1;vertical;modsscroll;]\
        button["..(size.x /4)..","..(size.y *0.75 -0.875)..";"..(size.x /2)..",0.75;close_dialog;Back]\
        "
    end

    -- Connection dialog.    
    if state.joining_server or state.connecting_to_server then
        local offset = 0
        fs = fs.."\
        box[0,0;"..size.x..","..size.y..";#0008]\
        image["..(size.x *0.25 -0.2)..","..(size.y *0.25 -0.1)..";"..(size.x *0.5 +0.4)..","..(size.y *0.5 +0.2)..";"..assets.."btn_bg.png;8,8]\
        scroll_container["..(size.x *0.25)..","..(size.y *0.25)..";"..(size.x *0.5)..","..(size.y *0.5 +0.2)..";serverconfscroll;vertical;;0,0]\
        "
        
        if state.server_connection_error then
            offset = offset +0.75
            fs = fs.."\
            hypertext[0,0;"..(size.x *0.5)..",0.75;;<global valign=middle color=#9d5b5b>"..fe(hte(state.server_connection_error.msg)).."]\
            set_focus["..fe(state.server_connection_error.element)..";true]\
            "
        else
            fs = fs.."\
            set_focus[server_"..(state.joining_server and (minetest.settings:get("name") and "password" or "username") or "address")..";true]\
            "
        end
        
        -- Make the address and port immutable if joining from the serverlist.
        if state.joining_server then
            fs = fs.."\
            hypertext[0,"..offset..";"..(size.x *0.5)..",0.75;;<global valign=middle halign=center color=#aaa>Connecting to <b>"..fe(hte(state.current_server.name or state.current_server.address..":"..state.current_server.port)).."</b>...]\
            "
            offset = offset +1
        else
            fs = fs.."\
            set_focus[server_address;true]\
            hypertext[0,"..offset..";"..(size.x *0.5)..",0.75;;<global valign=middle halign=center color=#aaa>Connecting to server...]\
            "
            offset = offset +1
            fs = fs.."\
            image[0,"..(0.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
            label[0.2,"..(0.1 +offset)..";Address]\
            field[0.2,"..(0.25 +offset)..";"..(size.x *0.4 -0.4)..",0.75;server_address;;"..(state.menu_vars.server_address or "").."]\
            box["..(size.x *0.4 -0.25)..","..(0.25 +offset)..";0.1,0.75;#292d2fff]\
            label["..(size.x *0.4 -0.2)..","..(0.1 +offset)..";Port]\
            field["..(size.x *0.4)..","..(0.25 +offset)..";"..(size.x *0.1 -0.2)..",0.75;server_port;;"..(state.menu_vars.server_port or "").."]\
            "
            offset = offset +1
        end
        fs = fs.."\
        image[0,"..(0.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
        label[0.2,"..(0.1 +offset)..";Username]\
        field[0.2,"..(0.25 +offset)..";"..(size.x *0.5 -0.4)..",0.75;server_username;;"..minetest.settings:get("name").."]\
        image[0,"..(1.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
        style[server_password;border=false;textcolor=#aaa;bgimg="..assets.."btn_bg.png]\
        label[0.2,"..(1.1 +offset)..";Password]\
        pwdfield[0.2,"..(1.25 +offset)..";"..(size.x *0.5 -0.4)..",0.75;server_password;]\
        button[0,"..(2.25 +offset)..";"..(size.x *0.5)..",0.75;cancel_join_server;Cancel]\
        button[0,"..(3.25 +offset)..";"..(size.x *0.5)..",0.75;confirm_join_server;Join]\
        scroll_container_end[]\
        scrollbar[-800,0;1,1;vertical;serverconfscroll;"..(state.menu_vars.serverconfscroll and tonumber(state.menu_vars.serverconfscroll:sub(5)) or 0).."]\
        "
    end
    minetest.update_formspec(servers_header..fs)
end

-- Show the content manager.
function show_content_menu()
    local fs = ""
    
    if not state.all_content then
        get_all_content()
    end
    
    local w = size.x
    local h = size.y -1.1
    
    if not state.current_package then
    
        fs = "\
        scroll_container[0,1.1;"..w..","..h..";contentscroll;horizontal;;]\
        "
        
        local cols = math.floor(w /2.5)
        local spacing_x = w %2.5 /cols +0.5
        
        local rows = math.floor(h /3)
        local spacing_y = h %3 /rows +0.5
        
        local pages = math.ceil(#state.all_content /(rows *cols))
        local page = state.content_page or 1
        
        local offset = page /10
        
        local x = 0
        local y = 0
        local i = (page -1) *(rows *cols) +1
        
        while true do
            local pkg = state.all_content[i]
            if not pkg then break end
            if not pkg.icon then
                pkg.icon = pkg.type == "game" and pkg.path.."/menu/icon.png" or pkg.path.."/icon.png"
                if not file_exists(pkg.icon) then
                    pkg.icon = assets.."menu_content.png"
                end
            end
            local title = pkg.title:trim()
            if title == "" then
                title = pkg.name
            end
            fs = fs.."\
            image_button["..(x *(2 +spacing_x) +(spacing_x /2) +offset)..","..(y *(2 +spacing_y) +(spacing_y /2))..";2,2;"..fe(pkg.icon)..";view_package_"..i..";]\
            tooltip[view_package_"..i..";"..fe(title)..";#444;#aaa]\
            hypertext["..(x *(2 +spacing_x) +(spacing_x /2) +offset)..","..(y *(2 +spacing_y) +(spacing_y /2) +2)..";2,0.75;nobg;<global color=#aaa valign=middle halign=center>"..fe(hte(title)).."]\
            "
            i = i +1
            x = x +1
            if x > cols -1 then
                x = 0
                y = y +1
                if y > rows -1 then break end
            end
        end
        
        fs = fs.."\
        box[0,0;"..(pages *w)..",1;#0000]\
        scroll_container_end[]\
        scrollbaroptions[min=1;max="..pages..";smallstep=1]\
        scrollbar[-800,0;1,1;horizontal;contentscroll;"..page.."]\
        "
        
        fs = fs.."\
        box[0,0;"..size.x..",1;#403e39ff]\
        box[0,1;"..size.x..",0.1;#292d2fff]\
        style[content_search,content_cancel,go_back;bgimg="..assets.."btn_bg_2_dark.png;bgimg_middle=8,8]\
        image_button[0.125,0.125;0.75,0.75;"..assets.."search.png;content_search;]\
        tooltip[content_search;Search;#444;#aaa]\
        image_button[1,0.125;0.75,0.75;"..assets.."cancel.png;content_cancel;]\
        tooltip[content_cancel;Cancel;#444;#aaa]\
        style[test;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#403e39ff]\
        style[test:hovered;bgimg="..assets.."white.png;bgimg_middle=0]\
        image[2,0.125;"..(size.x -9)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
        field[2.1,0.125;"..(size.x -9.2)..",0.75;content_filter;;]\
        button["..(w -6)..",0;4,1;test;Browse Online Content...]\
        image_button["..(w -1.5)..",0.125;0.75,0.75;"..assets.."new_package.png;new_package;]\
        tooltip[new_package;New Package...;#444;#aaa]\
        "
        
        local start = w /2 -(pages *0.125)
        
        for i = 1, pages do
            fs = fs.."\
            image_button["..(start +(i *0.25))..","..(size.y -0.5)..";0.25,0.25;"..assets..(i == page and "circle_light.png" or "circle.png")..";page"..i..";]\
            "
        end
    
    else
        
        local pkg = state.all_content[state.current_package]
        local desc = ""
        if file_exists(pkg.path.."/README.md") then
            desc = read_file(pkg.path.."/README.md")
        end
        local pkg_type = pkg.type
        if pkg_type == "mod" then
            pkg_type = "<style color="..theme.mod_label_color..">\\[Mod\\]</style>"
        elseif pkg_type == "modpack" then
            pkg_type = "<style color="..theme.mod_label_color..">\\[Mod Collection\\]</style>"
        elseif pkg_type == "game" then
            pkg_type = "<style color="..theme.game_label_color..">\\[Game\\]</style>"
        elseif pkg_type == "txp" then
            pkg_type = "<style color="..theme.texture_pack_label_color..">\\[Texture Pack\\]</style>"
        end
        fs = fs.."\
        style[go_back;bgimg="..assets.."btn_bg_2_dark.png;bgimg_middle=8,8]\
        image_button["..(size.x -1)..",0.5;0.5,0.5;"..assets.."cancel.png;go_back;]\
        imdage["..(size.x *0.1 -0.1)..","..(size.y *0.1 -0.1)..";"..(size.x *0.6)..","..(size.y *0.8 +0.2)..";"..assets.."btn_bg.png;8,8]\
        hypertext["..(size.x *0.1)..","..(size.y *0.1)..";"..(size.x *0.6 -0.2)..","..(size.y *0.8)..";aa;\
            <global halign=center color="..theme.text_color..">\
            "..markdown_to_hypertext(desc).."\
        ]\
        image["..(size.x *0.7 +0.1)..","..(size.y *0.1 -0.1)..";"..(size.x *0.2 +0.2)..","..(size.y *0.8 +0.2)..";"..assets.."btn_bg.png;8,8]\
        hypertext["..(size.x *0.7 +0.2)..","..(size.y *0.1)..";"..(size.x *0.2)..","..(size.y *0.8)..";bb;\
        <global halign=center color="..theme.text_color..">\
        <img name='"..fe(hte(pkg.icon)).."' width="..(size.x *0.1 *dpi.x).." height="..(size.x *0.1 *dpi.y)..">\
        <center><big>"..fe(hte(pkg.title or pkg.name)).."</big></center>\
        "..pkg_type.." <style color="..theme.muted_text_color..">"..fe(hte(pkg.name)).."</style>\
        Creator: <b>"..fe(hte(pkg.author)).."</b>\
        Location: "..(pkg.path:sub(2):gsub("/", "<style size=12> \\> </style>")).."\
        "..(pkg.type == "modpack" and "Modules: "..#minetest.get_dir_list(pkg.path, true) or "").."\
        ]\
        button["..(size.x *0.7 +0.2)..","..(size.y *0.9 -0.75)..";"..(size.x *0.2)..",0.75;browse_source;Browse Source]\
        "
    end

    fs = fs.."\
    button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
    "
    
    minetest.update_formspec(content_header..fs)
end

function show_about_menu()
    if not state.lua_api then
        state.lua_api = {}
        state.lua_api_sections = {}
        -- TODO: Make this portable
        local doc = read_file(minetest.get_builtin_path().."/../luanti/lua_api.md")
        local offset = 1
        while true do
            local a, b, heading = doc:find("([^\n]+)\n=+\n", offset)
            if not a then break end
            if offset > 1 then
                state.lua_api[state.lua_api_sections[#state.lua_api_sections]] = doc:sub(offset, a -1)
            end
            state.lua_api_sections[#state.lua_api_sections +1] = heading
            offset = b
        end
        state.lua_api[state.lua_api_sections[#state.lua_api_sections]] = doc:sub(offset)
    end
    local fs = "\
    style[docs_section;bgimg="..assets.."white.png;bgcolor=#fff0]\
    style[docs_section:hovered;bgimg="..assets.."white.png;bgcolor=#fff1]\
    box[0,0;"..(size.x *0.25 -0.55)..","..size.y..";#403e39ff]\
    box["..(size.x *0.25 -0.55)..",0;0.1,"..size.y..";#292d2fff]\
    scroll_container[0,0;"..(size.x *0.25 -0.55)..","..size.y..";sidebarscroll;vertical;;0,0]\
    "
    for i, x in ipairs(state.lua_api_sections) do
        fs = fs.."\
        box[0,"..(i *0.8 -0.05)..";"..(size.x *0.25 -0.55)..",0.05;#292d2fff]\
        button[0,"..((i -1) *0.8)..";"..(size.x *0.25 -0.55)..",0.75;docs_section;"..fe(x).."]\
        "
    end
    fs = fs.."\
    scroll_container_end[]\
    scrollbar[-800,-800;0,0;vertical;sidebarscroll;"..(state.menu_vars.sidebarscroll and state.menu_vars.sidebarscroll:sub(5) or "").."]\
    hypertext["..(size.x *0.25)..",0;"..size.x..","..size.y..";aa;<global color="..theme.text_color..">\
    "..markdown_to_hypertext(state.lua_api[state.menu_vars.docs_section or state.lua_api_sections[1]]).."\
    ]"
    
    fs = fs.."\
    button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
    "
    
    minetest.update_formspec(content_header..fs)
end

function show_settings_menu()
    if not state.settings then
        state.settings = {}
        state.settings_categories = {}
        local settings = settingtypes.parse_config_file(minetest.get_builtin_path().."/settingtypes.txt")
        for _, x in ipairs(settings) do
            if x.type == "category" then
                if x.level == 1 and not x.name:find "Hide:" then
                    state.settings_categories[#state.settings_categories +1] = x.name
                    state.settings[x.name] = {}
                end
            else
                local s = state.settings[state.settings_categories[#state.settings_categories]]
                s[#s +1] = x
            end
        end
    end

    local fs = "\
    box[0,0;"..(size.x *0.25 -0.75)..","..size.y..";#403e39ff]\
    box["..(size.x *0.25 -0.75)..",0;0.1,"..size.y..";#292d2fff]\
    scroll_container[0,0;"..(size.x *0.25 -0.75)..","..size.y..";sidebarscroll;vertical;;0,0]\
    style[show_theme;bgimg="..assets.."white.png;bgcolor=#fff0]\
    style[show_theme:hovered;bgimg="..assets.."white.png;bgcolor=#fff1]\
    button[0,0;"..(size.x *0.25 -0.55)..",0.75;show_theme;Theme]\
    "
    
    for i, category in ipairs(state.settings_categories) do
        fs = fs.."\
        style[show_category;bgimg="..assets.."white.png;bgcolor=#fff0]\
        style[show_category:hovered;bgimg="..assets.."white.png;bgcolor=#fff1]\
        box[0,"..(i *0.8 -0.05)..";"..(size.x *0.25 -0.55)..",0.05;#292d2fff]\
        button[0,"..(i *0.8)..";"..(size.x *0.25 -0.55)..",0.75;show_category;"..fe(category).."]\
        "
    end
    
    fs = fs.."scroll_container_end[]\
    scrollbar[-800,-800;0,0;vertical;sidebarscroll;]\
    scroll_container["..(size.x *0.25)..",0;"..(size.x *0.75)..","..size.y..";settingsscroll;vertical;;0,0]\
    "
    
    if state.settings_category then
        local page = state.settings[state.settings_category]
        local y = 0.25
        for i, x in ipairs(page) do
            if x.type == "string" then
                fs = fs.."label[0,"..y..";"..fe(x.readable_name).."]\
                image[0,"..(y +0.2)..";"..(size.x *0.3)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
                field[0.1,"..(y +0.2)..";"..(size.x *0.3 -0.2)..",0.75;setting_"..fe(x.name)..";;"..(minetest.settings:get(x.name) or x.default).."]\
                button["..(size.x *0.75 -3)..","..(y +0.2)..";2,0.75;apply_setting_"..fe(x.name)..";Apply]\
                "
                y = y +1.2
            elseif x.type == "bool" then
                local state = minetest.settings:get_bool(x.name, x.default)
                fs = fs.."image_button[0,"..y..";0.75,0.75;"..assets..(state and "checkbox_filled" or "checkbox_empty")..".png;toggle_setting_"..fe(x.name)..";]\
                label[1,"..(y +0.375)..";"..fe(x.readable_name).."]\
                "
                y = y +1
            elseif x.type == "int" or x.type == "float" then
                fs = fs.."label[0,"..y..";"..fe(x.readable_name).."]\
                image[0,"..(y +0.2)..";"..(size.x *0.3)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\
                field[0.1,"..(y +0.2)..";"..(size.x *0.3 -0.2)..",0.75;setting_"..fe(x.name)..";;"..(minetest.settings:get(x.name) or x.default).."]\
                button["..(size.x *0.75 -3)..","..(y +0.2)..";2,0.75;apply_setting_"..fe(x.name)..";Apply]\
                "
                y = y +1.2
            end
        end
    end
    
    
    fs = fs.."\
    scroll_container_end[]\
    scrollbar[-800,-800;0,0;vertical;settingsscroll;]\
    button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
    "
    
    minetest.update_formspec(content_header..fs)
end


function minetest.button_handler(data)
    if not state.menu_vars then state.menu_vars = {} end
    setmetatable(state.menu_vars, {__index = data})
    if state.loc == "games" then
        if data.content or data.ht == "action:content" then
            if state.loc ~= "content" then show_content() end
        elseif data.games then
            if state.loc ~= "games" then show_meta_menu() end
        elseif data.carousel and data.carousel:sub(1, 4) == "CHG:" then
            local v = tonumber(data.carousel:sub(5))
            show_meta_menu(v)
        else
            for k, v in pairs(data) do
                if k:sub(1, 4) == "game" then
                    local id = k:sub(5)
                    state.current_game = get_game_info(id)
                    state.menu_current = {"main"}
                    state.loc = "game"
                    state.menu_vars = {}
                    show_game_menu()
                    minetest.settings:set("menu_last_game", id)
                    last_game = id
                elseif k:sub(1, 4) == "jump" then
                    local v = tonumber(k:sub(5))
                    show_meta_menu(v)
                end
            end
        end
    elseif state.loc == "servers" then
        if data.servers_refresh then
            refresh_server_list()
        elseif data.servers_search or data.key_enter_field == "servers_filter" then
            state.servers_filter = data.servers_filter
            state.serverlist_filtered = search_server_list(data.servers_filter)
            show_servers_menu()
        elseif data.servers_cancel then
            state.serverlist_filtered = nil
            state.servers_filter = nil
            data.servers_filter = ""
            show_servers_menu()
        elseif data.unfavorite_server then
            state.current_server.favorite = nil
            remove_favorite_server(state.current_server.address, state.current_server.port)
            show_servers_menu()
        elseif data.show_server_mods then
            state.showing_server_mods = true
            show_servers_menu()
        elseif data.show_server_players then
            state.showing_server_players = true
            show_servers_menu()
        elseif data.close_dialog then
            state.showing_server_players = nil
            state.showing_server_mods = nil
            show_servers_menu()
        elseif data.direct_connection then
            state.connecting_to_server = true
            show_servers_menu()
        elseif data.join_server then
            state.joining_server = true
            show_servers_menu()
        elseif data.cancel_join_server then
            state.joining_server = nil
            state.connecting_to_server = nil
            show_servers_menu()
        elseif data.confirm_join_server or data.key_enter_field == "server_username" or data.key_enter_field == "server_password" or data.key_enter_field == "server_address" then
            minetest.settings:set("name", data.server_username)
            local address
            local port
            if state.connecting_to_server then
                if not tonumber(data.server_port) then
                    state.server_connection_error = {
                        msg = "Invalid port.",
                        element = "server_port"
                    }
                    show_servers_menu()
                    state.server_connection_error = nil
                    return
                end
                address = data.server_address:lower()
                port = data.server_port:lower()
            else
                address = state.current_server.address:lower()
                port = state.current_server.port
            end
            local is_favorite = false
            for _, x in ipairs(state.favorite_servers) do
                if x.address == address and x.port == port then
                    is_favorite = true
                end
            end
            if not is_favorite then
                add_favorite_server(address, port)
            end
            minetest.settings:set("address", address)
            minetest.settings:set("remote_port", port)
            gamedata = {
                playername = data.server_username,
                password = data.server_password,
                address = address,
                port = port,
                selected_world = 0,
                singleplayer = false
            }

            minetest.start()
        else
            for k, v in pairs(data) do
                if k:sub(1, string.len("serverinfo")) == "serverinfo" then
                    local idx = k:sub(string.len("serverinfo>"))
                    if idx:sub(1, 1) == "f" then
                        state.current_server = state.favorite_servers[tonumber(idx:sub(2))]
                    else
                        state.current_server = (state.serverlist_filtered or state.serverlist)[tonumber(idx)]
                    end
                    show_servers_menu()
                end
            end
        end
    elseif state.loc == "content" then
        if data.contentscroll and data.contentscroll:sub(1, 4) == "CHG:" then
            state.content_page = tonumber(data.contentscroll:sub(5))
            show_content_menu()
        elseif data.go_back then
            state.current_package = nil
            show_content_menu()
        else
            for k, v in pairs(data) do
                if k:sub(1, 4) == "page" then
                    state.content_page = tonumber(k:sub(5))
                    show_content_menu()
                elseif k:sub(1, string.len("view_package_")) == "view_package_" then
                    state.current_package = tonumber(k:sub(string.len("view_package_>")))
                    show_content_menu()
                end
            end
        end
    elseif state.loc == "game" then
        for k, v in pairs(data) do
            if k == ".play" then
                gamedata = {
                    playername = "singleplayer",
                    password = "",
                    address = nil,
                    port = nil,
                    selected_world = get_world_index(state.menu_vars.selected_world),
                    singleplayer = true
                }
                minetest.start()
            elseif k:sub(1, string.len(".show_view_")) == ".show_view_" then
                show_game_menu {
                    show_dialog = k:sub(string.len(".show_view_>"))
                }
            elseif k:sub(1, string.len(".overlay_view_")) == ".overlay_view_" then
                show_game_menu {
                    overlay_dialog = k:sub(string.len(".overlay_view_>"))
                }
            elseif k:sub(1, string.len(".select_world_")) == ".select_world_" then
                state.menu_vars.selected_world = k:sub(string.len(".select_world_>"))
                state.menu_vars.selected_world_name = k:match "/([^/]+)$"  or "<unknown world>"
                local conf = Settings(state.menu_vars.selected_world.."/world.mt")
                local de = conf:get("enable_damage")
                if de == "true" then
                    state.menu_vars.damage_enabled = true
                elseif de == "false" then
                    state.menu_vars.damage_enabled = false
                else
                    state.menu_vars.creative_damage = not state.current_game.disabled_settings.damage
                end
                local cm = conf:get("creative_mode")
                if cm == "true" then
                    state.menu_vars.creative_enabled = true
                elseif cm == "false" then
                    state.menu_vars.creative_enabled = false
                else
                    state.menu_vars.creative_enabled = not not state.current_game.disabled_settings.creative
                end
                show_game_menu()
            elseif k == ".unoverlay_dialog" then
                show_game_menu {
                    unoverlay_dialog = true
                }
            elseif k:sub(1, string.len(".set_")) == ".set_" then
                local name, value = k:match "%.set_|(.+)|_to_(.*)"
                state.menu_vars[name] = minetest.parse_json(value:gsub("\\([];,$\\[])", "%1"))
                show_game_menu()
            end
        end
    elseif state.loc == "about" then
        if data.docs_section then
            show_about_menu()
        end
    elseif state.loc == "settings" then
        if data.show_category then
            state.settings_category = data.show_category
            show_settings_menu()
        else
            for k, v in pairs(data) do
                
            end
        end
    end
    if data.to_meta_menu then
        state.current_game = nil
        state.loc = "games"
        show_meta_menu()
    elseif data.show_servers_view then
        state.current_game = nil
        state.loc = "servers"
        show_servers_menu()
    elseif data.show_content_view then
        state.current_game = nil
        state.loc = "content"
        show_content_menu()
    elseif data.show_about_view then
        state.current_game = nil
        state.loc = "about"
        show_about_menu()
    elseif data.show_settings_view then
        state.current_game = nil
        state.loc = "settings"
        show_settings_menu()
    end
end

function minetest.event_handler(ev)
    -- When Esc is pressed, close the current dialog, or the game if we are on the meta menu.
    if ev == "MenuQuit" then
        if state.joining_server or state.connecting_to_server or state.showing_server_mods or state.showing_server_players then
            state.joining_server = nil
            state.connecting_to_server = nil
            state.showing_server_mods = nil
            state.showing_server_players = nil
            show_servers_menu()
        elseif state.loc ~= "games" then
            show_meta_menu()
        else
            minetest.close()
        end
    end
end

state.theme = build_stylesheet("")

minetest.set_clouds(false)

if last_game then
    state.current_game = get_game_info(last_game)
    if state.current_game then
        state.menu_current = {"main"}
        state.loc = "game"
        show_game_menu()
    else
        show_meta_menu()
    end
else
    show_meta_menu()
end

--]]
