
local 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

local 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

local function search_server_list(state, input)
    if input:trim() == "" then
        return nil
    end
    
    local search_str = ""
    local words = {}
    local mods = {}
    local game
    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
            game = x:sub(6)
        else
            words[#words + 1] = x
        end
    end
        
    local out = {}
    for _, x in ipairs(state.servers) 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 game and x.gameid and game ~= x.gameid:lower():trim() then
                passed = false
            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():trim() == 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

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

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

            local retval = core.parse_json(response.data)
            return retval and retval.list or {}
        end,
        nil,
        function(result)
            local list = table.copy(state.favorite_servers)
            local num_servers = #result
            local i = 0
            while i < num_servers do
                i = i + 1
                -- Drop incompatible servers. You can't join them,
                -- so showing them is rather pointless.
                if (result[i].proto_max or version.proto_min) < version.proto_min then
                    table.remove(result, i)
                    num_servers = num_servers - 1
                end
            end
            table.insert_all(list, result)
            state.servers = list
            if state.search ~= "" then
                state.servers_filtered = search_server_list(state, state.search)
                list = state.servers_filtered
            end
            state.serverlist(list)
        end
    )
end

local function join_server(state)
    core.settings:set("name", state.username())
    local address
    local port
    if state.joining() == true then
        if not tonumber(state.port()) then
            state.join_error("Invalid port.")
            return
        end
        address = state.address():lower()
        port = state.port():lower()
    else
        address = state.joining().address:lower()
        port = state.joining().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
    core.settings:set("address", address)
    core.settings:set("remote_port", port)
    gamedata = {
        playername = state.username(),
        password = state.password(),
        address = address,
        port = port,
        selected_world = 0,
        singleplayer = false
    }

    core.start()
end

