/**
 *  Copyright (C) 2002-2015   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.common.model;

import java.util.ArrayList;
import java.util.Collection;
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.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.xml.stream.XMLStreamException;

import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.io.FreeColXMLWriter;
import net.sf.freecol.common.model.Occupation;
import net.sf.freecol.common.model.Stance;

import static net.sf.freecol.common.util.CollectionUtils.*;

import net.sf.freecol.common.util.LogBuilder;
import net.sf.freecol.common.util.RandomChoice;


/**
 * Represents a colony. A colony contains {@link Building}s and
 * {@link ColonyTile}s. The latter represents the tiles around the
 * <code>Colony</code> where working is possible.
 */
public class Colony extends Settlement implements Nameable, TradeLocation {

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

    public static final String REARRANGE_WORKERS = "rearrangeWorkers";
    public static final int LIBERTY_PER_REBEL = 200;
    /** The number of turns of advanced warning of starvation. */
    public static final int FAMINE_TURNS = 3;
    
    public static enum ColonyChangeEvent {
        POPULATION_CHANGE,
        PRODUCTION_CHANGE,
        BONUS_CHANGE,
        WAREHOUSE_CHANGE,
        BUILD_QUEUE_CHANGE,
        UNIT_TYPE_CHANGE
    }

    /** Reasons for not building a buildable. */
    public static enum NoBuildReason {
        NONE,
        NOT_BUILDING,
        NOT_BUILDABLE,
        POPULATION_TOO_SMALL,
        MISSING_BUILD_ABILITY,
        MISSING_ABILITY,
        WRONG_UPGRADE,
        COASTAL,
        LIMIT_EXCEEDED
    }

    /** A map of Buildings, indexed by the id of their basic type. */
    protected final java.util.Map<String, Building> buildingMap = new HashMap<>();

    /** A list of the ColonyTiles. */
    protected final List<ColonyTile> colonyTiles = new ArrayList<>();

    /** A map of ExportData, indexed by the ids of GoodsTypes. */
    protected final java.util.Map<String, ExportData> exportData = new HashMap<>();

    /**
     * The number of liberty points.  Liberty points are an
     * abstract game concept.  They are generated by but are not
     * identical to bells, and subject to further modification.
     */
    protected int liberty;

    /** The SoL membership this turn. */
    protected int sonsOfLiberty;

    /** The SoL membership last turn. */
    protected int oldSonsOfLiberty;

    /** The number of tories this turn. */
    protected int tories;

    /** The number of tories last turn. */
    protected int oldTories;

    /** The current production bonus. */
    protected int productionBonus;

    /**
     * The number of immigration points.  Immigration points are an
     * abstract game concept.  They are generated by but are not
     * identical to crosses.
     */
    protected int immigration;

    /** The turn in which this colony was established. */
    protected Turn established = new Turn(0);

    /** A list of items to be built. */
    protected final BuildQueue<BuildableType> buildQueue
        = new BuildQueue<>(this,
            BuildQueue.CompletionAction.REMOVE_EXCEPT_LAST,
            Consumer.COLONY_PRIORITY);

    /** The colonists that may be born. */
    protected final BuildQueue<UnitType> populationQueue
        = new BuildQueue<>(this,
            BuildQueue.CompletionAction.SHUFFLE,
            Consumer.POPULATION_PRIORITY);

    // Will only be used on enemy colonies:
    protected int displayUnitCount = -1;

    // Do not serialize below.

    /** Contains information about production and consumption. */
    private final ProductionCache productionCache = new ProductionCache(this);

    /** The occupation tracing status.  Do not serialize. */
    private boolean traceOccupation = false;



    /**
     * Constructor for ServerColony.
     *
     * @param game The enclosing <code>Game</code>.
     * @param owner The <code>Player</code> owning this <code>Colony</code>.
     * @param name The name of the new <code>Colony</code>.
     * @param tile The containing <code>Tile</code>.
     */
    protected Colony(Game game, Player owner, String name, Tile tile) {
        super(game, owner, name, tile);
    }

    /**
     * Create a new <code>Colony</code> with the given
     * identifier. The object should later be initialized by calling
     * either {@link #readFromXML(FreeColXMLReader)}.
     *
     * @param game The enclosing <code>Game</code>.
     * @param id The object identifier.
     */
    public Colony(Game game, String id) {
        super(game, id);
    }


    // Primitive accessors.

    /**
     * Gets a <code>List</code> of every {@link Building} in this
     * <code>Colony</code>.
     *
     * @return A list of <code>Building</code>s.
     * @see Building
     */
    public List<Building> getBuildings() {
        return new ArrayList<>(buildingMap.values());
    }

    /**
     * Get building of the specified general type (note: *not*
     * necessarily the exact building type supplied, but the building
     * present in the colony that is a descendant of the ultimate
     * ancestor of the specified type).
     *
     * @param type The type of the building to get.
     * @return The <code>Building</code> found.
     */
    public Building getBuilding(BuildingType type) {
        return buildingMap.get(type.getFirstLevel().getId());
    }

    /**
     * Gets a <code>List</code> of every {@link ColonyTile} in this
     * <code>Colony</code>.
     *
     * @return A list of <code>ColonyTile</code>s.
     * @see ColonyTile
     */
    public List<ColonyTile> getColonyTiles() {
        return colonyTiles;
    }

    /**
     * Get the <code>ColonyTile</code> matching the given
     * <code>Tile</code>.
     *
     * @param t The <code>Tile</code> to check.
     * @return The corresponding <code>ColonyTile</code>, or null if not found.
     */
    public ColonyTile getColonyTile(Tile t) {
        return find(colonyTiles, ct -> ct.getWorkTile() == t);
    }

    /**
     * Get the export date for a goods type.
     *
     * @param goodsType The <code>GoodsType</code> to check.
     * @return The required <code>ExportData</code>.
     */
    public ExportData getExportData(final GoodsType goodsType) {
        ExportData result = exportData.get(goodsType.getId());
        if (result == null) {
            result = new ExportData(goodsType);
            setExportData(result);
        }
        return result;
    }

    /**
     * Set some export data.
     *
     * @param newExportData A new <code>ExportData</code> value.
     */
    public final void setExportData(final ExportData newExportData) {
        exportData.put(newExportData.getId(), newExportData);
    }

    /**
     * Gets the liberty points.
     *
     * @return The current liberty.
     */
    public int getLiberty() {
        return liberty;
    }

    /**
     * Gets the production bonus of the colony.
     *
     * @return The current production bonus of the colony.
     */
    public int getProductionBonus() {
        return productionBonus;
    }

    /**
     * Gets the immigration points.
     *
     * @return The current immigration.
     */
    public int getImmigration() {
        return immigration;
    }

    /**
     * Modify the immigration points by amount given.
     *
     * @param amount An amount of immigration.
     */
    public void modifyImmigration(int amount) {
        immigration += amount;
    }

    /**
     * Get the turn this colony was established.
     *
     * @return The establishment <code>Turn</code>.
     */
    public Turn getEstablished() {
        return established;
    }

    /**
     * Set the turn of establishment.
     *
     * @param newEstablished The new <code>Turn</code> of establishment.
     */
    public void setEstablished(final Turn newEstablished) {
        this.established = newEstablished;
    }

    /**
     * Get the <code>BuildQueue</code> contents.
     *
     * @return A list of <code>Buildable</code>s.
     */
    public List<BuildableType> getBuildQueue() {
        return buildQueue.getValues();
    }

    /**
     * Set the build queue value.
     *
     * @param newBuildQueue A list of new values for the build queue.
     */
    public void setBuildQueue(final List<BuildableType> newBuildQueue) {
        buildQueue.setValues(newBuildQueue);
    }


    // Occupation routines

    /**
     * Gets the occupation tracing status.
     *
     * @return The occupation tracing status.
     */
    public boolean getOccupationTrace() {
        return this.traceOccupation;
    }

    /**
     * Sets the occupation tracing status.
     *
     * @param trace The new occupation tracing status.
     * @return The original occupation tracing status.
     */
    public boolean setOccupationTrace(boolean trace) {
        boolean ret = this.traceOccupation;
        this.traceOccupation = trace;
        return ret;
    }

    private void accumulateChoices(Collection<GoodsType> workTypes,
                                   Collection<GoodsType> tried,
                                   List<Collection<GoodsType>> result) {
        workTypes.removeAll(tried);
        if (!workTypes.isEmpty()) {
            result.add(workTypes);
            tried.addAll(workTypes);
        }
    }

    private void accumulateChoice(GoodsType workType,
                                  Collection<GoodsType> tried,
                                  List<Collection<GoodsType>> result) {
        if (workType == null) return;
        accumulateChoices(workType.getEquivalentTypes(), tried, result);
    }

    /**
     * Get a list of collections of goods types, in order of priority
     * to try to produce in this colony by a given unit.
     *
     * @param unit The <code>Unit</code> to check.
     * @param userMode If a user requested this, favour the current
     *     work type, if not favour goods that the unit requires.
     * @return The list of collections of <code>GoodsType</code>s.
     */
    public List<Collection<GoodsType>> getWorkTypeChoices(Unit unit,
                                                          boolean userMode) {
        final Specification spec = getSpecification();
        List<Collection<GoodsType>> result = new ArrayList<>();
        Set<GoodsType> tried = new HashSet<>();

        // Find the food and non-food goods types required by the unit.
        Set<GoodsType> food = new HashSet<>();
        Set<GoodsType> nonFood = new HashSet<>();
        for (AbstractGoods ag : unit.getType().getConsumedGoods()) {
            if (productionCache.getNetProductionOf(ag.getType())
                < ag.getAmount()) {
                if (ag.getType().isFoodType()) {
                    food.addAll(ag.getType().getEquivalentTypes());
                } else {
                    nonFood.addAll(ag.getType().getEquivalentTypes());
                }
            }
        }

        if (userMode) { // Favour current and expert types in user mode
            accumulateChoice(unit.getWorkType(), tried, result);
            accumulateChoice(unit.getType().getExpertProduction(), tried, result);
            accumulateChoice(unit.getExperienceType(), tried, result);
            accumulateChoices(food, tried, result);
            accumulateChoices(nonFood, tried, result);
        } else { // Otherwise favour the required goods types 
            accumulateChoices(food, tried, result);
            accumulateChoices(nonFood, tried, result);
            accumulateChoice(unit.getWorkType(), tried, result);
            accumulateChoice(unit.getType().getExpertProduction(), tried, result);
            accumulateChoice(unit.getExperienceType(), tried, result);
        }
        accumulateChoices(spec.getFoodGoodsTypeList(), tried, result);
        accumulateChoices(spec.getNewWorldLuxuryGoodsTypeList(), tried, result);
        accumulateChoices(spec.getGoodsTypeList(), tried, result);
        return result;
    }

    /**
     * Gets the best occupation for a given unit to produce one of
     * a given set of goods types.
     *
     * @param unit The <code>Unit</code> to find an
     *     <code>Occupation</code> for.
     * @param workTypes A collection of <code>GoodsType</code> to
     *     consider producing.
     * @param lb A <code>LogBuilder</code> to log to.
     * @return An <code>Occupation</code> for the given unit, or null
     *     if none found.
     */
    private Occupation getOccupationFor(Unit unit,
                                        Collection<GoodsType> workTypes,
                                        LogBuilder lb) {
        if (workTypes.isEmpty()) return null;

        Occupation best = new Occupation(null, null, null);
        int bestAmount = 0;
        for (WorkLocation wl : getCurrentWorkLocations()) {
            bestAmount = best.improve(unit, wl, bestAmount, workTypes, lb);
        }

        if (best.workLocation != null) {
            lb.add("\n  => ", best, " = ", bestAmount);
        }
        return (best.workLocation == null) ? null : best;
    }

