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

package net.sf.freecol.server.ai.mission;

import java.util.logging.Logger;

import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;

import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.Location;
import net.sf.freecol.common.model.PathNode;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.pathfinding.CostDeciders;
import net.sf.freecol.common.model.pathfinding.GoalDecider;
import net.sf.freecol.common.networking.Connection;
import net.sf.freecol.common.networking.NetworkConstants;
import net.sf.freecol.common.util.Utils;
import net.sf.freecol.server.ai.AIMain;
import net.sf.freecol.server.ai.AIMessage;
import net.sf.freecol.server.ai.AIUnit;

import org.w3c.dom.Element;


/**
 * Mission for building a <code>Colony</code>.
 *
 * This mission can be used in two different ways:
 * <ul>
 * <li>Build a colony at a specific location.</li>
 * <li>Find a site for a colony and build it there.</li>
 * </ul>
 *
 * This mission will be aborted in the former case if the value gets
 * below a given threshold, while a colony will always get built (if
 * there is sufficient space on the map) in the latter case. Use the
 * appropriate constructor to get the desired behaviour.
 *
 * @see net.sf.freecol.common.model.Colony Colony
 */
public class BuildColonyMission extends Mission {

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

    /** The maximum number of turns to travel to a building site. */
    private static final int MAX_TURNS = 5;

    /**
     * The <code>Tile</code> where the <code>Colony</code> should be built.
     * Alternately, this can be the site of an existing colony--- the unit
     * is to go there and then retarget a new site.
     *
     * This target must be non-null for this mission to remain valid.
     * doMission() maintains this as much as possible.
     */
    private Tile target = null;

    /** The value of the target <code>Tile</code>. */
    private int colonyValue = -1;


    /**
     * Creates a mission for the given <code>AIUnit</code>.
     *
     * @param aiMain The main AI-object.
     * @param aiUnit The <code>AIUnit</code> this mission is created for.
     * @param target The target <code>Tile</code> to build a colony at.
     */
    public BuildColonyMission(AIMain aiMain, AIUnit aiUnit, Tile target) {
        super(aiMain, aiUnit);
        this.target = target;
        colonyValue = (target == null || target.getColony() != null) ? -1
            : aiUnit.getUnit().getOwner().getColonyValue(target);
        logger.finest("AI colony builder starts with target " + target
            + " and value " + colonyValue
            + ": " + aiUnit.getUnit());
    }

    /**
     * Creates a new <code>BuildColonyMission</code>.
     *
     * @param aiMain The main AI-object.
     * @param element An <code>Element</code> containing an XML-representation
     *            of this object.
     */
    public BuildColonyMission(AIMain aiMain, Element element) {
        super(aiMain);
        readFromXMLElement(element);
    }

    /**
     * Creates a new <code>BuildColonyMission</code> and reads the given
     * element.
     *
     * @param aiMain The main AI-object.
     * @param in The input stream containing the XML.
     * @throws XMLStreamException if a problem was encountered during parsing.
     * @see net.sf.freecol.server.ai.AIObject#readFromXML
     */
    public BuildColonyMission(AIMain aiMain, XMLStreamReader in)
        throws XMLStreamException {
        super(aiMain);
        readFromXML(in);
    }

    /**
     * Gets the target of this mission.
     *
     * @return The tile where a colony is to be built.
     */
    public Tile getTarget() {
        return target;
    }


    /**
     * Gets the value of a path to a colony building site.  The value is
     * proportional to the general desirability of the site, and inversely
     * proportional to the number of turns to get there.
     *
     * @param path The <code>PathNode</code> to check.
     * @param player The <code>Player</code> that will found the colony,
     *     needed to check that the site can be acquired.
     * @return A score for the building site.
     */
    private static float getSiteValue(PathNode path, Player player) {
        final Tile tile = path.getTile();
        if (tile == null || !tile.isLand()) return -1.0f;
        float turns = path.getTotalTurns() + 1.0f;

        // Can the player acquire the tile?
        switch (player.canClaimToFoundSettlementReason(tile)) {
        case NONE:
            break;
        case NATIVES:
            // Penalize value when the tile will need to be stolen
            int price = player.getLandPrice(tile);
            if (price > 0 && !player.checkGold(price)) turns *= 2.0f;
            break;
        default:
            return -1.0f;
        }

        return player.getColonyValue(tile) / turns;
    }

