local mod_name = minetest.get_current_modname()
local mod_path = minetest.get_modpath(mod_name)
local S = minetest.get_translator(mod_name)
local save_path = minetest.get_worldpath()

pmb_gamemodes = {}
local pl = {}

if true then
    local folder_path = save_path..DIR_DELIM.."pmb_gamemodes"
    minetest.mkdir(folder_path)
end

local _on_start = {}
local _on_end = {}

local gamemode_tags = {}

local function player_filename(playername)
    local sha1 = minetest.sha1(playername)
    sha1 = string.sub(sha1, 1, 12)
    return sha1
end

local function save_player(name)
    local file_path = save_path..DIR_DELIM.."pmb_gamemodes"..DIR_DELIM..player_filename(name)..".txt"
    local file = io.open(file_path, "w")
    if not file then return end

    file:write(minetest.serialize(pl[name]))
    file:close()
end

local function load_player(name)
    -- error(player_filename(name))
    local file_path = save_path..DIR_DELIM.."pmb_gamemodes"..DIR_DELIM..player_filename(name)..".txt"
    local file = io.open(file_path, "r")
    if not file then
        -- error("NO FILE")
        return false end

    local text = file:read("a")
    local data = minetest.deserialize(text)
    file:close()
    -- minetest.log(dump(data))

    if (not data) or (text == "return nil") then
        -- error("NO DATA")
        minetest.log("no saved data") return false end

    pl[name] = data
    -- error(dump(data))
    return true
end

local function check_player(name)
    if pl[name] then
        return true
    end

    local saved = load_player(name)
    if saved then
        if not pl[name].stats then pl[name].stats = {} end
        return true
    end

    -- nothing saved so give a blank slate
    pl[name] = {
        gamemode = "survival",
        inv = {--[[
            survival = { -- example
                outside={}, -- what you had before this gamemode
                inside={}, -- what you had inside this gamemode
            },]]
        },
        stats = {},
    }
    -- save_player(name)
    return false
end

