/**
 *  Copyright (C) 2002-2017   The FreeCol Team
 *
 *  This file is part of FreeCol.
 *
 *  FreeCol is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  FreeCol is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with FreeCol.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.sf.freecol.server.ai;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.ToDoubleFunction;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import javax.xml.stream.XMLStreamException;

import net.sf.freecol.common.FreeColException;
import net.sf.freecol.common.i18n.Messages;
import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.model.Ability;
import net.sf.freecol.common.model.AbstractUnit;
import net.sf.freecol.common.model.Building;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.Constants;
import net.sf.freecol.common.model.DiplomaticTrade;
import net.sf.freecol.common.model.DiplomaticTrade.TradeContext;
import net.sf.freecol.common.model.DiplomaticTrade.TradeStatus;
import net.sf.freecol.common.model.Europe;
import net.sf.freecol.common.model.FeatureContainer;
import net.sf.freecol.common.model.FoundingFather;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.GoldTradeItem;
import net.sf.freecol.common.model.Goods;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.HistoryEvent;
import net.sf.freecol.common.model.IndianSettlement;
import net.sf.freecol.common.model.Location;
import net.sf.freecol.common.model.Map;
import net.sf.freecol.common.model.Market;
import net.sf.freecol.common.model.Modifier;
import net.sf.freecol.common.model.NationSummary;
import net.sf.freecol.common.model.NativeTrade;
import net.sf.freecol.common.model.NativeTrade.NativeTradeAction;
import net.sf.freecol.common.model.PathNode;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Player.PlayerType;
import net.sf.freecol.common.model.Stance;
import net.sf.freecol.common.model.Role;
import net.sf.freecol.common.model.Settlement;
import net.sf.freecol.common.model.Specification;
import net.sf.freecol.common.model.StanceTradeItem;
import net.sf.freecol.common.model.Tension;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.TradeItem;
import net.sf.freecol.common.model.Turn;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.Unit.UnitState;
import net.sf.freecol.common.model.UnitType;
import net.sf.freecol.common.model.pathfinding.CostDeciders;
import net.sf.freecol.common.model.pathfinding.GoalDeciders;
import net.sf.freecol.common.option.GameOptions;
import net.sf.freecol.common.util.CachingFunction;
import static net.sf.freecol.common.util.CollectionUtils.*;
import net.sf.freecol.common.util.LogBuilder;
import net.sf.freecol.common.util.RandomChoice;
import static net.sf.freecol.common.util.RandomUtils.*;

import net.sf.freecol.server.ai.GoodsWish;
import net.sf.freecol.server.ai.mission.BuildColonyMission;
import net.sf.freecol.server.ai.mission.CashInTreasureTrainMission;
import net.sf.freecol.server.ai.mission.DefendSettlementMission;
import net.sf.freecol.server.ai.mission.IdleAtSettlementMission;
import net.sf.freecol.server.ai.mission.Mission;
import net.sf.freecol.server.ai.mission.MissionaryMission;
import net.sf.freecol.server.ai.mission.PioneeringMission;
import net.sf.freecol.server.ai.mission.PrivateerMission;
import net.sf.freecol.server.ai.mission.ScoutingMission;
import net.sf.freecol.server.ai.mission.TransportMission;
import net.sf.freecol.server.ai.mission.UnitSeekAndDestroyMission;
import net.sf.freecol.server.ai.mission.UnitWanderHostileMission;
import net.sf.freecol.server.ai.mission.WishRealizationMission;
import net.sf.freecol.server.ai.mission.WorkInsideColonyMission;
import net.sf.freecol.server.ai.ValuedAIObject;
import net.sf.freecol.server.ai.WorkerWish;
import net.sf.freecol.server.model.ServerPlayer;


/**
 * Objects of this class contains AI-information for a single European
 * {@link Player} and is used for controlling this player.
 *
 * The method {@link #startWorking} gets called by the
 * {@link AIInGameInputHandler} when it is this player's turn.
 */
public class EuropeanAIPlayer extends MissionAIPlayer {

    private static final Logger logger = Logger.getLogger(EuropeanAIPlayer.class.getName());

    /** Predicate to select units that can be equipped. */
    private static final Predicate<Unit> equipPred = u ->
        u.hasDefaultRole() && u.hasAbility(Ability.CAN_BE_EQUIPPED);

    /** Predicate to select party modifiers. */
    private static final Predicate<Modifier> partyPred
        = matchKey(Specification.COLONY_GOODS_PARTY_SOURCE,
                   Modifier::getSource);

    /** Maximum number of turns to travel to a building site. */
    private static final int buildingRange = 5;

    /** Maximum number of turns to travel to a cash in location. */
    private static final int cashInRange = 20;

    /** Maximum number of turns to travel to a missionary target. */
    private static final int missionaryRange = 20;

    /**
     * Maximum number of turns to travel to make progress on
     * pioneering.  This is low-ish because it is usually more
     * efficient to ship the tools where they are needed and either
     * create a new pioneer on site or send a hardy pioneer on
     * horseback.  The AI is probably smart enough to do the former
     * already, and one day the latter.
     */
    private static final int pioneeringRange = 10;

    /**
     * Maximum number of turns to travel to a privateering target.
     * Low number because of large naval moves.
     */
    private static final int privateerRange = 1;

    /** Maximum number of turns to travel to a scouting target. */
    private static final int scoutingRange = 20;

    /** A comparator to sort units by decreasing builder score. */
    private static final Comparator<AIUnit> builderComparator
        = Comparator.comparingInt(AIUnit::getBuilderScore).reversed();

    /**
     * A comparator to sort units by suitability for a PioneeringMission.
     *
     * We do not check if a unit is near to a colony that can provide tools,
     * as that is likely to be too expensive.  FIXME: perhaps we should.
     */
    public static final Comparator<AIUnit> pioneerComparator
        = Comparator.comparingInt(AIUnit::getPioneerScore).reversed();

    /**
     * A comparator to sort units by suitability for a ScoutingMission.
     *
     * We do not check if a unit is near to a colony that can provide horses,
     * as that is likely to be too expensive.  FIXME: perhaps we should.
     */
    public static final Comparator<AIUnit> scoutComparator
        = Comparator.comparingInt(AIUnit::getScoutScore).reversed();


    // These should be final, but need the spec.

    /** Cheat chances. */
    private static int liftBoycottCheatPercent;
    private static int equipScoutCheatPercent;
    private static int equipPioneerCheatPercent;
    private static int landUnitCheatPercent;
    private static int offensiveLandUnitCheatPercent;
    private static int offensiveNavalUnitCheatPercent;
    private static int transportNavalUnitCheatPercent;
    /** The pioneer role. */
    private static Role pioneerRole = null;
    /** The scouting role. */
    private static Role scoutRole = null;

    // Caches/internals.  Do not serialize.

    /**
     * A cached map of Tile to best TileImprovementPlan.
     * Used to choose a tile improvement for a pioneer to work on.
     */
    private final java.util.Map<Tile, TileImprovementPlan> tipMap
        = new HashMap<>();

    /**
     * A cached map of destination Location to Wishes awaiting transport.
     */
    private final java.util.Map<Location, List<Wish>> transportDemand
        = new HashMap<>();

    /** A cached list of transportables awaiting transport. */
    private final List<TransportableAIObject> transportSupply
        = new ArrayList<>();

    /**
     * A mapping of goods type to the goods wishes where a colony has
     * requested that goods type.  Used to retarget goods that have
     * gone astray.
     */
    private final java.util.Map<GoodsType, List<GoodsWish>> goodsWishes
        = new HashMap<>();

    /**
     * A mapping of unit type to the worker wishes for that type.
     * Used to allocate WishRealizationMissions for units.
     */
    private final java.util.Map<UnitType, List<WorkerWish>> workerWishes
        = new HashMap<>();

    /**
     * A mapping of contiguity number to number of wagons needed in
     * that landmass.
     */
    private final java.util.Map<Integer, Integer> wagonsNeeded
        = new HashMap<>();

    /** The colonies that start the turn badly defended. */
    private final List<AIColony> badlyDefended = new ArrayList<>();

    /**
     * Current estimate of the number of new
     * {@code BuildColonyMission}s to create.
     */
    private int nBuilders = 0;

    /**
     * Current estimate of the number of new
     * {@code PioneeringMission}s to create.
     */
    private int nPioneers = 0;

    /**
     * Current estimate of the number of new
     * {@code ScoutingMission}s to create.
     */
    private int nScouts = 0;

    /** Count of the number of transports needing a naval unit. */
    private int nNavalCarrier = 0;


    /**
     * Creates a new {@code EuropeanAIPlayer}.
     *
     * @param aiMain The main AI-class.
     * @param player The player that should be associated with this
     *            {@code AIPlayer}.
     */
    public EuropeanAIPlayer(AIMain aiMain, ServerPlayer player) {
        super(aiMain, player);

        uninitialized = getPlayer() == null;
    }