    /**
     * Finds the best existing colony to use as a target.
     * When the unit gets there, then retarget.
     * This encourages the colonies to clump.
     *
     * @param player The <code>Player</code> that is searching.
     * @return A good colony to restart from.
     */
    private static Tile getBestColonyTile(Player player) {
        int bestValue = -1;
        Colony best = null;
        for (Colony colony : player.getColonies()) {
            int value = ((colony.isConnected()) ? 10 : 0) // Favour coastal
                + colony.getUnitCount()
                + colony.getAvailableWorkLocations().size();
            if (value > bestValue) {
                bestValue = value;
                best = colony;
            }
        }
        return (best == null) ? null : best.getTile();
    }

    /**
     * Makes a goal decider that checks colony sites.
     *
     * @param deferOK Keep track of the nearest of our colonies, to use
     *     as a fallback destination.
     * @return A suitable <code>GoalDecider</code>.
     */
    private static GoalDecider getColonyDecider(final boolean deferOK) {
        return new GoalDecider() {
            private PathNode best = null;
            private float bestValue = 0.0f;
            private PathNode backup = null;
            private int backupTurns = INFINITY;

            public PathNode getGoal() {
                return (best != null) ? best : backup;
            }

            public boolean hasSubGoals() { return true; }

            public boolean check(Unit u, PathNode path) {
                Colony colony = path.getTile().getColony();
                if (colony != null) {
                    if (deferOK
                        && colony.getOwner() == u.getOwner()
                        && colony.isConnected()
                        && path.getTotalTurns() < backupTurns) {
                        backupTurns = path.getTotalTurns();
                        backup = path;
                    }
                    return false;
                }
                float value = getSiteValue(path, u.getOwner());
                if (value > bestValue) {
                    bestValue = value;
                    best = path;
                    return true;
                }
                return false;
            }
        };
    }
            
    /**
     * Finds a site for a new colony.  Favour closer sites.
     *
     * @param unit The <code>Unit</code> to find a colony site with.
     * @param deferOK If true, allow the search to return a nearby existing
     *     colony as a temporary target.     
     * @return A suitable tile for a new colony, or backup target.
     */
    public static Tile findTargetTile(AIUnit aiUnit, boolean deferOK) {
        final Unit unit = aiUnit.getUnit();
        if (unit == null || unit.isDisposed()) return null;

        final Tile startTile = getPathStartTile(unit);
        final Player player = unit.getOwner();
        if (startTile == null) return getBestColonyTile(player);

        PathNode path;
        final Unit carrier = (unit.isOnCarrier()) ? ((Unit)unit.getLocation())
            : null;
        final GoalDecider colonyDecider = getColonyDecider(deferOK);
        if ((path = unit.search(startTile, colonyDecider,
                    CostDeciders.avoidIllegal(), MAX_TURNS,
                    carrier)) != null) return path.getLastNode().getTile();

        // Retry, but increase the range.
        if ((path = unit.search(startTile, colonyDecider,
                    CostDeciders.avoidIllegal(), MAX_TURNS * 3,
                    carrier)) != null) return path.getLastNode().getTile();

        // One more try with a relaxed cost decider and no range limit.
        if ((path = unit.search(startTile, colonyDecider,
                    CostDeciders.numberOfTiles(), INFINITY,
                    carrier)) != null) return path.getLastNode().getTile();
        
        // Enough.
        return null;
    }

    // Fake Transportable interface

    /**
     * Gets the transport destination for the unit with this mission.
     *
     * @return The destination for this <code>Transportable</code>.
     */
    public Location getTransportDestination() {
        return (shouldTakeTransportToTile(target)) ? target : null;
    }

    // Mission interface

    /**
     * Checks if this mission is valid for the given unit.
     *
     * @param aiUnit The <code>AIUnit</code> to test.
     * @return True if the unit can be usefully assigned this mission.
     */
    public static boolean isValid(AIUnit aiUnit) {
        if (!Mission.isValid(aiUnit)) return false;
        return aiUnit.getUnit().hasAbility("model.ability.foundColony");
    }

    /**
     * Checks if this mission is still valid to perform.
     *
     * @return True if this mission is still valid to perform.
     */
    public boolean isValid() {
        return super.isValid()
            && getUnit().hasAbility("model.ability.foundColony")
            && target != null;
    }

