

aom_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)
    local pi = pl[player]
    if not pi then pi = {
        armor = {},
        blocking = 0.5,
        block_callback = nil,
    }; pl[player] = pi end
    return pi
end

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



function aom_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 aom_combat.attack.declare_blocking(player, time, callback)
    local pi = pl[player]
    pi.blocking = time
    if callback then
        pi.block_callback = callback
    else
        local stack = player:get_wielded_item()
        local idef = stack and stack:get_definition()
        if idef and idef._can_block_attack then
            pi.block_callback = idef._can_block_attack
        end
    end
end


function aom_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
                aom_combat.attack.declare_blocking(player, 0, nil)
                ret = true
            end

            return ret
        end
    end
    return false
end


function aom_combat.attack.is_blocking(object, attackdef)
    if not object then return true end
    if object:is_player() then
        return aom_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 aom_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 aom_combat.attack.get_atk_def_from_itemstack(itemstack)
    local atk = {}
    local idef = itemstack:get_definition()
    if idef._aom_combat_get_attack then
        atk = idef._aom_combat_get_attack(itemstack)
    end
    atk = fix_attack(atk)
    return atk
end


function aom_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 aom_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 = aom_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)
        aom_combat.attack.get_player_armor(victim)
        v_armor = pl[victim].armor
    else
        v_armor = v_ent._aom_combat_armor or {}
    end

    local a_armor
    if minetest.is_player(attacker) then
        check_player(attacker)
        aom_combat.attack.get_player_armor(attacker)
        a_armor = pl[attacker].armor
    else
        a_armor = table.copy(a_ent._aom_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(attacker, stack, 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(victim, stack, atk) or atk
        end
    end

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

    if v_ent._on_attack_received then
        atk = v_ent._on_attack_received(v_ent, itemstack, 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 = aom_combat.attack.is_blocking(victim, atk)
    if is_blocked then
        local idef = itemstack and minetest.registered_items[itemstack:get_name()]
        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
        elseif minetest.is_player(victim) then
            local wstack = victim:get_wielded_item()
            local wdef = wstack and wstack:get_definition()
            if wdef and wdef._on_block_attack then
                itemstack = wdef._on_block_attack(wstack, victim, atk) or itemstack
            end
        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
        if v_ent._on_damage_received then
            maxdmg = v_ent._on_damage_received(v_ent, a_ent, maxdmg) or maxdmg
        end
        if maxdmg > 0 then
            victim:set_hp(math.max(hp - maxdmg, 1), "combat")
        end
    end

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

function aom_combat.attack.get_victim_with_raycast(itemstack, attacker, dist, dir, filter, flags)
    itemstack = ItemStack(itemstack)
    local pos1
    local attacker_obj
    if attacker:is_player() then
        pos1 = aom_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)

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

-- attacker is player or luaentity
function aom_combat.attack.blindly_attack_with_raycast(itemstack, attacker, dist, dir, filter, flags)
    if not attacker then return end
    local victim = aom_combat.attack.get_victim_with_raycast(itemstack, attacker, dist, dir, filter, flags)
    if not victim then return end
    return aom_combat.attack.send_attack(itemstack, attacker, victim, flags)
end


local on_globalstep = function(dtime)
    for i, player in ipairs(minetest.get_connected_players()) do
        aom_combat.attack.player_tick(player, dtime)
    end
end
minetest.register_globalstep(on_globalstep)



function aom_combat.attack.player_tick(player, dtime)
    local pi = check_player(player)
    if pi.blocking > 0 then
        pi.blocking = pi.blocking - dtime
    else
        pi.block_callback = nil
    end
end




