local AH = wesnoth.require "ai/lua/ai_helper.lua"
local LS = wesnoth.require "location_set"
local M = wesnoth.map

-- Evaluation process:
--
-- Find all enemy units that could be caused to level up by an attack
--  - If only units that would cause them to level up can attack, CA score = 100,010.
--    This means the attack will be done before the default AI attacks, so that AI
--    units do not get used otherwise by the default AI.
--  - If units that would not cause a leveling can also attack, CA score = 99,990,
--    meaning we see whether the default AI attacks that unit with one of those first.
-- We also check whether it is possible to move an own unit out of the way
--
-- Attack rating:
-- 0. If the CTD (chance to die) of the AI unit is larger than the value of
--    aggression for the side, do not do the attack
-- 1. Otherwise, if the attack might result in a kill, do that preferentially:
--     rating = CTD of defender - CTD of attacker
-- 2. Otherwise, if the enemy is poisoned, do not attack (wait for it
--    weaken and attack on a later turn)
-- 3. Otherwise, calculate damage done to enemy (as if it were not leveling) and
--    own unit, expressed in partial loss of unit value (gold) and minimize both.
--    Damage to enemy is minimized because we want to level it with the weakest AI unit,
--    so that we can follow up with stronger units. In addition, use of poison or
--    slow attacks is strongly discouraged. See code for exact equations.

local XP_attack

local ca_attack_highxp = {}

