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_change = {}
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(player)
    player:get_meta():set_string("pmb_gamemodes", minetest.serialize(pl[player]))
end

local function load_player(player)
    local text = player:get_meta():get_string("pmb_gamemodes")
    local data = minetest.deserialize(text)

    if (not data) or (text == "") then return false end

    pl[player] = data
    return true
end

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

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

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

function pmb_gamemodes.register_on_gamemode_changed(func)
    _on_change[#_on_change+1] = func
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.on_change_gamemode(player, from, to)
    for i, func in ipairs(_on_change) do
        func(player, from, to)
    end
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

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

    if is_leaving_this_gamemode then
        pl[player].inv[gamemode].inside = get_saveable_inventory(player)
        delete_entire_inventory(player)
        load_inventory(player, pl[player].inv[gamemode].outside)
        pl[player].inv[gamemode].outside = nil
    elseif (not is_leaving_this_gamemode) then
        pl[player].inv[gamemode].outside = get_saveable_inventory(player)
        delete_entire_inventory(player)
        if tags_for_gm.persistent_inside_inventory then
            load_inventory(player, pl[player].inv[gamemode].inside)
        end
        pl[player].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

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

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

function pmb_gamemodes.set_gamemode(player, gamemode)
    -- VERY important to prevent destroying entire inventories
    if pl[player].gamemode == gamemode then return end

    pmb_gamemodes.on_change_gamemode(player, pl[player].gamemode, gamemode)

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

    pl[player].gamemode = gamemode

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

    save_player(player)
end

function pmb_gamemodes.get_gamemode(player)
    if not player then return end
    check_player(player)
    return pl[player].gamemode
end

minetest.register_on_joinplayer(function(player, last_login)
    load_player(player)

    check_player(player)

    -- delay because other functions will be using register_on_joinplayer
    minetest.after(0.1, function ()
        if pl[player] and player then
            pmb_gamemodes.start_gamemode(pl[player].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)
    if pl[player] then
        save_player(player)
        pl[player] = nil
    end
end)

function pmb_gamemodes.player_has_tag(player, tag)
    if not player then return false end
    check_player(player)
    local gm = pl[player].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,
    persistent_inside_inventory = true,
    visible_to_mobs = true,
})
pmb_gamemodes.add_gamemode_tags("creative", {
    inventory = true,
    crafting = true,
    creative = true,
    persistent_inside_inventory = 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)
