-------- Castle Switch CA --------------

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

local CS_leader_score
-- Note that CS_leader and CS_leader_target are also needed by the recruiting CA, so they must be stored in 'data'

local high_score = 195000
local low_score = 15000

local function get_reachable_enemy_leaders(unit, avoid_map)
    -- We're cheating a little here and also find hidden enemy leaders. That's
    -- because a human player could make a pretty good educated guess as to where
    -- the enemy leaders are likely to be while the AI does not know how to do that.
    local potential_enemy_leaders = AH.get_live_units { canrecruit = 'yes',
        { "filter_side", { { "enemy_of", {side = wesnoth.current.side} } } }
    }
    local enemy_leaders = {}
    for _,e in ipairs(potential_enemy_leaders) do
        -- Cannot use AH.find_path_with_avoid() here as there might be enemies all around the enemy leader
        if (not avoid_map:get(e.x, e.y)) then
            wesnoth.interface.handle_user_interact()
            local path, cost = wesnoth.paths.find_path(unit, e.x, e.y, { ignore_units = true, ignore_visibility = true })
            if cost < AH.no_path then
                table.insert(enemy_leaders, e)
            end
        end
    end

    return enemy_leaders
end

local function other_units_on_keep(leader)
    -- if we're on a keep, wait until there are no movable non-leader units on the castle before moving off
    local leader_score = high_score
    if wesnoth.terrain_types[wesnoth.current.map[leader]].keep then
        local castle = AH.get_locations_no_borders {
            { "and", {
                x = leader.x, y = leader.y, radius = 200,
                { "filter_radius", { terrain = 'C*,K*,C*^*,K*^*,*^K*,*^C*' } }
            }}
        }
        local should_wait = false
        for i,loc in ipairs(castle) do
            local unit = wesnoth.units.get(loc[1], loc[2])
            if unit and (unit.side == wesnoth.current.side) and (not unit.canrecruit) and (unit.moves > 0) then
                should_wait = true
                break
            end
        end
        if should_wait then
            leader_score = low_score
        end
    end

    return leader_score
end

local ca_castle_switch = {}