    /**
     * Gets the best occupation for a given unit.
     *
     * @param unit The <code>Unit</code> to find an
     *     <code>Occupation</code> for.
     * @param userMode If a user requested this, favour the current
     *     work type, if not favour goods that the unit requires.
     * @param lb A <code>LogBuilder</code> to log to.
     * @return An <code>Occupation</code> for the given unit, or
     *     null if none found.
     */
    private Occupation getOccupationFor(Unit unit, boolean userMode,
                                        LogBuilder lb) {
        for (Collection<GoodsType> types : getWorkTypeChoices(unit, userMode)) {
            lb.add("\n  ");
            FreeColObject.logFreeColObjects(types, lb);
            Occupation occupation = getOccupationFor(unit, types, lb);
            if (occupation != null) return occupation;
        }
        lb.add("\n  => FAILED");
        return null;
    }

    /**
     * Gets the best occupation for a given unit to produce one of
     * a given set of goods types.
     *
     * @param unit The <code>Unit</code> to find an
     *     <code>Occupation</code> for.
     * @param workTypes A collection of <code>GoodsType</code> to
     *     consider producing.
     * @return An <code>Occupation</code> for the given unit, or null
     *     if none found.
     */
    private Occupation getOccupationFor(Unit unit,
                                        Collection<GoodsType> workTypes) {
        LogBuilder lb = new LogBuilder((getOccupationTrace()) ? 64 : 0);
        lb.add(getName(), ".getOccupationFor(", unit, ", ");
        FreeColObject.logFreeColObjects(workTypes, lb);
        lb.add(")");

        Occupation occupation = getOccupationFor(unit, workTypes, lb);
        lb.log(logger, Level.WARNING);
        return occupation;
    }

    /**
     * Gets the best occupation for a given unit.
     *
     * @param unit The <code>Unit</code> to find an
     *     <code>Occupation</code> for.
     * @param userMode If a user requested this, favour the current
     *     work type, if not favour goods that the unit requires.
     * @return An <code>Occupation</code> for the given unit, or
     *     null if none found.
     */
    private Occupation getOccupationFor(Unit unit, boolean userMode) {
        LogBuilder lb = new LogBuilder((getOccupationTrace()) ? 64 : 0);
        lb.add(getName(), ".getOccupationFor(", unit, ")");

        Occupation occupation = getOccupationFor(unit, userMode, lb);
        lb.log(logger, Level.WARNING);
        return occupation;
    }


    // WorkLocations, Buildings, ColonyTiles

    /**
     * Get a stream of all the possible work locations for this colony.
     *
     * @return A suitable <code>Stream</code>.
     */
    private Stream<WorkLocation> getAllWorkLocationsStream() {
        return Stream.concat(colonyTiles.stream(),
                             buildingMap.values().stream());
    }
    
    /**
     * Gets a list of every work location in this colony.
     *
     * @return The list of work locations.
     */
    public List<WorkLocation> getAllWorkLocations() {
        return getAllWorkLocationsStream()
            .collect(Collectors.toList());
    }

    /**
     * Gets a list of all freely available work locations
     * in this colony.
     *
     * @return The list of available <code>WorkLocation</code>s.
     */
    public List<WorkLocation> getAvailableWorkLocations() {
        return getAllWorkLocationsStream()
            .filter(WorkLocation::isAvailable)
            .collect(Collectors.toList());
    }

    /**
     * Gets a list of all current work locations in this colony.
     *
     * @return The list of current <code>WorkLocation</code>s.
     */
    public List<WorkLocation> getCurrentWorkLocations() {
        return getAllWorkLocationsStream()
            .filter(WorkLocation::isCurrent)
            .collect(Collectors.toList());
    }

    /**
     * Add a Building to this Colony.
     *
     * Lower level routine, do not use directly in-game (use buildBuilding).
     * Used for serialization and public for the test suite.
     *
     * -til: Could change the tile appearance if the building is
     * stockade-type
     *
     * @param building The <code>Building</code> to build.
     * @return True if the building was added.
     */
    public boolean addBuilding(final Building building) {
        if (building == null || building.getType() == null) return false;
        final BuildingType buildingType = building.getType().getFirstLevel();
        if (buildingType == null || buildingType.getId() == null) return false;
        buildingMap.put(buildingType.getId(), building);
        addFeatures(building.getType());
        return true;
    }

    /**
     * Remove a building from this Colony.
     *
     * -til: Could change the tile appearance if the building is
     * stockade-type
     *
     * @param building The <code>Building</code> to remove.
     * @return True if the building was removed.
     */
    protected boolean removeBuilding(final Building building) {
        final BuildingType buildingType = building.getType().getFirstLevel();
        if (buildingMap.remove(buildingType.getId()) == null) return false;
        removeFeatures(building.getType());
        return true;
    }

    /**
     * Gets a building for producing a given type of goods.
     * 
     * @param goodsType The <code>GoodsType</code> to produce.
     * @return A <code>Building</code> which produces
     *      the given type of goods, or null if not found.
     */
    public Building getBuildingForProducing(final GoodsType goodsType) {
        for (Building b : buildingMap.values()) {
            if (AbstractGoods.findByType(goodsType, b.getOutputs()) != null)
                return b;
        }
        return null;
    }

    /**
     * Gets a work location with a given ability.
     *
     * @param ability An ability key.
     * @return A <code>WorkLocation</code> with the required
     *     <code>Ability</code>, or null if not found.
     */
    public WorkLocation getWorkLocationWithAbility(String ability) {
        for (WorkLocation wl : getCurrentWorkLocations()) {
            if (wl.hasAbility(ability)) return wl;
        }
        return null;
    }

    /**
     * Gets a work location of a specific class with a given ability.
     *
     * @param ability An ability key.
     * @param returnClass The expected subclass.
     * @return A <code>WorkLocation</code> with the required
     *     <code>Ability</code>, or null if not found.
     */
    public <T extends WorkLocation> T getWorkLocationWithAbility(String ability,
        Class<T> returnClass) {
        WorkLocation wl = getWorkLocationWithAbility(ability);
        if (wl != null) try { return returnClass.cast(wl); } catch (ClassCastException cce) {};
        return null;
    }

    /**
     * Gets a work location with a given modifier.
     *
     * @param modifier A modifier key.
     * @return A <code>WorkLocation</code> with the required
     *     <code>Modifier</code>, or null if not found.
     */
    public WorkLocation getWorkLocationWithModifier(String modifier) {
        for (WorkLocation wl : getCurrentWorkLocations()) {
            if (wl.hasModifier(modifier)) return wl;
        }
        return null;
    }

    /**
     * Gets a work location of a specific class with a given modifier.
     *
     * @param modifier A modifier key.
     * @param returnClass The expected subclass.
     * @return A <code>WorkLocation</code> with the required
     *     <code>Modifier</code>, or null if not found.
     */
    public <T extends WorkLocation> T getWorkLocationWithModifier(String modifier,
        Class<T> returnClass) {
        WorkLocation wl = getWorkLocationWithModifier(modifier);
        if (wl != null) try { return returnClass.cast(wl); } catch (ClassCastException cce) {}
        return null;
    }
    
    /**
     * Collect the work locations for consuming a given type of goods.
     *
     * @param goodsType The <code>GoodsType</code> to consume.
     * @return A list of <code>WorkLocation</code>s which consume
     *     the given type of goods.
     */
    public List<WorkLocation> getWorkLocationsForConsuming(GoodsType goodsType) {
        return getCurrentWorkLocations().stream()
            .filter(wl -> any(wl.getInputs(), ag -> ag.getType() == goodsType))
            .collect(Collectors.toList());
    }

    /**
     * Collect the work locations for producing a given type of goods.
     *
     * @param goodsType The <code>GoodsType</code> to produce.
     * @return A list of <code>WorkLocation</code>s which produce
     *     the given type of goods.
     */
    public List<WorkLocation> getWorkLocationsForProducing(GoodsType goodsType) {
        return getCurrentWorkLocations().stream()
            .filter(wl -> any(wl.getOutputs(), ag -> ag.getType() == goodsType))
            .collect(Collectors.toList());
    }

    /**
     * Find a work location for producing a given type of goods.
     * Beware that this may not be the optimal location for the
     * production, for which {@link #getWorkLocationFor} is better.
     *
     * @param goodsType The <code>GoodsType</code> to produce.
     * @return A <code>WorkLocation</code>s which produces
     *      the given type of goods, or null if not found.
     */
    public WorkLocation getWorkLocationForProducing(GoodsType goodsType) {
        List<WorkLocation> wls = getWorkLocationsForProducing(goodsType);
        return (wls.isEmpty()) ? null : wls.get(0);
    }

    /**
     * Gets the work location best suited for the given unit to
     * produce a type of goods.
     *
     * @param unit The <code>Unit</code> to get the building for.
     * @param goodsType The <code>GoodsType</code> to produce.
     * @return The best <code>WorkLocation</code> found.
     */
    public WorkLocation getWorkLocationFor(Unit unit, GoodsType goodsType) {
        if (goodsType == null) return getWorkLocationFor(unit);
        Occupation occupation
            = getOccupationFor(unit, goodsType.getEquivalentTypes());
        return (occupation == null) ? null : occupation.workLocation;
    }

    /**
     * Gets the work location best suited for the given unit.
     *
     * @param unit The <code>Unit</code> to check for.
     * @return The best <code>WorkLocation</code> found.
     */
    public WorkLocation getWorkLocationFor(Unit unit) {
        Occupation occupation = getOccupationFor(unit, false);
        return (occupation == null) ? null : occupation.workLocation;
    }

    /**
     * Is a tile actually in use by this colony?
     *
     * @param tile The <code>Tile</code> to test.
     * @return True if this tile is actively in use by this colony.
     */
    public boolean isTileInUse(Tile tile) {
        ColonyTile colonyTile = getColonyTile(tile);
        return colonyTile != null && !colonyTile.isEmpty();
    }

    /**
     * Get the warehouse-type building in this colony.
     *
     * @return The warehouse <code>Building</code>.
     */
    public Building getWarehouse() {
        return getWorkLocationWithModifier(Modifier.WAREHOUSE_STORAGE,
                                           Building.class);
    }

    /**
     * Does this colony have a stockade?
     *
     * @return True if the colony has a stockade.
     */
    public boolean hasStockade() {
        return getStockade() != null;
    }

    /**
     * Gets the stockade building in this colony.
     *
     * @return The stockade <code>Building</code>.
     */
    public Building getStockade() {
        return getWorkLocationWithModifier(Modifier.DEFENCE, Building.class);
    }

    /**
     * Gets the stockade key, as should be visible to the owner
     * or a player that can see this colony.
     *
     * @return The stockade key, or null if no stockade-building is present.
     */
    public String getStockadeKey() {
        Building stockade = getStockade();
        return (stockade == null) ? null : stockade.getType().getSuffix();
    }

    /**
     * Get a weighted list of natural disasters than can strike this
     * colony.  This list comprises all natural disasters that can
     * strike the colony's tiles.
     *
     * @return A weighted list of <code>Disaster</code>s.
     */
    public List<RandomChoice<Disaster>> getDisasters() {
        List<RandomChoice<Disaster>> disasters = new ArrayList<>();
        for (ColonyTile tile : colonyTiles) {
            disasters.addAll(tile.getWorkTile().getDisasters());
        }
        return disasters;
    }


