

pmb_combat.attack = {}


--[[
{
    -- multipliers is applied to this, then the max is used to actually reduce HP.
    base_damage = {
        sharp = 5
    },
    -- applied at the very end of an attack after everything else
    multiplier = {
        _crit = 2,
        sharp = 1,
    },
    knockback_mult = 1,
}
]]


local pl = {}

local function check_player(player)
    if not pl[player] then pl[player] = {
        armor = {},
        blocking = 0.5,
        block_callback = nil,
    } end
end

minetest.register_on_joinplayer(function(player, last_login)
    check_player(player)
end)



function pmb_combat.attack.get_eyepos(player)
    local eyepos = vector.add(player:get_pos(), vector.multiply(player:get_eye_offset(), 0.1))
    eyepos.y = eyepos.y + player:get_properties().eye_height
    return eyepos
end


---------------
-- BLOCKING
---------------

--#region BLOCKING
function pmb_combat.attack.declare_blocking(player, time, callback)
    local pi = pl[player]
    pi.blocking = time
    pi.block_callback = callback
end


function pmb_combat.attack.is_player_blocking(player, attackdef)
    local pi = pl[player]
    if pi.blocking > 0 then
        if pi.block_callback then
            local ret = pi.block_callback(player, attackdef)

            -- nil == destory after blocking
            if ret == nil then
                pmb_combat.attack.declare_blocking(player, 0, nil)
                ret = true
            end

            return ret
        end
    end
    return false
end


function pmb_combat.attack.is_blocking(object, attackdef)
    if not object then return true end
    if object:is_player() then
        return pmb_combat.attack.is_player_blocking(object, attackdef)
    else
        if object.object then object = object.object end -- genius
        local ent = object:get_luaentity()
        local blocking = (ent and ent._is_blocking) and ent._is_blocking(ent, attackdef)
        blocking = blocking or (ent and ent._blocking or 0 > 0)
        return blocking
    end
end

--#endregion




function pmb_combat.attack.get_armor(player)
end



---------------
-- ATTACKING
---------------

local function fix_attack(atk)
    if not atk then atk = {} end
    if not atk.multiplier then atk.multiplier = {} end
    if not atk.base_damage then atk.base_damage = {blunt=1} end
    if not atk.effects then atk.effects = {} end
    if not atk.knockback_mult then atk.knockback_mult = 1 end
    return atk
end


function pmb_combat.attack.get_atk_def_from_itemstack(itemstack)
    local atk = {}
    local idef = itemstack:get_definition()
    if idef._pmb_combat_get_attack then
        atk = idef._pmb_combat_get_attack(itemstack)
    end
    atk = fix_attack(atk)
    return atk
end