function ca_attack_highxp:evaluation(cfg, data, filter_own)
    -- Note: (most of) the code below is set up to maximize speed. Do not
    -- "simplify" this unless you understand exactly what that means

    local attacks_aspect = ai.aspects.attacks
    local max_unit_level = 0
    local units = {}
    for _,unit in ipairs(attacks_aspect.own) do
        if (unit.attacks_left > 0) and (#unit.attacks > 0) and (not unit.canrecruit) and unit:matches(filter_own) then
            table.insert(units, unit)

            local level = unit.level
            if (level > max_unit_level) then
                max_unit_level = level
            end
        end
    end
    if (not units[1]) then return 0 end

    -- Mark enemies as potential targets if they are close enough to an AI unit
    -- that could trigger them leveling up; this is not a sufficient criterion,
    -- but it is much faster than path finding, so it is done for preselection.
    local target_infos = {}
    for i_t,enemy in ipairs(attacks_aspect.enemy) do
        local enemy_can_advance = true
        if (not enemy.advances_to[1]) and (not wml.get_child(enemy.__cfg, 'advancement')) then
            enemy_can_advance = false
        end

        if enemy_can_advance and AH.is_attackable_enemy(enemy) then
            local XP_to_levelup = enemy.max_experience - enemy.experience
            if (max_unit_level * wesnoth.game_config.combat_experience >= XP_to_levelup) then
                local potential_target = false
                local ind_attackers, ind_other_units = {}, {}
                for i_u,unit in ipairs(units) do
                    if (M.distance_between(enemy.x, enemy.y, unit.x, unit.y) <= unit.moves + 1) then
                        if (unit.level * wesnoth.game_config.combat_experience >= XP_to_levelup) then
                            potential_target = true
                            table.insert(ind_attackers, i_u)
                        else
                            table.insert(ind_other_units, i_u)
                        end
                    end
                end

                if potential_target then
                    local target_info = {
                        ind_target = i_t,
                        ind_attackers = ind_attackers,
                        ind_other_units = ind_other_units
                    }
                    table.insert(target_infos, target_info)
                end
            end
        end
    end
    if (not target_infos[1]) then return 0 end

    -- The following location sets are used so that we at most need to call
    -- find_reach() and unit:clone() once per unit
    local reaches = LS.create()
    local attacker_copies = LS.create()

    local aggression = ai.aspects.aggression
    local avoid_map = LS.of_pairs(ai.aspects.avoid)
    local max_ca_score, max_rating, best_attack = 0, 0, nil
    for _,target_info in ipairs(target_infos) do
        wesnoth.interface.handle_user_interact()
        local target = attacks_aspect.enemy[target_info.ind_target]
        local can_force_level = {}
        local attack_hexes = LS.create()
        for xa,ya in wesnoth.current.map:iter_adjacent(target) do
            if (not avoid_map:get(xa, ya)) then
                local unit_in_way = wesnoth.units.get(xa, ya)

                if AH.is_visible_unit(wesnoth.current.side, unit_in_way) then
                    if (unit_in_way.side == wesnoth.current.side) then
                        local uiw_reach
                        if reaches:get(unit_in_way.x, unit_in_way.y) then
                            uiw_reach = reaches:get(unit_in_way.x, unit_in_way.y)
                        else
                            uiw_reach = wesnoth.paths.find_reach(unit_in_way)
                            reaches:insert(unit_in_way.x, unit_in_way.y, uiw_reach)
                        end

                        -- Check whether the unit to move out of the way has an unoccupied hex to move to.
                        -- We do not deal with cases where a unit can move out of the way for a
                        -- unit that is moving out of the way of the initial unit (etc.).
                        local can_move = false
                        for _,uiw_loc in ipairs(uiw_reach) do
                            -- Unit in the way of the unit in the way
                            local uiw_uiw = wesnoth.units.get(uiw_loc[1], uiw_loc[2])
                            if (not AH.is_visible_unit(wesnoth.current.side, uiw_uiw)) then
                                can_move = true
                                break
                            end
                        end
                        if (not can_move) then
                            -- Keep this case as the unit in the way might be a potential attacker
                            attack_hexes:insert(xa, ya, unit_in_way.id)
                        else
                            attack_hexes:insert(xa, ya, 'can_move_away')
                        end
                    end
                else
                    attack_hexes:insert(xa, ya, 'empty')
                end
            end
        end

        attack_hexes:iter(function(xa, ya, occupied)
            for _,i_a in ipairs(target_info.ind_attackers) do
                local attacker = units[i_a]
                if (occupied == 'empty') or (occupied == 'can_move_away') then
                    -- If the hex is not blocked, check all potential attackers
                    local reach
                    if reaches:get(attacker.x, attacker.y) then
                        reach = reaches:get(attacker.x, attacker.y)
                    else
                        reach = wesnoth.paths.find_reach(attacker)
                        reaches:insert(attacker.x, attacker.y, reach)
                    end

                    for _,loc in ipairs(reach) do
                        if (loc[1] == xa) and (loc[2] == ya) then
                            local tmp = {
                                ind_attacker = i_a,
                                dst = { x = xa, y = ya },
                                src = { x = attacker.x, y = attacker.y }
                            }
                            table.insert(can_force_level, tmp)
                            break
                        end
                    end
                else
                    -- If hex is blocked by own units, check whether this unit
                    -- is one of the potential attackers
                    if (attacker.id == occupied) then
                        local tmp = {
                            ind_attacker = i_a,
                            dst = { x = xa, y = ya },
                            src = { x = attacker.x, y = attacker.y }
                        }

                        table.insert(can_force_level, tmp)
                    end
                end
            end
        end)

        -- If a leveling attack is possible, check whether any of the other
        -- units (those with too low a level to force leveling up) can get there
        local ca_score = 100010

        attack_hexes:iter(function(xa, ya, occupied)
            if (ca_score == 100010) then  -- cannot break out of the iteration with goto
                for _,i_u in ipairs(target_info.ind_other_units) do
                    local unit = units[i_u]
                    if (occupied == 'empty') or (occupied == 'can_move_away') then
                        -- If the hex is not blocked, check if unit can get there
                        local reach
                        if reaches:get(unit.x, unit.y) then
                            reach = reaches:get(unit.x, unit.y)
                        else
                            reach = wesnoth.paths.find_reach(unit)
                            reaches:insert(unit.x, unit.y, reach)
                        end

                        for _,loc in ipairs(reach) do
                            if (loc[1] == xa) and (loc[2] == ya) then
                                ca_score = 99990
                                goto found_unit
                            end
                        end
                    else
                        -- If hex is blocked by own units, check whether this unit
                        -- is one of the potential attackers
                        if (unit.id == occupied) then
                            ca_score = 99990
                            goto found_unit
                        end
                    end
                end
                -- It is sufficient to find one unit that can get to any attack hex
                ::found_unit::
            end
        end)

        if (ca_score >= max_ca_score) then
            for _,attack_info in ipairs(can_force_level) do
                local attacker = units[attack_info.ind_attacker]
                local attacker_copy
                if attacker_copies:get(attacker.x, attacker.y) then
                    attacker_copy = attacker_copies:get(attacker.x, attacker.y)
                else
                    attacker_copy = attacker:clone()
                    attacker_copies:insert(attacker.x, attacker.y, attacker_copy)
                end

                attacker_copy.x = attack_info.dst.x
                attacker_copy.y = attack_info.dst.y

                -- Choose the attacker that would do the *least* damage.
                -- We want the damage distribution here as if the target were not to level up
                -- the chance to die is the same in either case
                local old_experience = target.experience
                target.experience = 0
                local att_stats, def_stats, att_weapon = wesnoth.simulate_combat(attacker_copy, target)
                target.experience = old_experience

                local rating = -1000
                if (att_stats.hp_chance[0] <= aggression) then
                    if (def_stats.hp_chance[0] > 0) then
                        rating = 5000 + def_stats.hp_chance[0] - att_stats.hp_chance[0]
                    elseif target.status.poisoned then
                        rating = -1002
                    else
                        rating = 1000

                        local enemy_value_loss = (target.hitpoints - def_stats.average_hp) / target.max_hitpoints
                        enemy_value_loss = enemy_value_loss * target.cost

                        -- We want the _least_ damage to the enemy, so the minus sign is no typo!
                        rating = rating - enemy_value_loss

                        local own_value_loss = (attacker_copy.hitpoints - att_stats.average_hp) / attacker_copy.max_hitpoints
                        own_value_loss = own_value_loss + att_stats.hp_chance[0]
                        own_value_loss = own_value_loss * attacker_copy.cost

                        rating = rating - own_value_loss

                        -- Strongly discourage poison or slow attacks
                        if att_weapon.poisons or att_weapon.slows then
                            rating = rating - 100
                        end

                        -- Minor penalty if the attack hex is occupied
                        if (attack_hexes:get(attack_info.dst.x, attack_info.dst.y) == 'can_move_away') then
                            rating = rating - 0.001
                        end
                    end
                end

                if (rating > max_rating)
                    or ((rating > 0) and (ca_score > max_ca_score))
                then
                    max_rating = rating
                    max_ca_score = ca_score
                    best_attack = attack_info
                    best_attack.target = { x = target.x, y = target.y }
                    best_attack.ca_score = ca_score
                    -- Also need to save weapon number because attack simulation above
                    -- is for a different situation than the actual units on the map.
                    -- +1 because of difference between Lua and C++ indices
                    best_attack.attack_num = att_weapon.attack_num + 1
                end
            end
        end

    end

    if best_attack then
        XP_attack = best_attack
    end

    return max_ca_score
end

function ca_attack_highxp:execution(cfg, data)
    AH.robust_move_and_attack(ai, XP_attack.src, XP_attack.dst, XP_attack.target, { weapon = XP_attack.attack_num })
    XP_attack = nil
end

return ca_attack_highxp