    // What are we building?  What can we build?

    /**
     * Is a building type able to be automatically built at no cost.
     * True when the player has a modifier that collapses the cost to zero.
     *
     * @param buildingType a <code>BuildingType</code> value
     * @return True if the building is available at zero cost.
     */
    public boolean isAutomaticBuild(BuildingType buildingType) {
        float value = owner.applyModifiers(100f, getGame().getTurn(),
            Modifier.BUILDING_PRICE_BONUS, buildingType);
        return value == 0f && canBuild(buildingType);
    }

    /**
     * Gets a list of every unit type this colony may build.
     *
     * @return A list of buildable <code>UnitType</code>s.
     */
    public List<UnitType> getBuildableUnits() {
        return getSpecification().getUnitTypeList().stream()
            .filter(ut -> ut.needsGoodsToBuild() && canBuild(ut))
            .collect(Collectors.toList());
    }

    /**
     * Returns how many turns it would take to build the given
     * <code>BuildableType</code>.
     *
     * @param buildable The <code>BuildableType</code> to build.
     * @return The number of turns to build the buildable, negative if
     *     some goods are not being built, UNDEFINED if none is.
     */
    public int getTurnsToComplete(BuildableType buildable) {
        return getTurnsToComplete(buildable, null);
    }

    /**
     * Returns how many turns it would take to build the given
     * <code>BuildableType</code>.
     *
     * @param buildable The <code>BuildableType</code> to build.
     * @param needed The <code>AbstractGoods</code> needed to continue
     *     the build.
     * @return The number of turns to build the buildable (which may
     *     be zero, UNDEFINED if no useful work is being done, negative
     *     if some requirement is or will block completion (value is
     *     the negation of (turns-to-blockage + 1), and if the needed
     *     argument is supplied it is set to the goods deficit).
     */
    public int getTurnsToComplete(BuildableType buildable,
                                  AbstractGoods needed) {
        final List<AbstractGoods> required = buildable.getRequiredGoods();
        int turns = 0, satisfied = 0, failing = 0, underway = 0;
        
        ProductionInfo info = productionCache.getProductionInfo(buildQueue);
        for (AbstractGoods ag : required) {
            final GoodsType type = ag.getType();
            final int amountNeeded = ag.getAmount();
            final int amountAvailable = getGoodsCount(type);
            if (amountAvailable >= amountNeeded) {
                satisfied++;
                continue;
            }
            int production = productionCache.getNetProductionOf(type);
            if (info != null) {
                AbstractGoods consumption = AbstractGoods.findByType(type,
                    info.getConsumption());
                if (consumption != null) {
                    // add the amount the build queue itself will consume
                    production += consumption.getAmount();
                }
            }
            if (production <= 0) {
                failing++;
                if (needed != null) {
                    needed.setType(type);
                    needed.setAmount(amountNeeded - amountAvailable);
                }
                continue;
            }

            underway++;
            int amountRemaining = amountNeeded - amountAvailable;
            int eta = amountRemaining / production;
            if (amountRemaining % production != 0) eta++;
            turns = Math.max(turns, eta);
        }

        return (satisfied + underway == required.size()) ? turns // Will finish
            : (failing == required.size()) ? UNDEFINED // Not even trying
            : -(turns + 1); // Blocked by something
    }

    /**
     * Returns <code>true</code> if this Colony can breed the given
     * type of Goods. Only animals (such as horses) are expected to be
     * breedable.
     *
     * @param goodsType a <code>GoodsType</code> value
     * @return a <code>boolean</code> value
     */
    public boolean canBreed(GoodsType goodsType) {
        int breedingNumber = goodsType.getBreedingNumber();
        return (breedingNumber < GoodsType.INFINITY &&
                breedingNumber <= getGoodsCount(goodsType));
    }

    /**
     * Gets the type of building currently being built.
     *
     * @return The type of building currently being built.
     */
    public BuildableType getCurrentlyBuilding() {
        return buildQueue.getCurrentlyBuilding();
    }

    /**
     * Sets the current type of buildable to be built and if it is a building
     * insist that there is only one in the queue.
     *
     * @param buildable The <code>BuildableType</code> to build.
     */
    public void setCurrentlyBuilding(BuildableType buildable) {
        buildQueue.setCurrentlyBuilding(buildable);
    }

    public boolean canBuild() {
        return canBuild(getCurrentlyBuilding());
    }

    /**
     * Returns true if this Colony can build the given BuildableType.
     *
     * @param buildableType a <code>BuildableType</code> value
     * @return a <code>boolean</code> value
     */
    public boolean canBuild(BuildableType buildableType) {
        return getNoBuildReason(buildableType, null) == NoBuildReason.NONE;
    }

    /**
     * Return the reason why the give <code>BuildableType</code> can
     * not be built.
     *
     * @param buildableType A <code>BuildableType</code> to build.
     * @param assumeBuilt An optional list of other buildable types
     *     which can be assumed to be built, for the benefit of build
     *     queue checks.
     * @return A <code>NoBuildReason</code> value decribing the failure,
     *     including <code>NoBuildReason.NONE</code> on success.
     */
    public NoBuildReason getNoBuildReason(BuildableType buildableType,
                                          List<BuildableType> assumeBuilt) {
        if (buildableType == null) {
            return NoBuildReason.NOT_BUILDING;
        } else if (!buildableType.needsGoodsToBuild()) {
            return NoBuildReason.NOT_BUILDABLE;
        } else if (buildableType.getRequiredPopulation() > getUnitCount()) {
            return NoBuildReason.POPULATION_TOO_SMALL;
        } else if (buildableType.hasAbility(Ability.COASTAL_ONLY)
            && !getTile().isCoastland()) {
            return NoBuildReason.COASTAL;
        } else {
            if (!all(buildableType.getRequiredAbilities().entrySet(),
                    e -> e.getValue() == hasAbility(e.getKey()))) {
                return NoBuildReason.MISSING_ABILITY;
            }
            if (!all(buildableType.getLimits(), l -> l.evaluate(this))) {
                return NoBuildReason.LIMIT_EXCEEDED;
            }
        }
        if (assumeBuilt == null) {
            assumeBuilt = Collections.<BuildableType>emptyList();
        }
        if (buildableType instanceof BuildingType) {
            BuildingType newBuildingType = (BuildingType) buildableType;
            Building colonyBuilding = this.getBuilding(newBuildingType);
            if (colonyBuilding == null) {
                // the colony has no similar building yet
                BuildingType from = newBuildingType.getUpgradesFrom();
                if (from != null && !assumeBuilt.contains(from)) {
                    // we are trying to build an advanced factory, we
                    // should build lower level shop first
                    return NoBuildReason.WRONG_UPGRADE;
                }
            } else {
                // a building of the same family already exists
                BuildingType from = colonyBuilding.getType().getUpgradesTo();
                if (from != newBuildingType && !assumeBuilt.contains(from)) {
                    // the existing building's next upgrade is not the
                    // new one we want to build
                    return NoBuildReason.WRONG_UPGRADE;
                }
            }
        } else if (buildableType instanceof UnitType) {
            // Non-person units need a BUILD ability, present or assumed.
            if (!buildableType.hasAbility(Ability.PERSON)
                && !hasAbility(Ability.BUILD, buildableType)
                && none(assumeBuilt, bt -> bt.hasAbility(Ability.BUILD,
                        buildableType))) {
                return NoBuildReason.MISSING_BUILD_ABILITY;
            }
        }
        return NoBuildReason.NONE;
    }

    /**
     * Returns the price for the remaining hammers and tools for the
     * {@link Building} that is currently being built.
     *
     * @return The price.
     * @see net.sf.freecol.client.control.InGameController#payForBuilding
     */
    public int getPriceForBuilding() {
        return getPriceForBuilding(getCurrentlyBuilding());
    }

    /**
     * Gets the price for the remaining resources to build a given buildable.
     *
     * @param type The <code>BuildableType</code> to build.
     * @return The price.
     * @see net.sf.freecol.client.control.InGameController#payForBuilding
     */
    public int getPriceForBuilding(BuildableType type) {
        return priceGoodsForBuilding(getRequiredGoods(type));
    }

    /**
     * Gets a price for a map of resources to build a given buildable.
     *
     * @param required A list of required <code>AbstractGoods</code>.
     * @return The price.
     * @see net.sf.freecol.client.control.InGameController#payForBuilding
     */
    public int priceGoodsForBuilding(List<AbstractGoods> required) {
        final Market market = getOwner().getMarket();
        // FIXME: magic number!
        return required.stream().mapToInt(ag -> (ag.getType().isStorable())
            ? (market.getBidPrice(ag.getType(), ag.getAmount()) * 110) / 100
            : ag.getType().getPrice() * ag.getAmount()).sum();
    }

    /**
     * Gets a map of the types of goods and amount thereof required to
     * finish a buildable in this colony.
     *
     * @param type The <code>BuildableType</code> to build.
     * @return The map to completion.
     */
    public List<AbstractGoods> getRequiredGoods(BuildableType type) {
        List<AbstractGoods> result = new ArrayList<>();
        for (AbstractGoods goods : type.getRequiredGoods()) {
            GoodsType goodsType = goods.getType();
            int remaining = goods.getAmount() - getGoodsCount(goodsType);
            if (remaining > 0) {
                result.add(new AbstractGoods(goodsType, remaining));
            }
        }
        return result;
    }

    /**
     * Gets all the goods required to complete a build.  The list
     * includes the prerequisite raw materials as well as the direct
     * requirements (i.e. hammers, tools).  If enough of a required
     * goods is present in the colony, then that type is not returned.
     * Take care to order types with raw materials first so that we
     * can prioritize gathering what is required before manufacturing.
     *
     * Public for the benefit of AI planning and the test suite.
     *
     * @param buildable The <code>BuildableType</code> to consider.
     * @return A list of required abstract goods.
     */
    public List<AbstractGoods> getFullRequiredGoods(BuildableType buildable) {
        if (buildable == null) return Collections.<AbstractGoods>emptyList();

        List<AbstractGoods> required = new ArrayList<>();
        for (AbstractGoods ag : buildable.getRequiredGoods()) {
            int amount = ag.getAmount();
            GoodsType type = ag.getType();
            while (type != null) {
                if (amount <= this.getGoodsCount(type)) break; // Shortcut
                required.add(0, new AbstractGoods(type,
                        amount - this.getGoodsCount(type)));
                type = type.getInputType();
            }
        }
        return required;
    }

    /**
     * Check if the owner can buy the remaining hammers and tools for
     * the {@link Building} that is currently being built.
     *
     * @exception IllegalStateException If the owner of this <code>Colony</code>
     *                has an insufficient amount of gold.
     * @see #getPriceForBuilding
     */
    public boolean canPayToFinishBuilding() {
        return canPayToFinishBuilding(getCurrentlyBuilding());
    }

    /**
     * Check if the owner can buy the remaining hammers and tools for
     * the {@link Building} given.
     *
     * @param buildableType a <code>BuildableType</code> value
     * @return a <code>boolean</code> value
     * @exception IllegalStateException If the owner of this <code>Colony</code>
     *                has an insufficient amount of gold.
     * @see #getPriceForBuilding
     */
    public boolean canPayToFinishBuilding(BuildableType buildableType) {
        return buildableType != null
            && getOwner().checkGold(getPriceForBuilding(buildableType));
    }


    // Liberty and the consequences