function pmb_combat.attack.get_player_armor(player)
    local inv = player:get_inventory()
    local lists = inv:get_lists()
    local pi = pl[player]
    pi.armor = {}
    if lists["armor"] then
        for i, stack in ipairs(lists["armor"]) do
            pi.armor[#pi.armor+1] = stack
        end
    end
end


function pmb_combat.attack.send_attack(itemstack, attacker, victim, flags)
    if (not attacker) or (not victim) then return end
    if not flags then flags = {} end

    -- allow overriding attack
    local atk = {}
    if flags.attack then
        atk = fix_attack(flags.attack)
    else
        atk = pmb_combat.attack.get_atk_def_from_itemstack(ItemStack(itemstack or ""))
    end

    atk = fix_attack(atk)

    if not atk then
        return itemstack
    end

    atk.attacker = attacker
    atk.victim = victim

    local a_ent = (attacker.get_luaentity and attacker:get_luaentity()) or attacker
    local v_ent = (victim.get_luaentity and victim:get_luaentity()) or victim

    local v_armor
    if minetest.is_player(victim) then
        check_player(victim)
        pmb_combat.attack.get_player_armor(victim)
        v_armor = pl[victim].armor
    else
        v_armor = v_ent._pmb_combat_armor or {}
    end

    local a_armor
    if minetest.is_player(attacker) then
        check_player(attacker)
        pmb_combat.attack.get_player_armor(attacker)
        a_armor = pl[attacker].armor
    else
        a_armor = table.copy(a_ent._pmb_combat_armor or {})
    end

    if itemstack then
        a_armor[#a_armor+1] = itemstack:to_string()
    end

    for i, armorstack in ipairs(a_armor) do
        local stack = ItemStack(armorstack)
        local def = stack:get_definition()
        if def and def._on_attack_sent then
            atk = def._on_attack_sent(stack, attacker, atk) or atk
        end
    end
    for i, armorstack in ipairs(v_armor) do
        local stack = ItemStack(armorstack)
        local def = stack:get_definition()
        if def and def._on_attack_received then
            atk = def._on_attack_received(stack, victim, atk) or atk
        end
    end

    if a_ent._on_attack_sent then
        atk = a_ent._on_attack_sent(itemstack, a_ent, atk) or atk
    end

    if v_ent._on_attack_received then
        atk = v_ent._on_attack_received(itemstack, v_ent, atk) or atk
    end

    -- apply mults
    local maxdmg = 0
    for t, v in pairs(atk.base_damage) do
        atk.base_damage[t] = (atk.multiplier[t] or 1) * atk.base_damage[t]
        if atk.base_damage[t] > maxdmg then maxdmg = atk.base_damage[t] end
    end

    local is_blocked = pmb_combat.attack.is_blocking(victim, atk)
    if is_blocked then
        local idef = itemstack and itemstack:get_definition()
        if idef and idef._on_block_attack then
            itemstack = idef._on_block_attack(ItemStack(itemstack), victim, atk) or itemstack
        elseif v_ent and v_ent._on_block_attack then
            itemstack = v_ent._on_block_attack(ItemStack(itemstack), victim, atk) or itemstack
        end

        if a_ent and a_ent._on_attack_blocked then
            itemstack = a_ent._on_sent_attack_blocked(ItemStack(itemstack), victim, atk) or itemstack
        end
        return false
    end

    local apos = attacker:get_pos()
    local vpos = victim:get_pos()
    vpos.y = apos.y
    local dir = vector.direction(apos, vpos)

    -- change hp
    local hp = victim:get_hp()
    if hp - maxdmg <= 0 then
        victim:set_hp(1, "combat")
        if v_ent._on_death then
            v_ent:_on_death(atk)
        else
            -- force them to die by punch so that the other systems work as intended
            -- TODO: fix this affecting dead mobs, by 
            victim:set_armor_groups({
                system = 100
            })
            victim:punch(attacker, 1, {
                full_punch_interval = 1,
                damage_groups = {system=100},
            }, dir)
        end
    else
        victim:set_hp(hp - maxdmg, "combat")
    end

    dir.y = 0.6
    victim:add_velocity(vector.multiply(dir, atk.knockback_mult))
end



-- attacker is player or luaentity
function pmb_combat.attack.blindly_attack_with_raycast(itemstack, attacker, dist, dir, filter, flags)
    if not attacker then return end
    itemstack = ItemStack(itemstack)
    local pos1
    local attacker_obj
    if attacker:is_player() then
        pos1 = pmb_combat.attack.get_eyepos(attacker)
        if not dir then dir = attacker:get_look_dir() end
        attacker_obj = attacker
    else
        pos1 = attacker.object:get_pos()
        attacker_obj = attacker.object
    end

    local pos2 = pos1 + (dir * dist)
    local ray = minetest.raycast(pos1, pos2, true, false)

    local victim
    for pointed_thing in ray do
        if pointed_thing.ref and (pointed_thing.ref ~= attacker_obj) and ((not filter) or filter(pointed_thing)) then
            victim = pointed_thing.ref
            break
        end
    end

    if not victim then return end

    return pmb_combat.attack.send_attack(itemstack, attacker, victim, flags)
end



minetest.register_globalstep(function(dtime)
    for i, player in ipairs(minetest.get_connected_players()) do
        pmb_combat.attack.player_tick(player, dtime)
    end
end)



function pmb_combat.attack.player_tick(player, dtime)
    local pi = pl[player]

    if pi.blocking > 0 then
        pi.blocking = pi.blocking - dtime
    else
        pi.block_callback = nil
    end
end