function ca_castle_switch:evaluation(cfg, data, filter_own, recruiting_leader)
    -- @recruiting_leader is passed from the recuit_rushers CA for the leader_takes_villages evaluation
    -- evaluation. If it is set, we do the castle switch evaluation only for that leader

    local start_time, ca_name = wesnoth.ms_since_init() / 1000., 'castle_switch'
    if AH.print_eval() then AH.print_ts('     - Evaluating castle_switch CA:') end

    if ai.aspects.passive_leader then
        -- Turn off this CA if the leader is passive
        return 0
    end

    local leaders
    if recruiting_leader then
        -- Note that doing this might set the stored castle switch information to a different leader.
        -- This is fine though, the order in which these are done is not particularly important.
        leaders = { recruiting_leader }
    else
        leaders = AH.get_units_with_moves({
            side = wesnoth.current.side,
            canrecruit = 'yes',
            formula = '(movement_left = total_movement) and (hitpoints = max_hitpoints)',
            { "and", filter_own }
        }, true)
    end

    local non_passive_leader
    for _,l in pairs(leaders) do
        if (not AH.is_passive_leader(ai.aspects.passive_leader, l.id)) then
            non_passive_leader = l
            break
        end
    end

    if not non_passive_leader then
        -- CA is irrelevant if no leader or the leader may have moved from another CA
        data.CS_leader, data.CS_leader_target = nil, nil
        if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
        return 0
    end

    -- Also need to check that the stored leader has not been killed
    if data.CS_leader and not data.CS_leader.valid then
        data.CS_leader, data.CS_leader_target = nil, nil
    end

    local avoid_map = AH.get_avoid_map(ai, nil, true)

    if data.CS_leader and wesnoth.sides[wesnoth.current.side].gold >= AH.get_cheapest_recruit_cost(data.CS_leader)
        and ((not recruiting_leader) or (recruiting_leader.id == data.CS_leader.id))
    then
        -- If the saved score is the low score, check whether there are still other units on the keep
        if (CS_leader_score == low_score) then
            CS_leader_score = other_units_on_keep(data.CS_leader)
        end

        -- make sure move is still valid
        local path, cost = AH.find_path_with_avoid(data.CS_leader, data.CS_leader_target[1], data.CS_leader_target[2], avoid_map)
        local next_hop = AH.next_hop(data.CS_leader, nil, nil, { path = path, avoid_map = avoid_map })
        if next_hop and next_hop[1] == data.CS_leader_target[1]
            and next_hop[2] == data.CS_leader_target[2]
        then
            return CS_leader_score
        else
            data.CS_leader, data.CS_leader_target = nil, nil
        end
    end

    -- Look for the best keep
    local overall_best_score = 0
    for _,leader in ipairs(leaders) do
        local best_score, best_loc, best_turns, best_path = 0, {}, 3, nil
        local keeps = AH.get_locations_no_borders {
            terrain = 'K*,K*^*,*^K*', -- Keeps
            { "not", { {"filter", {}} }}, -- That have no unit
            { "not", { radius = 6, {"filter", { canrecruit = 'yes',
                { "filter_side", { { "enemy_of", {side = wesnoth.current.side} } } }
            }} }}, -- That are not too close to an enemy leader
            { "not", {
                x = leader.x, y = leader.y, terrain = 'K*,K*^*,*^K*',
                radius = 3,
                { "filter_radius", { terrain = 'C*,K*,C*^*,K*^*,*^K*,*^C*' } }
            }}, -- That are not close and connected to a keep the leader is on
            { "filter_adjacent_location", {
                terrain = 'C*,K*,C*^*,K*^*,*^K*,*^C*'
            }} -- That are not one-hex keeps
        }
        if #keeps < 1 then
            -- Skip if there aren't extra keeps to evaluate
            -- In this situation we'd only switch keeps if we were running away
            if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
            return 0
        end

        local enemy_leaders = get_reachable_enemy_leaders(leader, avoid_map)

        for i,loc in ipairs(keeps) do
            -- Only consider keeps within 2 turns movement
            wesnoth.interface.handle_user_interact()
            local path, cost = AH.find_path_with_avoid(leader, loc[1], loc[2], avoid_map)
            local score = 0
            -- Prefer closer keeps to enemy
            local turns = math.ceil(cost/leader.max_moves)
            if turns <= 2 then
                score = 1/turns
                for j,e in ipairs(enemy_leaders) do
                    score = score + 1 / M.distance_between(loc[1], loc[2], e.x, e.y)
                end

                if score > best_score then
                    best_score = score
                    best_loc = loc
                    best_turns = turns
                    best_path = path
                end
            end
        end

        -- If we're on a keep,
        -- don't move to another keep unless it's much better when uncaptured villages are present
        if best_score > 0 and wesnoth.terrain_types[wesnoth.current.map[leader]].keep then
            local close_unowned_village = (wesnoth.map.find {
                wml.tag['and']{
                    x = leader.x,
                    y = leader.y,
                    radius = leader.max_moves
                },
                gives_income = true,
                owner_side = 0
            })[1]
            if close_unowned_village then
                local score = 1/best_turns
                for j,e in ipairs(enemy_leaders) do
                    -- count all distances as three less than they actually are
                    score = score + 1 / (M.distance_between(leader.x, leader.y, e.x, e.y) - 3)
                end

                if score > best_score then
                    best_score = 0
                end
            end
        end

        if best_score > 0 then
            local next_hop = AH.next_hop(leader, nil, nil, { path = best_path, avoid_map = avoid_map })

            if next_hop and ((next_hop[1] ~= leader.x) or (next_hop[2] ~= leader.y)) then
                -- See if there is a nearby village that can be captured without delaying progress
                local close_villages = wesnoth.map.find( {
                    wml.tag["and"]{ x = next_hop[1], y = next_hop[2], radius = leader.max_moves },
                    gives_income = true,
                    owner_side = 0 })
                local cheapest_unit_cost = AH.get_cheapest_recruit_cost(leader)
                for i,loc in ipairs(close_villages) do
                    wesnoth.interface.handle_user_interact()
                    local path_village, cost_village = AH.find_path_with_avoid(leader, loc[1], loc[2], avoid_map)
                    if cost_village <= leader.moves then
                        local dummy_leader = leader:clone()
                        dummy_leader.x = loc[1]
                        dummy_leader.y = loc[2]
                        local path_keep, cost_keep = wesnoth.paths.find_path(dummy_leader, best_loc[1], best_loc[2], avoid_map)
                        local turns_from_keep = math.ceil(cost_keep/leader.max_moves)
                        if turns_from_keep < best_turns
                        or (turns_from_keep == 1 and wesnoth.sides[wesnoth.current.side].gold < cheapest_unit_cost)
                        then
                            -- There is, go there instead
                            next_hop = loc
                            break
                        end
                    end
                end
            end

            local leader_score = other_units_on_keep(leader)
            best_score = best_score + leader_score

            if (best_score > overall_best_score) then
                overall_best_score = best_score
                CS_leader_score = leader_score
                data.CS_leader = leader
                data.CS_leader_target = next_hop
            end
        end
    end

    if (overall_best_score > 0) then
        if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
        return CS_leader_score
    end

    if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
    return 0
end

function ca_castle_switch:execution(cfg, data, filter_own)
    if AH.print_exec() then AH.print_ts('   Executing castle_switch CA') end
    if AH.show_messages() then wesnoth.wml_actions.message { speaker = data.leader.id, message = 'Switching castles' } end

    AH.robust_move_and_attack(ai, data.CS_leader, data.CS_leader_target, nil, { partial_move = true })
    data.CS_leader, data.CS_leader_target = nil, nil
end

return ca_castle_switch