    /**
     * Adds to the liberty points by increasing the liberty goods present.
     * Used only by DebugMenu.
     *
     * @param amount The number of liberty to add.
     */
    public void addLiberty(int amount) {
        List<GoodsType> libertyTypeList = getSpecification()
            .getLibertyGoodsTypeList();
        final int uc = getUnitCount();
        if (calculateRebels(uc, sonsOfLiberty) <= uc + 1
            && amount > 0
            && !libertyTypeList.isEmpty()) {
            addGoods(libertyTypeList.get(0), amount);
        }
        updateSoL();
        updateProductionBonus();
    }

    /**
     * Modify the liberty points by amount given.
     *
     * @param amount An amount of liberty.
     */
    public void modifyLiberty(int amount) {
        // Produced liberty always applies to the player (for FFs etc)
        getOwner().modifyLiberty(amount);

        liberty += amount;
        // Liberty can not meaningfully go negative.
        liberty = Math.max(0, liberty);

        updateSoL();
        updateProductionBonus();

        // If the bell accumulation cap option is set, and the colony
        // has reached 100%, liberty can not rise higher.
        boolean capped = getSpecification()
            .getBoolean(GameOptions.BELL_ACCUMULATION_CAPPED);
        if (capped && sonsOfLiberty >= 100) {
            liberty = LIBERTY_PER_REBEL * getUnitCount();
        }
    }

    /**
     * Calculates the current SoL membership of the colony based on
     * the liberty value and colonists.
     */
    public void updateSoL() {
        int uc = getUnitCount();
        oldSonsOfLiberty = sonsOfLiberty;
        oldTories = tories;
        sonsOfLiberty = calculateSoLPercentage(uc, getLiberty());
        tories = uc - calculateRebels(uc, sonsOfLiberty);
    }

    /**
     * Calculate the SoL membership percentage of the colony based on the
     * number of colonists and liberty.
     *
     * @param uc The proposed number of units in the colony.
     * @param liberty The amount of liberty.
     * @return The percentage of SoLs, negative if not calculable.
     */
    private int calculateSoLPercentage(int uc, int liberty) {
        if (uc <= 0) return -1;

        float membership = (liberty * 100.0f) / (LIBERTY_PER_REBEL * uc);
        membership = applyModifiers(membership, getGame().getTurn(),
                                    getOwner().getModifiers(Modifier.SOL));
        if (membership < 0.0f) {
            membership = 0.0f;
        } else if (membership > 100.0f) {
            membership = 100.0f;
        }
        return (int)membership;
    }

    /**
     * Calculate the SoL membership percentage of a colony.
     *
     * @return The percentage of SoLs, negative if not calculable.
     */
    public int getSoLPercentage() {
        return calculateSoLPercentage(getUnitCount(), getLiberty());
    }

    /**
     * Calculate the number of rebels given a SoL percentage and unit count.
     *
     * @param uc The number of units in the colony.
     * @param solPercent The percentage of SoLs.
     */
    public static int calculateRebels(int uc, int solPercent) {
        return (int)Math.floor(0.01 * solPercent * uc);
    }

    /**
     * Gets the Tory membership percentage of the colony.
     *
     * @return The current Tory membership of the colony.
     */
    public int getTory() {
        return 100 - getSoL();
    }

    /**
     * Update the colony's production bonus.
     *
     * @return True if the bonus changed.
     */
    protected boolean updateProductionBonus() {
        final Specification spec = getSpecification();
        final int veryBadGovernment
            = spec.getInteger("model.option.veryBadGovernmentLimit");
        final int badGovernment
            = spec.getInteger("model.option.badGovernmentLimit");
        final int veryGoodGovernment
            = spec.getInteger("model.option.veryGoodGovernmentLimit");
        final int goodGovernment
            = spec.getInteger("model.option.goodGovernmentLimit");
        int newBonus = (sonsOfLiberty >= veryGoodGovernment) ? 2
            : (sonsOfLiberty >= goodGovernment) ? 1
            : (tories > veryBadGovernment) ? -2
            : (tories > badGovernment) ? -1
            : 0;
        if (productionBonus != newBonus) {
            invalidateCache();
            productionBonus = newBonus;
            return true;
        }
        return false;
    }

    /**
     * Gets the number of units that would be good to add/remove from this
     * colony.  That is the number of extra units that can be added without
     * damaging the production bonus, or the number of units to remove to
     * improve it.
     *
     * @return The number of units to add to the colony, or if negative
     *      the negation of the number of units to remove.
     */
    public int getPreferredSizeChange() {
        int i, limit, pop = getUnitCount();
        if (productionBonus < 0) {
            limit = pop;
            for (i = 1; i < limit; i++) {
                if (governmentChange(pop - i) == 1) break;
            }
            return -i;
        } else {
            final Specification spec = getSpecification();
            limit = spec.getInteger("model.option.badGovernmentLimit");
            for (i = 1; i < limit; i++) {
                if (governmentChange(pop + i) == -1) break;
            }
            return i - 1;
        }
    }


    // Unit manipulation and population

    /**
     * Special routine to handle non-specific add of unit to colony.
     *
     * @param unit The <code>Unit</code> to add.
     * @return True if the add succeeds.
     */
    public boolean joinColony(Unit unit) {
        Occupation occupation = getOccupationFor(unit, false);
        if (occupation == null) {
            if (!traceOccupation) {
                LogBuilder lb = new LogBuilder(64);
                getOccupationFor(unit, false, lb);
                lb.log(logger, Level.WARNING);
            }
            return false;
        }
        return occupation.install(unit);
    }

    /**
     * Can this colony reduce its population voluntarily?
     *
     * This is generally the case, but can be prevented by buildings
     * such as the stockade in classic mode.
     *
     * @return True if the population can be reduced.
     */
    public boolean canReducePopulation() {
        return getUnitCount() > applyModifiers(0f, getGame().getTurn(),
                                               Modifier.MINIMUM_COLONY_SIZE);
    }

    /**
     * Gets the message to display if the colony can not reduce its
     * population.
     *
     * @return A <code>StringTemplate</code> describing why a colony
     *     can not reduce its population, or null if it can.
     */
    public StringTemplate getReducePopulationMessage() {
        if (canReducePopulation()) return null;
        Set<Modifier> modifierSet = getModifiers(Modifier.MINIMUM_COLONY_SIZE);
        if (modifierSet.isEmpty()) return null;
        Modifier modifier = modifierSet.iterator().next();
        FreeColObject source = modifier.getSource();
        if (source instanceof BuildingType) {
            // If the modifier source is a building type, use the
            // building in the colony, which may be of a different
            // level to the modifier source.
            // This prevents the stockade modifier from matching a
            // colony-fort, and thus the message attributing the
            // failure to reduce population to a non-existing
            // stockade, BR#3522055.
            source = getBuilding((BuildingType)source).getType();
        }
        return StringTemplate.template("model.colony.minimumColonySize")
            .addName("%object%", source);
    }

    /**
     * Gets the message to display if a colony can not build something.
     *
     * @param buildable The <code>BuildableType</code> that can not be built.
     * @return A <code>ModelMessage</code> describing the build failure.
     */
    public ModelMessage getUnbuildableMessage(BuildableType buildable) {
        return new ModelMessage(ModelMessage.MessageType.WARNING,
            "model.colony.unbuildable", this, buildable)
            .addName("%colony%", getName())
            .addNamed("%object%", buildable);
    }

    /**
     * Returns 1, 0, or -1 to indicate that government would improve,
     * remain the same, or deteriorate if the colony had the given
     * population.
     *
     * @param unitCount The proposed population for the colony.
     * @return 1, 0 or -1.
     */
    public int governmentChange(int unitCount) {
        final Specification spec = getSpecification();
        final int veryBadGovernment
            = spec.getInteger(GameOptions.VERY_BAD_GOVERNMENT_LIMIT);
        final int badGovernment
            = spec.getInteger(GameOptions.BAD_GOVERNMENT_LIMIT);
        final int veryGoodGovernment
            = spec.getInteger(GameOptions.VERY_GOOD_GOVERNMENT_LIMIT);
        final int goodGovernment
            = spec.getInteger(GameOptions.GOOD_GOVERNMENT_LIMIT);

        int rebelPercent = calculateSoLPercentage(unitCount, getLiberty());
        int rebelCount = calculateRebels(unitCount, rebelPercent);
        int loyalistCount = unitCount - rebelCount;

        int result = 0;
        if (rebelPercent >= veryGoodGovernment) { // There are no tories left.
            if (sonsOfLiberty < veryGoodGovernment) {
                result = 1;
            }
        } else if (rebelPercent >= goodGovernment) {
            if (sonsOfLiberty >= veryGoodGovernment) {
                result = -1;
            } else if (sonsOfLiberty < goodGovernment) {
                result = 1;
            }
        } else {
            if (sonsOfLiberty >= goodGovernment) {
                result = -1;
            } else { // Now that no bonus is applied, penalties may.
                if (loyalistCount > veryBadGovernment) {
                    if (tories <= veryBadGovernment) {
                        result = -1;
                    }
                } else if (loyalistCount > badGovernment) {
                    if (tories <= badGovernment) {
                        result = -1;
                    } else if (tories > veryBadGovernment) {
                        result = 1;
                    }
                } else {
                    if (tories > badGovernment) {
                        result = 1;
                    }
                }
            }
        }
        return result;
    }

    public ModelMessage checkForGovMgtChangeMessage() {
        final Specification spec = getSpecification();
        final int veryBadGovernment
            = spec.getInteger(GameOptions.VERY_BAD_GOVERNMENT_LIMIT);
        final int badGovernment
            = spec.getInteger(GameOptions.BAD_GOVERNMENT_LIMIT);
        final int veryGoodGovernment
            = spec.getInteger(GameOptions.VERY_GOOD_GOVERNMENT_LIMIT);
        final int goodGovernment
            = spec.getInteger(GameOptions.GOOD_GOVERNMENT_LIMIT);

        String msgId = null;
        int number = 0;
        ModelMessage.MessageType msgType = ModelMessage.MessageType.GOVERNMENT_EFFICIENCY;
        if (sonsOfLiberty >= veryGoodGovernment) {
            // there are no tories left
            if (oldSonsOfLiberty < veryGoodGovernment) {
                msgId = "model.colony.veryGoodGovernment";
                msgType = ModelMessage.MessageType.SONS_OF_LIBERTY;
                number = veryGoodGovernment;
            }
        } else if (sonsOfLiberty >= goodGovernment) {
            if (oldSonsOfLiberty == veryGoodGovernment) {
                msgId = "model.colony.lostVeryGoodGovernment";
                msgType = ModelMessage.MessageType.SONS_OF_LIBERTY;
                number = veryGoodGovernment;
            } else if (oldSonsOfLiberty < goodGovernment) {
                msgId = "model.colony.goodGovernment";
                msgType = ModelMessage.MessageType.SONS_OF_LIBERTY;
                number = goodGovernment;
            }
        } else {
            if (oldSonsOfLiberty >= goodGovernment) {
                msgId = "model.colony.lostGoodGovernment";
                msgType = ModelMessage.MessageType.SONS_OF_LIBERTY;
                number = goodGovernment;
            }

            // Now that no bonus is applied, penalties may.
            if (tories > veryBadGovernment) {
                if (oldTories <= veryBadGovernment) {
                    // government has become very bad
                    msgId = "model.colony.veryBadGovernment";
                }
            } else if (tories > badGovernment) {
                if (oldTories <= badGovernment) {
                    // government has become bad
                    msgId = "model.colony.badGovernment";
                } else if (oldTories > veryBadGovernment) {
                    // government has improved, but is still bad
                    msgId = "model.colony.governmentImproved1";
                }
            } else if (oldTories > badGovernment) {
                // government was bad, but has improved
                msgId = "model.colony.governmentImproved2";
            }
        }

        GoodsType bells = getSpecification().getGoodsType("model.goods.bells");
        return (msgId == null) ? null
            : new ModelMessage(msgType, msgId, this, bells)
            .addName("%colony%", getName())
            .addAmount("%number%", number);
    }

