/**
 *  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.common.model;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.xml.stream.XMLStreamException;

import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.io.FreeColXMLWriter;
import static net.sf.freecol.common.model.Constants.*;
import net.sf.freecol.common.model.Map.Layer;
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 tile improvement, such as a river or road.
 */
public class TileImprovement extends TileItem {

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

    public static final String TAG = "tileImprovement";

    public static final String EMPTY_RIVER_STYLE = "0000";
    public static final String EMPTY_ROAD_STYLE = "00000000";
    
    /** River magnitudes */
    public static final int NO_RIVER = 0;
    public static final int SMALL_RIVER = 1;
    public static final int LARGE_RIVER = 2;
    public static final int FJORD_RIVER = 3;


    /** The type of this improvement. */
    private TileImprovementType type;

    /** Turns remaining until the improvement is complete, if any. */
    private int turnsToComplete;

    /**
     * The improvement magnitude.  Default is type.getMagnitude(), but
     * this will override.
     */
    private int magnitude;

    /** Image and overlay style information for the improvement. */
    private TileImprovementStyle style;

    /**
     * Whether this is a virtual improvement granted by some structure
     * on the tile (a Colony, for example).  Virtual improvements will
     * be removed along with the structure that granted them.
     */
    private boolean virtual;


    /**
     * Creates a standard {@code TileImprovement}-instance.
     *
     * This constructor asserts that the game, tile and type are valid.
     * Does not set the style.
     *
     * @param game The enclosing {@code Game}.
     * @param tile The {@code Tile} on which this object sits.
     * @param type The {@code TileImprovementType} of this
     *     improvement.
     * @param style The {@code TileImprovementStyle} of this improvement.
     */
    public TileImprovement(Game game, Tile tile, TileImprovementType type,
                           TileImprovementStyle style) {
        super(game, tile);
        if (type == null) {
            throw new RuntimeException("Type must not be null: " + this);
        }
        this.type = type;
        if (!type.isNatural()) {
            this.turnsToComplete = tile.getType().getBasicWorkTurns()
                + type.getAddWorkTurns();
        }
        this.magnitude = type.getMagnitude();
        this.style = style;
    }

    /**
     * Create a new {@code TileImprovement} with the given identifier.
     *
     * The object should be initialized later.
     *
     * @param game The enclosing {@code Game}.
     * @param id The object identifier.
     */
    public TileImprovement(Game game, String id) {
        super(game, id);
    }


    /**
     * Gets the type of this tile improvement.
     *
     * @return The type of this improvement.
     */
    public TileImprovementType getType() {
        return type;
    }

    /**
     * Is this tile improvement a river?
     *
     * @return True if this is a river improvement.
     */
    public boolean isRiver() {
        return "model.improvement.river".equals(type.getId());
    }

    /**
     * Is this tile improvement a road?
     *
     * @return True if this is a road improvement.
     */
    public boolean isRoad() {
        return "model.improvement.road".equals(type.getId());
    }

    /**
     * Gets the directions that a connection can form across for this
     * this type of improvement.
     *
     * - For rivers, it is just the longSided directions.
     * - For roads, it is all directions.
     * - In other cases, no directions are relevant.
     *
     * @return An array of relevant directions, or null if none.
     */
    public List<Direction> getConnectionDirections() {
        return (isRoad()) ? Direction.allDirections
            : (isRiver()) ? Direction.longSides
            : null;
    }

    /**
     * How many turns remain until this improvement is complete?
     *
     * @return The current turns to completion.
     */
    public int getTurnsToComplete() {
        return turnsToComplete;
    }

    /**
     * Sets the turns required to complete the improvement.
     *
     * @param turns The new turns to completion.
     */
    public void setTurnsToComplete(int turns) {
        turnsToComplete = turns;
    }

    /**
     * Gets the magnitude of this improvement.
     *
     * @return The magnitude of this immprovement.
     */
    public int getMagnitude() {
        return magnitude;
    }

    /**
     * Sets the magnitude of this improvement.
     *
     * @param magnitude The new magnitude.
     */
    public void setMagnitude(int magnitude) {
        this.magnitude = magnitude;
    }

    /**
     * Gets the style of this improvement.
     *
     * @return The style
     */
    public TileImprovementStyle getStyle() {
        return style;
    }