    /**
     * Creates a new {@code AIPlayer}.
     *
     * @param aiMain The main AI-object.
     * @param xr The input stream containing the XML.
     * @throws XMLStreamException if a problem was encountered during parsing.
     */
    public EuropeanAIPlayer(AIMain aiMain,
                            FreeColXMLReader xr) throws XMLStreamException {
        super(aiMain, xr);

        uninitialized = getPlayer() == null;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void removeAIObject(AIObject ao) {
        if (ao instanceof AIColony) {
            removeAIColony((AIColony)ao);
        } else {
            super.removeAIObject(ao);
        }
    }

    /**
     * Remove one of our colonies.
     *
     * @param aic The {@code AIColony} to remove.
     */
    private void removeAIColony(AIColony aic) {
        final Colony colony = aic.getColony();
        
        Set<TileImprovementPlan> tips = new HashSet<>();
        for (Tile t : colony.getOwnedTiles()) {
            TileImprovementPlan tip = tipMap.remove(t);
            if (tip != null) tips.add(tip);
        }

        for (AIGoods aig : aic.getExportGoods()) {
            if (Map.isSameLocation(aig.getLocation(), colony)) {
                aig.changeTransport(null);
                aig.dispose();
            }
        }

        transportDemand.remove(colony);

        Set<Wish> wishes = new HashSet<>(aic.getWishes());
        for (AIUnit aiu : getAIUnits()) {
            PioneeringMission pm = aiu.getMission(PioneeringMission.class);
            if (pm != null) {
                if (tips.contains(pm.getTileImprovementPlan())) {
                    logger.info(pm + " collapses with loss of " + colony);
                    aiu.changeMission(null);
                }
                continue;
            }
            WishRealizationMission
                wm = aiu.getMission(WishRealizationMission.class);
            if (wm != null) {
                if (wishes.contains(wm.getWish())) {
                    logger.info(wm + " collapses with loss of " + colony);
                    aiu.changeMission(null);
                }
                continue;
            }
        }
    }

    /**
     * Initialize the static fields that would be final but for
     * needing the specification.
     *
     * @param spec The {@code Specification} to initialize from.
     */
    public static synchronized void initializeFromSpecification(Specification spec) {
        if (pioneerRole != null) return;
        pioneerRole = spec.getRoleWithAbility(Ability.IMPROVE_TERRAIN, null);
        scoutRole = spec.getRoleWithAbility(Ability.SPEAK_WITH_CHIEF, null);
        liftBoycottCheatPercent
            = spec.getInteger(GameOptions.LIFT_BOYCOTT_CHEAT);
        equipScoutCheatPercent
            = spec.getInteger(GameOptions.EQUIP_SCOUT_CHEAT);
        equipPioneerCheatPercent
            = spec.getInteger(GameOptions.EQUIP_PIONEER_CHEAT);
        landUnitCheatPercent
            = spec.getInteger(GameOptions.LAND_UNIT_CHEAT);
        offensiveLandUnitCheatPercent
            = spec.getInteger(GameOptions.OFFENSIVE_LAND_UNIT_CHEAT);
        offensiveNavalUnitCheatPercent
            = spec.getInteger(GameOptions.OFFENSIVE_NAVAL_UNIT_CHEAT);
        transportNavalUnitCheatPercent
            = spec.getInteger(GameOptions.TRANSPORT_NAVAL_UNIT_CHEAT);
    }

    /**
     * Get the list of badly defended colonies.
     *
     * @return A list of {@code AIColony}s that were badly
     *     defended at the start of this turn.
     */
    protected List<AIColony> getBadlyDefended() {
        return badlyDefended;
    }

    /**
     * Simple initialization of AI missions given that we know the starting
     * conditions.
     *
     * @param lb A {@code LogBuilder} to log to.
     */
    private void initializeMissions(LogBuilder lb) {
        final AIMain aiMain = getAIMain();
        List<AIUnit> aiUnits = getAIUnits();
        lb.add("\n  Initialize ");
        
        // Find all the carriers with potential colony builders on board,
        // give them missions.
        final Map map = getGame().getMap();
        final int maxRange = map.getWidth() + map.getHeight();
        Location target;
        Mission m;
        TransportMission tm;
        for (AIUnit aiCarrier : aiUnits) {
            if (aiCarrier.hasMission()) continue;
            Unit carrier = aiCarrier.getUnit();
            if (!carrier.isNaval()) continue;
            target = null;
            for (Unit u : carrier.getUnitList()) {
                AIUnit aiu = aiMain.getAIUnit(u);
                for (int range = buildingRange; range < maxRange;
                     range += buildingRange) {
                    target = BuildColonyMission.findTarget(aiu, range, false);
                    if (target != null) break;
                }
                if (target == null) {
                    throw new RuntimeException("Initial colony fail!");
                }
                if ((m = getBuildColonyMission(aiu, target)) != null) {
                    lb.add(m, ", ");
                }
            }
            // Initialize the carrier mission after the cargo units
            // have a valid mission so that the transport list and
            // mission target do not break.
            tm = (TransportMission)getTransportMission(aiCarrier);
            if (tm != null) {
                lb.add(tm);
                for (Unit u : carrier.getUnitList()) {
                    AIUnit aiu = getAIMain().getAIUnit(u);
                    if (aiu == null) continue;
                    tm.queueTransportable(aiu, false, lb);
                }
            }
        }

        // Put in some backup missions.
        lb.mark();
        for (AIUnit aiu : aiUnits) {
            if (aiu.hasMission()) continue;
            if ((m = getSimpleMission(aiu)) != null) lb.add(m, ", ");
        }
        if (lb.grew("\n  Backup: ")) lb.shrink(", ");
    }

    /**
     * Cheat by adding gold to guarantee the player has a minimum amount.
     *
     * @param amount The minimum amount of gold required.
     * @param lb A {@code LogBuilder} to log to.
     */
    public void cheatGold(int amount, LogBuilder lb) {
        final Player player = getPlayer();
        int gold = player.getGold();
        if (gold < amount) {
            amount -= gold;
            player.modifyGold(amount);
            lb.add("added ", amount, " gold");
        }
        player.logCheat(amount + " gold");
    }

    /**
     * Cheats for the AI.  Please try to centralize cheats here.
     *
     * FIXME: Remove when the AI is good enough.
     *
     * @param lb A {@code LogBuilder} to log to.
     */
    private void cheat(LogBuilder lb) {
        final AIMain aiMain = getAIMain();
        if (!aiMain.getFreeColServer().getSinglePlayer()) return;

        final Player player = getPlayer();
        if (player.getPlayerType() != PlayerType.COLONIAL) return;
        lb.mark();

        final Specification spec = getSpecification();
        final Game game = getGame();
        final Market market = player.getMarket();
        final Europe europe = player.getEurope();
        final Random air = getAIRandom();
        final List<GoodsType> arrears = new ArrayList<>();
        if (market != null) {
            arrears.addAll(transform(spec.getGoodsTypeList(),
                                     gt -> market.getArrears(gt) > 0));
        }
        final int nCheats = arrears.size() + 6; // 6 cheats + arrears
        int[] randoms = randomInts(logger, "cheats", air, 100, nCheats);
        int cheatIndex = 0;

        for (GoodsType goodsType : arrears) {
            if (randoms[cheatIndex++] < liftBoycottCheatPercent) {
                market.setArrears(goodsType, 0);
                // Just remove one goods party modifier (we can not
                // currently identify which modifier applies to which
                // goods type, but that is not worth fixing for the
                // benefit of `temporary' cheat code).  If we do not
                // do this, AI colonies accumulate heaps of party
                // modifiers because of the cheat boycott removal.
                final CachingFunction<Colony, Modifier> partyModifierMapper
                    = new CachingFunction<>(c ->
                        first(transform(c.getModifiers(), partyPred)));
                Colony party = getRandomMember(logger, "end boycott",
                    transform(player.getColonies(),
                              isNotNull(c -> partyModifierMapper.apply(c))),
                    air);
                if (party != null) {
                    party.removeModifier(partyModifierMapper.apply(party));
                    lb.add("lift-boycott at ", party, ", ");
                    player.logCheat("lift boycott at " + party.getName());
                }
            }
        }
    
        if (!europe.isEmpty()
            && scoutsNeeded() > 0
            && randoms[cheatIndex++] < equipScoutCheatPercent) {
            for (Unit u : transform(europe.getUnits(), equipPred)) {
                try {
                    int g = europe.priceGoods(u.getGoodsDifference(scoutRole, 1));
                    cheatGold(g, lb);
                } catch (FreeColException fce) {
                    continue;
                }
                if (getAIUnit(u).equipForRole(spec.getRoleWithAbility(Ability.SPEAK_WITH_CHIEF, null))) {
                    lb.add(" to equip scout ", u, ", ");
                    player.logCheat("Equip scout " + u.toShortString());
                    break;
                }
            }
        }

        if (!europe.isEmpty()
            && pioneersNeeded() > 0
            && randoms[cheatIndex++] < equipPioneerCheatPercent) {
            for (Unit u : transform(europe.getUnits(), equipPred)) {
                try {
                    int g = europe.priceGoods(u.getGoodsDifference(pioneerRole, 1)); 
                    cheatGold(g, lb);
                } catch (FreeColException fce) {
                    continue;
                }
                if (getAIUnit(u).equipForRole(spec.getRoleWithAbility(Ability.IMPROVE_TERRAIN, null))) {
                    lb.add(" to equip pioneer ", u, ", ");
                    player.logCheat("Equip pioneer " + u.toShortString());
                    break;
                }
            }
        }

        if (randoms[cheatIndex++] < landUnitCheatPercent) {
            final Predicate<Entry<UnitType, List<WorkerWish>>> bestWishPred = e -> {
                UnitType ut = e.getKey();
                return ut != null && ut.isAvailableTo(player)
                    && europe.getUnitPrice(ut) != UNDEFINED
                    && any(e.getValue());
            };
            WorkerWish bestWish = maximize(transform(workerWishes.entrySet(),
                                                     bestWishPred,
                                                     e -> first(e.getValue()),
                                                     Collectors.toSet()),
                ValuedAIObject.ascendingValueComparator);
            int cost = (bestWish != null)
                ? europe.getUnitPrice(bestWish.getUnitType())
                : (player.getImmigration() < player.getImmigrationRequired() / 2)
                ? player.getEuropeanRecruitPrice()
                : INFINITY;
            if (cost != INFINITY) {
                cheatGold(cost, lb);
                AIUnit aiu;
                if (bestWish == null) {
                    if ((aiu = recruitAIUnitInEurope(-1)) != null) {
                        // let giveNormalMissions look after the mission
                        lb.add(" to recruit ", aiu.getUnit(), ", ");
                    }
                } else {
                    if ((aiu = trainAIUnitInEurope(bestWish.getUnitType())) != null) {
                        Mission m = getWishRealizationMission(aiu, bestWish);
                        if (m != null) {
                            lb.add(" to train for ", m, ", ");
                        } else {
                            lb.add(" to train ", aiu.getUnit(), ", ");
                        }
                    }
                }
                if (aiu != null) player.logCheat("Make " + aiu.getUnit());
            }
        }

        if (game.getTurn().getNumber() > 300
            && player.isAtWar()
            && randoms[cheatIndex++] < offensiveLandUnitCheatPercent) {
            // - collect enemies, prefer not to antagonize the strong or
            //   crush the weak
            List<Player> enemies = new ArrayList<>();
            List<Player> preferred = new ArrayList<>();
            for (Player p : transform(game.getLivePlayers(player),
                                      x -> player.atWarWith(x))) {
                enemies.add(p);
                double strength = getStrengthRatio(p);
                if (strength < 3.0/2.0 && strength > 2.0/3.0) {
                    preferred.add(p);
                }
            }
            if (!preferred.isEmpty()) {
                enemies.clear();
                enemies.addAll(preferred);
            }
            List<Colony> colonies = player.getColonyList();
            // Find a target to attack.
            Location target = null;
            // Few colonies?  Attack the weakest European port
            if (colonies.size() < 3) {
                final Comparator<Colony> targetScore
                    = cachingDoubleComparator(c -> {
                            double score = 100000.0 / c.getUnitCount();
                            Building stockade = c.getStockade();
                            return (stockade == null) ? 1.0
                                : score / (stockade.getLevel() + 1.5);
                        });
                target = maximize(flatten(enemies, Player::isEuropean,
                                          Player::getConnectedPorts),
                                  targetScore);
            }
            // Otherwise attack something near a weak colony
            if (target == null && !colonies.isEmpty()) {
                List<AIColony> bad = new ArrayList<>(getBadlyDefended());
                if (bad.isEmpty()) bad.addAll(getAIColonies());
                AIColony defend = getRandomMember(logger,
                    "AIColony to defend", bad, air);
                Tile center = defend.getColony().getTile();
                Tile t = game.getMap().searchCircle(center,
                    GoalDeciders.getEnemySettlementGoalDecider(enemies),
                    30);
                if (t != null) target = t.getSettlement();
            }
            if (target != null) {
                List<AbstractUnit> aMercs = new ArrayList<>();
                int aPrice = player.getMonarch().loadMercenaries(air, aMercs);
                if (aPrice > 0) {
                    List<Unit> mercs = ((ServerPlayer)player)
                        .createUnits(aMercs, europe);
                    for (Unit u : mercs) {
                        AIUnit aiu = getAIUnit(u);
                        if (aiu == null) continue; // Can not happen
                        player.logCheat("Enlist " + aiu.getUnit());
                        Mission m = getSeekAndDestroyMission(aiu, target);
                        if (m != null) {
                            lb.add("enlisted ", m, ", ");
                        } else {
                            lb.add("enlisted ", aiu.getUnit(), ", ");
                        }
                    }
                }
            }
        }
            
        // Always cheat a new armed ship if the navy is destroyed,
        // otherwise if the navy is below average the chance to cheat
        // is proportional to how badly below average.
        double naval = getNavalStrengthRatio();
        int nNaval = (player.getUnitCount(true) == 0) ? 100
            : (0.0f < naval && naval < 0.5f)
            ? (int)(naval * offensiveNavalUnitCheatPercent)
            : -1;
        final Function<UnitType, RandomChoice<UnitType>> mapper = ut ->
            new RandomChoice<>(ut, 100000 / europe.getUnitPrice(ut));
        if (randoms[cheatIndex++] < nNaval) {
            cheatUnit(transform(spec.getUnitTypeList(),
                                ut -> ut.hasAbility(Ability.NAVAL_UNIT)
                                    && ut.isAvailableTo(player)
                                    && ut.hasPrice()
                                    && ut.isOffensive(),
                                mapper), "offensive-naval", lb);
        }
        // Only cheat carriers if they have work to do.
        int nCarrier = (nNavalCarrier > 0) ? transportNavalUnitCheatPercent
            : -1;
        if (randoms[cheatIndex++] < nCarrier) {
            cheatUnit(transform(spec.getUnitTypeList(),
                                ut -> ut.hasAbility(Ability.NAVAL_UNIT)
                                    && ut.isAvailableTo(player)
                                    && ut.hasPrice()
                                    && ut.getSpace() > 0,
                                mapper), "transport-naval", lb);
        }

        if (lb.grew("\n  Cheats: ")) lb.shrink(", ");
    }

    /**
     * Cheat-build a unit in Europe.
     *
     * @param rc A list of random choices to choose from.
     * @param what A description of the unit.
     * @param lb A {@code LogBuilder} to log to.
     * @return The {@code AIUnit} built.
     */
    private AIUnit cheatUnit(List<RandomChoice<UnitType>> rc, String what,
                             LogBuilder lb) {
        UnitType unitToPurchase
            = RandomChoice.getWeightedRandom(logger, "Cheat which unit",
                                             rc, getAIRandom());
        return (unitToPurchase == null) ? null
            : cheatUnit(unitToPurchase, what, lb);
    }

    /**
     * Cheat-build a unit in Europe.
     *
     * @param unitType The {@code UnitType} to build.
     * @param what A description of the unit.
     * @param lb A {@code LogBuilder} to log to.
     * @return The {@code AIUnit} built.
     */
    private AIUnit cheatUnit(UnitType unitType, String what, LogBuilder lb) {
        final Player player = getPlayer();
        final Europe europe = player.getEurope();
        int cost = europe.getUnitPrice(unitType);
        cheatGold(cost, lb);
        AIUnit result = trainAIUnitInEurope(unitType);
        lb.add(" to build ", what, " ", unitType.getSuffix(),
            ((result != null) ? "" : "(failed)"), ", ");
        if (result == null) return null;
        player.logCheat("Build " + result.getUnit());
        return result;
    }

    /**
     * Assign transportable units and goods to available carriers.
     *
     * These supply driven assignments supplement the demand driven
     * calls inside TransportMission.
     *
     * @param transportables A list of {@code TransportableAIObject}s to
     *     allocated transport for.
     * @param missions A list of {@code TransportMission}s to potentially
     *     assign more transportables to.
     * @param lb A {@code LogBuilder} to log to.
     */
    public void allocateTransportables(List<TransportableAIObject> transportables,
                                        List<TransportMission> missions,
                                        LogBuilder lb) {
        if (transportables.isEmpty()) return;
        if (missions.isEmpty()) return;

        lb.add("\n  Allocate Transport cargo=", transportables.size(),
               " carriers=", missions.size());
        //for (TransportableAIObject t : urgent) lb.add(" ", t);
        //lb.add(" ->");
        //for (Mission m : missions) lb.add(" ", m);

        LogBuilder lb2 = new LogBuilder(0);
        TransportMission best;
        float bestValue;
        boolean present;
        int i = 0;
        outer: while (i < transportables.size()) {
            if (missions.isEmpty()) break;
            TransportableAIObject t = transportables.get(i);
            lb.add(" for ", t);
            best = null;
            bestValue = 0.0f;
            present = false;
            for (TransportMission tm : missions) {
                if (!tm.spaceAvailable(t)) continue;
                Cargo cargo = tm.makeCargo(t, lb2);
                if (cargo == null) { // Serious problem with this cargo
                    transportables.remove(i);
                    continue outer;
                }
                int turns = cargo.getTurns();
                float value;
                if (turns == 0) {
                    value = tm.destinationCapacity();
                    if (!present) bestValue = 0.0f; // reset
                    present = true;
                } else {
                    value = (present) ? -1.0f
                        : (float)t.getTransportPriority() / turns;
                }
                if (bestValue < value) {
                    bestValue = value;
                    best = tm;
                }
            }
            if (best == null) {
                lb.add(" nothing found");
            } else {
                lb.add(" ", best.getUnit(), " chosen");
                if (best.queueTransportable(t, false, lb)) {
                    claimTransportable(t);
                    if (best.destinationCapacity() <= 0) {
                        missions.remove(best);
                    }
                } else {
                    missions.remove(best);
                }
            }
            i++;
        }
    }

    /**
     * Brings gifts to nice players with nearby colonies.
     *
     * FIXME: European players can also bring gifts!  However, this
     * might be folded into a trade mission, since European gifts are
     * just a special case of trading.
     *
     * @param lb A {@code LogBuilder} to log to.
     */
    private void bringGifts(LogBuilder lb) {
        return;
    }

    /**
     * Demands goods from players with nearby colonies.
     *
     * FIXME: European players can also demand tribute!
     *
     * @param lb A {@code LogBuilder} to log to.
     */
    private void demandTribute(LogBuilder lb) {
        return;
    }


    // Tile Improvement handling

    /**
     * Rebuilds a map of locations to TileImprovementPlans.
     *
     * Called by startWorking at the start of every turn.
     * Public for the test suite.
     *
     * @param lb A {@code LogBuilder} to log to.
     */
    public void buildTipMap(LogBuilder lb) {
        tipMap.clear();
        for (AIColony aic : getAIColonies()) {
            for (TileImprovementPlan tip : aic.getTileImprovementPlans()) {
                if (tip == null || tip.isComplete()) {
                    aic.removeTileImprovementPlan(tip);
                } else if (tip.getPioneer() != null) {
                    // Do nothing, remove when complete
                } else if (!tip.validate()) {
                    aic.removeTileImprovementPlan(tip);
                    tip.dispose();
                } else if (tip.getTarget() == null) {
                    logger.warning("No target for tip: " + tip);
                } else {
                    TileImprovementPlan other = tipMap.get(tip.getTarget());
                    if (other == null || other.getValue() < tip.getValue()) {
                        tipMap.put(tip.getTarget(), tip);
                    }
                }
            }
        }
        if (!tipMap.isEmpty()) {
            lb.add("\n  Improvements:");
            forEachMapEntry(tipMap, e -> {
                    Tile t = e.getKey();
                    TileImprovementPlan tip = e.getValue();
                    AIUnit pioneer = tip.getPioneer();
                    lb.add(" ", t, "=", tip.getType().getSuffix());
                    if (pioneer != null) lb.add("/", pioneer.getUnit());
                });
        }                
    }

    /**
     * Update the tip map with tips from a new colony.
     *
     * @param aic The new {@code AIColony}.
     */
    private void updateTipMap(AIColony aic) {
        for (TileImprovementPlan tip : aic.getTileImprovementPlans()) {
            tipMap.put(tip.getTarget(), tip);
        }
    }

    /**
     * Gets the best plan for a tile from the tipMap.
     *
     * @param tile The {@code Tile} to lookup.
     * @return The best plan for a tile.
     */
    public TileImprovementPlan getBestPlan(Tile tile) {
        return (tipMap == null) ? null : tipMap.get(tile);
    }

    /**
     * Gets the best plan for a colony from the tipMap.
     *
     * @param colony The {@code Colony} to check.
     * @return The tile with the best plan for a colony, or null if none found.
     */
    public Tile getBestPlanTile(Colony colony) {
        final Comparator<TileImprovementPlan> valueComp
            = Comparator.comparingInt(TileImprovementPlan::getValue);
        final Function<Tile, TileImprovementPlan> tileMapper = t ->
            tipMap.get(t);
        TileImprovementPlan best
            = maximize(map(colony.getOwnedTiles(), tileMapper),
                       isNotNull(), valueComp);
        return (best == null) ? null : best.getTarget();
    }

    /**
     * Remove a {@code TileImprovementPlan} from the relevant colony.
     *
     * @param plan The {@code TileImprovementPlan} to remove.
     */
    public void removeTileImprovementPlan(TileImprovementPlan plan) {
        if (plan == null) return;
        if (plan.getTarget() != null) tipMap.remove(plan.getTarget());
        for (AIColony aic : getAIColonies()) {
            if (aic.removeTileImprovementPlan(plan)) break;
        }
    }


    // Transport handling

    /**
     * Update the transport of a unit following a target change.
     *
     * If the target has changed
     * - drop all non-boarded transport unless the target is the same
     * - dump boarded transport with no target
     * - requeue all boarded transport unless the target is the same
     *
     * @param aiu The {@code AIUnit} to check.
     * @param oldTarget The old target {@code Location}.
     * @param lb A {@code LogBuilder} to log to.
     */
    public void updateTransport(AIUnit aiu, Location oldTarget, LogBuilder lb) {
        final AIUnit aiCarrier = aiu.getTransport();
        final Mission newMission = aiu.getMission();
        final Location newTarget = (newMission == null) ? null
            : newMission.getTarget();
        TransportMission tm;
        if (aiCarrier != null
            && (tm = aiCarrier.getMission(TransportMission.class)) != null
            && !Map.isSameLocation(oldTarget, newTarget)) {
            if (aiu.getUnit().getLocation() != aiCarrier.getUnit()) {
                lb.add(", drop transport ", aiCarrier.getUnit());
                aiu.dropTransport();
            } else if (newTarget == null) {
                tm.dumpTransportable(aiu, lb);
            } else {
                tm.requeueTransportable(aiu, lb);
            }
        }
    }

    /**
     * Checks if a transportable needs transport.
     *
     * @param t The {@code TransportableAIObject} to check.
     * @return True if no transport is already present or the
     *     transportable is already aboard a carrier, and there is a
     *     well defined source and destination location.
     */
    private boolean requestsTransport(TransportableAIObject t) {
        return t.getTransport() == null
            && t.getTransportDestination() != null
            && t.getTransportSource() != null
            && !(t.getLocation() instanceof Unit);
    }

    /**
     * Checks that the carrier assigned to a transportable is has a
     * transport mission and the transport is queued thereon.
     *
     * @param t The {@code TransportableAIObject} to check.
     * @return True if all is well.
     */
    private boolean checkTransport(TransportableAIObject t) {
        AIUnit aiCarrier = t.getTransport();
        if (aiCarrier == null) return false;
        TransportMission tm = aiCarrier.getMission(TransportMission.class);
        if (tm != null && tm.isTransporting(t)) return true;
        t.changeTransport(null);
        return false;
    }

    /**
     * Gets the needed wagons for a tile/contiguity.
     *
     * @param tile The {@code Tile} to derive the contiguity from.
     * @return The number of wagons needed.
     */
    public int getNeededWagons(Tile tile) {
        if (tile != null) {
            int contig = tile.getContiguity();
            if (contig > 0) {
                Integer i = wagonsNeeded.get(contig);
                if (i != null) return i;
            }
        }
        return 0;
    }

    /**
     * Changes the needed wagons map for a specified tile/contiguity.
     * If the change is zero, that is a special flag that a connected
     * port is available, and thus that the map should be initialized
     * for that contiguity.
     *
     * @param tile The {@code Tile} to derive the contiguity from.
     * @param amount The change to make.
     */
    private void changeNeedWagon(Tile tile, int amount) {
        if (tile == null) return;
        int contig = tile.getContiguity();
        if (contig > 0) {
            Integer i = wagonsNeeded.get(contig);
            if (i == null) {
                if (amount == 0) wagonsNeeded.put(contig, 0);
            } else {
                wagonsNeeded.put(contig, i + amount);
            }
        }
    }

    /**
     * Rebuild the transport maps.
     * Count the number of transports requiring naval/land carriers.
     *
     * @param lb A {@code LogBuilder} to log to.
     */
    private void buildTransportMaps(LogBuilder lb) {
        transportDemand.clear();
        transportSupply.clear();
        wagonsNeeded.clear();
        nNavalCarrier = 0;

        // Prime the wagonsNeeded map with contiguities with a connected port
        for (AIColony aic : getAIColonies()) {
            Colony colony = aic.getColony();
            if (colony.isConnectedPort()) changeNeedWagon(colony.getTile(), 0);
        }

        for (AIUnit aiu : getAIUnits()) {
            if (aiu.hasMission() && !aiu.getMission().isValid()) continue;
            Unit u = aiu.getUnit();
            if (u.isCarrier()) {
                if (u.isNaval()) {
                    nNavalCarrier--;
                } else {
                    changeNeedWagon(u.getTile(), -1);
                }
            } else {
                checkTransport(aiu);
                if (requestsTransport(aiu)) {
                    transportSupply.add(aiu);
                    aiu.incrementTransportPriority();
                    nNavalCarrier++;
                }
            }
        }

        for (AIColony aic : getAIColonies()) {
            for (AIGoods aig : aic.getExportGoods()) {
                checkTransport(aig);
                if (requestsTransport(aig)) {
                    transportSupply.add(aig);
                    aig.incrementTransportPriority();
                    Location src = aig.getTransportSource();
                    Location dst = aig.getTransportDestination();
                    if (!Map.isSameContiguity(src, dst)) {
                        nNavalCarrier++;
                    }
                }
            }
            Colony colony = aic.getColony();
            if (!colony.isConnectedPort()) {
                changeNeedWagon(colony.getTile(), 1);
            }
        }

        for (Wish w : getWishes()) {
            TransportableAIObject t = w.getTransportable();
            if (t != null && t.getTransport() == null
                && t.getTransportDestination() != null) {
                Location loc = Location.upLoc(t.getTransportDestination());
                appendToMapList(transportDemand, loc, w);
            }
        }

        if (!transportSupply.isEmpty()) {
            lb.add("\n  Transport Supply:");
            for (TransportableAIObject t : transportSupply) {
                lb.add(" ", t.getTransportPriority(), "+", t);
            }
        }
        if (!transportDemand.isEmpty()) {
            lb.add("\n  Transport Demand:");
            forEachMapEntry(transportDemand, e -> {
                    Location ld = e.getKey();
                    lb.add("\n    ", ld, "[");
                    for (Wish w : e.getValue()) lb.add(" ", w);
                    lb.add(" ]");
                });
        }
    }

    /**
     * Gets the most urgent transportables.
     *
     * @return The most urgent 10% of the available transportables.
     */
    public List<TransportableAIObject> getUrgentTransportables() {
        List<TransportableAIObject> urgent
            = sort(transportSupply, ValuedAIObject.descendingValueComparator);
        // Do not let the list exceed 10% of all transports
        int urge = urgent.size();
        urge = Math.max(2, (urge + 5) / 10);
        while (urgent.size() > urge) urgent.remove(urge);
        return urgent;
    }

    /**
     * Allows a TransportMission to signal that it has taken responsibility
     * for a TransportableAIObject.
     *
     * @param t The {@code TransportableAIObject} being claimed.
     * @return True if the transportable was claimed from the supply map.
     */
    public boolean claimTransportable(TransportableAIObject t) {
        return transportSupply.remove(t);
    }

    /**
     * Rearrange colonies.
     *
     * @param lb A {@code LogBuilder} to log to.
     */
    private void rearrangeColonies(LogBuilder lb) {
        for (AIColony aic : getAIColonies()) aic.rearrangeColony(lb);
    }


    // Wish handling

    /**
     * Suppress European trade in a goods type.  A goods party and
     * boycott is incoming.
     *
     * @param type The {@code GoodsType} to suppress.
     * @param lb A {@code LogBuilder} to log to.
     */
    private void suppressEuropeanTrade(GoodsType type, LogBuilder lb) {
        final Player player = getPlayer();
        final Europe europe = player.getEurope();

        lb.add("  Suppressing trade in ", type.getSuffix());
        List<Unit> units = new ArrayList<>(europe.getUnitList());
        units.addAll(player.getHighSeas().getUnitList());
        for (Unit u : units) {
            int amount;
            AIUnit aiu;
            if (u.isCarrier() && (amount = u.getGoodsCount(type)) > 0
                && (aiu = getAIUnit(u)) != null
                && AIMessage.askUnloadGoods(type, amount, aiu)) {
                lb.add(", ", u, " sold ", amount);
            }
        }
        for (AIUnit aiu : getAIUnits()) {
            TransportMission tm = aiu.getMission(TransportMission.class);
            if (tm != null) tm.suppressEuropeanTrade(type, lb);
        }           

        int n = 0;
        List<GoodsWish> wishes = goodsWishes.get(type);
        if (wishes != null) {
            for (GoodsWish gw : wishes) {
                if (gw.getGoodsType() == type
                    && gw.getDestination() == europe) {
                    if (gw.getTransportable() instanceof AIGoods) {
                        AIGoods aig = (AIGoods)gw.getTransportable();
                        consumeGoodsWish(aig, gw);
                        aig.setTransportDestination(null);
                    }
                    gw.dispose();
                    n++;
                }
            }
            if (n > 0) lb.add(", dropped ", n, " goods wishes");
        }
        lb.add(".");
    }
                
    /**
     * Gets a list of the wishes at a given location for a unit type.
     *
     * @param loc The {@code Location} to look for wishes at.
     * @param type The {@code UnitType} to look for.
     * @return A list of {@code WorkerWish}es.
     */
    public List<WorkerWish> getWorkerWishesAt(Location loc, UnitType type) {
        List<Wish> demand = transportDemand.get(Location.upLoc(loc));
        return (demand == null) ? Collections.<WorkerWish>emptyList()
            : transform(demand,
                        w -> w instanceof WorkerWish
                            && ((WorkerWish)w).getUnitType() == type,
                        w -> (WorkerWish)w);
    }

    /**
     * Gets a list of the wishes at a given location for a goods type.
     *
     * @param loc The {@code Location} to look for wishes at.
     * @param type The {@code GoodsType} to look for.
     * @return A list of {@code GoodsWish}es.
     */
    public List<GoodsWish> getGoodsWishesAt(Location loc, GoodsType type) {
        List<Wish> demand = transportDemand.get(Location.upLoc(loc));
        return (demand == null) ? Collections.<GoodsWish>emptyList()
            : transform(demand,
                        w -> w instanceof GoodsWish
                            && ((GoodsWish)w).getGoodsType() == type,
                        w -> (GoodsWish)w);
    }

    /**
     * Gets the best worker wish for a carrier unit.
     *
     * @param aiUnit The carrier {@code AIUnit}.
     * @param unitType The {@code UnitType} to find a wish for.
     * @return The best worker wish for the unit.
     */
    public WorkerWish getBestWorkerWish(AIUnit aiUnit, UnitType unitType) {
        List<WorkerWish> wishes = workerWishes.get(unitType);
        if (wishes == null) return null;

        final Unit carrier = aiUnit.getUnit();
        WorkerWish carried = null;
        WorkerWish other = null;
        double bestCarriedValue = -1.0, bestOtherValue = -1.0;
        for (WorkerWish w : wishes) {
            int turns = carrier.getTurnsToReach(w.getDestination());
            if (turns < Unit.MANY_TURNS) {
                if (bestCarriedValue < (double)w.getValue() / turns) {
                    bestCarriedValue = (double)w.getValue() / turns;
                    carried = w;
                }
            } else {
                if (bestOtherValue < w.getValue()) {
                    bestOtherValue = w.getValue();
                    other = w;
                }
            }
        }
        return (carried != null) ? carried : (other != null) ? other : null;
    }

    /**
     * Gets the best goods wish for a carrier unit.
     *
     * @param aiUnit The carrier {@code AIUnit}.
     * @param goodsType The {@code GoodsType} to wish for.
     * @return The best {@code GoodsWish} for the unit.
     */
    public GoodsWish getBestGoodsWish(AIUnit aiUnit, GoodsType goodsType) {
        final Unit carrier = aiUnit.getUnit();
        final ToDoubleFunction<GoodsWish> wishValue
            = cacheDouble(gw -> {
                    int turns = carrier.getTurnsToReach(carrier.getLocation(),
                                                        gw.getDestination());
                    return (turns >= Unit.MANY_TURNS) ? -1.0
                        : (double)gw.getValue() / turns;
                });
        final Comparator<GoodsWish> comp
            = Comparator.comparingDouble(wishValue);

        List<GoodsWish> wishes = goodsWishes.get(goodsType);
        return (wishes == null) ? null
            : maximize(wishes, gw -> wishValue.applyAsDouble(gw) > 0.0, comp);
    }

    /**
     * Rebuilds the goods and worker wishes maps.
     *
     * @param lb A {@code LogBuilder} to log to.
     */
    private void buildWishMaps(LogBuilder lb) {
        for (UnitType unitType : getSpecification().getUnitTypeList()) {
            List<WorkerWish> wl = workerWishes.get(unitType);
            if (wl == null) {
                workerWishes.put(unitType, new ArrayList<WorkerWish>());
            } else {
                wl.clear();
            }
        }
        for (GoodsType goodsType : getSpecification().getStorableGoodsTypeList()) {
            List<GoodsWish> gl = goodsWishes.get(goodsType);
            if (gl == null) {
                goodsWishes.put(goodsType, new ArrayList<GoodsWish>());
            } else {
                gl.clear();
            }
        }

        for (Wish w : getWishes()) {
            if (w instanceof WorkerWish) {
                WorkerWish ww = (WorkerWish)w;
                if (ww.getTransportable() == null) {
                    appendToMapList(workerWishes, ww.getUnitType(), ww);
                }
            } else if (w instanceof GoodsWish) {
                GoodsWish gw = (GoodsWish)w;
                if (gw.getDestination() instanceof Colony) {
                    appendToMapList(goodsWishes, gw.getGoodsType(), gw);
                }
            }
        }

        if (!workerWishes.isEmpty()) {
            lb.add("\n  Wishes (workers):");
            forEachMapEntry(workerWishes, e -> {
                    UnitType ut = e.getKey();
                    List<WorkerWish> wl = e.getValue();
                    if (!wl.isEmpty()) {
                        lb.add("\n    ", ut.getSuffix(), ":");
                        for (WorkerWish ww : wl) {
                            lb.add(" ", ww.getDestination(),
                                "(", ww.getValue(), ")");
                        }
                    }
                });
        }
        if (!goodsWishes.isEmpty()) {
            lb.add("\n  Wishes (goods):");
            forEachMapEntry(goodsWishes, e -> {
                    GoodsType gt = e.getKey();
                    List<GoodsWish> gl = e.getValue();
                    if (!gl.isEmpty()) {
                        lb.add("\n    ", gt.getSuffix(), ":");
                        for (GoodsWish gw : gl) {
                            lb.add(" ", gw.getDestination(),
                                "(", gw.getValue(), ")");
                        }
                    }
                });
        }
    }

    /**
     * Notify that a wish has been completed.  Called from AIColony.
     *
     * @param w The {@code Wish} to complete.
     */
    @Override
    public void completeWish(Wish w) {
        if (w instanceof WorkerWish) {
            WorkerWish ww = (WorkerWish)w;
            List<WorkerWish> wl = workerWishes.get(ww.getUnitType());
            if (wl != null) wl.remove(ww);
        } else if (w instanceof GoodsWish) {
            GoodsWish gw = (GoodsWish)w;
            List<GoodsWish> gl = goodsWishes.get(gw.getGoodsType());
            if (gl != null) gl.remove(gw);
        } else {
            throw new IllegalStateException("Bogus wish: " + w);
        }
    }

    /**
     * Consume a WorkerWish, yielding a WishRealizationMission for a unit.
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @param ww The {@code WorkerWish} to consume.
     */
    public void consumeWorkerWish(AIUnit aiUnit, WorkerWish ww) {
        final Unit unit = aiUnit.getUnit();
        List<WorkerWish> wwL = workerWishes.get(unit.getType());
        wwL.remove(ww);
        List<Wish> wl = transportDemand.get(ww.getDestination());
        if (wl != null) wl.remove(ww);
        ww.setTransportable(aiUnit);
    }

    /**
     * Consume a GoodsWish.
     *
     * @param aig The {@code AIGoods} to use.
     * @param gw The {@code GoodsWish} to consume.
     */
    public void consumeGoodsWish(AIGoods aig, GoodsWish gw) {
        final Goods goods = aig.getGoods();
        List<GoodsWish> gwL = goodsWishes.get(goods.getType());
        gwL.remove(gw);
        List<Wish> wl = transportDemand.get(gw.getDestination());
        if (wl != null) wl.remove(gw);
        gw.setTransportable(aig);
    }


    // Useful public routines

    /**
     * Gets the number of units that should build a colony.
     *
     * This is the desired total number, not the actual number which would
     * take into account the number of existing BuildColonyMissions.
     *
     * @return The desired number of colony builders for this player.
     */
    public int buildersNeeded() {
        Player player = getPlayer();
        if (!player.canBuildColonies()) return 0;

        int nColonies = 0, nPorts = 0, nWorkers = 0, nEuropean = 0;
        for (Settlement settlement : player.getSettlementList()) {
            nColonies++;
            if (settlement.isConnectedPort()) nPorts++;
            nWorkers += count(settlement.getAllUnitsList(), Unit::isPerson);
        }
        Europe europe = player.getEurope();
        nEuropean = (europe == null) ? 0
            : count(europe.getUnits(), Unit::isPerson);
            
        // If would be good to have at least two colonies, and at least
        // one port.  After that, determine the ratio of workers to colonies
        // (which should be the average colony size), and if that is above
        // a threshold, send out another colonist.
        // The threshold probably should be configurable.  2 is too
        // low IMHO as it makes a lot of brittle colonies, 3 is too
        // high at least initially as it makes it hard for the initial
        // colonies to become substantial.  For now, arbitrarily choose e.
        return (nColonies == 0 || nPorts == 0) ? 2
            : ((nPorts <= 1) && (nWorkers + nEuropean) >= 3) ? 1
            : ((double)(nWorkers + nEuropean) / nColonies > Math.E) ? 1
            : 0;
    }

    /**
     * How many pioneers should we have?
     *
     * This is the desired total number, not the actual number which would
     * take into account the number of existing PioneeringMissions.
     *
     * @return The desired number of pioneers for this player.
     */
    public int pioneersNeeded() {
        return (tipMap.size() + 1) / 2;
    }

    /**
     * How many scouts should we have?
     *
     * This is the desired total number, not the actual number which would
     * take into account the number of existing ScoutingMissions.
     *
     * Current scheme for European AIs is to use up to three scouts in
     * the early part of the game, then one.
     *
     * @return The desired number of scouts for this player.
     */
    @Override
    public int scoutsNeeded() {
        return 3 - (getGame().getTurn().getNumber() / 100);
    }

    /**
     * Asks the server to recruit a unit in Europe on behalf of the AIPlayer.
     *
     * FIXME: Move this to a specialized Handler class (AIEurope?)
     * FIXME: Give protected access?
     *
     * @param slot The migration slot to recruit from.
     * @return The new AIUnit created by this action or null on failure.
     */
    public AIUnit recruitAIUnitInEurope(int slot) {
        AIUnit aiUnit = null;
        Europe europe = getPlayer().getEurope();
        if (europe == null) return null;
        int n = europe.getUnitCount();
        final String selectAbility = Ability.SELECT_RECRUIT;
        if (!Europe.MigrationType.validMigrantSlot(slot)) {
            slot = (getPlayer().hasAbility(selectAbility))
                ? Europe.MigrationType.getDefaultSlot()
                : Europe.MigrationType.getUnspecificSlot();
        }
        if (AIMessage.askEmigrate(this, slot)
            && europe.getUnitCount() == n+1) {
            aiUnit = getAIUnit(europe.getUnitList().get(n));
            if (aiUnit != null) addAIUnit(aiUnit);
        }
        return aiUnit;
    }

    /**
     * Helper function for server communication - Ask the server
     * to train a unit in Europe on behalf of the AIGetPlayer().
     *
     * FIXME: Move this to a specialized Handler class (AIEurope?)
     * FIXME: Give protected access?
     *
     * @param unitType The {@code UnitType} to train.
     * @return the new AIUnit created by this action. May be null.
     */
    public AIUnit trainAIUnitInEurope(UnitType unitType) {
        if (unitType==null) {
            throw new IllegalArgumentException("Invalid UnitType.");
        }

        AIUnit aiUnit = null;
        Europe europe = getPlayer().getEurope();
        if (europe == null) return null;
        int n = europe.getUnitCount();

        if (AIMessage.askTrainUnitInEurope(this, unitType)
            && europe.getUnitCount() == n+1) {
            aiUnit = getAIUnit(europe.getUnitList().get(n));
            if (aiUnit != null) addAIUnit(aiUnit);
        }
        return aiUnit;
    }

    /**
     * Gets the wishes for all this player's colonies, sorted by the
     * {@link Wish#getValue value}.
     *
     * @return A list of wishes.
     */
    public List<Wish> getWishes() {
        return sort(flatten(getAIColonies(), aic -> aic.getWishes().stream()),
                    ValuedAIObject.descendingValueComparator);
    }


    // Diplomacy support

    /**
     * Determines the stances towards each player.
     *
     * @param lb A {@code LogBuilder} to log to.
     */
    private void determineStances(LogBuilder lb) {
        final ServerPlayer serverPlayer = (ServerPlayer)getPlayer();
        lb.mark();

        for (Player p : getGame().getLivePlayerList(serverPlayer)) {
            Stance newStance = determineStance(p);
            if (newStance != serverPlayer.getStance(p)) {
                if (newStance == Stance.WAR && peaceHolds(p)) {
                    ; // Peace treaty holds for now
                } else {
                    getAIMain().getFreeColServer().getInGameController()
                        .changeStance(serverPlayer, newStance,
                                      (ServerPlayer)p, true);
                    lb.add(" ", p.getDebugName(), "->", newStance, ", ");
                }
            }
        }
        if (lb.grew("\n  Stance changes:")) lb.shrink(", ");
    }

    /**
     * See if a recent peace treaty still has force.
     *
     * @param p The {@code Player} to check for a peace treaty with.
     * @return True if peace gets another chance.
     */
    private boolean peaceHolds(Player p) {
        final Player player = getPlayer();
        final Turn turn = getGame().getTurn();
        final double peaceProb = getSpecification()
            .getPercentageMultiplier(GameOptions.PEACE_PROBABILITY);

        int peaceTurn = -1;
        for (HistoryEvent h : player.getHistory()) {
            if (p.getId().equals(h.getPlayerId())
                && h.getTurn().getNumber() > peaceTurn) {
                switch (h.getEventType()) {
                case MAKE_PEACE: case FORM_ALLIANCE:
                    peaceTurn = h.getTurn().getNumber();
                    break;
                case DECLARE_WAR:
                    peaceTurn = -1;
                    break;
                default:
                    break;
                }
            }
        }
        if (peaceTurn < 0) return false;

        int n = turn.getNumber() - peaceTurn;
        float prob = (float)Math.pow(peaceProb, n);
        // Apply Franklin's modifier
        prob = p.applyModifiers(prob, turn, Modifier.PEACE_TREATY);
        return prob > 0.0f
            && (randomInt(logger, "Peace holds?",  getAIRandom(), 100)
                < (int)(100.0f * prob));
    }


    /**
     * Get a nation summary for another player.
     *
     * @param other The other {@code Player} to get the summary for.
     * @return The current {@code NationSummary} for a player.
     */
    protected NationSummary getNationSummary(Player other) {
        final Player player = getPlayer();
        NationSummary ns = player.getNationSummary(other);
        if (ns != null) return ns;
        AIMessage.askNationSummary(this, other);
        return player.getNationSummary(other);
    }

    /**
     * Get the land force strength ratio of this player with respect
     * to another.
     *
     * @param other The other {@code Player}.
     * @return The strength ratio (strength/sum(strengths)).
     */
    protected double getStrengthRatio(Player other) {
        return getPlayer().getStrengthRatio(other, false);
    }

    /**
     * Is this player lagging in naval strength?  Calculate the ratio
     * of its naval strength to the average strength of other European
     * colonial powers.
     *
     * @return The naval strength ratio, or negative if there are no other
     *     European colonial nations.
     */
    protected double getNavalStrengthRatio() {
        final Player player = getPlayer();
        double navalAverage = 0.0;
        double navalStrength = 0.0;
        int nPlayers = 0;
        for (Player p : transform(getGame().getLiveEuropeanPlayers(player),
                                  x -> !x.isREF())) {
            NationSummary ns = getNationSummary(p);
            if (ns == null) continue;
            if (p == player) {
                navalStrength = ns.getNavalStrength();
            } else {
                int st = ns.getNavalStrength();
                if (st >= 0) navalAverage += st;
                nPlayers++;
            }
        }
        if (nPlayers <= 0 || navalStrength < 0) return -1.0;
        navalAverage /= nPlayers;
        return (navalAverage == 0.0) ? -1.0 : navalStrength / navalAverage;
    }

    /**
     * Reject a trade agreement, except if a Franklin-derived stance
     * is supplied.
     *
     * @param stance A stance {@code TradeItem}.
     * @param agreement The {@code DiplomaticTrade} to reset.
     * @return The {@code TradeStatus} for the agreement.
     */
    private TradeStatus rejectAgreement(TradeItem stance,
                                        DiplomaticTrade agreement) {
        if (stance == null) return TradeStatus.REJECT_TRADE;
        
        agreement.clear();
        agreement.add(stance);
        return TradeStatus.PROPOSE_TRADE;
    }


    // Mission handling

    /**
     * Ensures all units have a mission.
     *
     * @param lb A {@code LogBuilder} to log to.
     */
    protected void giveNormalMissions(LogBuilder lb) {
        final AIMain aiMain = getAIMain();
        final Player player = getPlayer();
        java.util.Map<Unit, String> reasons = new HashMap<>();
        BuildColonyMission bcm = null;
        Mission m;

        nBuilders = buildersNeeded();
        nPioneers = pioneersNeeded();
        nScouts = scoutsNeeded();

        List<AIUnit> aiUnits = getAIUnits();
        List<AIUnit> navalUnits = new ArrayList<>();
        List<AIUnit> done = new ArrayList<>();
        List<TransportMission> transportMissions = new ArrayList<>();

        // For all units, check if it is a candidate for a new
        // mission.  If it is not a candidate remove it from the
        // aiUnits list (reporting why not).  Adjust the
        // Build/Pioneer/Scout counts according to the existing valid
        // missions.  Accumulate potentially usable transport missions.
        lb.mark();
        for (AIUnit aiUnit : aiUnits) {
            final Unit unit = aiUnit.getUnit();
            final Colony colony = unit.getColony();
            m = aiUnit.getMission();
            final Location oldTarget = (m == null) ? null : m.getTarget();

            if (!unit.isInitialized() || unit.isDisposed()) {
                reasons.put(unit, "Invalid");

            } else if (unit.isDamaged()) { // Damaged units must wait
                if (!(m instanceof IdleAtSettlementMission)) {
                    if ((m = getIdleAtSettlementMission(aiUnit)) != null) {
                        lb.add(", ", m);
                    }
                }
                reasons.put(unit, "Damaged");
                    
            } else if (unit.getState() == UnitState.IN_COLONY
                && colony.getUnitCount() <= 1) {
                // The unit has its hand full keeping the colony alive.
                if (!(m instanceof WorkInsideColonyMission)
                    && (m = getWorkInsideColonyMission(aiUnit,
                            aiMain.getAIColony(colony))) != null) {
                    logger.warning(aiUnit + " should WorkInsideColony at "
                        + colony.getName());
                    lb.add(", ", m);
                    updateTransport(aiUnit, oldTarget, lb);
                }
                reasons.put(unit, "Vital");

            } else if (unit.isInMission()) {
                reasons.put(unit, "Mission");

            } else if (m != null && m.isValid() && !m.isOneTime()) {
                if (m instanceof BuildColonyMission) {
                    bcm = (BuildColonyMission)m;
                    nBuilders--;
                } else if (m instanceof PioneeringMission) {
                    nPioneers--;
                } else if (m instanceof ScoutingMission) {
                    nScouts--;
                } else if (m instanceof TransportMission) {
                    TransportMission tm = (TransportMission)m;
                    // Consider reassigning quiescent transport
                    // missions to privateer missions
                    if (tm.isEmpty() && unit.isNaval()
                        && unit.isOffensiveUnit()) {
                        navalUnits.add(aiUnit);
                        done.add(aiUnit);
                        continue;
                    }
                    // If there is capacity in this mission, consider adding
                    // more cargoes
                    if (tm.destinationCapacity() > 0) {
                        transportMissions.add(tm);
                    }
                } else if (m instanceof PrivateerMission) {
                    if (!(m.getTarget() instanceof Unit)) {
                        // Privateering but not chasing a unit, consider
                        // reassigning to transport.
                        navalUnits.add(aiUnit);
                        done.add(aiUnit);
                        continue;
                    }
                }
                reasons.put(unit, "Valid");

            } else if (unit.isNaval()) {
                navalUnits.add(aiUnit);

            } else if (unit.isAtSea()) { // Wait for it to emerge
                reasons.put(unit, "At-Sea");

            } else { // Needs mission
                continue;
            }                
            done.add(aiUnit);
        }
        aiUnits.removeAll(done);
        done.clear();

        // First try to satisfy the demand for missions with a defined
        // quota.  Builders first to keep weak players in the game,
        // scouts next as they are profitable.  Pile onto any existing
        // building mission if there are no colonies.
        if (!player.hasSettlements() && bcm != null) {
            final Location bcmTarget = bcm.getTarget();
            for (AIUnit aiUnit : sort(aiUnits, builderComparator)) {
                final Location oldTarget = ((m = aiUnit.getMission()) == null)
                    ? null : m.getTarget();
                if ((m = getBuildColonyMission(aiUnit, bcmTarget)) == null)
                    continue;
                lb.add(", ", m);
                updateTransport(aiUnit, oldTarget, lb);
                done.add(aiUnit);
                if (requestsTransport(aiUnit)) transportSupply.add(aiUnit);
                reasons.put(aiUnit.getUnit(), "0Builder");
            }
            aiUnits.removeAll(done);
            done.clear();
        }
        if (nBuilders > 0) {
            for (AIUnit aiUnit : sort(aiUnits, builderComparator)) {
                final Location oldTarget = ((m = aiUnit.getMission()) == null)
                    ? null : m.getTarget();
                if ((m = getBuildColonyMission(aiUnit, null)) == null)
                    continue;
                lb.add(", ", m);
                updateTransport(aiUnit, oldTarget, lb);
                done.add(aiUnit);
                if (requestsTransport(aiUnit)) transportSupply.add(aiUnit);
                reasons.put(aiUnit.getUnit(), "Builder" + nBuilders);
                if (--nBuilders <= 0) break;
            }
            aiUnits.removeAll(done);
            done.clear();
        }
        if (nScouts > 0) {
            for (AIUnit aiUnit : sort(aiUnits, scoutComparator)) {
                final Location oldTarget = ((m = aiUnit.getMission()) == null)
                    ? null : m.getTarget();
                final Unit unit = aiUnit.getUnit();
                if ((m = getScoutingMission(aiUnit)) == null) continue;
                lb.add(", ", m);
                updateTransport(aiUnit, oldTarget, lb);
                done.add(aiUnit);
                if (requestsTransport(aiUnit)) transportSupply.add(aiUnit);
                reasons.put(unit, "Scout" + nScouts);
                if (--nScouts <= 0) break;
            }
            aiUnits.removeAll(done);
            done.clear();
        }
        if (nPioneers > 0) {
            for (AIUnit aiUnit : sort(aiUnits, pioneerComparator)) {
                final Unit unit = aiUnit.getUnit();
                final Location oldTarget = ((m = aiUnit.getMission()) == null)
                    ? null : m.getTarget();
                if ((m = getPioneeringMission(aiUnit, null)) == null) continue;
                lb.add(", ", m);
                updateTransport(aiUnit, oldTarget, lb);
                done.add(aiUnit);
                if (requestsTransport(aiUnit)) transportSupply.add(aiUnit);
                reasons.put(unit, "Pioneer" + nPioneers);
                if (--nPioneers <= 0) break;
            }
            aiUnits.removeAll(done);
            done.clear();
        }

        // Give the remaining land units a valid mission.
        for (AIUnit aiUnit : aiUnits) {
            final Unit unit = aiUnit.getUnit();
            final Location oldTarget = ((m = aiUnit.getMission()) == null)
                ? null : m.getTarget();
            if ((m = getSimpleMission(aiUnit)) == null) continue;
            lb.add(", ", m);
            updateTransport(aiUnit, oldTarget, lb);
            reasons.put(unit, "New-Land");
            done.add(aiUnit);
            if (requestsTransport(aiUnit)) transportSupply.add(aiUnit);
        }
        aiUnits.removeAll(done);
        done.clear();

        // Process the free naval units, possibly adding to the usable
        // transport missions.
        for (AIUnit aiUnit : navalUnits) {
            final Unit unit = aiUnit.getUnit();
            Mission old = ((m = aiUnit.getMission()) != null && m.isValid())
                ? m : null;
            if ((m = getSimpleMission(aiUnit)) == null) continue;
            lb.add(", ", m, ((m == old) ? " (preserved)" : " (new)"));
            reasons.put(unit, "New-Naval");
            done.add(aiUnit);
            if (m instanceof TransportMission) {
                TransportMission tm = (TransportMission)m;
                if (tm.destinationCapacity() > 0) {
                    transportMissions.add(tm);
                }
                // A new transport mission might have retargeted
                // its passengers into new valid missions.
                for (Unit u : aiUnit.getUnit().getUnitList()) {
                    AIUnit aiu = getAIUnit(u);
                    Mission um = aiu.getMission();
                    if (um != null && um.isValid()
                        && aiUnits.contains(aiu)) {
                        aiUnits.remove(aiu);
                        reasons.put(aiu.getUnit(), "TNew");
                    }
                }
            }
        }
        navalUnits.removeAll(done);
        done.clear();

        // Give remaining units the fallback mission.
        aiUnits.addAll(navalUnits);
        List<Colony> ports = null;
        int nPorts = player.getNumberOfPorts();
        for (AIUnit aiUnit : aiUnits) {
            final Unit unit = aiUnit.getUnit();
            m = aiUnit.getMission();
            final Location oldTarget = (m == null) ? null : m.getTarget();
            if (m != null && m.isValid() && !m.isOneTime()) {
                logger.warning("Trying fallback mission for unit " + unit
                    + " with valid mission " + m
                    + " reason " + reasons.get(unit));
                continue;
            }

            if (unit.isInEurope() && unit.isPerson() && nPorts > 0) {
                // Choose a port to add to
                if (ports == null) ports = player.getConnectedPortList();
                Colony c = ports.remove(0);
                AIColony aic = aiMain.getAIColony(c);
                if ((m = getWorkInsideColonyMission(aiUnit, aic)) != null) {
                    lb.add(", ", m);
                    updateTransport(aiUnit, oldTarget, lb);
                    reasons.put(unit, "To-work");
                    ports.add(c);
                }

            } else if (m instanceof IdleAtSettlementMission) {
                reasons.put(unit, "Idle"); // already idle
            } else {
                if ((m = getIdleAtSettlementMission(aiUnit)) != null) {
                    lb.add(", ", m);
                    updateTransport(aiUnit, oldTarget, lb);
                    reasons.put(unit, "Idle");
                }
            }
        }
        lb.grew("\n  Mission changes");

        // Now see if transport can be found
        allocateTransportables(getUrgentTransportables(),
                               transportMissions, lb);

        // Log
        if (!aiUnits.isEmpty()) {
            lb.add("\n  Free Land Units:");
            for (AIUnit aiu : aiUnits) {
                lb.add(" ", aiu.getUnit());
            }
        }
        if (!navalUnits.isEmpty()) {
            lb.add("\n  Free Naval Units:");
            for (AIUnit aiu : navalUnits) {
                lb.add(" ", aiu.getUnit());
            }
        }
        lb.add("\n  Missions(colonies=", player.getSettlementCount(),
            " builders=", nBuilders,
            " pioneers=", nPioneers,
            " scouts=", nScouts,
            " naval-carriers=", nNavalCarrier,
            ")");
        logMissions(reasons, lb);
    }

    /**
     * Choose a mission for an AIUnit.
     *
     * @param aiUnit The {@code AIUnit} to choose for.
     * @return A suitable {@code Mission}, or null if none found.
     */
    public Mission getSimpleMission(AIUnit aiUnit) {
        final Unit unit = aiUnit.getUnit();
        Mission m, ret;
        final Mission old = ((m = aiUnit.getMission()) != null && m.isValid())
            ? m : null;

        if (unit.isNaval()) {
            ret = (old instanceof PrivateerMission) ? old
                : ((m = getPrivateerMission(aiUnit, null)) != null) ? m
                : (old instanceof TransportMission) ? old
                : ((m = getTransportMission(aiUnit)) != null) ? m
                : (old instanceof UnitSeekAndDestroyMission) ? old
                : ((m = getSeekAndDestroyMission(aiUnit, 8)) != null) ? m
                : (old instanceof UnitWanderHostileMission) ? old
                : getWanderHostileMission(aiUnit);

        } else if (unit.isCarrier()) {
            ret = getTransportMission(aiUnit);

        } else {
            // CashIn missions are obvious
            ret = (old instanceof CashInTreasureTrainMission) ? old
                : ((m = getCashInTreasureTrainMission(aiUnit)) != null) ? m

                // Working in colony is obvious
                : (unit.isInColony()
                    && old instanceof WorkInsideColonyMission) ? old
                : (unit.isInColony()
                    && (m = getWorkInsideColonyMission(aiUnit, null)) != null) ? m

                // Try to maintain local defence
                : (old instanceof DefendSettlementMission) ? old
                : ((m = getDefendCurrentSettlementMission(aiUnit)) != null) ? m

                // REF override
                : (unit.hasAbility(Ability.REF_UNIT))
                ? ((old instanceof UnitSeekAndDestroyMission) ? old
                    : ((m = getSeekAndDestroyMission(aiUnit, 12)) != null) ? m
                    : (m = getWanderHostileMission(aiUnit)))

                // Favour wish realization for expert units
                : (unit.isColonist() && unit.getSkillLevel() > 0
                    && old instanceof WishRealizationMission) ? old
                : (unit.isColonist() && unit.getSkillLevel() > 0
                    && (m = getWishRealizationMission(aiUnit, null)) != null) ? m

                // Ordinary defence
                : ((m = getDefendSettlementMission(aiUnit, false)) != null) ? m

                // Try nearby offence
                : (old instanceof UnitSeekAndDestroyMission) ? old
                : ((m = getSeekAndDestroyMission(aiUnit, 8)) != null) ? m

                // Missionary missions are only available to some units
                : (old instanceof MissionaryMission) ? old
                : ((m = getMissionaryMission(aiUnit)) != null) ? m

                // Try to satisfy any remaining wishes, such as population
                : (old instanceof WishRealizationMission) ? old
                : ((m = getWishRealizationMission(aiUnit, null)) != null) ? m

                // Another try to defend, with relaxed cost decider
                : ((m = getDefendSettlementMission(aiUnit, true)) != null) ? m

                // Another try to attack, at longer range
                : ((m = getSeekAndDestroyMission(aiUnit, 16)) != null) ? m

                // Leftover offensive units should go out looking for trouble
                : (old instanceof UnitWanderHostileMission) ? old
                : ((m = getWanderHostileMission(aiUnit)) != null) ? m

                : null;
        }
        return ret;
    }

    // Mission creation convenience routines.
    // Aggregated here for uniformity.  Might have been more logical
    // to disperse them to the individual classes.

    /**
     * Gets a new BuildColonyMission for a unit.
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @param target An optional target {@code Location}.
     * @return A new mission, or null if impossible.
     */
    public Mission getBuildColonyMission(AIUnit aiUnit, Location target) {
        String reason = BuildColonyMission.invalidReason(aiUnit);
        if (reason != null) return null;
        final Unit unit = aiUnit.getUnit();
        if (target == null) {
            target = BuildColonyMission.findTarget(aiUnit, buildingRange,
                                                   unit.isInEurope());
        }
        return (target == null) ? null
            : new BuildColonyMission(getAIMain(), aiUnit, target);
    }

    /**
     * Gets a new CashInTreasureTrainMission for a unit.
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @return A new mission, or null if impossible.
     */
    public Mission getCashInTreasureTrainMission(AIUnit aiUnit) {
        String reason = CashInTreasureTrainMission.invalidReason(aiUnit);
        if (reason != null) return null;
        final Unit unit = aiUnit.getUnit();
        Location loc = CashInTreasureTrainMission.findTarget(aiUnit,
            cashInRange, unit.isInEurope());
        return (loc == null) ? null
            : new CashInTreasureTrainMission(getAIMain(), aiUnit, loc);
    }

    /**
     * Gets a new DefendSettlementMission for a unit.
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @param relaxed Use a relaxed cost decider to choose the target.
     * @return A new mission, or null if impossible.
     */
    public Mission getDefendSettlementMission(AIUnit aiUnit, boolean relaxed) {
        if (DefendSettlementMission.invalidReason(aiUnit) != null) return null;
        final Unit unit = aiUnit.getUnit();
        final Location loc = unit.getLocation();
        double worstValue = 1000000.0;
        Colony worstColony = null;
        for (AIColony aic : getAIColonies()) {
            Colony colony = aic.getColony();
            if (aic.isBadlyDefended()) {
                if (unit.isAtLocation(colony.getTile())) {
                    worstColony = colony;
                    break;
                }
                int ttr = 1 + unit.getTurnsToReach(loc, colony.getTile(),
                    unit.getCarrier(),
                    ((relaxed) ? CostDeciders.numberOfTiles() : null));
                if (ttr >= Unit.MANY_TURNS) continue;
                double value = colony.getDefenceRatio() * 100.0 / ttr;
                if (worstValue > value) {
                    worstValue = value;
                    worstColony = colony;
                }
            }
        }
        return (worstColony == null) ? null
            : getDefendSettlementMission(aiUnit, worstColony);
    }

    /**
     * Gets a new MissionaryMission for a unit.
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @return A new mission, or null if impossible.
     */
    public Mission getMissionaryMission(AIUnit aiUnit) {
        if (MissionaryMission.prepare(aiUnit) != null) return null;
        Location loc = MissionaryMission.findTarget(aiUnit, missionaryRange,
                                                    true);
        if (loc == null) {
            aiUnit.equipForRole(getSpecification().getDefaultRole());
            return null;
        }
        return new MissionaryMission(getAIMain(), aiUnit, loc);
    }

    /**
     * Gets a new PioneeringMission for a unit.
     *
     * FIXME: pioneers to make roads between colonies
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @param target An optional target {@code Location}.
     * @return A new mission, or null if impossible.
     */
    public Mission getPioneeringMission(AIUnit aiUnit, Location target) {
        if (PioneeringMission.prepare(aiUnit) != null) return null;
        if (target == null) {
            target = PioneeringMission.findTarget(aiUnit, pioneeringRange,
                                                  true);
        }
        if (target == null) {
            Unit unit = aiUnit.getUnit();
            if (unit.isInEurope() || unit.getSettlement() != null) {
                aiUnit.equipForRole(getSpecification().getDefaultRole());
            }
            return null;
        }
        return new PioneeringMission(getAIMain(), aiUnit, target);
    }

    /**
     * Gets a new PrivateerMission for a unit.
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @param target An optional target {@code Location}.
     * @return A new mission, or null if impossible.
     */
    public Mission getPrivateerMission(AIUnit aiUnit, Location target) {
        if (PrivateerMission.invalidReason(aiUnit) != null) return null;
        if (target == null) {
            target = PrivateerMission.findTarget(aiUnit, privateerRange, true);
        }
        return (target == null) ? null
            : new PrivateerMission(getAIMain(), aiUnit, target);
    }

    /**
     * Gets a new ScoutingMission for a unit.
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @return A new mission, or null if impossible.
     */
    public Mission getScoutingMission(AIUnit aiUnit) {
        if (ScoutingMission.prepare(aiUnit) != null) return null;
        Location loc = ScoutingMission.findTarget(aiUnit, scoutingRange, true);
        if (loc == null) {
            Unit unit = aiUnit.getUnit();
            if (unit.isInEurope() || unit.getSettlement() != null) {
                aiUnit.equipForRole(getSpecification().getDefaultRole());
            }
            return null;
        }            
        return new ScoutingMission(getAIMain(), aiUnit, loc);
    }

    /**
     * Gets a new TransportMission for a unit.
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @return A new mission, or null if impossible.
     */
    public Mission getTransportMission(AIUnit aiUnit) {
        if (TransportMission.invalidReason(aiUnit) != null) return null;
        return new TransportMission(getAIMain(), aiUnit);
    }

    /**
     * Gets a new WishRealizationMission for a unit.
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @param wish An optional {@code WorkerWish} to realize.
     * @return A new mission, or null if impossible.
     */
    public Mission getWishRealizationMission(AIUnit aiUnit, WorkerWish wish) {
        if (WishRealizationMission.invalidReason(aiUnit) != null) return null;
        final Unit unit = aiUnit.getUnit();
        if (wish == null) {
            wish = getBestWorkerWish(aiUnit, unit.getType());
        }
        if (wish == null) return null;
        consumeWorkerWish(aiUnit, wish);
        return new WishRealizationMission(getAIMain(), aiUnit, wish);
    }

    /**
     * Gets a WorkInsideColonyMission for a unit.
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @param aiColony An optional {@code AIColony} to work at.
     * @return A new mission, or null if impossible.
     */
    public Mission getWorkInsideColonyMission(AIUnit aiUnit,
                                              AIColony aiColony) {
        if (WorkInsideColonyMission.invalidReason(aiUnit) != null) return null;
        if (aiColony == null) {
            aiColony = getAIColony(aiUnit.getUnit().getColony());
        }
        return (aiColony == null) ? null
            : new WorkInsideColonyMission(getAIMain(), aiUnit, aiColony);
    }


    // AIPlayer interface

    /**
     * {@inheritDoc}
     */
    @Override
    protected Stance determineStance(Player other) {
        final Player player = getPlayer();
        return (other.isREF())
            ? ((player.getREFPlayer() == other) 
                // At war with our REF if rebel, otherwise at peace.
                ? ((player.isRebel()) ? Stance.WAR : Stance.PEACE)
                // Do not mess with other player's REF unless they conquer
                // their rebellious colonies.
                : ((!other.getRebels().isEmpty()) ? Stance.PEACE
                    : super.determineStance(other)))
            // Use normal stance determination for non-REF nations.
            : super.determineStance(other);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void startWorking() {
        final Player player = getPlayer();
        final Turn turn = getGame().getTurn();
        final Specification spec = getSpecification();
        initializeFromSpecification(spec);

        // This is happening, very rarely.  Hopefully now fixed by
        // synchronizing access to AIMain.aiObjects.
        if (getAIMain().getAIPlayer(player) != this) {
            throw new RuntimeException("EuropeanAIPlayer integrity fail");
        }
        clearAIUnits();
        player.clearNationCache();
        badlyDefended.clear();

        // Note call to getAIUnits().  This triggers
        // AIPlayer.createAIUnits which we want to do early, certainly
        // before cheat() or other operations that might make new units
        // happen.
        LogBuilder lb = new LogBuilder(1024);
        int colonyCount = getAIColonies().size();
        lb.add(player.getDebugName(),
               " in ", turn, "/", turn.getNumber(),
               " units=", getAIUnits().size(),
               " colonies=", colonyCount,
               " declare=", (player.checkDeclareIndependence() == null),
               " v-land-REF=", player.getRebelStrengthRatio(false),
               " v-naval-REF=", player.getRebelStrengthRatio(true));
        if (turn.isFirstTurn()) initializeMissions(lb);
        determineStances(lb);

        if (colonyCount > 0) {
            lb.add("\n  Badly defended:"); // FIXME: prioritize defence
            for (AIColony aic : getAIColonies()) {
                if (aic.isBadlyDefended()) {
                    badlyDefended.add(aic);
                    lb.add(" ", aic.getColony());
                }
            }

            lb.add("\n  Update colonies:");
            for (AIColony aic : getAIColonies()) aic.update(lb);

            buildTipMap(lb);
            buildWishMaps(lb);
        }
        cheat(lb);
        buildTransportMaps(lb);

        // Note order of operations below.  We allow rearrange et al to run
        // even when there are no movable units left because this expedites
        // mission assignment.
        List<AIUnit> aiUnits = getAIUnits();
        for (int i = 0; i < 3; i++) {
            rearrangeColonies(lb);
            giveNormalMissions(lb);
            bringGifts(lb);
            demandTribute(lb);
            if (aiUnits.isEmpty()) break;
            aiUnits = doMissions(aiUnits, lb);
        }
        lb.log(logger, Level.FINE);

        clearAIUnits();
        tipMap.clear();
        transportDemand.clear();
        transportSupply.clear();
        wagonsNeeded.clear();
        goodsWishes.clear();
        workerWishes.clear();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected List<AIUnit> doMissions(List<AIUnit> aiUnits, LogBuilder lb) {
        lb.add("\n  Do missions:");
        List<AIUnit> result = new ArrayList<>();

        // For all units, do their mission and collect the ones that need
        // to be revisited.
        for (AIUnit aiu : aiUnits) {
            final Unit unit = aiu.getUnit();
            if (unit == null || unit.isDisposed()) continue;

            // giveNormalMissions should have given all units a
            // mission, but TransportMissions may have delivered a
            // unit and completed its WishRealizationMission, so it is
            // possible for a null mission to happen here.  Refer such
            // units back to giveNormalMissions.
            final Mission oldMission = aiu.getMission();
            if (oldMission == null) {
                result.add(aiu);
                continue;
            }
            final Location oldTarget = oldMission.getTarget();
            final Location oldLocation = unit.getLocation();
            final Colony oldColony = oldLocation.getColony();

            // Do the mission.  Clean up dead units.
            lb.add("\n  ", unit, " ");
            try {
                aiu.doMission(lb);
            } catch (Exception e) {
                lb.add(", EXCEPTION: ", e.getMessage());
                logger.log(Level.WARNING, "doMissions failed for: " + aiu, e);
            }
            if (unit.isDisposed() || unit.getLocation() == null) {
                aiu.dropTransport();
                lb.add(", DIED.");
                continue;
            }
            
            updateTransport(aiu, oldTarget, lb);
            // Check again that the unit is alive, updateTransport() can
            // cause unit to disembark onto a fatal LCR!
            if (unit.isDisposed() || unit.getLocation() == null) {
                lb.add(", DIED.");
                continue;
            }
            
            // Units with moves left should be requeued.  If they are on a
            // carrier the carrier needs to have moves left.
            if (unit.getMovesLeft() > 0 && (!unit.isOnCarrier()
                    || unit.getCarrier().getMovesLeft() > 0)) {
                lb.add("+");
                result.add(aiu);
            } else {
                lb.add(".");
            }

            // Immediately update a newly built colony so that other
            // units that are about to wake up can see its tile
            // improvement plans.
            Colony newColony = unit.getLocation().getColony();
            if (oldColony == null && newColony != null
                && Map.isSameLocation(oldLocation, newColony)) {
                AIColony aiColony = getAIColony(newColony);
                aiColony.update(lb);
                updateTipMap(aiColony);
            }
        }
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int adjustMission(AIUnit aiUnit, PathNode path, Class type,
                             int value) {
        if (value > 0) {
            if (type == DefendSettlementMission.class) {
                // Reduce value in proportion to the number of defenders.
                Location loc = DefendSettlementMission.extractTarget(aiUnit, path);
                if (!(loc instanceof Colony)) {
                    throw new IllegalStateException("European players defend colonies: " + loc);
                }
                Colony colony = (Colony)loc;
                int defenders = getSettlementDefenders(colony);
                value -= 25 * defenders;
                // Reduce value according to the stockade level.
                if (colony.hasStockade()) {
                    if (defenders > colony.getStockade().getLevel() + 1) {
                        value -= 100 * colony.getStockade().getLevel();
                    } else {
                        value -= 20 * colony.getStockade().getLevel();
                    }
                }
            }
        }
        return value;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Boolean indianDemand(Unit unit, Colony colony,
                                GoodsType goods, int gold, Boolean accept) {
        // FIXME: make a better choice, check whether the colony is
        // well defended
        return !"conquest".equals(getAIAdvantage());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TradeStatus acceptDiplomaticTrade(DiplomaticTrade agreement) {
        final Player player = getPlayer();
        final Player other = agreement.getOtherPlayer(player);
        final boolean franklin
            = other.hasAbility(Ability.ALWAYS_OFFERED_PEACE);
        final java.util.Map<TradeItem, Integer> scores = new HashMap<>();
        TradeItem peace = null;
        TradeStatus result = null;
        LogBuilder lb = new LogBuilder(64);
        lb.add("Evaluate trade offer to ", player.getName(),
            " from ", other.getName());
        if (agreement.getVersion() == 0) {
            // Synthetic event
            result = TradeStatus.PROPOSE_TRADE;
        } else {
            int unacceptable = 0, value = 0;
            for (TradeItem item : agreement.getItems()) {
                if (item instanceof StanceTradeItem) {
                    getNationSummary(other); // Freshen the name summary cache
                }                    
                int score = item.evaluateFor(player);
                if (item instanceof StanceTradeItem) {
                    // Handle some special cases
                    switch (item.getStance()) {
                    case ALLIANCE: case CEASE_FIRE:
                        if (franklin) {
                            peace = item;
                            score = 0;
                        }
                        break;
                    case UNCONTACTED: case PEACE:
                        if (agreement.getContext() == TradeContext.CONTACT) {
                            peace = item;
                            score = 0;
                        }
                        break;
                    default:
                        break;
                    }
                }
                if (score == TradeItem.INVALID_TRADE_ITEM) {
                    unacceptable++;
                } else {
                    value += score;
                }
                scores.put(item, score);
                lb.add(", ", Messages.message(item.getLabel()), " = ", score);
            }
            lb.add(".");

            if (unacceptable == 0 && value >= 0) { // Accept if all good
                result = TradeStatus.ACCEPT_TRADE;
                lb.add("  All accepted at ", value, ".");
            } else { // If too many items are unacceptable, reject
                double ratio = (double)unacceptable
                    / (unacceptable + agreement.getItems().size());
                if (ratio > 0.5 - 0.5 * agreement.getVersion()) {
                    result = rejectAgreement(peace, agreement);
                    lb.add("  Too many (", unacceptable, ") unacceptable.");
                }
            }
       
            if (result == null) {
                // Dump the unacceptable offers, sum the rest
                value = 0;
                for (Entry<TradeItem, Integer> entry : scores.entrySet()) {
                    if (entry.getValue() == TradeItem.INVALID_TRADE_ITEM) {
                        agreement.remove(entry.getKey());
                        lb.add("  Dropped invalid ", entry.getKey(), ".");
                    } else {
                        value += entry.getValue();
                        lb.add("  Added valid ", entry.getKey(),
                            ", total = ", value, ".");
                    }
                }
                // If nothing is left then fail, 
                if (agreement.isEmpty()) {
                    result = rejectAgreement(peace, agreement);
                }
            }

            // Give up?
            if (randomInt(logger, "Enough diplomacy?", getAIRandom(),
                    1 + agreement.getVersion()) > 5) {
                result = rejectAgreement(peace, agreement);
                lb.add("  Ran out of patience at ", agreement.getVersion(), ".");
            }

            if (result == null) {
                // Dump the negative offers until the sum is non-negative.
                // Return a proposal with items we like/can accept, or reject
                // if none are left.
                for (Entry<TradeItem, Integer> e
                         : mapEntriesByValue(scores, ascendingIntegerComparator)) {
                    if (value >= 0) break;
                    TradeItem item = e.getKey();
                    value -= e.getValue();
                    if (value >= 50 && item instanceof GoldTradeItem) {
                        // Counter offer smaller amount of gold, FIXME: magic#
                        GoldTradeItem gti = (GoldTradeItem)item;
                        gti.setGold(gti.getGold() - value / 2);
                        value /= 2;
                        lb.add("  Reducing gold item to ", gti.getGold(), ".");
                    } else {
                        agreement.remove(item);
                        lb.add("  Dropped ", item, ", value now = ", value, ".");
                    }
                }
                if (value >= 0 && !agreement.isEmpty()) {
                    result = TradeStatus.PROPOSE_TRADE;
                    lb.add("  Pruned until acceptable at ", value, ".");
                } else {
                    result = rejectAgreement(peace, agreement);
                    lb.add("  Agreement unsalvageable at ", value, ".");
                }
            }
        }

        lb.add(" => ", result);
        lb.log(logger, Level.INFO);
        return result;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public NativeTradeAction handleTrade(NativeTradeAction action,
                                         NativeTrade nt) {
        return NativeTradeAction.NAK_INVALID;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean acceptTax(int tax) {
        boolean ret = true;
        LogBuilder lb = new LogBuilder(64);
        Goods toBeDestroyed = getPlayer().getMostValuableGoods();
        lb.add("Tax demand to ", getPlayer().getName(), " of ", tax, "% with ",
            getPlayer().getMostValuableGoods(), " ");
        GoodsType goodsType = (toBeDestroyed == null) ? null
            : toBeDestroyed.getType();

        if (tax <= 2) {
            // Accept small increase.
            ret = true;
            lb.add("accepted: small rise.");
        } else if (toBeDestroyed == null) {
            // Is this cheating to look at what the crown will destroy?
            ret = false;
            lb.add("rejected: no-goods-under-threat.");
        } else if (goodsType.isFoodType()) {
            ret = false;
            lb.add("rejected: food-type.");
        } else if (goodsType.isBreedable()) {
            // Refuse if we already have this type under production in
            // multiple places.
            int n = count(getPlayer().getSettlements(),
                          s -> s.getGoodsCount(goodsType) > 0);
            ret = n < 2;
            if (ret) {
                lb.add("accepted: breedable-type-", goodsType.getSuffix(), 
                       "-missing.");
            } else {
                lb.add("rejected: breedable-type-", goodsType.getSuffix(),
                       "-present-in-", n, "-settlements.");
            }
        } else if (goodsType.getMilitary()
            || goodsType.isTradeGoods()
            || goodsType.isBuildingMaterial()) {
            // By age 3 we should be able to produce enough ourselves.
            // FIXME: check whether we have an armory, at least
            int turn = getGame().getTurn().getNumber();
            ret = turn < 300;
            lb.add(((ret) ? "accepted" : "rejected"),
                   ": special-goods-in-turn-", turn, ".");
        } else {
            // FIXME: consider the amount of goods produced. If we
            // depend on shipping huge amounts of cheap goods, we
            // don't want these goods to be boycotted.
            final List<GoodsType> goodsTypes = getSpecification()
                .getStorableGoodsTypeList();
            int averageIncome = sum(goodsTypes,
                gt -> getPlayer().getIncomeAfterTaxes(gt)) / goodsTypes.size();
            int income = getPlayer().getIncomeAfterTaxes(toBeDestroyed.getType());
            ret = income <= 0 || income > averageIncome;
            lb.add(((ret) ? "accepted" : "rejected"),
                ": goods(", goodsType.getSuffix(), ")-with-income(", income,
                ((ret) ? ")non-positive-or-more-than(" : ")less-than-average("),
                averageIncome, ").");
        }
        if (!ret) suppressEuropeanTrade(goodsType, lb);
        lb.log(logger, Level.INFO);
        return ret;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean acceptMercenaries() {
        return getPlayer().isAtWar() || "conquest".equals(getAIAdvantage());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FoundingFather selectFoundingFather(List<FoundingFather> ffs) {
        final int age = getGame().getAge();
        FoundingFather bestFather = null;
        int bestWeight = Integer.MIN_VALUE;
        for (FoundingFather father : ffs) {
            if (father == null) continue;

            // For the moment, arbitrarily: always choose the one
            // offering custom houses.  Allowing the AI to build CH
            // early alleviates the complexity problem of handling all
            // TransportMissions correctly somewhat.
            if (father.hasAbility(Ability.BUILD_CUSTOM_HOUSE)) {
                bestFather = father;
                break;
            }

            int weight = father.getWeight(age);
            if (weight > bestWeight) {
                bestWeight = weight;
                bestFather = father;
            }
        }
        return bestFather;
    }


    // Serialization

    // getXMLTagName not needed, uses parent
}