/**
 *  Copyright (C) 2002-2022   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.Comparator;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.stream.XMLStreamException;

import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.io.FreeColXMLWriter;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.Constants;
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.CostDecider;
import net.sf.freecol.common.model.pathfinding.CostDeciders;
import net.sf.freecol.common.model.pathfinding.GoalDecider;
import net.sf.freecol.common.model.pathfinding.GoalDeciders;
import static net.sf.freecol.common.util.CollectionUtils.*;
import net.sf.freecol.common.util.LogBuilder;
import static net.sf.freecol.common.util.RandomUtils.*;
import net.sf.freecol.server.ai.AIColony;
import net.sf.freecol.server.ai.AIMain;
import net.sf.freecol.server.ai.AIMessage;
import net.sf.freecol.server.ai.AIUnit;
import net.sf.freecol.server.ai.EuropeanAIPlayer;


/**
 * Mission for building a {@code Colony}.
 *
 * @see net.sf.freecol.common.model.Colony Colony
 */
public final class BuildColonyMission extends Mission {

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

    public static final String TAG = "buildColonyMission";

    /** The tag for this mission. */
    private static final String tag = "AI colony builder";

    /**
     * The target of this mission.  It can either be a Tile where a
     * Colony should be built, or an existing connected Colony owned
     * by this player to go to before retargeting.
     */
    private Location target;

    /** The value of a target {@code Tile}. */
    private int colonyValue;


    /**
     * Creates a mission for the given {@code AIUnit}.
     *
     * @param aiMain The main AI-object.
     * @param aiUnit The {@code AIUnit} this mission is created for.
     * @param target The target {@code Location} for this mission.
     */
    public BuildColonyMission(AIMain aiMain, AIUnit aiUnit, Location target) {
        super(aiMain, aiUnit);

        setTarget(target);
    }

    /**
     * Creates a new {@code BuildColonyMission} from a reader.
     *
     * @param aiMain The main AI-object.
     * @param aiUnit The {@code AIUnit} this mission is created for.
     * @param xr 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, AIUnit aiUnit,
                              FreeColXMLReader xr) throws XMLStreamException {
        super(aiMain, aiUnit);

        readFromXML(xr);
    }


    /**
     * Get the colony value for a tile.
     *
     * @param tile The {@code Tile} to test.
     * @return The colony value for this player.
     */
    private int getColonyValue(Tile tile) {
        final Player owner = getAIUnit().getUnit().getOwner();
        return owner.getColonyValue(tile);
    }

    /**
     * Extract a valid target for this mission from a path.
     *
     * @param aiUnit A {@code AIUnit} to perform the mission.
     * @param path A {@code PathNode} to extract a target from,
     *     (uses the unit location if null).
     * @return A target for this mission, or null if none found.
     */
    public static Location extractTarget(AIUnit aiUnit, PathNode path) {
        if (path == null) return null;
        final Location loc = path.getLastNode().getLocation();
        Tile tile = loc.getTile();
        Colony colony = loc.getColony();
        return (invalidMissionReason(aiUnit, tile) == null) ? tile
            : (invalidMissionReason(aiUnit, colony) == null) ? colony
            : null;
    }

    /**
     * Gets the value of a path to a colony building site.
     *
     * @param aiUnit The {@code AIUnit} to build the colony.
     * @param path The {@code PathNode} to check.
     * @return A score for the target.
     */
    public static float scorePath(AIUnit aiUnit, PathNode path) {
        Location loc;
        if (path == null
            || !((loc = extractTarget(aiUnit, path)) instanceof Tile)) 
            return Integer.MIN_VALUE;

        final Tile tile = (Tile)loc;
        final Player player = aiUnit.getUnit().getOwner();
        float turns = path.getTotalTurns() + 3.0f;
        return (3 * player.getColonyValue(tile)) / turns;
    }