    /**
     * Signal to the colony that its population is changing.
     * Called from Unit.setLocation when a unit moves into or out of this
     * colony, but *not* if it is moving within the colony.
     */
    public void updatePopulation() {
        updateSoL();
        updateProductionBonus();
        if (getOwner().isAI()) {
            firePropertyChange(Colony.REARRANGE_WORKERS, true, false);
        }
    }

    /**
     * Signal to the colony that a unit is moving in or out or
     * changing its internal work location to one with a different
     * teaching ability.  This requires either checking for a new
     * teacher or student, or clearing any existing education
     * relationships.
     *
     * @param unit The <code>Unit</code> that is changing its education state.
     * @param enable If true, check for new education opportunities, otherwise
     *     clear existing ones.
     */
    public void updateEducation(Unit unit, boolean enable) {
        WorkLocation wl = unit.getWorkLocation();
        if (wl == null) {
            throw new RuntimeException("updateEducation(" + unit
                + ") unit not at work location.");
        } else if (wl.getColony() != this) {
            throw new RuntimeException("updateEducation(" + unit
                + ") unit not at work location in this colony.");
        }
        if (enable) {
            if (wl.canTeach()) {
                Unit student = unit.getStudent();
                if (student == null
                    && (student = findStudent(unit)) != null) {
                    unit.setStudent(student);
                    student.setTeacher(unit);
                    unit.setTurnsOfTraining(0);// Teacher starts teaching
                    unit.changeWorkType(null);
                }
            } else {
                Unit teacher = unit.getTeacher();
                if (teacher == null
                    && (teacher = findTeacher(unit)) != null) {
                    unit.setTeacher(teacher);
                    teacher.setStudent(unit);
                }
            }
        } else {
            if (wl.canTeach()) {
                Unit student = unit.getStudent();
                if (student != null) {
                    student.setTeacher(null);
                    unit.setStudent(null);
                    unit.setTurnsOfTraining(0);// Teacher stops teaching
                }
            } else {
                Unit teacher = unit.getTeacher();
                if (teacher != null) {
                    teacher.setStudent(null);
                    unit.setTeacher(null);
                }
            }
        }
    }

    /**
     * Does this colony have undead units?
     *
     * @return True if this colony has undead units.
     */
    public boolean isUndead() {
        Unit u = getFirstUnit();
        return u != null && u.isUndead();
    }

    /**
     * Gets the apparent number of units at this colony.
     * Used in client enemy colonies
     *
     * @return The apparent number of <code>Unit</code>s at this colony.
     */
    public int getDisplayUnitCount() {
        return (displayUnitCount > 0) ? displayUnitCount : getUnitCount();
    }

    /**
     * Sets the apparent number of units at this colony.
     * Used in client enemy colonies
     *
     * @param count The new apparent number of <code>Unit</code>s at
     *     this colony.
     */
    public void setDisplayUnitCount(int count) {
        this.displayUnitCount = count;
    }


    // Defence, offense and trade response

    /**
     * Gets the best defender type available to this colony.
     *
     * @return The best available defender type.
     */
    public UnitType getBestDefenderType() {
        UnitType bestDefender = null;
        for (UnitType unitType : getSpecification().getUnitTypeList()) {
            if (unitType.getDefence() > 0
                && (bestDefender == null
                    || bestDefender.getDefence() < unitType.getDefence())
                && !unitType.hasAbility(Ability.NAVAL_UNIT)
                && unitType.isAvailableTo(getOwner())) {
                bestDefender = unitType;
            }
        }
        return bestDefender;
    }

    /**
     * Gets the total defence power.
     *
     * @return The total defence power.
     */
    public double getTotalDefencePower() {
        final CombatModel cm = getGame().getCombatModel();
        return getTile().getUnitList().stream()
            .filter(Unit::isDefensiveUnit)
            .mapToDouble(u -> cm.getDefencePower(null, u)).sum();
    }

    /**
     * Determines whether this colony is sufficiently unprotected and
     * contains something worth pillaging.  To be called by CombatModels
     * when the attacker has defeated an unarmed colony defender.
     *
     * @param attacker The <code>Unit</code> that has defeated the defender.
     * @return True if the attacker can pillage this colony.
     */
    public boolean canBePillaged(Unit attacker) {
        return !hasStockade()
            && attacker.hasAbility(Ability.PILLAGE_UNPROTECTED_COLONY)
            && !(getBurnableBuildings().isEmpty()
                && getTile().getNavalUnits().isEmpty()
                && (getLootableGoodsList().isEmpty()
                    || !attacker.getType().canCarryGoods()
                    || !attacker.hasSpaceLeft())
                && !canBePlundered());
    }

    /**
     * Checks if this colony can be plundered.  That is, can it yield
     * non-zero gold.
     *
     * @return True if at least one piece of gold can be plundered from this
     *     colony.
     */
    public boolean canBePlundered() {
        return owner.checkGold(1);
    }

    /**
     * Gets the buildings in this colony that could be burned by a raid.
     *
     * @return A list of burnable buildings.
     */
    public List<Building> getBurnableBuildings() {
        return getBuildings().stream()
            .filter(Building::canBeDamaged).collect(Collectors.toList());
    }

    /**
     * Gets a list of all stored goods in this colony, suitable for
     * being looted.
     *
     * @return A list of lootable goods in this colony.
     */
    public List<Goods> getLootableGoodsList() {
        return getGoodsContainer().getGoods().stream()
            .filter(g -> g.getType().isStorable())
            .collect(Collectors.toList());
    }

    /**
     * Returns <code>true</code> if the number of enemy combat units
     * on all tiles that belong to the colony exceeds the number of
     * friendly combat units. At the moment, only the colony owner's
     * own units are considered friendly, but that could be extended
     * to include the units of allied players.
     *
     * FIXME: if a colony is under siege, it should not be possible to
     * put units outside the colony, unless those units are armed.
     *
     * @return a <code>boolean</code> value
     */
    public boolean isUnderSiege() {
        int friendlyUnits = 0;
        int enemyUnits = 0;
        for (ColonyTile colonyTile : colonyTiles) {
            for (Unit unit : colonyTile.getWorkTile().getUnitList()) {
                if (unit.getOwner() == getOwner()) {
                    if (unit.isDefensiveUnit()) {
                        friendlyUnits++;
                    }
                } else if (getOwner().atWarWith(unit.getOwner())) {
                    if (unit.isOffensiveUnit()) {
                        enemyUnits++;
                    }
                }
            }
        }
        return enemyUnits > friendlyUnits;
    }

    /**
     * Evaluate this colony for a given player.
     *
     * @param player The <code>Player</code> to evaluate for.
     * @return A value for the player.
     */
    public int evaluateFor(Player player) {
        if (player.isAI()
            && player.getNumberOfSettlements() < 5) {// FIXME: magic#
            return Integer.MIN_VALUE;
        }
        int result;
        if (player.owns(this)) {
            result = getAvailableWorkLocations().stream()
                    .mapToInt(wl -> wl.evaluateFor(player)).sum()
                + getTile().getUnitList().stream()
                    .mapToInt(u -> u.evaluateFor(player)).sum()
                + getCompactGoods().stream()
                    .mapToInt(g -> g.evaluateFor(player)).sum();
        } else { // Much guesswork
            result = getDisplayUnitCount() * 1000
                + 500 // Some useful goods?
                + 200 * (int)getTile().getSurroundingTiles(0, 1).stream()
                    .filter(t -> t.getOwningSettlement() == this).count();
            Building stockade = getStockade();
            if (stockade != null) result *= stockade.getLevel();
        }
        return result;
    }


    // Education

    /**
     * Returns true if this colony has a schoolhouse and the unit type is a
     * skilled unit type with a skill level not exceeding the level of the
     * schoolhouse. @see Building#canAdd
     *
     * @param unit The unit to add as a teacher.
     * @return <code>true</code> if this unit type could be added.
     */
    public boolean canTrain(Unit unit) {
        return canTrain(unit.getType());
    }

    /**
     * Returns true if this colony has a schoolhouse and the unit type is a
     * skilled unit type with a skill level not exceeding the level of the
     * schoolhouse. The number of units already in the schoolhouse and
     * the availability of pupils are not taken into account. @see
     * Building#canAdd
     *
     * @param unitType The unit type to add as a teacher.
     * @return <code>true</code> if this unit type could be added.
     */
    public boolean canTrain(UnitType unitType) {
        return hasAbility(Ability.TEACH)
            && any(buildingMap.values(),
                b -> b.canTeach() && b.canAddType(unitType));
    }

    /**
     * Gets a list of all teachers currently present in the school
     * building.
     *
     * @return A list of teacher <code>Unit</code>s.
     */
    public List<Unit> getTeachers() {
        List<Unit> teachers = new ArrayList<>();
        for (Building building : buildingMap.values()) {
            if (building.canTeach()) {
                teachers.addAll(building.getUnitList());
            }
        }
        return teachers;
    }

    /**
     * Find a teacher for the specified student.
     * Do not search if ALLOW_STUDENT_SELECTION is true--- it is the
     * player's job then.
     *
     * @param student The student <code>Unit</code> that needs a teacher.
     * @return A potential teacher, or null of none found.
     */
    public Unit findTeacher(Unit student) {
        if (getSpecification().getBoolean(GameOptions.ALLOW_STUDENT_SELECTION))
            return null; // No automatic assignment
        for (Building building : getBuildings()) {
            if (building.canTeach()) {
                for (Unit unit : building.getUnitList()) {
                    if (unit.getStudent() == null
                        && student.canBeStudent(unit)) return unit;
                }
            }
        }
        return null;
    }

    /**
     * Find a student for the specified teacher.
     * Do not search if ALLOW_STUDENT_SELECTION is true--- its the
     * player's job then.
     *
     * @param teacher The teacher <code>Unit</code> that needs a student.
     * @return A potential student, or null of none found.
     */
    public Unit findStudent(final Unit teacher) {
        if (getSpecification().getBoolean(GameOptions.ALLOW_STUDENT_SELECTION))
            return null; // No automatic assignment
        Unit student = null;
        GoodsType expertProduction = teacher.getType().getExpertProduction();
        int skillLevel = INFINITY;
        for (Unit potentialStudent : getUnitList()) {
            /**
             * Always pick the student with the least skill first.
             * Break ties by favouring the one working in the teacher's trade,
             * otherwise first applicant wins.
             */
            if (potentialStudent.getTeacher() == null
                && potentialStudent.canBeStudent(teacher)
                && (student == null
                    || potentialStudent.getSkillLevel() < skillLevel
                    || (potentialStudent.getSkillLevel() == skillLevel
                        && potentialStudent.getWorkType() == expertProduction))) {
                student = potentialStudent;
                skillLevel = student.getSkillLevel();
            }
        }
        return student;
    }


    // Production and consumption

