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

local sfx = aom_statusfx.registered_status_effects
local pl = aom_statusfx.player_effects


function aom_statusfx.register_status(name, def)
    if def.fx_use_player_functions_for_objects then
        def.on_apply_object = def.on_apply_player
        def.on_timeout_object = def.on_timeout_player
        def.on_remove_object = def.on_remove_player
    end
    sfx[name] = def
end


function aom_statusfx.load_player_data(player)
    return minetest.deserialize(player:get_meta():get_string("aom_statusfx")) or {}
end

function aom_statusfx.save_player_data(player)
    if (not player) then return end
    if not pl[player] then pl[player] = aom_statusfx.load_player_data(player) end
    player:get_meta():set_string("aom_statusfx", minetest.serialize(pl[player]))
end


local messages = {
    "            hey you, you're finally awake...",
    "            you were trying to cross the border, weren't you",
    "            just like us, and that thief over there",
}
aom_statusfx.register_status("awakening", {
    ----
    fx_allow_player = true,
    fx_allow_object = false,
    -- if true, counts down one stack then the next, rather than all of them, allows stacking time up infinitely
    fx_only_tick_once = false,
    fx_only_apply_once = true,
    fx_timeout_individual = true,
    ----
    on_apply_player = function(object, count, meta)
        meta.next_trigger = meta.time - 1
        meta.next_message = (meta.next_message or 1)
        return true -- false to prevent apply
    end,
    on_remove_player = function(object, count, meta)
        return false -- true to reset timer
    end,
    on_step_player = function(object, count, meta, dtime)
        if meta.next_trigger and meta.time
        and meta.time < meta.next_trigger then
            minetest.chat_send_player(object:get_player_name(), messages[meta.next_message or 1] or "")
            meta.next_trigger = meta.next_trigger - 1
            meta.next_message = (meta.next_message or 1) + 1
        end
        return false -- true to remove
    end,
    ----
})

local function check_player(player)
    local pi = pl[player]
    if not pi then
        pi = {}
        pl[player] = pi
    end
    return pi
end

local function check_player_status_effect(player, status_name)
    local pi = check_player(player)
    if status_name and not pi[status_name] then
        pi[status_name] = {
            meta = {time=0},
            stack = {}
        }
    end
    return pi[status_name]
end

function aom_statusfx.reapply_status_on_join(player)
    local pi = check_player(player)
    for status_name, def in pairs(pi) do
        local meta = def.meta
        local stack_count = #def.stack
        local sdef = sfx[status_name]
        if stack_count == 1 or not sdef.fx_only_apply_once then
            sdef.on_apply_player(player, stack_count, meta, {is_reapply = true})
        end
    end
end