    /**
     * Set the style of this improvement.
     *
     * @param style The new {@code TileImprovementStyle}.
     */
    public void setStyle(TileImprovementStyle style) {
        this.style = style;
    }

    /**
     * Is this a virtual improvement?
     *
     * @return True if this is a virtual improvement.
     */
    public final boolean getVirtual() {
        return virtual;
    }

    /**
     * Set the virtual status of this improvement.
     * Used for the roads in a colony center tile.
     *
     * @param virtual The new virtual value.
     */
    public final void setVirtual(final boolean virtual) {
        this.virtual = virtual;
    }

    /**
     * Get the magnitude of the river branch in the given direction.
     * Precondition: This tile improvement is a river!
     *
     * @param direction The {@code Direction} to check.
     * @return The magnitude of the river branch or 0 if there is none.
     */
    public int getRiverConnection(Direction direction) {
        int index = Direction.longSides.indexOf(direction);
        if (index == -1 || style == null)
            return 0;
        int mag = Character.digit(style.getString().charAt(index), 10);
        return (mag == -1) ? 0 : mag;
    }

    /**
     * Is this tile improvement connected to a similar improvement on
     * a neighbouring tile?
     *
     * Public for the test suite.
     *
     * @param direction The {@code Direction} to check.
     * @return True if this improvement is connected.
     */
    public boolean isConnectedTo(Direction direction) {
        int index = isRoad() ? direction.ordinal()
            : isRiver() ? Direction.longSides.indexOf(direction) : -1;
        return (index == -1 || style == null) ? false
            : style.getString().charAt(index) != '0';
    }

    /**
     * Internal helper method to set the connection status in a given direction.
     * There is no check for backwards connections on neighbouring tiles!
     *
     * @param direction The {@code Direction} to set.
     * @param value The new status for the connection.
     */
    private void setConnected(Direction direction, boolean value) {
        if (style == null || isConnectedTo(direction) != value)
            setConnected(direction, value, Integer.toString(magnitude));
    }

    private void setConnected(Direction direction, boolean value, String magnitude) {
        if (style == null) {
            style = TileImprovementStyle.getInstance(EMPTY_ROAD_STYLE);
        }
        String old = style.toString();
        List<Direction> directions = getConnectionDirections();
        int end = directions.size();
        StringBuilder updated = new StringBuilder();
        for(int index = 0; index != end; ++index) {
            if(directions.get(index) == direction)
                updated.append(value ? magnitude : "0");
            else
                updated.append(old.charAt(index));
        }
        style = TileImprovementStyle.getInstance(updated.toString());
    }

    /**
     * Gets a map of connection-direction to magnitude.
     *
     * @return A map of the connections.
     */
    public Map<Direction, Integer> getConnections() {
        final List<Direction> dirns = getConnectionDirections();
        return (dirns == null) ? Collections.<Direction, Integer>emptyMap()
            : transform(dirns, d -> isConnectedTo(d),
                        Function.<Direction>identity(),
                        Collectors.toMap(Function.<Direction>identity(),
                                         d -> magnitude));
    }

    /**
     * Gets a Modifier for the production bonus this improvement provides
     * for a given type of goods.
     *
     * @param goodsType The {@code GoodsType} to test.
     * @return A production {@code Modifier}, or null if none applicable.
     */
    private Modifier getProductionModifier(GoodsType goodsType) {
        final Modifier modifier = (isComplete()) ? type.getProductionModifier(goodsType) : null;
        if (modifier == null || modifier.getValue() == 0 || magnitude <= 0) {
            return modifier;
        }
        
        final Modifier modifierWithMagnitudeBonus = Modifier.makeModifier(modifier);
        modifierWithMagnitudeBonus.setValue(modifierWithMagnitudeBonus.getValue() * magnitude);
        return modifierWithMagnitudeBonus;
    }

    /**
     * Calculates the movement cost on the basis of connected tile
     * improvements.
     *
     * @param direction The {@code Direction} to move.
     * @param moveCost The original movement cost.
     * @return The movement cost with this improvement.
     */
    public int getMoveCost(Direction direction, int moveCost) {
        return (isComplete() && isConnectedTo(direction))
            ? type.getMoveCost(moveCost)
            : moveCost;
    }

    /**
     * What type of tile does this improvement change a given type to?
     *
     * @param tileType The original {@code TileType}.
     * @return The {@code TileType} that results from completing this
     *     improvement, or null if nothing changes.
     */
    public TileType getChange(TileType tileType) {
        return (isComplete()) ? type.getChange(tileType) : null;
    }