    /**
     * Performs this mission.
     *
     * @param connection The <code>Connection</code> to the server.
     */
    public void doMission(Connection connection) {
        // Check the target
        final Unit unit = getUnit();
        final Player player = unit.getOwner();
        final AIUnit aiUnit = getAIUnit();
        int value;
        if (target == null
            || (value = player.getColonyValue(target)) < colonyValue
            || (target.getColony() != null
                && target.getColony().getOwner() != player)) {
            target = findTargetTile(aiUnit, true);
            if (target == null) {
                logger.finest("AI colony builder could not find a target: "
                    + unit);
                return;
            }
            colonyValue = player.getColonyValue(target);
        }

        if (travelToTarget("AI colony builder", target)
            != Unit.MoveType.MOVE) return;

        // If arrived at one of our colonies it is time to either
        // retarget or just join the current colony.  Only continue
        // looking for a building site if `few colonies', but insist on
        // finding a building site this time.
        if (target.getColony() != null) {
            String name = unit.getTile().getColony().getName();
            if (getEuropeanAIPlayer().hasFewColonies()) {
                target = findTargetTile(aiUnit, false);
                logger.finest("AI colony builder arrived at " + name
                    + ", retargeting " + target
                    + ": " + unit);
            } else {
                logger.finest("AI colony builder gives up and joins " + name
                    + ": " + unit);
                target = null;
            }
            return;
        }

        // Arrived at the target (non-colony) tile.
        if (target.getOwner() == null) {
            ; // All is well
        } else if (player.owns(target)) { // Already ours, clear users
            Colony colony = (Colony) target.getOwningSettlement();
            if (colony != null
                && colony.getColonyTile(target) != null) {
                colony.getColonyTile(target).relocateWorkers();
            }
        } else {
            // Not our tile, so claim it first.  Fail if someone has
            // claimed the tile and will not sell.  Otherwise try to
            // buy it (TODO: remove cheat) or steal it.
            int price = player.getLandPrice(target);
            boolean fail = price < 0;
            if (price > 0 && !player.checkGold(price)) {
                if (Utils.randomInt(logger, "Cheat gold",
                                    getAIRandom(), 4) == 0) {
                    // CHEAT: provide the gold needed 
                    player.modifyGold(price);
                }
            }
            if (price >= 0) {
                fail = !AIMessage.askClaimLand(connection, target, null,
                    ((price == 0) ? 0 : (player.checkGold(price)) ? price
                        : NetworkConstants.STEAL_LAND))
                    || !player.owns(target);
            }
            if (fail) {
                logger.finest("AI colony builder failed to claim land at "
                    + unit.getTile() + ": " + unit);
                target = null;
                return;
            }
        }

        // Check that the unit has moves left, which are required for building.
        if (unit.getMovesLeft() <= 0) {
            logger.finest("AI colony builder waiting to build at " + target
                + ": " + unit);
            return;
        }
            
        // Clear to build the colony.
        if (AIMessage.askBuildColony(aiUnit, Player.ASSIGN_SETTLEMENT_NAME)
            && target.getColony() != null) {
            aiUnit.setMission(new WorkInsideColonyMission(getAIMain(), aiUnit,
                    getAIMain().getAIColony(target.getColony())));
            logger.finest("AI colony builder"
                + " built " + target.getColony().getName() + ": " + unit);
        } else {
            logger.warning("AI colony builder failed to build at " + target
                + ": " + unit);
        }
        target = null;
    }

    /**
     * Gets debugging information about this mission. This string is a short
     * representation of this object's state.
     *
     * @return The <code>String</code>: "(x, y) z" or "(x, y) z!"
     *         where <code>x</code> and <code>y</code> is the
     *         coordinates of the target tile for this mission, and
     *         <code>z</code> is the value of building the colony. The
     *         exclamation mark is added if the unit should continue
     *         searching for a colony site if the targeted site is
     *         lost.
     */
    public String getDebuggingInfo() {
        final String targetName = (target != null) ? target.toString()
            : "unassigned";
        return targetName + " " + colonyValue;
    }


    // Serialization

    /**
     * Writes all of the <code>AIObject</code>s and other AI-related
     * information to an XML-stream.
     *
     * @param out The target stream.
     * @throws XMLStreamException if there are any problems writing to the
     *             stream.
     */
    protected void toXMLImpl(XMLStreamWriter out) throws XMLStreamException {
        if (isValid()) {
            toXML(out, getXMLElementTagName());
        }
    }

    /**
     * {@inherit-doc}
     */
    protected void writeAttributes(XMLStreamWriter out)
        throws XMLStreamException {
        super.writeAttributes(out);
        writeAttribute(out, "target", target);
    }

    /**
     * Reads all the <code>AIObject</code>s and other AI-related information
     * from XML data.
     *
     * @param in The input stream with the XML.
     */
    protected void readAttributes(XMLStreamReader in)
        throws XMLStreamException {
        super.readAttributes(in);
        final String targetStr = in.getAttributeValue(null, "target");
        target = (targetStr == null) ? null
            : (Tile) getGame().getFreeColGameObject(targetStr);
        final Unit unit = getUnit();
        colonyValue = (unit == null) ? -1
            : unit.getOwner().getColonyValue(target);
    }

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