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

package net.sf.freecol.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.function.Function;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;

import javax.xml.stream.XMLStreamException;

import net.sf.freecol.FreeCol;
import net.sf.freecol.client.gui.Canvas;
import net.sf.freecol.common.debug.FreeColDebugger;
import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.io.FreeColXMLWriter;
import net.sf.freecol.common.option.GameOptions;
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} where working is possible.
 */
public class Colony extends Settlement implements Nameable, TradeLocation {

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

    /** Class index for colonies. */
    private static final int COLONY_CLASS_INDEX = 20;

    public static final String TAG = "colony";

    public static final String REARRANGE_COLONY = "rearrangeColony";

    public static final int LIBERTY_PER_REBEL = 200;
    public static final int CHANGE_UPPER_BOUND = 10;

    /** 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}.
     * @param owner The {@code Player} owning this {@code Colony}.
     * @param name The name of the new {@code Colony}.
     * @param tile The containing {@code Tile}.
     */
    protected Colony(Game game, Player owner, String name, Tile tile) {
        super(game, owner, name, tile);
    }

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


    // Primitive accessors.

    /**
     * Get a list of every {@link Building} in this {@code Colony}.
     *
     * @return A list of {@code Building}s.
     */
    public List<Building> getBuildings() {
        synchronized (this.buildingMap) {
            return new ArrayList<>(this.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} found.
     */
    public Building getBuilding(BuildingType type) {
        synchronized (this.buildingMap) {
            return this.buildingMap.get(type.getFirstLevel().getId());
        }
    }

    /**
     * Reset the building map.
     *
     * @param buildings The list of buildings to use.
     */
    protected void setBuildingMap(List<Building> buildings) {
        clearBuildingMap();
        for (Building b : buildings) addBuilding(b);
    }

    /**
     * Clear the building map.
     */
    protected void clearBuildingMap() {
        synchronized (this.buildingMap) { this.buildingMap.clear(); }
    }

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

    /**
     * Set the colony tile list.
     *
     * @param colonyTiles The new list of {@code ColonyTile}s.
     */
    protected void setColonyTiles(List<ColonyTile> colonyTiles) {
        synchronized (this.colonyTiles) {
            this.colonyTiles.clear();
            this.colonyTiles.addAll(colonyTiles);
        }
    }
        
    /**
     * Clear the colony tiles.
     */
    private void clearColonyTiles() {
        synchronized (this.colonyTiles) { this.colonyTiles.clear(); }
    }
        
    /**
     * Get the {@code ColonyTile} matching the given {@code Tile}.
     *
     * @param tile The {@code Tile} to check.
     * @return The corresponding {@code ColonyTile}, or null if not found.
     */
    public ColonyTile getColonyTile(Tile tile) {
        return find(getColonyTiles(), matchKey(tile, ColonyTile::getWorkTile));
    }

    /**
     * Get the export data.
     *
     * @return The list of {@code ExportData}.
     */
    protected Collection<ExportData> getExportData() {
        return this.exportData.values();
    }

    /**
     * Set the export data.
     *
     * @param exportData The new list of {@code ExportData}.
     */
    protected void setExportData(Collection<ExportData> exportData) {
        this.exportData.clear();
        for (ExportData ed : exportData) setExportData(ed);
    }

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

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

    protected int getSonsOfLiberty() {
        return this.sonsOfLiberty;
    }
    protected int getOldSonsOfLiberty() {
        return this.oldSonsOfLiberty;
    }
    protected int getTories() {
        return this.tories;
    }
    protected int getOldTories() {
        return this.oldTories;
    }
    
    /**
     * Gets the production bonus of the colony.
     *
     * @return The current production bonus of the colony.
     */
    public int getProductionBonus() {
        return this.productionBonus;
    }

    /**
     * Sets the production bonus of the colony.
     *
     * Only public for the convenience of the test suite.
     *
     * @param productionBonus The new production bonus of the colony.
     */
    public void setProductionBonus(int productionBonus) {
        this.productionBonus = productionBonus;
    }

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

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

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

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

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

    /**
     * Get the population queue contents.
     *
     * @return A list of {@code Buildable}s.
     */
    public List<UnitType> getPopulationQueue() {
        return this.populationQueue.getValues();
    }

    /**
     * Set the population queue.
     *
     * @param newPopulationQueue A list of new values for the population queue.
     */
    public void setPopulationQueue(final List<UnitType> populationQueue) {
        this.populationQueue.setValues(populationQueue);
    }
    
    /**
     * Get the display unit count.
     *
     * @return The explicit unit count for display purposes.
     */
    public int getDisplayUnitCount() {
        return this.displayUnitCount;
    }

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


    // 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} 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}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 this unit
        // and which are underproduced at present.
        Set<GoodsType> food = new HashSet<>();
        Set<GoodsType> nonFood = new HashSet<>();
        for (AbstractGoods ag : transform(unit.getType().getConsumedGoods(),
                g -> productionCache.getNetProductionOf(g.getType())
                        < g.getAmount())) {
            if (ag.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} to find an
     *     {@code Occupation} for.
     * @param workTypes A collection of {@code GoodsType} to
     *     consider producing.
     * @param lb A {@code LogBuilder} to log to.
     * @return An {@code Occupation} 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 : getCurrentWorkLocationsList()) {
            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} to find an
     *     {@code Occupation} 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} to log to.
     * @return An {@code Occupation} 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} to find an
     *     {@code Occupation} for.
     * @param workTypes A collection of {@code GoodsType} to
     *     consider producing.
     * @return An {@code Occupation} 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} to find an
     *     {@code Occupation} 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} 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

    /**
     * Gets a list of every work location in this colony.
     *
     * @return The list of work locations.
     */
    public List<WorkLocation> getAllWorkLocationsList() {
        List<WorkLocation> ret = new ArrayList<>();
        synchronized (this.colonyTiles) {
            ret.addAll(this.colonyTiles);
        }
        synchronized (this.buildingMap) {
            ret.addAll(this.buildingMap.values());
        }
        return ret;
    }

    /**
     * Gets a stream of every work location in this colony.
     *
     * @return The stream of work locations.
     */
    public Stream<WorkLocation> getAllWorkLocations() {
        Stream<WorkLocation> ret = Stream.<WorkLocation>empty();
        synchronized (this.colonyTiles) {
            ret = concat(ret, map(this.colonyTiles, ct -> (WorkLocation)ct));
        }
        synchronized (this.buildingMap) {
            ret = concat(ret, map(this.buildingMap.values(), b -> (WorkLocation)b));
        }
        return ret;
    }

    /**
     * Gets a list of all freely available work locations
     * in this colony.
     *
     * @return The list of available {@code WorkLocation}s.
     */
    public List<WorkLocation> getAvailableWorkLocationsList() {
        return transform(getAllWorkLocations(), WorkLocation::isAvailable);
    }

    /**
     * Get a stream of all freely available work locations in this
     * colony.
     *
     * @return The stream of available {@code WorkLocation}s.
     */
    public Stream<WorkLocation> getAvailableWorkLocations() {
        return getAvailableWorkLocationsList().stream();
    }

    /**
     * Gets a list of all current work locations in this colony.
     *
     * @return The list of current {@code WorkLocation}s.
     */
    public List<WorkLocation> getCurrentWorkLocationsList() {
        return transform(getAllWorkLocations(), WorkLocation::isCurrent);
    }

    /**
     * Get a stream of all current work locations in this colony.
     *
     * @return The stream of current {@code WorkLocation}s.
     */
    public Stream<WorkLocation> getCurrentWorkLocations() {
        return getCurrentWorkLocationsList().stream();
    }

    /**
     * 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} 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;
        synchronized (buildingMap) {
            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} to remove.
     * @return True if the building was removed.
     */
    protected boolean removeBuilding(final Building building) {
        final BuildingType buildingType = building.getType().getFirstLevel();
        synchronized (buildingMap) {
            if (buildingMap.remove(buildingType.getId()) == null) return false;
        }
        removeFeatures(building.getType());
        return true;
    }

    /**
     * Add a colony tile.
     *
     * @param ct The {@code ColonyTile} to add.
     */
    private void addColonyTile(ColonyTile ct) {
        if (ct == null) return;
        synchronized (colonyTiles) {
            colonyTiles.add(ct);
        }
    }

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

    /**
     * Gets a work location of a specific class with a given ability.
     *
     * @param <T> The actual return type.
     * @param ability An ability key.
     * @param returnClass The expected subclass.
     * @return A {@code WorkLocation} with the required
     *     {@code Ability}, or null if not found.
     */
    public <T extends WorkLocation> T getWorkLocationWithAbility(String ability,
                                                                 Class<T> returnClass) {
        WorkLocation wl = getWorkLocationWithAbility(ability);
        try {
            if (wl != null) 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} with the required
     *     {@code Modifier}, or null if not found.
     */
    public WorkLocation getWorkLocationWithModifier(String modifier) {
        return find(getCurrentWorkLocations(), wl -> wl.hasModifier(modifier));
    }

    /**
     * Gets a work location of a specific class with a given modifier.
     *
     * @param <T> The actual return type.
     * @param modifier A modifier key.
     * @param returnClass The expected subclass.
     * @return A {@code WorkLocation} with the required
     *     {@code Modifier}, 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} to consume.
     * @return A list of {@code WorkLocation}s which consume
     *     the given type of goods.
     */
    public List<WorkLocation> getWorkLocationsForConsuming(GoodsType goodsType) {
        return transform(getCurrentWorkLocations(),
                wl -> any(wl.getInputs(), AbstractGoods.matches(goodsType)));
    }

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

    /**
     * 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} to produce.
     * @return A {@code WorkLocation}s which produces
     *      the given type of goods, or null if not found.
     */
    public WorkLocation getWorkLocationForProducing(GoodsType goodsType) {
        return first(getWorkLocationsForProducing(goodsType));
    }

    /**
     * Gets the work location best suited for the given unit to
     * produce a type of goods.
     *
     * @param unit The {@code Unit} to get the building for.
     * @param goodsType The {@code GoodsType} to produce.
     * @return The best {@code WorkLocation} 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} to check for.
     * @return The best {@code WorkLocation} 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} 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}.
     */
    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}.
     */
    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 stream of {@code Disaster}s.
     */
    public Stream<RandomChoice<Disaster>> getDisasterChoices() {
        return flatten(getColonyTiles(),
                ct -> ct.getWorkTile().getDisasterChoices());
    }


    // 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} 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}s.
     */
    public List<UnitType> getBuildableUnits() {
        return transform(getSpecification().getUnitTypeList(),
                ut -> ut.needsGoodsToBuild() && canBuild(ut));
    }

    /**
     * Returns how many turns it would take to build the given
     * {@code BuildableType}.
     *
     * @param buildable The {@code BuildableType} 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}.
     *
     * @param buildable The {@code BuildableType} to build.
     * @param needed The {@code AbstractGoods} 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.getRequiredGoodsList();
        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 = find(info.getConsumption(),
                        AbstractGoods.matches(type));
                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} 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} value
     * @return a {@code boolean} 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} 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} value
     * @return a {@code boolean} value
     */
    public boolean canBuild(BuildableType buildableType) {
        return getNoBuildReason(buildableType, null) == NoBuildReason.NONE;
    }

    /**
     * Return the reason why the give {@code BuildableType} can
     * not be built.
     *
     * @param buildableType A {@code BuildableType} 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} value decribing the failure,
     *     including {@code NoBuildReason.NONE} 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 (any(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();
        }
        return buildableType.canBeBuiltInColony(this.getColony(), assumeBuilt);
    }

    /**
     * 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} 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}.
     * @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 sum(required,
                ag -> (ag.getType().isStorable())
                        ? (market.getBidPrice(ag.getType(), ag.getAmount()) * 110)/100
                        : ag.getType().getPrice() * ag.getAmount());
    }

    /**
     * Gets a map of the types of goods and amount thereof required to
     * finish a buildable in this colony.
     *
     * @param type The {@code BuildableType} to build.
     * @return The map to completion.
     */
    public List<AbstractGoods> getRequiredGoods(BuildableType type) {
        return transform(type.getRequiredGoods(),
                ag -> ag.getAmount() > getGoodsCount(ag.getType()),
                ag -> new AbstractGoods(ag.getType(),
                        ag.getAmount() - getGoodsCount(ag.getType())));
    }

    /**
     * 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} 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.getRequiredGoodsList()) {
            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.
     *
     * @return True if the user can afford to pay.
     * @exception IllegalStateException If the owner of this
     *     {@code Colony} 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} value
     * @return True if the user can afford to pay.
     * @exception IllegalStateException If the owner of this
     *     {@code Colony} 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.
     * @return The number of rebels.
     */
    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(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 newBonus = (sonsOfLiberty >= veryGoodGovernment) ? 2
                : (sonsOfLiberty >= goodGovernment) ? 1
                : (tories > veryBadGovernment) ? -2
                : (tories > badGovernment) ? -1
                : 0;
        if (productionBonus != newBonus) {
            invalidateCache();
            setProductionBonus(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 = CHANGE_UPPER_BOUND;
            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} to add.
     * @return True if the add succeeds.
     */
    public boolean joinColony(Unit unit) {
        boolean ret;
        Occupation occupation = getOccupationFor(unit, false);
        if (occupation == null) {
            if (!traceOccupation) {
                LogBuilder lb = new LogBuilder(64);
                getOccupationFor(unit, false, lb);
                lb.log(logger, Level.WARNING);
            }
            ret = false;
        } else {
            ret = occupation.install(unit);
        }
        if (!ret) {
            unit.setLocation(getTile()); // Fall back to safe value
            logger.warning("Failed to join " + getName() + ": " + unit);
        }
        return ret;
    }

    /**
     * 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} describing why a colony
     *     can not reduce its population, or null if it can.
     */
    public StringTemplate getReducePopulationMessage() {
        if (canReducePopulation()) return null;
        Modifier min = first(getModifiers(Modifier.MINIMUM_COLONY_SIZE));
        if (min == null) return null;
        FreeColObject source = min.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} that can not be built.
     * @return A {@code ModelMessage} 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_COLONY, 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} 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}s at this colony.
     */
    public int getApparentUnitCount() {
        return (this.displayUnitCount > 0) ? this.displayUnitCount
            : getUnitCount();
    }


    // Defence, offense and trade response

    /**
     * Gets the best defender type available to this colony.
     *
     * @return The best available defender type.
     */
    public UnitType getBestDefenderType() {
        final Predicate<UnitType> defenderPred = ut ->
                ut.getDefence() > 0
                        && !ut.isNaval()
                        && ut.isAvailableTo(getOwner());
        return maximize(getSpecification().getUnitTypeList(), defenderPred,
                UnitType.defenceComparator);
    }

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

    /**
     * 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} 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 transform(getBuildings(), Building::canBeDamaged);
    }

    /**
     * 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 transform(getGoodsList(), AbstractGoods::isStorable);
    }

    /**
     * Decide 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 Whether the colony is under siege.
     */
    public boolean isUnderSiege() {
        int friendlyUnits = 0;
        int enemyUnits = 0;
        for (Unit u : iterable(flatten(getColonyTiles(),
                ct -> ct.getWorkTile().getUnits()))) {
            if (u.getOwner() == getOwner()) {
                if (u.isDefensiveUnit()) friendlyUnits++;
            } else if (getOwner().atWarWith(u.getOwner())) {
                if (u.isOffensiveUnit()) enemyUnits++;
            }
        }
        return enemyUnits > friendlyUnits;
    }

    /**
     * Evaluate this colony for a given player.
     *
     * @param player The {@code Player} to evaluate for.
     * @return A value for the player.
     */
    public int evaluateFor(Player player) {
        if (player.isAI()
                && player.getSettlementCount() < 5) {// FIXME: magic#
            return Integer.MIN_VALUE;
        }
        int result, v;
        if (player.owns(this)) {
            result = 0;
            for (WorkLocation wl : getAvailableWorkLocationsList()) {
                v = wl.evaluateFor(player);
                if (v == Integer.MIN_VALUE) return Integer.MIN_VALUE;
                result += v;
            }
            for (Unit u : getTile().getUnitList()) {
                v = u.evaluateFor(player);
                if (v == Integer.MIN_VALUE) return Integer.MIN_VALUE;
                result += v;
            }
            for (Goods g : getCompactGoodsList()) {
                v = g.evaluateFor(player);
                if (v == Integer.MIN_VALUE) return Integer.MIN_VALUE;
                result += v;
            }
        } else { // Much guesswork
            result = getApparentUnitCount() * 1000
                    + 500 // Some useful goods?
                    + 200 * count(getTile().getSurroundingTiles(0, 1),
                    matchKey(this, Tile::getOwningSettlement));
            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} 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} if this unit type could be added.
     */
    public boolean canTrain(UnitType unitType) {
        return hasAbility(Ability.TEACH)
                && any(getBuildings(),
                b -> b.canTeach() && b.canAddType(unitType));
    }

    /**
     * Gets a list of all teachers currently present in the school
     * building.
     *
     * @return A stream of teacher {@code Unit}s.
     */
    public Stream<Unit> getTeachers() {
        return flatten(getBuildings(), Building::canTeach, Building::getUnits);
    }

    /**
     * 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} that needs a teacher.
     * @return A potential teacher, or null of none found.
     */
    public Unit findTeacher(Unit student) {
        return (getSpecification().getBoolean(GameOptions.ALLOW_STUDENT_SELECTION))
                ? null // No automatic assignment
                : find(flatten(getBuildings(), Building::canTeach,
                Building::getUnits),
                u -> u.getStudent() == 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} 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
        final GoodsType expertProduction
                = teacher.getType().getExpertProduction();
        final Predicate<Unit> teacherPred = u ->
                u.getTeacher() == null && u.canBeStudent(teacher);
        // 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.
        final Comparator<Unit> skillComparator
                = Comparator.comparingInt(Unit::getSkillLevel);
        final Comparator<Unit> tradeComparator
                = Comparator.comparingInt(u ->
                (u.getWorkType() == expertProduction) ? 0 : 1);
        final Comparator<Unit> fullComparator
                = skillComparator.thenComparing(tradeComparator);
        return minimize(getUnits(), teacherPred, fullComparator);
    }


    // 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} 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} 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(getBuildings());
        result.add(buildQueue);
        result.add(populationQueue);
        result.sort(Consumer.COMPARATOR);
        return result;
    }

    /**
     * Returns the number of goods of a given type used by the settlement
     * each turn.
     *
     * @param goodsType {@code GoodsType} values
     * @return an {@code int} 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(GameOptions.UNITS_THAT_USE_NO_BELLS);
        }
        return Math.max(0, result);
    }

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

    /**
     * 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}, which is
     * generated from the current production bonus.
     *
     * @param goodsType The {@code GoodsType} to produce.
     * @param unitType An optional {@code UnitType} to do the work.
     * @param wl The {@link WorkLocation}
     * @return A stream of suitable {@code Modifier}s.
     */
    public Stream<Modifier> getProductionModifiers(GoodsType goodsType,
                                                   UnitType unitType, WorkLocation wl) {
        if (productionBonus == 0) return Stream.<Modifier>empty();
        int bonus = (int)Math.floor(productionBonus * wl.getRebelFactor());
        Modifier mod = new Modifier(goodsType.getId(), bonus,
                Modifier.ModifierType.ADDITIVE,
                Specification.SOL_MODIFIER_SOURCE);
        mod.setModifierIndex(Modifier.COLONY_PRODUCTION_INDEX);
        return Stream.of(mod);
    }

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

    /**
     * Is a work location productive?
     *
     * @param workLocation The {@code WorkLocation} to check.
     * @return True if something is being produced at the
     *     {@code WorkLocation}.
     */
    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} value
     * @return an {@code int} value
     */
    public int getAdjustedNetProductionOf(final GoodsType goodsType) {
        final ToIntFunction<BuildQueue> consumes = q -> {
            ProductionInfo pi = productionCache.getProductionInfo(q);
            return (pi == null) ? 0
                    : AbstractGoods.getCount(goodsType, pi.getConsumption());
        };
        return productionCache.getNetProductionOf(goodsType)
                + sum(Stream.of(buildQueue, populationQueue), consumes);
    }

    /**
     * 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} value
     * @return a {@code ProductionInfo} value
     */
    public ProductionInfo getProductionInfo(Object object) {
        return productionCache.getProductionInfo(object);
    }

    /**
     * Update all the production types.
     *
     * Called at initialization, to default to something rational when
     * nothing was specified.  This can not be done until all the tiles are
     * present.
     */
    public void updateProductionTypes() {
        for (WorkLocation wl : getAvailableWorkLocationsList()) {
            wl.updateProductionType();
        }
    }

    /**
     * Can this colony produce certain goods?
     *
     * @param goodsType The {@code GoodsType} 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
                = Comparator.comparingInt(TileImprovementSuggestion::getAmount)
                .reversed()
                .thenComparing(tis -> (FreeColObject)tis.tile);

        /** 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;
        }

        public int getAmount() {
            return this.amount;
        }
    };

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

        // Encourage exploration of neighbouring rumours.
        List<TileImprovementSuggestion> result
                = transform(getTile().getSurroundingTiles(1, 1),
                Tile::hasLostCityRumour,
                t -> new TileImprovementSuggestion(t, null, INFINITY));

        // Consider improvements for all available colony tiles.
        for (final ColonyTile ct : transform(getColonyTiles(),
                WorkLocation::isAvailable)) {
            final ToIntFunction<TileImprovementType> improve = cacheInt(ti ->
                    ct.improvedBy(ti));
            result.addAll(transform(spec.getTileImprovementTypeList(),
                    ti -> !ti.isNatural() && improve.applyAsInt(ti) > 0,
                    ti -> new TileImprovementSuggestion(ct.getWorkTile(),
                            ti, improve.applyAsInt(ti))));
        }
        result.sort(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} 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 : transform(getUnits(), u ->
                u.getWorkType() == expertise && u.getType() != expertType)) {

            // 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.getWorkLocation();
            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} to check.
     * @return A collection of warning messages.
     */
    public Collection<StringTemplate> getProductionWarnings(GoodsType goodsType) {
        final int amount = getGoodsCount(goodsType);
        final int production = getNetProductionOf(goodsType);
        List<StringTemplate> result = new ArrayList<>();

        if (goodsType.isStorable()) {
            if (goodsType.limitIgnored()) {
                if (goodsType.isFoodType()) { // Check for famine/starvation
                    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 { // Check for overflow
                int waste;
                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));
                }
            }
        }

        // Add a message for goods required for the current building if any.
        BuildableType currentlyBuilding = getCurrentlyBuilding();
        if (currentlyBuilding != null) {
            final Function<AbstractGoods, StringTemplate> bMapper = ag ->
                    StringTemplate.template("model.colony.buildableNeedsGoods")
                            .addName("%colony%", getName())
                            .addNamed("%buildable%", currentlyBuilding)
                            .addAmount("%amount%", ag.getAmount() - amount)
                            .addNamed("%goodsType%", goodsType);
            result.addAll(transform(currentlyBuilding.getRequiredGoods(),
                    ag -> ag.getType() == goodsType
                            && amount < ag.getAmount(),
                    bMapper));
        }

        // Add insufficient production messages for each production location
        // that has a deficit in producing the goods type.
        final Function<WorkLocation, ProductionInfo> piMapper = wl ->
                getProductionInfo(wl);
        final Predicate<WorkLocation> prodPred = isNotNull(piMapper);
        final Function<WorkLocation, StringTemplate> pMapper = wl ->
                getInsufficientProductionMessage(getProductionInfo(wl),
                        wl.getProductionDeficit(goodsType));
        result.addAll(transform(getWorkLocationsForProducing(goodsType),
                prodPred, pMapper, toListNoNulls()));

        // Add insufficient production messages for each consumption
        // location for the goods type where there is a consequent
        // deficit in production of a dependent goods.
        final Function<WorkLocation, List<StringTemplate>> cMapper = wl -> {
            final ProductionInfo info = getProductionInfo(wl);
            final Function<AbstractGoods, StringTemplate> gMapper = ag ->
                    getInsufficientProductionMessage(info,
                            wl.getProductionDeficit(ag.getType()));
            return transform(wl.getOutputs(), AbstractGoods::isStorable,
                    gMapper, toListNoNulls());
        };
        result.addAll(transform(getWorkLocationsForConsuming(goodsType),
                prodPred, cMapper, toAppendedList()));

        return result;
    }

    /**
     * Get a message about insufficient production for a building
     *
     * @param info The {@code ProductionInfo} for the work location.
     * @param deficit The {@code AbstractGoods} in deficit.
     * @return A suitable {@code StringTemplate} 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} to check.
     * @return True if these goods are still useful here.
     */
    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} to modify.
     * @param amount The amount of modification.
     */
    private void modifySpecialGoods(GoodsType goodsType, int amount) {
        final Turn turn = getGame().getTurn();
        List<Modifier> mods;

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

        mods = toList(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);
        Colony colony = tileCopy.getColony();
        for (ColonyTile ct : colony.getColonyTiles()) {
            Tile wt;
            if (ct.isColonyCenterTile()) {
                wt = tileCopy;
            } else {
                wt = ct.getWorkTile();
                wt = wt.copy(game);
                if (wt.getOwningSettlement() == this) {
                    wt.setOwningSettlement(colony);
                }
            }
            ct.setWorkTile(wt);
        }
        return colony;
    }

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


    // Override FreeColObject

    /**
     * {@inheritDoc}
     */
    @Override
    public Stream<Ability> getAbilities(String id, FreeColSpecObjectType type,
                                        Turn turn) {
        if (turn == null) turn = getGame().getTurn();
        return concat(super.getAbilities(id, type, turn),
                ((owner == null) ? Stream.<Ability>empty()
                        : owner.getAbilities(id, type, turn)));
    }


    // Override FreeColGameObject

    /**
     * {@inheritDoc}
     */
    @Override
    public Stream<FreeColGameObject> getDisposables() {
        return concat(flatten(getAllWorkLocations(),
                WorkLocation::getDisposables),
                super.getDisposables());
    }


    // Interface Location (from Settlement via GoodsLocation via UnitLocation)
    //   UnitLocation.units is not used in Colony.  getUnits/List is defined
    //   to return the union of the units in the work locations, which may
    //   or not be the best idea.  Another choice would be to return all
    //   the units in the work locations, plus those present on the tile,
    //   which is provided by Settlement.getAllUnitsList.
    //   TODO: look at all the uses, see if this makes sense.
    // 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, but it can be invalid
        final String name = getName();
        return StringTemplate.name((name == null) ? "?" : name);
    }

    /**
     * {@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 sum(getCurrentWorkLocations(), UnitLocation::getUnitCount);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Stream<Unit> getUnits() {
        return flatten(getCurrentWorkLocations(), WorkLocation::getUnits);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Unit> getUnitList() {
        return toList(getUnits());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Colony getColony() {
        return this;
    }

    /**
     * {@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}
     */
    public void invalidateCache() {
        this.productionCache.invalidate();
    }

    /**
     * {@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 int getImmigration() {
        return immigration;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getLiberty() {
        return liberty;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getImageKey() {
        String key;
        if (isUndead()) {
            key = ".undead";
        } else {
            int count = getApparentUnitCount();
            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.
        final CombatModel cm = getGame().getCombatModel();
        final Comparator<Unit> comp
                = cachingDoubleComparator(u -> cm.getDefencePower(attacker, u));
        return maximize(getUnits(), comp);
    }

    /**
     * {@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 sum(getBuildings(), b -> b.getType().getUpkeep());
    }

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

    /**
     * {@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.getRequiredGoodsList());
            }

            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());
    }


    /**
     * Determines the value of a potential attack on a {@code Colony}
     *
     * @param value The previously calculated input value from
     *          {@link net.sf.freecol.server.ai.mission.UnitSeekAndDestroyMission
     *                  #scoreSettlementPath(AIUnit, PathNode, Settlement)}
     * @param unit The Unit doing the attacking.
     * @return The newly calculated value.
     */
    @Override
    public int calculateSettlementValue(int value, Unit unit) {
        // Favour high population (more loot:-).
        value += this.getUnitCount();
        if (this.hasStockade()) { // Avoid fortifications.
            value -= 200 * this.getStockade().getLevel();
        }
        return value;
    }

    /**
     * Display the appropriate panel for a given {@code Colony}
     *
     * @param canvas The instance of the FreeCol canvas to display.
     * @param p The FreeCol Player, used to check ownership of a Colony.
     * @throws IllegalStateException If a non-Colony or non-IndianSettlement
     *                               attempts to call this method.
     */
    @Override
    public void showSettlement(Canvas canvas, Player p) throws IllegalStateException {
        if (this.getOwner().equals(p)) {
            canvas.showColonyPanel(this, null);
        } else if (FreeColDebugger.isInDebugMode(FreeColDebugger.DebugMode.MENUS)) {
            canvas.showForeignColony(this);
        }
    }


    // Interface TradeLocation
    //   getGoodsCount provided in GoodsContainer

    /**
     * {@inheritDoc}
     */
    @Override
    public int getExportAmount(GoodsType goodsType, int turns) {
        final int present = ReturnPresent(goodsType, turns);
        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 = ReturnPresent(goodsType, turns);
        final ExportData ed = getExportData(goodsType);
        int capacity = ed.getEffectiveImportLevel(getWarehouseCapacity());
        return Math.max(0, capacity - present);
    }

    @Override
    public String getLocationName(TradeLocation tradeLocation) {
        Colony colony = (Colony) tradeLocation;
        return colony.getName();
    }

    @Override
    public Boolean canBeInput() {
        return true;
    }

    /**
     *  Used to calculated the present field.
     *
     * @param goodsType The {@link GoodsType} to check for got import/export.
     * @param turns The number of turns before the goods is required.
     * @return  The amount of goods to export.
     */
    private int ReturnPresent(GoodsType goodsType, int turns) {
        final int present = Math.max(0, getGoodsCount(goodsType)
                + turns * getNetProductionOf(goodsType));
        return 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.
     * @param lb An optional {@code LogBuilder} to log to.
     * @return Negative if there are problems remaining, zero if
     *     problems were fixed, positive if no problems found at all.
     */
    public int checkBuildQueueIntegrity(boolean fix, LogBuilder lb) {
        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) {
                if (lb != null) lb.add("\n  Invalid build queue item removed: ", bt.getId());
                buildQueue.remove(i);
                result = Math.min(result, 0);
            } else {
                if (lb != null) lb.add("\n  Invalid build queue item: ", bt.getId());
                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) {
                if (lb != null) lb.add("\n  Invalid population queue item removed: ", ut.getId());
                populationQueue.remove(i);
                result = Math.min(result, 0);
            } else {
                if (lb != null) lb.add("\n  Invalid population queue item: ", ut.getId());
                result = -1;
            }
        }
        return result;
    }


    // Override FreeColGameObject

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

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


    // Override FreeColObject
    
    /**
     * {@inheritDoc}
     */
    @Override
    public int getClassIndex () {
        return COLONY_CLASS_INDEX;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T extends FreeColObject> boolean copyIn(T other) {
        Colony o = copyInCast(other, Colony.class);
        if (o == null || !super.copyIn(o)) return false;
        final Game game = getGame();
        this.setBuildingMap(game.update(o.getBuildings(), true));
        this.setColonyTiles(game.update(o.getColonyTiles(), true));
        this.setExportData(o.getExportData());
        this.liberty = o.getLiberty();
        this.sonsOfLiberty = o.getSonsOfLiberty();
        this.oldSonsOfLiberty = o.getOldSonsOfLiberty();
        this.tories = o.getTories();
        this.oldTories = o.getOldTories();
        this.productionBonus = o.getProductionBonus();
        this.immigration = o.getImmigration();
        this.established = o.getEstablished();
        this.setBuildQueue(o.getBuildQueue());
        this.setPopulationQueue(o.getPopulationQueue());
        this.displayUnitCount = o.getDisplayUnitCount();

        for (WorkLocation wl : getAllWorkLocationsList()) wl.setColony(this);
        return true;
    }


    // 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 = getApparentUnitCount();
            if (uc <= 0) {
                FreeCol.trace(logger, "Unit count fail: " + uc
                    + " id=" + getId()
                    + " unitCount=" + getUnitCount()
                    + " scope=" + xw.getWriteScope()
                    + "/" + xw.getClientPlayer());
            }
            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 wl : sort(getAllWorkLocations())) {
                wl.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.
        clearBuildingMap();
        clearColonyTiles();
        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.TAG.equals(tag)) {
            addBuilding(xr.readFreeColObject(game, Building.class));

        } else if (ColonyTile.TAG.equals(tag)) {
            addColonyTile(xr.readFreeColObject(game, ColonyTile.class));

        } else if (ExportData.TAG.equals(tag)) {
            ExportData data = new ExportData(xr);
            setExportData(data);

        } else {
            super.readChild(xr);
        }
    }

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


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