    /**
     * Gets a {@code GoalDecider} for finding the best colony
     * {@code Tile}, optionally falling back to the nearest colony.
     *
     * @param aiUnit The {@code AIUnit} that is searching.
     * @param deferOK Enable colony fallback.
     * @return A suitable {@code GoalDecider}.
     */
    private static GoalDecider getGoalDecider(final AIUnit aiUnit,
                                              boolean deferOK) {
        GoalDecider gd = new GoalDecider() {
                private PathNode bestPath = null;
                private float bestValue = 0f;

                @Override
                public PathNode getGoal() { return bestPath; }
                @Override
                public boolean hasSubGoals() { return true; }
                @Override
                public boolean check(Unit u, PathNode path) {
                    Location loc = extractTarget(aiUnit, path);
                    if (loc instanceof Tile) {
                        float value = scorePath(aiUnit, path);
                        if (bestValue < value) {
                            bestValue = value;
                            bestPath = path;
                            return true;
                        }
                    }
                    return false;
                }
            };
        return (deferOK) ? GoalDeciders.getComposedGoalDecider(false, gd,
            GoalDeciders.getOurClosestSettlementGoalDecider())
            : gd;
    }

    /**
     * Finds a site for a new colony.  Favour closer sites.
     *
     * @param aiUnit The {@code AIUnit} to execute this mission.
     * @param range An upper bound on the number of moves.
     * @param deferOK Enables deferring to a fallback colony.
     * @return A path to the new target, or null if none found.
     */
    private static PathNode findTargetPath(AIUnit aiUnit, int range,
                                           boolean deferOK) {
        if (invalidAIUnitReason(aiUnit) != null) return null;
        final Unit unit = aiUnit.getUnit();
        final Location start = unit.getPathStartLocation();
        final Unit carrier = unit.getCarrier();
        final GoalDecider gd = getGoalDecider(aiUnit, deferOK);
        final CostDecider standardCd
            = CostDeciders.avoidSettlementsAndBlockingUnits();

        // Try for something sensible nearby.
        return unit.search(start, gd, standardCd, range, carrier);
    }

    /**
     * Finds a site for a new colony or a backup colony to go to.
     *
     * @param aiUnit The {@code AIUnit} to find a colony site with.
     * @param range An upper bound on the number of moves.
     * @param deferOK Enables deferring to a fallback colony.
     * @return A new target for this mission.
     */
    public static Location findMissionTarget(AIUnit aiUnit, int range,
                                             boolean deferOK) {
        PathNode path = findTargetPath(aiUnit, range, deferOK);
        return (path != null) ? extractTarget(aiUnit, path)
            : Location.upLoc(findCircleTarget(aiUnit,
                    getGoalDecider(aiUnit, deferOK), range*3, deferOK));
    }

    /**
     * Why would this mission be invalid with the given unit?
     *
     * @param aiUnit The {@code AIUnit} to test.
     * @return A reason why the mission would be invalid with the unit,
     *     or null if none found.
     */
    private static String invalidUnitReason(AIUnit aiUnit) {
        String reason = invalidAIUnitReason(aiUnit);
        return (reason != null)
            ? reason
            : (!aiUnit.getUnit().getOwner().canBuildColonies())
            ? "player-not-a-colony-founder"
            : (!aiUnit.getUnit().getType().canBuildColony())
            ? "unit-not-a-colony-founder"
            : null;
    }

    /**
     * Why is this mission invalid with a given colony target?
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @param colony The potential target {@code Colony}.
     * @return A reason for mission invalidity, or null if none found.
     */
    private static String invalidColonyReason(AIUnit aiUnit, Colony colony) {
        return invalidTargetReason(colony, aiUnit.getUnit().getOwner());
    }

    /**
     * Why is this mission invalid with a given tile target?
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @param tile The potential target {@code Tile}.
     * @return A reason for mission invalidity, or null if none found.
     */
    private static String invalidTileReason(AIUnit aiUnit, Tile tile) {
        Player owner = aiUnit.getUnit().getOwner();
        Player.NoClaimReason reason
            = owner.canClaimToFoundSettlementReason(tile);
        switch (reason) {
        case NONE: case NATIVES:
            return null;
        default:
            break;
        }
        return "target-" + reason;
    }