    /**
     * Can a unit build this improvement?
     *
     * @param unit A {@code Unit} to do the building.
     * @return True if the supplied unit can build this improvement.
     */
    public boolean isWorkerAllowed(Unit unit) {
        return (unit == null || isComplete()) ? false
            : type.isWorkerAllowed(unit);
    }

    /**
     * Updates the connections from the current style.
     *
     * Public for the test suite.
     *
     * @return The connections implied by the current style.
     */
    public final long getConnectionsFromStyle() {
        long conn = 0L;
        if (style != null) {
            List<Direction> directions = getConnectionDirections();
            if (directions != null) {
                String mask = style.getMask();
                for (int i = 0; i < directions.size(); i++) {
                    if (mask.charAt(i) != '0') {
                        conn |= 1L << directions.get(i).ordinal();
                    }
                }
            }
        }
        return conn;
    }

    /**
     * Sets the river style to be as close as possible to the requested
     * style, even when it has to create neighbouring rivers to prevent
     * broken connections or change the magnitude.
     *
     * @param conns The river style to set.
     */
    public void setRiverStyle(String conns) {
        if (!isRiver()) return;
        final Tile tile = getTile();
        int i = 0;
        int[] counts = {0, 0};
        for (Direction d : Direction.longSides) {
            Direction dReverse = d.getReverseDirection();
            Tile t = tile.getNeighbourOrNull(d);
            TileImprovement river = (t == null) ? null : t.getRiver();
            String c = (conns == null) ? "0" : conns.substring(i, i+1);

            if ("0".equals(c)) {
                if (river != null) {
                    river.setConnected(dReverse, false);
                }
                setConnected(d, false);
            } else {
                int mag = Integer.parseInt(c);
                if (river != null) {
                    river.setConnected(dReverse, true);
                    setConnected(d, true, c);
                    counts[mag-1]++;
                } else if (t != null) {
                    if (!t.getType().isWater()) {
                        t.addRiver(mag, "0000");
                        t.getRiver().setConnected(dReverse, true);
                    }
                    setConnected(d, true, c);
                    counts[mag-1]++;
                }
            }
            i++;
        }
        magnitude = counts[1] >= counts[0] ? 2 : 1;
    }

    /**
     * Updates the connections from/to this river improvement on the basis
     * of the expected encoded river style, as long as this would not
     * create broken connections.
     * Uses magnitude, not the connection strengths inside conns, when
     * adding new connections.
     *
     * @param conns The encoded river connections, or null to disconnect.
     * @return The actual encoded connections found.
     */
    public String updateRiverConnections(String conns) {
        // FIXME: Consider checking conns for incompatible length and content
        //        to prevent more bugs.
        if (!isRiver()) return null;
        final Tile tile = getTile();
        int i = 0;
        for (Direction d : Direction.longSides) {
            Direction dReverse = d.getReverseDirection();
            Tile t = tile.getNeighbourOrNull(d);
            TileImprovement river = (t == null) ? null : t.getRiver();
            String c = (conns == null) ? "0" : conns.substring(i, i+1);

            if ("0".equals(c)) {
                if (river != null) {
                    river.setConnected(dReverse, false);
                }
                setConnected(d, false);
            } else {
                if (river != null) {
                    river.setConnected(dReverse, true);
                    setConnected(d, true);
                } else if (t != null && t.getType().isWater()) {
                    setConnected(d, true);
                }
            }
            i++;
        }
        return (style == null) ? null : style.getString();
    }

    /**
     * Updates the connections from/to this road improvement.
     *
     * @param connect If true, add connections, otherwise remove them.
     * @return A string encoding of the correct connections for this
     *     improvement.
     */
    public String updateRoadConnections(boolean connect) {
        if (!isRoad() || !isComplete()) return null;
        final Tile tile = getTile();
        for (Direction d : Direction.values()) {
            Tile t = tile.getNeighbourOrNull(d);
            TileImprovement road = (t == null) ? null : t.getRoad();
            if (road != null && road.isComplete()) {
                road.setConnected(d.getReverseDirection(), connect);
                setConnected(d, connect);
            } else {
                setConnected(d, false);
            }
        }
        return (this.style == null) ? null : this.style.getString();
    }