    /**
     * Does this colony produce a goods type?
     *
     * This is more reliable than checking net or total production,
     * either of which might be cancelling to zero.
     *
     * @param goodsType The <code>GoodsType</code> to check.
     * @return True if goods type is produced.
     */
    public boolean isProducing(GoodsType goodsType) {
        return productionCache.isProducing(goodsType);
    }

    /**
     * Does this colony consume a goods type?
     *
     * This is more reliable than checking net or total consumption,
     * either of which might be cancelling to zero.
     *
     * @param goodsType The <code>GoodsType</code> to check.
     * @return True if goods type is consumed.
     */
    public boolean isConsuming(GoodsType goodsType) {
        return productionCache.isConsuming(goodsType);
    }

    /**
     * Get a list of all {@link Consumer}s in the colony sorted by
     * priority. Consumers include all object that consume goods,
     * e.g. Units, Buildings and BuildQueues.
     *
     * @return a list of consumers
     */
    public List<Consumer> getConsumers() {
        List<Consumer> result = new ArrayList<>();
        result.addAll(getUnitList());
        result.addAll(buildingMap.values());
        result.add(buildQueue);
        result.add(populationQueue);

        Collections.sort(result, Consumer.COMPARATOR);
        return result;
    }

    /**
     * Returns the number of goods of a given type used by the settlement
     * each turn.
     *
     * @param goodsType <code>GoodsType</code> values
     * @return an <code>int</code> value
     */
    @Override
    public int getConsumptionOf(GoodsType goodsType) {
        final Specification spec = getSpecification();
        int result = super.getConsumptionOf(goodsType);
        if (spec.getGoodsType("model.goods.bells").equals(goodsType)) {
            result -= spec.getInteger("model.option.unitsThatUseNoBells");
        }
        return Math.max(0, result);
    }

    /**
     * Gets the combined production of all food types.
     *
     * @return an <code>int</code> value
     */
    public int getFoodProduction() {
        return getSpecification().getFoodGoodsTypeList().stream()
            .mapToInt(ft -> getTotalProductionOf(ft)).sum();
    }

    /**
     * Get the number of turns before starvation occurs at this colony
     * with current production levels.
     *
     * @return The number of turns before starvation occurs, or negative
     *     if it will not.
     */
    public int getStarvationTurns() {
        final GoodsType foodType = getSpecification().getPrimaryFoodType();
        final int food = getGoodsCount(foodType);
        final int newFood = getAdjustedNetProductionOf(foodType);
        return (newFood >= 0) ? -1 : food / -newFood;
    }

    /**
     * Get the number of turns before a new colonist will be born in
     * this colony with current production levels.
     *
     * @return A number of turns, or negative if no colonist will be born.
     */
    public int getNewColonistTurns() {
        final GoodsType foodType = getSpecification().getPrimaryFoodType();
        final int food = getGoodsCount(foodType);
        final int newFood = getAdjustedNetProductionOf(foodType);
        return (food + newFood >= Settlement.FOOD_PER_COLONIST) ? 1
            : (newFood <= 0) ? -1
            : (Settlement.FOOD_PER_COLONIST - food) / newFood + 1;
    }
        
        
    /**
     * Get the current production <code>Modifier</code>, which is
     * generated from the current production bonus.
     *
     * @param goodsType The <code>GoodsType</code> to produce.
     * @return A list of suitable <code>Modifier</code>s.
     */
    public List<Modifier> getProductionModifiers(GoodsType goodsType) {
        if (productionBonus == 0) return Collections.<Modifier>emptyList();
        Modifier mod = new Modifier(goodsType.getId(), productionBonus,
                                    Modifier.ModifierType.ADDITIVE,
                                    Specification.SOL_MODIFIER_SOURCE);
        mod.setModifierIndex(Modifier.COLONY_PRODUCTION_INDEX);
        List<Modifier> result = new ArrayList<>();
        result.add(mod);
        return result;
    }

    /**
     * Get the net production of the given goods type.
     *
     * (Also part of interface TradeLocation)
     *
     * @param goodsType a <code>GoodsType</code> value
     * @return an <code>int</code> value
     */
    public int getNetProductionOf(GoodsType goodsType) {
        return productionCache.getNetProductionOf(goodsType);
    }

    /**
     * Is a work location productive?
     *
     * @param workLocation The <code>WorkLocation</code> to check.
     * @return True if something is being produced at the
     *     <code>WorkLocation</code>.
     */
    public boolean isProductive(WorkLocation workLocation) {
        ProductionInfo info = productionCache.getProductionInfo(workLocation);
        return info != null && info.getProduction() != null
            && !info.getProduction().isEmpty()
            && info.getProduction().get(0).getAmount() > 0;
    }

    /**
     * Returns the net production of the given GoodsType adjusted by
     * the possible consumption of BuildQueues.
     *
     * @param goodsType a <code>GoodsType</code> value
     * @return an <code>int</code> value
     */
    public int getAdjustedNetProductionOf(GoodsType goodsType) {
        int result = productionCache.getNetProductionOf(goodsType);
        for (BuildQueue<?> queue : new BuildQueue<?>[] { buildQueue,
                                                         populationQueue }) {
            ProductionInfo info = productionCache.getProductionInfo(queue);
            if (info != null) {
                result += AbstractGoods.getCount(goodsType,
                    info.getConsumption());
            }
        }
        return result;
    }

    /**
     * Gets a copy of the current production map.
     * Useful in the server at the point net production is applied to a colony.
     *
     * @return A copy of the current production map.
     */
    protected TypeCountMap<GoodsType> getProductionMap() {
        return productionCache.getProductionMap();
    }

    /**
     * Returns the ProductionInfo for the given Object.
     *
     * @param object an <code>Object</code> value
     * @return a <code>ProductionInfo</code> value
     */
    public ProductionInfo getProductionInfo(Object object) {
        return productionCache.getProductionInfo(object);
    }

    /**
     * Invalidates the production cache.
     */
    public void invalidateCache() {
        productionCache.invalidate();
    }

    /**
     * Can this colony produce certain goods?
     *
     * @param goodsType The <code>GoodsType</code> to check production of.
     * @return True if the goods can be produced.
     */
    public boolean canProduce(GoodsType goodsType) {
        return (getNetProductionOf(goodsType) > 0)
            ? true // Obviously:-)

            // Breeding requires the breedable number to be present
            : (goodsType.isBreedable())
            ? getGoodsCount(goodsType) >= goodsType.getBreedingNumber()

            // Is there a work location that can produce the goods, with
            // positive generic production potential and all inputs satisfied?
            : any(getWorkLocationsForProducing(goodsType),
                wl -> wl.getGenericPotential(goodsType) > 0
                    && all(wl.getInputs(),ag -> canProduce(ag.getType())));
    }

  
    // Planning support

    /** Container class for tile exploration or improvement suggestions. */
    public static class TileImprovementSuggestion {

        /**
         * Comparator to order suggestions by descending improvement
         * amount.
         */
        public static final Comparator<TileImprovementSuggestion> descendingAmountComparator
            = new Comparator<TileImprovementSuggestion>() {
                    @Override
                    public int compare(TileImprovementSuggestion tis1,
                                       TileImprovementSuggestion tis2) {
                        int cmp = tis2.amount - tis1.amount;
                        if (cmp == 0) cmp = tis2.tile.compareTo(tis1.tile);
                        return cmp;
                    }
                };
        
        /** The tile to explore or improve. */
        public Tile tile;
        /** The tile improvement to make, or if null to explore an LCR. */
        public TileImprovementType tileImprovementType;
        /** The expected improvement.  INFINITY for LCRs. */
        public int amount;
        
        public TileImprovementSuggestion(Tile tile, TileImprovementType t,
                                         int amount) {
            this.tile = tile;
            this.tileImprovementType = t;
            this.amount = amount;
        }

        public boolean isExploration() {
            return this.tileImprovementType == null;
        }
    };
    
    /**
     * Collect suggestions for tiles that need exploration or
     * improvement (which may depend on current use within the colony).
     *
     * @return A list of <code>TileImprovementSuggestion</code>s.
     */
    public List<TileImprovementSuggestion> getTileImprovementSuggestions() {
        final Specification spec = getSpecification();
        List<TileImprovementSuggestion> result = new ArrayList<>();

        // Encourage exploration of neighbouring rumours.
        for (Tile tile : getTile().getSurroundingTiles(1)) {
            if (tile.hasLostCityRumour()) {
                result.add(new TileImprovementSuggestion(tile, null, INFINITY));
            }
        }

        // Consider improvements for all colony tiles
        for (ColonyTile ct : getColonyTiles()) {
            final Tile tile = ct.getWorkTile();
            if (tile == null
                || tile.getOwningSettlement() != this) continue;
            
            for (TileImprovementType t : spec.getTileImprovementTypeList()) {
                if (t.isNatural()) continue; 
                int improvement = ct.improvedBy(t);
                if (improvement > 0) {
                    result.add(new TileImprovementSuggestion(tile, t,
                                                             improvement));
                }
            }
        }
        Collections.sort(result,
            TileImprovementSuggestion.descendingAmountComparator);
        return result;
    }

    /**
     * Finds another unit in this colony that would be better at doing the
     * job of the specified unit.
     *
     * @param expert The <code>Unit</code> to consider.
     * @return A better expert, or null if none available.
     */
    public Unit getBetterExpert(Unit expert) {
        GoodsType production = expert.getWorkType();
        UnitType expertType = expert.getType();
        GoodsType expertise = expertType.getExpertProduction();
        Unit bestExpert = null;
        int bestImprovement = 0;

        if (production == null || expertise == null
            || production == expertise) return null;

        // We have an expert not doing the job of their expertise.
        // Check if there is a non-expert doing the job instead.
        for (Unit nonExpert : getUnitList()) {
            if (nonExpert.getWorkType() != expertise
                || nonExpert.getType() == expertType) continue;

            // We have found a unit of a different type doing the
            // job of this expert's expertise now check if the
            // production would be better if the units swapped
            // positions.
            int expertProductionNow = 0;
            int nonExpertProductionNow = 0;
            int expertProductionPotential = 0;
            int nonExpertProductionPotential = 0;

            // Get the current and potential productions for the
            // work location of the expert.
            WorkLocation ewl = expert.getWorkLocation();
            if (ewl != null) {
                expertProductionNow = ewl.getPotentialProduction(expertise,
                    expert.getType());
                nonExpertProductionPotential
                    = ewl.getPotentialProduction(expertise,
                        nonExpert.getType());
            }

            // Get the current and potential productions for the
            // work location of the non-expert.
            WorkLocation nwl = nonExpert.getWorkTile();
            if (nwl != null) {
                nonExpertProductionNow = nwl.getPotentialProduction(expertise,
                    nonExpert.getType());
                expertProductionPotential
                    = nwl.getPotentialProduction(expertise, expertType);
            }

            // Find the unit that achieves the best improvement.
            int improvement = expertProductionPotential
                + nonExpertProductionPotential
                - expertProductionNow
                - nonExpertProductionNow;
            if (improvement > bestImprovement) {
                bestImprovement = improvement;
                bestExpert = nonExpert;
            }
        }
        return bestExpert;
    }