function pmb_gamemodes.register_on_start_gamemode(gamemode, func)
    if not _on_start[gamemode] then _on_start[gamemode] = {} end
    _on_start[gamemode][#_on_start[gamemode]+1] = func
end
function pmb_gamemodes.register_on_end_gamemode(gamemode, func)
    if not _on_end[gamemode] then _on_end[gamemode] = {} end
    _on_end[gamemode][#_on_end[gamemode]+1] = func
end


function pmb_gamemodes.start_gamemode(gamemode, player)
    for i, func in ipairs(_on_start[gamemode] or {}) do
        func(player)
    end
end
function pmb_gamemodes.end_gamemode(gamemode, player)
    for i, func in ipairs(_on_end[gamemode] or {}) do
        func(player)
    end
end


local function delete_entire_inventory(player)
    local inv = player:get_inventory()
    local lists = inv:get_lists()
    local empty = ItemStack("")
    for listname, list in pairs(lists) do
        for i=0, inv:get_size(listname) do
            inv:set_stack(listname, i, empty)
        end
    end
end

local function get_saveable_inventory(player)
    local ret = {}
    local inv = player:get_inventory()
    local lists = inv:get_lists()
    local empty_count = 0
    for listname, list in pairs(lists) do
        if inv:is_empty(listname) then
            ret[listname] = nil
            empty_count = empty_count + 1
        else
            ret[listname] = {}
            for i=0, inv:get_size(listname) do
                local stack = inv:get_stack(listname, i)
                ret[listname][#ret[listname]+1] = stack:to_string()
            end
        end
    end
    -- optimisation to not store tons of stuff if inv is empty
    if #lists == empty_count then
        return nil
    end
    return ret
end

local function load_inventory(player, invobj)
    if not invobj then return end
    local inv = player:get_inventory()
    for listname, list in pairs(invobj) do
        for i, itemstring in ipairs(list) do
            local stack = ItemStack(itemstring)
            inv:set_stack(listname, i-1, stack)
            list[i] = ""
        end
    end
end

local function get_stats(player)
    return {
        hp = player:get_hp(),
    }
end

local function load_stats(player, data)
    if not data then data = {} end
    player:set_hp(data.hp or 20, "gamemode")
end

local function handle_inventory_replacement(player, gamemode, is_leaving_this_gamemode)
    local tags_for_gm = gamemode_tags[gamemode]
    if (not tags_for_gm) or (not tags_for_gm.replace_inventory) then return end
    local name = player:get_player_name()

    if not pl[name].inv[gamemode] then
        pl[name].inv[gamemode] = {}
    end

    if is_leaving_this_gamemode then
        pl[name].inv[gamemode].inside = get_saveable_inventory(player)
        delete_entire_inventory(player)
        load_inventory(player, pl[name].inv[gamemode].outside)
        pl[name].inv[gamemode].outside = nil
    elseif (not is_leaving_this_gamemode) then
        pl[name].inv[gamemode].outside = get_saveable_inventory(player)
        delete_entire_inventory(player)
        if tags_for_gm.persistent_inside_inventory then
            load_inventory(player, pl[name].inv[gamemode].inside)
        end
        pl[name].inv[gamemode].inside = nil
    end
end

local function handle_stat_replacement(player, gamemode, is_leaving_this_gamemode)
    local tags_for_gm = gamemode_tags[gamemode]
    if (not tags_for_gm) or (not tags_for_gm.replace_stats) then return end
    local name = player:get_player_name()

    if not pl[name].stats[gamemode] then
        pl[name].stats[gamemode] = {}
    end

    if is_leaving_this_gamemode then
        pl[name].stats[gamemode].inside = get_stats(player)
        load_stats(player, pl[name].stats[gamemode].outside)
    elseif (not is_leaving_this_gamemode) then
        pl[name].stats[gamemode].outside = get_stats(player)
        if tags_for_gm.persistent_inside_stats then
            load_stats(player, pl[name].stats[gamemode].inside)
        else
            load_stats(player, {})
        end
    end
end

function pmb_gamemodes.set_gamemode(player, gamemode)
    local name = player:get_player_name()

    -- VERY important to prevent destroying entire inventories
    if pl[name].gamemode == gamemode then return end

    handle_inventory_replacement(player, pl[name].gamemode, true)
    pmb_gamemodes.end_gamemode(pl[name].gamemode, player)
    handle_stat_replacement(player, pl[name].gamemode, true)

    pl[name].gamemode = gamemode

    handle_inventory_replacement(player, gamemode, false)
    pmb_gamemodes.start_gamemode(gamemode, player)
    handle_stat_replacement(player, gamemode, false)

    save_player(name)
end

function pmb_gamemodes.get_gamemode(player)
    if not player then return end
    local name = player:get_player_name()
    check_player(name)
    return pl[name].gamemode
end

minetest.register_on_joinplayer(function(player, last_login)
    local name = player:get_player_name()

    if load_player(name) and (not last_login) then
        minetest.kick_player(name, "Someone has already registered with the cryptographic signature of your name. Choose another name!\
            \nThis is hilariously unlikely, but I guess you're just super lucky.")
        minetest.remove_player(name)
        minetest.remove_player_auth(name)
        minetest.log(player_filename(name))
        return
    end

    check_player(name)

    -- delay because other functions will be using register_on_joinplayer
    minetest.after(0.1, function ()
        if pl[name] and player then
            pmb_gamemodes.start_gamemode(pl[name].gamemode, player)
        end
    end)
end)

-- don't OOM if 1000 players join over a week of uptime
minetest.register_on_leaveplayer(function(player, timed_out)
    local name = player:get_player_name()
    if pl[name] then
        save_player(name)
        pl[name] = nil
    end
end)

function pmb_gamemodes.player_has_tag(player, tag)
    local name = player:get_player_name()
    check_player(name)
    local gm = pl[name].gamemode
    return (gamemode_tags[gm] or {})[tag]
end

-- returns a list with gamemode = index list of players
function pmb_gamemodes.get_all_player_gamemodes()
    local ret = {}
    for name, data in pairs(pl) do
        if not ret[data.gamemode] then ret[data.gamemode] = {} end
        ret[data.gamemode][#ret[data.gamemode]+1] = name
    end
    return ret
end

function pmb_gamemodes.get_gamemode_tags(gamemode)
    return gamemode_tags[gamemode] or {}
end

function pmb_gamemodes.add_gamemode_tags(gamemode, tags)
    if not gamemode_tags[gamemode] then gamemode_tags[gamemode] = {} end
    for i, v in pairs(tags) do
        gamemode_tags[gamemode][i] = v
    end
end


pmb_gamemodes.add_gamemode_tags("survival", {
    inventory = true,
    crafting = true,
    damage = true,
    visible_to_mobs = true,
})
pmb_gamemodes.add_gamemode_tags("creative", {
    inventory = true,
    crafting = true,
    -- damage = false,
    -- visible_to_mobs = false,
})
pmb_gamemodes.add_gamemode_tags("deathmatch", {
    inventory = true,
    replace_inventory = true,
    persistent_inside_inventory = true,
    replace_stats = true,
    -- crafting = false,
    damage = true,
    visible_to_mobs = true,
})

pmb_gamemodes.register_on_start_gamemode("deathmatch", function(player)
    pmb_hud.remove_hud(player, "hotbar_bg")
end)
pmb_gamemodes.register_on_end_gamemode("deathmatch", function(player)
    pmb_hud.reset_hud(player, "hotbar_bg")
end)