function aom_statusfx.apply_status(object, status_name, duration, params)
    local sdef = sfx[status_name]
    if not sdef then
        minetest.log("warning", "No status with name "..dump(status_name))
        return false end

    local husk = {
        time = duration or 1
    }

    if minetest.is_player(object) then
        local player = object
        -- make sure the player is in the system and has this status_name as a key so you don't `nil[status_name]` later
        check_player_status_effect(player, status_name)

        -- the stack contains all effects and their timeouts {time=3}. Could contain more info in future like source
        local stack = pl[player][status_name].stack
        if params and params.no_stack then
            stack[math.max(1,#stack)] = husk
        else
            stack[#stack+1] = husk
        end

        local no_apply
        -- contains persistent information about the effect, such as the total time / longest duration
        local meta = pl[player][status_name].meta
        if #stack == 1 or not sdef.fx_only_apply_once then
            no_apply = sdef.on_apply_player(object, #stack, meta, params or {})
            if no_apply then
                table.remove(stack, #stack)
            end
        end

        if not no_apply then
            if meta.time < duration then
                meta.time = duration
            end
            -- show the effect on the hud if it has a hud defined
            aom_statusfx.hud.add_hud(object, status_name)
        end
    else
        local ent = object:get_luaentity()
        -- only apply to mobs that allow it explicitly
        if ent and not ent._aom_statusfx_enable then return false end

        local can_save_static = (ent._aom_staticdata_load_list ~= nil)
        if can_save_static then
            ent._aom_staticdata_load_list[#ent._aom_staticdata_load_list+1] = "_aom_status_effects"
        end

        if not ent._aom_status_effects then ent._aom_status_effects = {} end
        if not ent._aom_status_effects[status_name] then
            ent._aom_status_effects[status_name] = {stack={}, meta={time=duration}}
        end

        -- the stack contains all effects and their timeouts {time=3}. Could contain more info in future like source
        local stack = ent._aom_status_effects[status_name].stack
        if params and params.no_stack then
            stack[math.max(1,#stack)] = husk
        else
            stack[#stack+1] = husk
        end

        local no_apply
        -- contains persistent information about the effect, such as the total time / longest duration
        local meta = ent._aom_status_effects[status_name].meta
        local old_meta_time = meta.time

        if #stack == 1 or not sdef.fx_only_apply_once then
            -- ask the effect whether it should affect this mob, and do its init stuff
            no_apply = sdef.on_apply_object(object, #stack, meta, params or {})
            -- if the mob has a function deciding if it should allow statuses, then check that
            if (not no_apply) and ent._aom_statusfx_on_apply then
                meta.time = duration
                no_apply = ent._aom_statusfx_on_apply(ent, status_name, meta, params or {})
            end
            if no_apply then
                stack[#stack] = nil
                meta.time = old_meta_time
            end
        end
    end
end

function aom_statusfx.player_has_status(player, status_name)
    local pi = pl[player]
    if pi and pi[status_name] and #pi[status_name].stack > 0 then
        return pi[status_name].meta or {}
    else
        return false
    end
end

-- forcibly remove status, but notify the status of this
function aom_statusfx.remove_status(player, status_name, no_notify)
    if not aom_statusfx.player_has_status(player, status_name) then return end
    local sdef = sfx[status_name]
    local stack = pl[player][status_name].stack

    local meta = pl[player][status_name].meta
    local count = #stack

    for i=#stack, 1, -1 do
        if sdef.fx_only_tick_once == true and i < #stack then break end
        if sdef.fx_timeout_individual then
            if not no_notify then sdef.on_remove_player(player, count, meta, true) end
        end
        table.remove(stack, i)
    end

    if (not sdef.fx_timeout_individual) then
        if not no_notify then sdef.on_remove_player(player, #stack, meta, true) end
        aom_statusfx.hud.remove_hud(player, status_name)
    end
    pl[player][status_name] = nil
end


function aom_statusfx.tick_player(player, dtime)
    for status_name, status_data in pairs(pl[player]) do
        local meta = pl[player][status_name].meta
        meta.time = meta.time - dtime

        local stack = status_data.stack
        local count = #stack
        local sdef = sfx[status_name]
        if not sdef then
            pl[player][status_name] = nil
            minetest.log("warning", "Removing status effect save data for "..status_name.." because there is no definition for it")
            goto continue2
        end

        local to_remove
        if sdef.on_step_player and count > 0 then
            to_remove = sdef.on_step_player(player, count, meta, dtime)
        end

        -- loop backward to make sure you don't upset the indexes
        for i=#stack, 1, -1 do
            if sdef.fx_only_tick_once == true and i < #stack then break end

            stack[i].time = stack[i].time - dtime
            if stack[i].time <= 0 then
                if sdef.fx_timeout_individual then
                    sdef.on_remove_player(player, count, meta)
                end
                table.remove(stack, i)
            end
        end

        if (not sdef.fx_timeout_individual) and (count ~= #stack) and (#stack == 0) then
            sdef.on_remove_player(player, #stack, meta)
            aom_statusfx.hud.remove_hud(player, status_name)
        end

        if #stack == 0 then
            pl[player][status_name] = nil
        end
        ::continue2::
    end
    -- update the hud
    aom_statusfx.hud.update_hud(player)
end


function aom_statusfx.remove_all_statuseffects(player)
    check_player_status_effect(player)
    for status_name, status_data in pairs(pl[player]) do
        for i, v in ipairs(status_data.stack) do
            status_data.stack[i].time = 0
        end
    end
    aom_statusfx.tick_player(player, 0.1)
end


local _timer = 2
function aom_statusfx.on_step(dtime)
    -- tick all effects for players
    for player, nss in pairs(pl) do
        if not player then goto continue end
        aom_statusfx.tick_player(player, dtime)
        ::continue::
    end

    if _timer < 0 then
        _timer = 2
        for i, player in ipairs(minetest.get_connected_players()) do
            aom_statusfx.save_player_data(player)
        end
    else
        _timer = _timer - dtime
    end
end

local function tick_entities(dtime)
    for j, object in ipairs(minetest.get_objects_in_area(vector.new(-30912,-30912,-30912), vector.new(30927,30927,30927))) do
        local ent = object:get_luaentity()
        if ent and ent._aom_status_effects then
            for status_name, status_data in pairs(ent._aom_status_effects) do
                local stack = status_data.stack
                local count = #stack
                local sdef = sfx[status_name]
                if not sdef then
                    ent._aom_status_effects[status_name] = nil
                    minetest.log("warning", "Removing status effect save data for "..status_name.." because there is no definition for it")
                    goto continue
                end

                local meta = ((#stack > 0) and stack[1]) or {}

                local to_remove
                if sdef.on_step_object and count > 0 then
                    to_remove = sdef.on_step_object(object, count, meta, dtime)
                end

                local rem = {}
                for i=1, #stack do
                    if sdef.fx_only_tick_once == true and i > 1 then break end

                    stack[i].time = stack[i].time - dtime
                    if stack[i].time <= 0 then
                        if i == 1 and stack[2] ~= nil then
                            local t = stack[2].time
                            stack[2] = table.copy(meta)
                            stack[2].time = t
                        end
                        rem[#rem+1] = i
                        if sdef.fx_timeout_individual then
                            sdef.on_remove_object(object, count, meta)
                        end
                    end
                end

                for k,i in ipairs(rem) do
                    table.remove(stack, i)
                    if #stack == 0 then
                    end
                end

                if (not sdef.fx_timeout_individual) and count ~= #stack and #stack == 0 then
                    sdef.on_remove_object(object, count, meta)
                    aom_statusfx.hud.remove_hud(object, status_name)
                end

                ent._aom_status_effects[status_name] = status_data
                ::continue::
            end
        end
    end
end

minetest.register_globalstep(function(dtime)
    aom_statusfx.on_step(dtime)
    tick_entities(dtime)
end)

minetest.register_on_joinplayer(function(player, last_login)
    pl[player] = aom_statusfx.load_player_data(player)
    aom_statusfx.reapply_status_on_join(player)
end)

minetest.register_on_leaveplayer(function(player, timed_out)
    aom_statusfx.save_player_data(player)
end)

minetest.register_on_dieplayer(function(player, reason)
    aom_statusfx.remove_all_statuseffects(player)
end)

if minetest.get_modpath("aom_gamemodes") then
    aom_gamemodes.add_gamemode_tags("gm_windmills", {
        replace_statuseffects = true
    })
    aom_gamemodes.add_gamemode_tags("survival", {
        persistent_statuseffects = true
    })

    aom_gamemodes.register_on_gamemode_changed(function(player, from, to)
        local meta = player:get_meta()

        local fromtags = aom_gamemodes.get_gamemode_tags(from)
        local totags = aom_gamemodes.get_gamemode_tags(to)

        if (not fromtags.replace_statuseffects) and (not totags.replace_statuseffects) then
            return
        end

        -- if the gamemode you came from wants to save data
        if fromtags.persistent_statuseffects then
            meta:set_string("pmbsfx_gm_"..from, minetest.serialize(pl[player]))
        end

        -- if the gamemode you're going to wants to use saved data and save it
        local new
        if totags.persistent_statuseffects then
            new = minetest.deserialize(meta:get_string("pmbsfx_gm_"..to))
        elseif totags.replace_statuseffects then
            new = {}
        end

        aom_statusfx.hud.remove_all_huds(player)
        aom_statusfx.remove_all_statuseffects(player)
        pl[player] = new
        aom_statusfx.save_player_data(player)
        aom_statusfx.hud.refresh_hud(player)
    end)
end