    /**
     * Determine if there is a problem with the production of a given
     * goods type.
     *
     * @param goodsType The <code>GoodsType</code> to check.
     * @return A collection of warning messages.
     */
    public Collection<StringTemplate> getProductionWarnings(GoodsType goodsType) {
        List<StringTemplate> result = new ArrayList<>();
        final int amount = getGoodsCount(goodsType);
        final int production = getNetProductionOf(goodsType);
        int waste;
        
        if (goodsType.isStorable()) {
            if (goodsType.limitIgnored()) {
                if (goodsType.isFoodType()) {
                    int starve = getStarvationTurns();
                    if (starve == 0) {
                        result.add(StringTemplate
                            .template("model.colony.starving")
                            .addName("%colony%", getName()));
                    } else if (starve <= Colony.FAMINE_TURNS) {
                        result.add(StringTemplate
                            .template("model.colony.famineFeared")
                            .addName("%colony%", getName())
                            .addAmount("%number%", starve));
                    }
                }
            } else if (!getExportData(goodsType).getExported()
                && (waste = amount + production - getWarehouseCapacity()) > 0) {
                result.add(StringTemplate
                    .template("model.building.warehouseSoonFull")
                    .addNamed("%goods%", goodsType)
                    .addName("%colony%", getName())
                    .addAmount("%amount%", waste));
            }
        }

        BuildableType currentlyBuilding = getCurrentlyBuilding();
        if (currentlyBuilding != null) {
            for (AbstractGoods goods : currentlyBuilding.getRequiredGoods()) {
                if (goods.getType().equals(goodsType)
                    && amount < goods.getAmount()) {
                    int needsAmount = goods.getAmount() - amount;
                    result.add(StringTemplate
                        .template("model.colony.buildableNeedsGoods")
                        .addName("%colony%", getName())
                        .addNamed("%buildable%", currentlyBuilding)
                        .addAmount("%amount%", needsAmount)
                        .addNamed("%goodsType%", goodsType));
                }
            }
        }

        for (WorkLocation wl : getWorkLocationsForProducing(goodsType)) {
            ProductionInfo info = getProductionInfo(wl);
            if (info == null) continue;
            StringTemplate t = getInsufficientProductionMessage(info,
                AbstractGoods.findByType(goodsType,
                    info.getProductionDeficit()));
            if (t != null) result.add(t);
        }
        for (WorkLocation wl : getWorkLocationsForConsuming(goodsType)) {
            ProductionInfo info = getProductionInfo(wl);
            if (info == null) continue;
            List<AbstractGoods> deficit = info.getProductionDeficit();
            if (deficit.isEmpty()) continue;
            for (AbstractGoods ag : wl.getOutputs()) {
                if (ag.getType().isStorable()) continue;
                StringTemplate t = getInsufficientProductionMessage(info,
                    AbstractGoods.findByType(ag.getType(), deficit));
                if (t != null) result.add(t);
            }
        }

        return result;
    }

    /**
     * Get a message about insufficient production for a building
     *
     * @param info The <code>ProductionInfo</code> for the work location.
     * @param deficit The <code>AbstractGoods</code> in deficit.
     * @return A suitable <code>StringTemplate</code> or null if none required.
     */
    private StringTemplate getInsufficientProductionMessage(ProductionInfo info,
        AbstractGoods deficit) {
        if (info == null || deficit == null) return null;

        List<AbstractGoods> input = info.getConsumptionDeficit();
        if (input.isEmpty()) return null;
        StringTemplate label = StringTemplate.label(", ");
        for (AbstractGoods ag : input) label.addStringTemplate(ag.getLabel());
        
        return StringTemplate.template("model.colony.insufficientProduction")
            .addName("%colony%", getName())
            .addNamed("%outputType%", deficit.getType())
            .addAmount("%outputAmount%", deficit.getAmount())
            .addStringTemplate("%consumptionDeficit%", label);
    }

    /**
     * Check if a goods type is still useful to this colony.
     *
     * In general, all goods are useful.  However post-independence there is
     * no need for more liberty once Sol% reaches 100, nor immigration.
     * Note the latter may change when we implement sailing to other European
     * ports.
     *
     * @param goodsType The <code>GoodsType</code> to check.
     */
    public boolean goodsUseful(GoodsType goodsType) {
        if (getOwner().getPlayerType() == Player.PlayerType.INDEPENDENT) {
            if ((goodsType.isLibertyType() && getSoLPercentage() >= 100)
                || goodsType.isImmigrationType()) return false;
        }
        return true;
    }

    /**
     * Special goods need modifiers applied when changed, and immigration
     * accumulates to the owner.
     *
     * @param goodsType The <code>GoodsType</code> to modify.
     * @param amount The amount of modification.
     */
    private void modifySpecialGoods(GoodsType goodsType, int amount) {
        final Turn turn = getGame().getTurn();
        Set<Modifier> mods;

        mods = goodsType.getModifiers(Modifier.LIBERTY);
        if (!mods.isEmpty()) {
            int liberty = (int)applyModifiers(amount, turn, mods);
            modifyLiberty(liberty);
        }

        mods = goodsType.getModifiers(Modifier.IMMIGRATION);
        if (!mods.isEmpty()) {
            int migration = (int)applyModifiers(amount, turn, mods);
            modifyImmigration(migration);
            getOwner().modifyImmigration(migration);
        }
    }
    
    /**
     * Creates a temporary copy of this colony for planning purposes.
     *
     * A simple colony.copy() can not work because all the colony
     * tiles will be left referring to uncopied work tiles which the
     * colony-copy does not own, which prevents them being used as
     * valid work locations.  We have to copy the colony tile (which
     * includes the colony), and fix up all the colony tile work tiles
     * to point to copies of the original tile, and fix the ownership
     * of those tiles.
     *
     * @return A scratch version of this colony.
     */
    public Colony copyColony() {
        final Game game = getGame();
        Tile tile = getTile();
        Tile tileCopy = tile.copy(game, tile.getClass());
        Colony colony = tileCopy.getColony();
        for (ColonyTile ct : colony.getColonyTiles()) {
            Tile wt;
            if (ct.isColonyCenterTile()) {
                wt = tileCopy;
            } else {
                wt = ct.getWorkTile();
                wt = wt.copy(game, wt.getClass());
                if (wt.getOwningSettlement() == this) {
                    wt.setOwningSettlement(colony);
                }
            }
            ct.setWorkTile(wt);
        }
        return colony;
    }

    /**
     * Finds the corresponding FreeColObject from another copy of this colony.
     *
     * @param fco The <code>FreeColObject</code> in the other colony.
     * @return The corresponding <code>FreeColObject</code> in this
     *     colony, or null if not found.
     */
    @SuppressWarnings("unchecked")
    public <T extends FreeColObject> T getCorresponding(T fco) {
        final String id = fco.getId();
        if (fco instanceof WorkLocation) {
            for (WorkLocation t : getAllWorkLocations()) {
                if (t.getId().equals(id)) return (T)t;
            }
        } else if (fco instanceof Tile) {
            if (getTile().getId().equals(id)) return (T)getTile();
            for (ColonyTile ct : getColonyTiles()) {
                if (ct.getWorkTile().getId().equals(id)) return (T)ct.getWorkTile();
            }
        } else if (fco instanceof Unit) {
            for (Unit t : getUnitList()) {
                if (t.getId().equals(id)) return (T)t;
            }
            for (Unit t : getTile().getUnitList()) {
                if (t.getId().equals(id)) return (T)t;
            }
        }
        return null;
    }


    // Override FreeColObject

    /**
     * {@inheritDoc}
     */
    @Override
    public Set<Ability> getAbilities(String id, FreeColGameObjectType type,
                                     Turn turn) {
        if (turn == null) turn = getGame().getTurn();
        Set<Ability> result = super.getAbilities(id, type, turn);
        // Owner abilities also apply to colonies
        if (owner != null) result.addAll(owner.getAbilities(id, type, turn));
        return result;
    }


    // Override FreeColGameObject

    /**
     * {@inheritDoc}
     */
    @Override
    public List<FreeColGameObject> getDisposeList() {
        List<FreeColGameObject> objects = new ArrayList<>();
        for (WorkLocation workLocation : getAllWorkLocations()) {
            objects.addAll(workLocation.getDisposeList());
        }
        objects.addAll(super.getDisposeList());
        return objects;
    }


    // Interface Location (from Settlement via GoodsLocation
    //   via UnitLocation)
    //   The unit list in UnitLocation is replaced in Colonies.
    // Inherits
    //   FreeColObject.getId
    //   Settlement.getTile
    //   Settlement.getLocationLabel
    //   GoodsLocation.canAdd
    //   GoodsLocation.getGoodsContainer
    //   Settlement.getSettlement