    /**
     * Why would this mission be invalid with the given AI unit?
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @return A reason for mission invalidity, or null if none found.
     */
    public static String invalidMissionReason(AIUnit aiUnit) {
        return invalidUnitReason(aiUnit);
    }

    /**
     * Why would this mission be invalid with the given AI unit and location?
     *
     * @param aiUnit The {@code AIUnit} to check.
     * @param loc The {@code Location} to check.
     * @return A reason for invalidity, or null if none found.
     */
    public static String invalidMissionReason(AIUnit aiUnit, Location loc) {
        String reason = invalidMissionReason(aiUnit);
        return (reason != null) ? reason
            : (loc instanceof Colony)
            ? invalidColonyReason(aiUnit, (Colony)loc)
            : (loc instanceof Tile) 
            ? invalidTileReason(aiUnit, (Tile)loc)
            : Mission.TARGETINVALID;
    }

      
    // Implement Mission
    //   Inherit dispose, getTransportDestination, isOneTime

    /**
     * {@inheritDoc}
     */
    @Override
    public int getBaseTransportPriority() {
        return NORMAL_TRANSPORT_PRIORITY + 10;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Location getTarget() {
        return target;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setTarget(Location target) {
        if (target == null
            || target instanceof Colony || target instanceof Tile) {
            this.target = target;
            this.colonyValue = (target instanceof Tile)
                ? getColonyValue((Tile)target)
                : -1;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Location findTarget() {
        return findMissionTarget(getAIUnit(), 5, true);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String invalidReason() {
        return invalidMissionReason(getAIUnit(), target);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Mission doMission(LogBuilder lb) {
        lb.add(tag);
        final AIMain aiMain = getAIMain();
        final AIUnit aiUnit = getAIUnit();
        final Unit unit = getUnit();
        final Player player = unit.getOwner();
        final EuropeanAIPlayer euaip = getEuropeanAIPlayer();

        boolean retarget = false;
        String reason = invalidReason();
        if (isTargetReason(reason)) {
            // Allow target invalidation by another builder succeeding, and
            // continue on to the target as an intermediate colony.
            Colony c;
            if (target instanceof Tile
                && (c = target.getColony()) != null
                && player.owns(c)) {
                // Favour improving colony center.
                Mission m = euaip.getPioneeringMission(aiUnit, c.getTile());
                if (m != null) return lbDrop(lb, ", improving with ", m);
                // Just go to the colony.
                setTarget(c);
                return lbRetarget(lb);
            }
        } else if (reason != null) {
            return lbFail(lb, false, reason);
        } else { // Target valid, but has it devalued?
            if (target instanceof Tile) {
                int newValue = getColonyValue((Tile)target);
                if (newValue < colonyValue) {
                    reason = "target tile " + target.toShortString()
                        + " value " + colonyValue + " -> " + newValue;
                    retarget = true;
                }
            }
        }
        if (retarget) return retargetMission(reason, lb);

        for (;;) {
            // Go there.
            Unit.MoveType mt = travelToTarget(getTarget(),
                CostDeciders.avoidSettlementsAndBlockingUnits(), lb);
            switch (mt) {
            case MOVE: // Arrived
                break;

            case MOVE_HIGH_SEAS: case MOVE_NO_MOVES: case MOVE_ILLEGAL:
                return lbWait(lb);

            case MOVE_NO_REPAIR:
                return lbFail(lb, false, AIUNITDIED);

            case MOVE_NO_ACCESS_EMBARK: case MOVE_NO_TILE:
                return this;

            default:
                return lbMove(lb, mt);
            }

            lbAt(lb);
            if (getTarget() instanceof Colony) {
                // If arrived at the target colony it is time to retarget
                // another building site, unless the existing one is small
                // or nothing is found.
                Colony colony = (Colony)getTarget();

                // Improve colony center?
                Mission m
                    = euaip.getPioneeringMission(aiUnit, colony.getTile());
                if (m != null) return lbDrop(lb, ", improving with ", m);

                // Colony too small?
                if (colony.getUnitCount() <= 1) {
                    setTarget(colony);
                    return lbDrop(lb, ", join small colony");
                }

                // Find a real tile target?
                Location newTarget;
                if ((newTarget = findMissionTarget(aiUnit, 5, false)) != null) {
                    setTarget(newTarget);
                    return lbRetarget(lb);
                }

                // Go to the nearest smaller colony?
                final Predicate<Colony> smallPred = c ->
                    c != colony && c.getUnitCount() < colony.getUnitCount();
                final Comparator<Colony> closeComp = cachingIntComparator(c ->
                    unit.getTurnsToReach(c));
                Colony best = minimize(player.getColonies(), smallPred,
                                       closeComp);
                if (best != null) {
                    lb.add(", going to smaller ", best.getUnitCount(), "<",
                        colony.getUnitCount(), " colony");
                    setTarget(best);
                    return lbRetarget(lb);
                }

                // Just join up then.
                return lbDrop(lb, ", joining");
            }

            if (!(getTarget() instanceof Tile)) {
                return lbFail(lb, false, "bogus target ", getTarget());
            }
            Tile tile = (Tile)getTarget();
            // Check that the unit has moves left, which are required
            // for building.
            if (unit.getMovesLeft() <= 0) {
                return lbWait(lb, ", waiting to build at ", tile);
            }

            if (tile.getOwner() == null) {
                ; // All is well
            } else if (player.owns(tile)) { // Already ours, clear users
                Colony colony = (Colony)tile.getOwningSettlement();
                if (colony != null) {
                    logger.warning("Building on colony tile: " + tile);
                    return lbFail(lb, false, "building on colony tile ", tile);
                }
            } else {
                // Not our tile, so claim it first.  Fail if someone
                // has claimed the tile and will not sell.  Otherwise
                // try to buy it or steal it.
                int price = player.getLandPrice(tile);
                boolean fail = price < 0;
                if (price > 0 && !player.checkGold(price)) {
                    if (randomInt(logger, "Land gold?", getAIRandom(), 4) == 0) {
                        lb.add(", ");
                        euaip.cheatGold(price, lb);
                        lb.add(" to buy ", tile);
                    }
                }
                if (price >= 0) {
                    fail = !AIMessage.askClaimLand(tile, aiUnit,
                        ((price == 0) ? 0 : (player.checkGold(price)) ? price
                            : Constants.STEAL_LAND))
                        || !player.owns(tile);
                }
                if (fail) return retargetMission("tile-claim-at-" + tile, lb);
                lb.add(", claimed colony tile");
            }

            // Log the colony values so we can improve things
            if (logger.isLoggable(Level.FINE)) {
                LogBuilder l2 = new LogBuilder(64);
                l2.add(tag, " score-at-foundation ", tile, ":");
                for (Double d : player.getAllColonyValues(tile)) {
                    l2.add(" ", d);
                }
                l2.log(logger, Level.FINE);
            }
            
            // Clear to build the colony.
            if (AIMessage.askBuildColony(aiUnit, Player.ASSIGN_SETTLEMENT_NAME)
                && tile.getColony() != null) {
                Colony colony = tile.getColony();
                AIColony aiColony = aiMain.getAIColony(colony);
                aiColony.requestRearrange();
                Mission m = euaip.getWorkInsideColonyMission(aiUnit, aiColony);
                return lbDone(lb, m != null, colony);
            }
            return lbFail(lb, false, "build at ", tile);
        }
    }


    // Serialization

    private static final String TARGET_TAG = "target";


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

        if (target != null) {
            xw.writeAttribute(TARGET_TAG, target.getId());

            if (colonyValue > 0) {
                xw.writeAttribute(VALUE_TAG, colonyValue);
            }
        }
    }

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

        target = xr.getLocationAttribute(getGame(), TARGET_TAG, false);

        colonyValue = xr.getAttribute(VALUE_TAG, -1);
    }

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