    /**
     * Get the disaster choices available for this tile improvement.
     *
     * @return A stream of {@code Disaster} choices.
     */
    public Stream<RandomChoice<Disaster>> getDisasterChoices() {
        return (this.type == null)
            ? Stream.<RandomChoice<Disaster>>empty()
            : this.type.getDisasterChoices();
    }


    // Interface Named
    
    /**
     * {@inheritDoc}
     */
    @Override
    public String getNameKey() {
        return (type == null) ? null : type.getNameKey();
    }


    // Interface TileItem

    /**
     * {@inheritDoc}
     */
    @Override
    public final int getZIndex() {
        return type.getZIndex();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isTileTypeAllowed(TileType tileType) {
        return type.isTileTypeAllowed(tileType);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int applyBonus(GoodsType goodsType, UnitType unitType,
                          int potential) {
        // Applies the production bonuses of this tile improvement to
        // the given base potential.  Currently, the unit type
        // argument is ignored and is only provided for the sake of
        // consistency.  The bonuses of future improvements might
        // depend on the unit type, however.

        int result = potential;
        // do not apply any bonuses if the base tile does not produce
        // any goods, and don't apply bonuses for incomplete
        // improvements (such as roads)
        if (potential > 0 && isComplete()) {
            result += getBonus(goodsType);
        }
        return result;
    }
    
    private int getBonus(GoodsType goodsType) {
        final Modifier result = getProductionModifier(goodsType);
        if (result == null) {
            return 0;
        } else {
            return (int) result.getValue();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean canProduce(GoodsType goodsType, UnitType unitType) {
        // TileImprovements provide bonuses, but do *not* allow a tile
        // that can not produce some goods to produce due to the bonus.
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Stream<Modifier> getProductionModifiers(GoodsType goodsType, UnitType unitType) {
        if (goodsType == null || !isComplete()) {
            return Stream.<Modifier>empty();
        }
        
        final Specification spec = getSpecification();
        
        if (unitType == null
                && isNatural()
                && goodsType.isFoodType()) {
            return Stream.<Modifier>empty();
        }
        
        if (unitType == null
                && !isNatural()
                && !goodsType.isFoodType()
                && spec.getBoolean(GameOptions.ONLY_NATURAL_IMPROVEMENTS)) {
            return Stream.<Modifier>empty();
        }
        
        Modifier m = getProductionModifier(goodsType);
        if (m == null) {
            return Stream.<Modifier>empty();
        }
        
        if (unitType != null
                && unitType.getExpertProduction() != null
                && unitType.getExpertProduction().equals(goodsType)) {
            final Stream<Modifier> expertMultiplicativeModifier = unitType.getModifiers(goodsType.getId(), tile.getType(), null)
                .filter(mod -> mod.getType() == Modifier.ModifierType.MULTIPLICATIVE);
            
            final float expertBonus = FeatureContainer.applyModifiers(m.getValue(), null, expertMultiplicativeModifier);
            m = Modifier.makeModifier(m);
            m.setValue(expertBonus);
        }
        
        return Stream.of(m);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isNatural() {
        return type.isNatural();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isComplete() {
        return turnsToComplete <= 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Layer getLayer() {
        return Layer.RIVERS;
    }


    // Override FreeColGameObject

    /**
     * {@inheritDoc}
     */
    @Override
    public IntegrityType checkIntegrity(boolean fix, LogBuilder lb) {
        IntegrityType result = super.checkIntegrity(fix, lb);
        Tile tile = getTile();
        if (isRiver()) {
            // @compat 0.11.5 Prevent NPE, TileItemContainer rechecks this.
            if (style == null) {
                lb.add("\n  Broken null style river at ", tile);
                return result.fail();
            }
            // end @compat 0.11.5

            // @compat 0.11.6 There could be broken river connections, without
            // the neighbouring tile having a corresponding connection or
            // a water tile.
            // These could at least be added using the map editor.
            String conns = style.getString();
            int i = 0;
            for (Direction d : Direction.longSides) {
                Direction dReverse = d.getReverseDirection();
                Tile t = tile.getNeighbourOrNull(d);
                TileImprovement river = (t == null) ? null : t.getRiver();
                if (conns.charAt(i) != '0') {
                    if (river != null) {
                        if (!river.isConnectedTo(dReverse)) {
                            if (fix) {
                                setConnected(d, false);
                                lb.add("\n  Removed broken river connection to ",
                                    d, " at ", tile);
                                result = result.fix();
                            } else {
                                lb.add("\n  Broken river connection to ", d,
                                    " at ", tile);
                                result = result.fail();
                            }
                        }
                    } else if (t == null || !t.getType().isWater()) {
                        if (fix) {
                            setConnected(d, false);
                            lb.add("\n  Removed broken river connection to ",
                                d, " at ", tile);
                            result = result.fix();
                        } else {
                            lb.add("\n  Broken river connection to ", d,
                                " at ", tile);
                            result = result.fail();
                        }
                    }
                }
                i++;
            }
            // end @compat 0.11.6
        } else if (isRoad() && isComplete()) {
            // @compat 0.11.6 Roads on tiles never having another adjacent
            // road tile had null styles, because updateRoadConnections
            // forgot to set one for roads without a connection.
            if (fix) {
                TileImprovementStyle oldStyle = style;
                updateRoadConnections(true);
                if (style != oldStyle) {
                    lb.add("\n  Bad road style from ", oldStyle,
                        " to ", style, " fixed at ", tile);
                    result = result.fix();
                }
            }
            if (style == null) {
                lb.add("\n  Broken road with null style at ", tile);
                result = result.fail();
            }
            // end @compat 0.11.6
        }
        return result;
    }


    // Override FreeColObject

    /**
     * {@inheritDoc}
     */
    @Override
    public <T extends FreeColObject> boolean copyIn(T other) {
        TileImprovement o = copyInCast(other, TileImprovement.class);
        if (o == null || !super.copyIn(o)) return false;
        this.type = o.getType();
        this.turnsToComplete = o.getTurnsToComplete();
        this.magnitude = o.getMagnitude();
        this.style = o.getStyle();
        this.virtual = o.getVirtual();
        return true;
    }


    // Serialization

    private static final String MAGNITUDE_TAG = "magnitude";
    private static final String STYLE_TAG = "style";
    private static final String TILE_TAG = "tile";
    private static final String TURNS_TAG = "turns";
    private static final String TYPE_TAG = "type";
    private static final String VIRTUAL_TAG = "virtual";


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

        xw.writeAttribute(TILE_TAG, getTile());

        xw.writeAttribute(TYPE_TAG, getType());

        xw.writeAttribute(TURNS_TAG, turnsToComplete);

        xw.writeAttribute(MAGNITUDE_TAG, magnitude);

        if (style != null) xw.writeAttribute(STYLE_TAG, style);

        if (virtual) xw.writeAttribute(VIRTUAL_TAG, virtual);
    }

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

        final Specification spec = getSpecification();
        final Game game = getGame();

        tile = xr.makeFreeColObject(game, TILE_TAG, Tile.class, true);

        type = xr.getType(spec, TYPE_TAG, TileImprovementType.class,
                          (TileImprovementType)null);

        turnsToComplete = xr.getAttribute(TURNS_TAG, 0);

        magnitude = xr.getAttribute(MAGNITUDE_TAG, 0);

        virtual = xr.getAttribute(VIRTUAL_TAG, false);

        style = null;
        String str = xr.getAttribute(STYLE_TAG, (String)null);
        List<Direction> dirns = getConnectionDirections();
        if (dirns == null) {
            if (str != null && !str.isEmpty())
                logger.warning("At " + tile + " ignored nonempty style for "
                    + type + ": " + str);
        } else if (str == null) {
            // Null style OK for incomplete roads.  Virtual roads used
            // to be null, but we are fixing that.  Some cached tiles
            // were wrongly getting null style.  Do not bother
            // complaining about these as they will get fixed and
            // logged in checkIntegrity().
        } else if (str.length() != dirns.size()) {
            logger.warning("At " + tile + " ignored bogus style for "
                + type + ": " + str);
        } else {
            style = TileImprovementStyle.getInstance(str);
        }
    }

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


    // Override Object

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder(64);
        TileImprovementType ty = getType();
        sb.append('[').append((ty == null) ? "NOTYPE" : ty.getId());
        if (turnsToComplete > 0) {
            sb.append(" (").append(turnsToComplete).append(" turns left)");
        }
        if (style != null) sb.append(' ').append(style.getString());
        sb.append(']');
        return sb.toString();
    }
}