    /**
     * {@inheritDoc}
     */
    @Override
    public StringTemplate getLocationLabelFor(Player player) {
        // Everyone can always work out a colony name.
        return StringTemplate.name(getName());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean add(Locatable locatable) {
        if (locatable instanceof Unit) {
            return joinColony((Unit)locatable);
        }
        return super.add(locatable);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean remove(Locatable locatable) {
        if (locatable instanceof Unit) {
            Location loc = locatable.getLocation();
            if (loc instanceof WorkLocation) {
                WorkLocation wl = (WorkLocation)loc;
                if (wl.getColony() == this) {
                    return wl.remove(locatable);
                }
            }                
            return false;
        }
        return super.remove(locatable);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean contains(Locatable locatable) {
        if (locatable instanceof Unit) {
            return any(getAvailableWorkLocations(),
                wl -> wl.contains(locatable));
        }
        return super.contains(locatable);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getUnitCount() {
        return getCurrentWorkLocations().stream()
            .mapToInt(wl -> wl.getUnitCount()).sum();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Unit> getUnitList() {
        ArrayList<Unit> units = new ArrayList<>();
        for (WorkLocation wl : getCurrentWorkLocations()) {
            units.addAll(wl.getUnitList());
        }
        return units;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Location up() {
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toShortString() {
        return getName();
    }


    // Interface UnitLocation
    // Inherits
    //   UnitLocation.getSpaceTaken [Irrelevant!]
    //   UnitLocation.moveToFront [Irrelevant!]
    //   UnitLocation.clearUnitList [Irrelevant!]
    //   Settlement.equipForRole
    //   Settlement.getNoAddReason


    // Interface GoodsLocation

    /**
     * {@inheritDoc}
     */
    @Override
    public int getGoodsCapacity() {
        return (int)applyModifiers(0f, getGame().getTurn(),
                                   Modifier.WAREHOUSE_STORAGE);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean addGoods(GoodsType type, int amount) {
        super.addGoods(type, amount);
        productionCache.invalidate(type);
        modifySpecialGoods(type, amount);
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Goods removeGoods(GoodsType type, int amount) {
        Goods removed = super.removeGoods(type, amount);
        productionCache.invalidate(type);
        if (removed != null) modifySpecialGoods(type, -removed.getAmount());
        return removed;
    }


    // Settlement

    /**
     * {@inheritDoc}
     */
    @Override
    public String getImageKey() {
        String key;
        if (isUndead()) {
            key = ".undead";
        } else {
            int count = getDisplayUnitCount();
            key = (count <= 3) ? ".small"
                : (count <= 7) ? ".medium"
                : ".large";
            String stockade = getStockadeKey();
            if (stockade != null) key += "." + stockade;
        }
        return "image.tileitem." + getType().getId() + key;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Unit getDefendingUnit(Unit attacker) {
        if (displayUnitCount > 0) {
            // There are units, but we don't see them
            return null;
        }

        // Note that this function will only return a unit working
        // inside the colony.  Typically, colonies are also defended
        // by units outside the colony on the same tile.  To consider
        // units outside the colony as well, use
        // @see Tile#getDefendingUnit instead.
        // 
        // Returns an arbitrary unarmed land unit unless Paul Revere
        // is present as founding father, in which case the unit can
        // be armed as well.
        List<Unit> unitList = getUnitList();

        Unit defender = null;
        double defencePower = -1.0;
        for (Unit nextUnit : unitList) {
            double unitPower = getGame().getCombatModel()
                .getDefencePower(attacker, nextUnit);
            if (Unit.betterDefender(defender, defencePower,
                    nextUnit, unitPower)) {
                defender = nextUnit;
                defencePower = unitPower;
            }
        }
        if (defender == null) {
            throw new IllegalStateException("Colony " + getName()
                + " contains no units!");
        }
        return defender;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public double getDefenceRatio() {
        return getTotalDefencePower() / (1 + getUnitCount());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isBadlyDefended() {
        return getTotalDefencePower() < 0.95 * getUnitCount() - 2.5;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public RandomRange getPlunderRange(Unit attacker) {
        if (canBePlundered()) {
            int upper = (owner.getGold() * (getUnitCount() + 1))
                / (owner.getColoniesPopulation() + 1);
            if (upper > 0) return new RandomRange(100, 1, upper+1, 1);
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getSoL() {
        return sonsOfLiberty;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getUpkeep() {
        return buildingMap.values().stream()
            .mapToInt(b -> b.getType().getUpkeep()).sum();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getTotalProductionOf(GoodsType goodsType) {
        return getCurrentWorkLocations().stream()
            .mapToInt(wl -> wl.getTotalProductionOf(goodsType)).sum();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean canProvideGoods(List<AbstractGoods> requiredGoods) {
        // Unlike priceGoods, this takes goods "reserved" for other
        // purposes into account.
        BuildableType buildable = getCurrentlyBuilding();
        for (AbstractGoods goods : requiredGoods) {
            int available = getGoodsCount(goods.getType());

            int breedingNumber = goods.getType().getBreedingNumber();
            if (breedingNumber != GoodsType.INFINITY) {
                available -= breedingNumber;
            }

            if (buildable != null) {
                available -= AbstractGoods.getCount(goods.getType(),
                    buildable.getRequiredGoods());
            }

            if (available < goods.getAmount()) return false;
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasContacted(Player player) {
        return player != null
            && (player.isEuropean()
                || getOwner().getStance(player) != Stance.UNCONTACTED);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public StringTemplate getAlarmLevelLabel(Player player) {
        Stance stance = getOwner().getStance(player);
        return StringTemplate.template("model.colony." + stance.getKey())
            .addStringTemplate("%nation%", getOwner().getNationLabel());
    }


    // Interface TradeLocation
    //   getGoodsCount provided in GoodsContainer

    /**
     * {@inheritDoc}
     */
    @Override
    public int getExportAmount(GoodsType goodsType, int turns) {
        final int present = Math.max(0, getGoodsCount(goodsType)
            + turns * getNetProductionOf(goodsType));
        final ExportData ed = getExportData(goodsType);
        return Math.max(0, present - ed.getExportLevel());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getImportAmount(GoodsType goodsType, int turns) {
        if (goodsType.limitIgnored()) return GoodsContainer.HUGE_CARGO_SIZE;

        final int present = Math.max(0, getGoodsCount(goodsType)
            + turns * getNetProductionOf(goodsType));
        int capacity = getWarehouseCapacity();
        return Math.max(0, capacity - present);
    }


    //
    // Miscellaneous low level
    //

    /**
     * Add port ability to non-landlocked colonies.
     */
    protected void addPortAbility() {
        addAbility(new Ability(Ability.HAS_PORT));
    }
    
    /**
     * Check the integrity of the build queues.  Catches build fails
     * due to broken requirements.
     *
     * @param fix Fix problems if possible.
     * @return Negative if there are problems remaining, zero if
     *     problems were fixed, positive if no problems found at all.
     */
    public int checkBuildQueueIntegrity(boolean fix) {
        int result = 1;
        List<BuildableType> buildables = buildQueue.getValues();
        List<BuildableType> assumeBuilt = new ArrayList<>();
        for (int i = 0; i < buildables.size(); i++) {
            BuildableType bt = buildables.get(i);
            NoBuildReason reason = getNoBuildReason(bt, assumeBuilt);
            if (reason == NoBuildReason.NONE) {
                assumeBuilt.add(bt);
            } else if (fix) {
                buildQueue.remove(i);
                result = Math.min(result, 0);
            } else {
                result = -1;
            }
        }
        List<UnitType> unitTypes = populationQueue.getValues();
        assumeBuilt.clear();
        for (int i = 0; i < unitTypes.size(); i++) {
            UnitType ut = unitTypes.get(i);
            NoBuildReason reason = getNoBuildReason(ut, assumeBuilt);
            if (reason == NoBuildReason.NONE) {
                assumeBuilt.add(ut);
            } else if (fix) {                
                populationQueue.remove(i);
                result = Math.min(result, 0);
            } else {
                result = -1;
            }
        }
        return result;
    }


    // Override FreeColGameObject

    /**
     * {@inheritDoc}
     */
    @Override
    public int checkIntegrity(boolean fix) {
        int result = super.checkIntegrity(fix);

        // @compat 0.10.x
        if (!isLandLocked() && !hasAbility(Ability.HAS_PORT)) {
            if (fix) {
                addPortAbility();
                result = Math.min(result, 0);
            } else {
                result = -1;
            }
        }
        // end @compat 0.10.x

        return Math.min(result, checkBuildQueueIntegrity(fix));
    }


    // Serialization

    private static final String BUILD_QUEUE_TAG = "buildQueueItem";
    private static final String ESTABLISHED_TAG = "established";
    private static final String IMMIGRATION_TAG = "immigration";
    private static final String LIBERTY_TAG = "liberty";
    private static final String PRODUCTION_BONUS_TAG = "productionBonus";
    private static final String NAME_TAG = "name";
    private static final String OLD_SONS_OF_LIBERTY_TAG = "oldSonsOfLiberty";
    private static final String OLD_TORIES_TAG = "oldTories";
    private static final String POPULATION_QUEUE_TAG = "populationQueueItem";
    private static final String SONS_OF_LIBERTY_TAG = "sonsOfLiberty";
    private static final String TORIES_TAG = "tories";
    private static final String UNIT_COUNT_TAG = "unitCount";


    /**
     * {@inheritDoc}
     */
    @Override
    protected void writeAttributes(FreeColXMLWriter xw) throws XMLStreamException {
        super.writeAttributes(xw);

        // Delegated from Settlement
        xw.writeAttribute(NAME_TAG, getName());

        xw.writeAttribute(ESTABLISHED_TAG, established.getNumber());

        // SoL has to be visible for the popular support bonus to be
        // visible to an attacking rebel player.
        xw.writeAttribute(SONS_OF_LIBERTY_TAG, sonsOfLiberty);

        if (xw.validFor(getOwner())) {

            xw.writeAttribute(OLD_SONS_OF_LIBERTY_TAG, oldSonsOfLiberty);

            xw.writeAttribute(TORIES_TAG, tories);

            xw.writeAttribute(OLD_TORIES_TAG, oldTories);

            xw.writeAttribute(LIBERTY_TAG, liberty);

            xw.writeAttribute(IMMIGRATION_TAG, immigration);

            xw.writeAttribute(PRODUCTION_BONUS_TAG, productionBonus);

        } else {

            int uc = getDisplayUnitCount();
            if (uc <= 0) {
                logger.warning("Unit count fail: " + uc + " id=" + getId()
                    + " unitCount=" + getUnitCount()
                    + " scope=" + xw.getWriteScope()
                    + " player=" + xw.getWriteScope().getClient() + "\n"
                    + net.sf.freecol.common.debug.FreeColDebugger.stackTraceToString());
            }
            xw.writeAttribute(UNIT_COUNT_TAG, uc);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void writeChildren(FreeColXMLWriter xw) throws XMLStreamException {
        super.writeChildren(xw);

        if (xw.validFor(getOwner())) {

            for (Entry<String, ExportData> e : mapEntriesByKey(exportData)) {
                e.getValue().toXML(xw);
            }

            for (WorkLocation workLocation : getSortedCopy(getAllWorkLocations())) {
                workLocation.toXML(xw);
            }

            for (BuildableType item : buildQueue.getValues()) { // In order!
                xw.writeStartElement(BUILD_QUEUE_TAG);

                xw.writeAttribute(ID_ATTRIBUTE_TAG, item);

                xw.writeEndElement();
            }

            for (BuildableType item : populationQueue.getValues()) { // In order
                xw.writeStartElement(POPULATION_QUEUE_TAG);

                xw.writeAttribute(ID_ATTRIBUTE_TAG, item);

                xw.writeEndElement();
            }

        } else {
            // Special case.  Serialize stockade-class buildings to
            // otherwise unprivileged clients as the stockade level is
            // visible to anyone who can see the colony.  This should
            // have no other information leaks because stockade
            // buildings have no production or units inside.
            Building stockade = getStockade();
            if (stockade != null) stockade.toXML(xw);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readAttributes(FreeColXMLReader xr) throws XMLStreamException {
        super.readAttributes(xr);

        established = new Turn(xr.getAttribute(ESTABLISHED_TAG, 0));

        sonsOfLiberty = xr.getAttribute(SONS_OF_LIBERTY_TAG, 0);

        oldSonsOfLiberty = xr.getAttribute(OLD_SONS_OF_LIBERTY_TAG, 0);

        tories = xr.getAttribute(TORIES_TAG, 0);

        oldTories = xr.getAttribute(OLD_TORIES_TAG, 0);

        liberty = xr.getAttribute(LIBERTY_TAG, 0);

        immigration = xr.getAttribute(IMMIGRATION_TAG, 0);

        productionBonus = xr.getAttribute(PRODUCTION_BONUS_TAG, 0);

        displayUnitCount = xr.getAttribute(UNIT_COUNT_TAG, -1);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readChildren(FreeColXMLReader xr) throws XMLStreamException {
        // Clear containers.
        colonyTiles.clear();
        buildingMap.clear();
        exportData.clear();
        buildQueue.clear();
        populationQueue.clear();

        super.readChildren(xr);

        invalidateCache();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void readChild(FreeColXMLReader xr) throws XMLStreamException {
        final Specification spec = getSpecification();
        final Game game = getGame();
        final String tag = xr.getLocalName();

        if (BUILD_QUEUE_TAG.equals(tag)) {
            BuildableType bt = xr.getType(spec, ID_ATTRIBUTE_TAG,
                BuildableType.class, (BuildableType)null);
            if (bt != null) buildQueue.add(bt);
            xr.closeTag(BUILD_QUEUE_TAG);

        } else if (POPULATION_QUEUE_TAG.equals(xr.getLocalName())) {
            UnitType ut = xr.getType(spec, ID_ATTRIBUTE_TAG,
                                     UnitType.class, (UnitType)null);
            if (ut != null) populationQueue.add(ut);
            xr.closeTag(POPULATION_QUEUE_TAG);

        } else if (Building.getXMLElementTagName().equals(tag)) {
            addBuilding(xr.readFreeColGameObject(game, Building.class));

        } else if (ColonyTile.getXMLElementTagName().equals(tag)) {
            colonyTiles.add(xr.readFreeColGameObject(game, ColonyTile.class));

        } else if (ExportData.getXMLElementTagName().equals(tag)) {
            ExportData data = new ExportData(xr);
            exportData.put(data.getId(), data);
        
        } else {
            super.readChild(xr);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return getName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getXMLTagName() { return getXMLElementTagName(); }

    /**
     * Gets the tag name of the root element representing this object.
     *
     * @return "colony".
     */
    public static String getXMLElementTagName() {
        return "colony";
    }
}