return function(state)
    imfs.begin(size.x, size.y)
        :padding(0, 0)
        :bgcolor(theme.styles.bg_color, true)
    
    local loading
    if not state.serverlist() then
        loading = true
        local favorite_servers = core.parse_json(read_file(core.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._val = favorite_servers
        refresh_server_list(state)
    end
    
    local enable_tooltips = not (state.joining() or state.detail_view())
    
    local list = state.serverlist()
    
    imfs.row("10%", size.y * 0.1 - 0.85, "80%", 0.75)
        :gap(0.1)
        
        theme.field(0, 0, "1x", "100%", state.search)
            :onchange(function(value)
                state.search = value
            end)
            :onenter(function(value)
                state.search = value
                state.servers_filtered = search_server_list(state, state.search)
                state.serverlist(state.servers_filtered)
                state.scroll_pos(1)
            end)
            
        imfs.button(0, 0, 0.75, 0.75)
            :image(theme.icon("search"))
            :style({
                bgimg = theme.get_background_image("button"),
                bgimg_middle = theme.styles.button.border_width,
            })
            :tooltip("Search server list")
            :onclick(function()
                state.servers_filtered = search_server_list(state, state.search)
                state.serverlist(state.servers_filtered)
                state.scroll_pos(1)
            end)
            
        imfs.button(0, 0, 0.75, 0.75)
            :image(theme.icon("cancel"))
            :style({
                bgimg = theme.get_background_image("button"),
                bgimg_middle = theme.styles.button.border_width,
            })
            :tooltip("Clear search")
            :onclick(function()
                state.search = ""
                state.servers_filtered = nil
                state.serverlist(state.servers)
                state.scroll_pos(1)
            end)
                
        imfs.button(0, 0, 0.75, 0.75)
            :image(theme.icon("refresh"))
            :style({
                bgimg = theme.get_background_image("button"),
                bgimg_middle = theme.styles.button.border_width,
            })
            :tooltip("Refresh server list")
            :onclick(function()
                refresh_server_list(state)
            end)
        
        imfs.button(0, 0, 3, 0.75, "Direct Connection...")
            :onclick(function()
                state.joining(true)
            end)
        
    imfs.row_end()

    local border_width = pixel_to_formspec(theme.styles.container.border_width)
    
    imfs.image("10%", "10%", "80%", "80%", theme.get_background_image("container"))
    imfs.group("10% + "..border_width, "10% + "..border_width, "80% - "..(border_width * 2), "80% - "..(border_width * 2))
    
        imfs.scroll_container(0, 0, state.detail() and "65% - 0.05" or "100%", "100%", "vertical", 0, "")
            :scrollbar(function()
                imfs.scrollbar(state.detail() and "65% - 0.55" or "100% - 0.5", 0, 0.5, "100%", "vertical", state.scroll_pos)
                    :options({
                        min = 1,
                        max = #list,
                        smallstep = 1,
                        thumbsize = 0,
                    })
                    :onchange(function(action, value)
                        if action == "CHG" then
                            state.scroll_pos(value)
                        end
                    end)
            end)
        
        imfs.scope("servers")
        local height = size.y * 0.8
        local i = 0
        for idx = state.scroll_pos(), state.scroll_pos() + height / 0.5 do
            local x = list[idx]
            -- End early if we've run out of list.
            if not x then break end
            
            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 = core.colorize(theme.styles.text_color_muted, x.address..":"..x.port)
            end
            
            imfs.button(0, i * 0.5, "100%", 0.5)
                :style({
                    border = false,
                    bgimg = "[fill:1x1:0,0:#fff",
                    bgimg_middle = 0,
                    bgcolor = i % 2 == 1 and theme.styles.container.bg_color or theme.styles.container.bg_color_alt,
                })
                :style("hovered", {
                    border = false,
                    bgimg = "[fill:1x1:0,0:#fff",
                    bgimg_middle = 0,
                })
                :style("pressed", {
                    border = false,
                    bgimg = "[fill:1x1:0,0:#fff",
                    bgimg_middle = 0,
                })
                :onclick(function()
                    state.detail(x)
                end)
            imfs.row(0.1, i * 0.5, "100% - 0.65", 0.5)
                if x.favorite then
                    imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_favorite"))
                        :tooltip("Favorite server", enable_tooltips)
                end
            
                local name = x.name and x.name:trim() or ""
                if name == "" then
                    name = core.colorize("#888", x.address..":"..x.port)
                end
                -- We use an arealabel to ensure that the name will be clipped
                -- if it is inordinately long, rather than overflowing onto
                -- other thigs.
                imfs.arealabel(0, 0.125, "1x", 0.375, name)
                
                if x.pvp == false then
                    imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_peaceful"))
                        :tooltip("Peaceful", enable_tooltips)
                end
                
                if x.creative then
                    imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_creative"))
                        :tooltip("Creative", enable_tooltips)
                end
                
                if x.clients then
                    local clients = x.clients..(
                        x.clients_max and "/"..x.clients_max or ""
                    )
                    local color = "#aaa"
                    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
                    imfs.hypertext(0, 0, 1, 0.5, "<global valign=middle halign=center color="..color..">"..core.hypertext_escape(clients))
                end
                
                if x.ping or x.lag then
                    imfs.image(0, 0, 0.5, 0.5, theme.icon("menu_servers_icon_ping_"..ping_lvl))
                        :tooltip("Ping: "..math.floor(lag), enable_tooltips)
                end
            
            imfs.row_end()
            
            i = i + 1
        end
        
        imfs.scope_end()
        
        if loading then
            imfs.hypertext(0, i * 0.5, "100%", 0.5, "<global valign=middle halign=center color=#777>Loading...")
        end
        
        imfs.scroll_container_end()
        
        local detail = state.detail()
        if detail then
            imfs.box("65% - 0.05", 0, 0.1, "100%", theme.styles.container.border_color)
            imfs.column("65% + 0.1", border_width, "35% - "..(border_width + 0.1), "100% - "..(border_width * 2))
                :gap(0.1)
                imfs.hypertext(0, 0, "100%", 0.75, "<global valign=middle halign=center color=#aaa><b>"..core.hypertext_escape(detail.name or detail.address..":"..detail.port).."</b>")
                if detail.mods or detail.clients_list or detail.favorites then
                    imfs.row(0.1, 0, "100% - 0.2", 0.5)
                        if detail.mods then
                            imfs.button(0, 0, 0.5, 0.5)
                                :image(theme.icon("menu_servers_mods"))
                                :tooltip("Show mod list")
                                :onclick(function()
                                    state.detail_view("mod_list")
                                end)
                        end
                        
                        if detail.clients_list then
                            imfs.button(0, 0, 0.5, 0.5)
                                :image(theme.icon("menu_servers_players"))
                                :tooltip("Show player list")
                                :onclick(function()
                                    state.detail_view("player_list")
                                end)
                        end
                        
                        if detail.favorite then
                            imfs.button(0, 0, 0.5, 0.5)
                                :image(theme.icon("menu_servers_unfavorite"))
                                :tooltip("Remove from favorites")
                                :onclick(function()
                                    detail.favorite = nil
                                    remove_favorite_server(detail.address, detail.port)
                                    -- Bump the serverlist to notify that we've changed it.
                                    state.serverlist(state.serverlist)
                                end)
                        end
                    imfs.row_end()
                end
                imfs.hypertext(0.1, 0, "100% - 0.2", "1x", "<global color=#aaa>"..core.hypertext_escape(detail.description or ""))
                imfs.button(0, 0, "100%", 0.75, "Join server")
                    :onclick(function()
                        state.joining(detail)
                    end)
            imfs.column_end()
        end
    
    imfs.group_end()
    
    imfs.button("100% - 1", 0.25, 0.75, 0.75, "<")
        :onclick(function()
            show_meta_menu()
        end)
    
    local joining = state.joining()
    if joining then
        imfs.box(0, 0, "100%", "100%", "#0008")
        imfs.button(0, 0, "100%", "100%")
            :styles(styles_no_background)
            :onclick(function()
                state.joining(false)
            end)
        
        imfs.group("20%", "20%", "60%", "60%")
            imfs.image(0, 0, "100%", "100%", theme.get_background_image("container"))
            imfs.scroll_container(border_width + 0.1, border_width + 0.1, "100% - "..(border_width * 2 + 0.2), "100% - "..(border_width * 2 + 0.2))
                imfs.column(0, 0, "100%", 100)
                    :gap(0.1)
                    
                    if state.join_error() then
                        imfs.hypertext(0, 0, "100%", 0.75, "<global valign=middle color=#9d5b5b>"..core.hypertext_escape(state.server_connection_error.msg))
                    end
                    
                    if joining == true then
                        imfs.hypertext(0, 0, "100%", 0.75, "<global valign=middle halign=center color=#aaa>Joining server...")
                        imfs.group(0, 0, "100%", 1.05)
                            imfs.image(0, 0.3, "100%", "100% - 0.3", theme.get_background_image("field"))
                            imfs.label(0.2, 0.125, "Address")
                            imfs.field(0.1, 0.3, "70% - 0.2", "100% - 0.3")
                                :onchange(function(value)
                                    state.address(value)
                                end)
                                :onenter(function()
                                    join_server(state)
                                end)
                            imfs.box("70% - 0.05", 0.3, 0.1, "100% - 0.3", theme.styles.field.border_color)
                            imfs.label("70% + 0.2", 0.125, "Port")
                            imfs.field("70% + 0.1", 0.3, "30% - 0.2", "100% - 0.3")
                                :onchange(function(value)
                                    state.port(value)
                                end)
                                :onenter(function()
                                    join_server(state)
                                end)
                        imfs.group_end()
                    else
                        imfs.hypertext(0, 0, "100%", 0.75, "<global valign=middle halign=center color=#aaa>Joining <b>"..core.hypertext_escape(joining.name or joining.address..":"..joining.port).."</b>...")
                    end
                    
                    theme.field(0, 0, "100%", 0.75, "Username", state.username)
                        :onchange(function(value)
                            state.username(value)
                        end)
                        :onenter(function()
                            join_server(state)
                        end)
                    
                    theme.field(0, 0, "100%", 0.75, "Password", "")
                        :password()
                        :onchange(function(value)
                            state.password(value)
                        end)
                        :onenter(function()
                            join_server(state)
                        end)
                    
                    imfs.row(0, 0, "100%", 0.75)
                        :gap(0.1)
                        
                        imfs.button(0, 0, "1x", "100%", "Cancel")
                            :onclick(function()
                                state.joining(false)
                            end)
                        imfs.button(0, 0, "1x", "100%", "Join")
                            :onclick(function()
                                join_server(state)
                            end)
                    imfs.row_end()
                imfs.column_end()
            imfs.scroll_container_end()
        imfs.group_end()
    elseif state.detail_view() then
        imfs.box(0, 0, "100%", "100%", "#0008")
        imfs.button(0, 0, "100%", "100%")
            :styles(styles_no_background)
            :onclick(function()
                state.detail_view(false)
            end)
        
        imfs.image("20%", "20%", "60%", "60%", theme.get_background_image("container"))
        imfs.group("20% + "..border_width, "20% + "..border_width, "60% - "..(border_width * 2), "60% - "..(border_width * 2))
            imfs.scroll_container(0, 0, "100%", "100% - 0.8")
                :scrollbar("100% - 0.5", 0, 0.5, "100% - 0.8", "vertical")
                local list = state.detail_view() == "mod_list" and detail.mods or detail.clients_list
                for i = 0, #list - 1 do
                    imfs.button(0, i * 0.5, "100%", 0.5)
                        :style({
                            border = false,
                            bgimg = "[fill:1x1:0,0:#fff",
                            bgimg_middle = 0,
                            bgcolor = i % 2 == 1 and theme.styles.container.bg_color or theme.styles.container.bg_color_alt,
                        })
                        :style("hovered", style_borderless_hovered)
                        :style("pressed", style_borderless_hovered)
                    -- This isn't just the button label because we want left-alignment.
                    imfs.arealabel(0.1, i * 0.5 + 0.125, "100%", 0.375, list[i + 1])
                end
            imfs.scroll_container_end()
            imfs.button(0, "100% - 0.75", "100%", 0.75, "Close")
                :onclick(function()
                    state.detail_view(false)
                end)
        imfs.group_end()
    end
    
    return imfs.end_()
end
