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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

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


public final class TileImprovementType extends FreeColGameObjectType {

    private boolean natural;
    private int magnitude;
    private int addWorkTurns;

    private TileImprovementType requiredImprovementType;

    private Set<String> allowedWorkers = new HashSet<String>();
    private EquipmentType expendedEquipmentType;
    private int expendedAmount;

    // @compat 0.10.4
    private GoodsType deliverGoodsType;
    private int deliverAmount;
    // end @compat

    private Map<TileType, TileTypeChange> tileTypeChanges
        = new HashMap<TileType, TileTypeChange>();

    private int movementCost;
    private float movementCostFactor;

    /**
     * The layer a TileItem belongs to. Items with higher zIndex
     * will be displayed above items with a lower zIndex. E.g. the
     * LostCityRumour would be displayed above the Plow improvement.
     */
    private int zIndex;

    /**
     * Can this improvement expose a resource when completed? This
     * should only apply to improvement types that change the
     * underlying tile type (e.g. clearing forests).
     */
    private int exposeResourcePercent;

    /**
     * The scopes define which TileTypes support this improvement. An
     * eligible TileType must match all scopes.
     */
    private List<Scope> scopes = new ArrayList<Scope>();

    // ------------------------------------------------------------ constructors

    public TileImprovementType(String id, Specification specification) {
        super(id, specification);
        setModifierIndex(Modifier.IMPROVEMENT_PRODUCTION_INDEX);
    }

    // ------------------------------------------------------------ retrieval methods

    public boolean isNatural() {
        return natural;
    }

    public int getMagnitude() {
        return magnitude;
    }

    public int getAddWorkTurns() {
        return addWorkTurns;
    }

    /**
     * Get the <code>ZIndex</code> value.
     *
     * @return an <code>int</code> value
     */
    public int getZIndex() {
        return zIndex;
    }

    /**
     * Set the <code>ZIndex</code> value.
     *
     * @param newZIndex The new ZIndex value.
     */
    public void setZIndex(final int newZIndex) {
        this.zIndex = newZIndex;
    }

    public TileImprovementType getRequiredImprovementType() {
        return requiredImprovementType;
    }

    public EquipmentType getExpendedEquipmentType() {
        return expendedEquipmentType;
    }

    public int getExpendedAmount() {
        return expendedAmount;
    }

    /**
     * Returns the goods produced by applying this TileImprovementType
     * to a Tile with the given TileType.
     *
     * @param from a <code>TileType</code> value
     * @return an <code>AbstractGoods</code> value
     */
    public AbstractGoods getProduction(TileType from) {
        TileTypeChange change = tileTypeChanges.get(from);
        return change == null ? null : change.getProduction();
    }


    /**
     * Get the <code>Scopes</code> value.
     *
     * @return a <code>List<Scope></code> value
     */
    public List<Scope> getScopes() {
        return scopes;
    }

    /**
     * Return an ID of an appropriate action.
     *
     * @return a <code>String</code> value
     */
    public String getShortId() {
        int index = getId().lastIndexOf('.') + 1;
        return getId().substring(index);
    }


    public boolean isWorkerTypeAllowed(UnitType unitType) {
        return allowedWorkers.isEmpty() || allowedWorkers.contains(unitType.getId());
    }

    /**
     * Check if a given <code>Unit</code> can perform this TileImprovement.
     * @return true if Worker UnitType is allowed and expended Goods are available
     */
    public boolean isWorkerAllowed(Unit unit) {
        if (!isWorkerTypeAllowed(unit.getType())) {
            return false;
        }
        return (unit.getEquipment().getCount(expendedEquipmentType) >= expendedAmount);
    }

    /**
     * This will check if in principle this type of improvement can be used on
     * this kind of tile, disregarding the current state of an actual tile.
     *
     * If you want to find out if an improvement is allowed for a tile, call
     * {@link #isTileAllowed(Tile)}.
     *
     * @param tileType The type of terrain
     * @return true if improvement is possible
     */
    public boolean isTileTypeAllowed(TileType tileType) {
        for (Scope scope : scopes) {
            if (!scope.appliesTo(tileType)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Check if a given <code>Tile</code> is valid for this TileImprovement.
     *
     * @return true if Tile TileType is valid and required Improvement (if any)
     *         is present.
     */
    public boolean isTileAllowed(Tile tile) {
        if (!isTileTypeAllowed(tile.getType())) {
            return false;
        }
        if (requiredImprovementType != null && tile.findTileImprovementType(requiredImprovementType) == null) {
            return false;
        }
        TileImprovement ti = tile.findTileImprovementType(this);
        return ti == null || !ti.isComplete();
    }

    public int getBonus(GoodsType goodsType) {
        Modifier result = getProductionModifier(goodsType);
        if (result == null) {
            return 0;
        } else {
            return (int) result.getValue();
        }
    }

    public Modifier getProductionModifier(GoodsType goodsType) {
        Set<Modifier> modifierSet = getFeatureContainer().getModifierSet(goodsType.getId());
        if (modifierSet == null || modifierSet.isEmpty()) {
            return null;
        } else {
            if (modifierSet.size() > 1) {
                logger.warning("Only one Modifier for " + goodsType.getId() + " expected!");
            }
            return modifierSet.iterator().next();
        }
    }

    /**
     * Returns true if this TileImprovementType changes the underlying
     * tile type.
     *
     * @return a <code>boolean</code> value
     */
    public boolean isChangeType() {
        return !tileTypeChanges.isEmpty();
    }

    /**
     * Returns the destination type of a tile type change (or null).
     *
     * @param tileType a <code>TileType</code> value
     * @return a <code>TileType</code> value
     */
    public TileType getChange(TileType tileType) {
        TileTypeChange change = tileTypeChanges.get(tileType);
        return change == null ? null : change.getTo();
    }

    /**
     * Returns true if this TileImprovementType can change a tile type
     * to the given tile type.
     *
     * @param tileType a <code>TileType</code> value
     * @return a <code>boolean</code> value
     */
    public boolean changeContainsTarget(TileType tileType) {
        for (TileTypeChange change : tileTypeChanges.values()) {
            if (change.getTo() == tileType) {
                return true;
            }
        }
        return false;
    }

    /**
     * Performs reduction of the movement-cost.
     * @param moveCost Original movement cost
     * @return The movement cost after any change
     */
    public int getMovementCost(int moveCost) {
        int cost = moveCost;
        if (movementCostFactor >= 0) {
            float cost2 = cost * movementCostFactor;
            cost = (int)cost2;
            if (cost < cost2) {
                cost++;
            }
        }
        if (movementCost > 0) {
            // Only >0 values are meaningful (see spec).
            // Do not return zero from a movement costing routine or
            // units get free moves!
            if (movementCost < cost) {
                cost = movementCost;
            }
        }
        return cost;
    }

    /**
     * Gets the percent chance that this tile improvement can expose a
     * resource on the tile. This only applies to TileImprovementTypes
     * that change the underlying tile type (e.g. clearing forests).
     *
     * @return The exposure chance.
     */
    public int getExposeResourcePercent() {
        return exposeResourcePercent;
    }

    /**
     * Gets the increase in production of the given GoodsType
     * this tile improvement type would yield at a specified tile.
     *
     * @param tile The <code>Tile</code> to be considered.
     * @param goodsType A preferred <code>GoodsType</code> or <code>null</code>
     * @return The increase in production
     */
    public int getImprovementValue(Tile tile, GoodsType goodsType) {
        int value = 0;
        if (goodsType.isFarmed()) {
            TileType newTileType = getChange(tile.getType());
            if (newTileType == null) { // simple bonus
                int production = tile.potential(goodsType, null);
                if (production > 0) {
                    float change = getFeatureContainer()
                        .applyModifier(production, goodsType.getId());
                    value = (int) (change - production);
                }
            } else { // tile type change
                int change = newTileType.getProductionOf(goodsType, null)
                    - tile.getType().getProductionOf(goodsType, null);
                value = change;
            }
        }
        return value;
    }

    /**
     * Makes an XML-representation of this object.
     *
     * @param out The output stream.
     * @throws XMLStreamException if there are any problems writing to the
     *             stream.
     */
    public void toXMLImpl(XMLStreamWriter out) throws XMLStreamException {
        super.toXML(out, getXMLElementTagName());
    }

    /**
     * Write the attributes of this object to a stream.
     *
     * @param out The target stream.
     * @throws XMLStreamException if there are any problems writing to
     *     the stream.
     */
    @Override
    protected void writeAttributes(XMLStreamWriter out)
        throws XMLStreamException {
        super.writeAttributes(out);

        out.writeAttribute("natural", Boolean.toString(natural));
        out.writeAttribute("add-work-turns", Integer.toString(addWorkTurns));
        out.writeAttribute("movement-cost", Integer.toString(movementCost));
        out.writeAttribute("magnitude", Integer.toString(magnitude));
        out.writeAttribute("zIndex", Integer.toString(zIndex));
        out.writeAttribute("exposeResourcePercent",
                           Integer.toString(exposeResourcePercent));
        if (requiredImprovementType != null) {
            out.writeAttribute("required-improvement",
                requiredImprovementType.getId());
        }
        if (expendedEquipmentType != null) {
            out.writeAttribute("expended-equipment-type",
                expendedEquipmentType.getId());
            out.writeAttribute("expended-amount",
                Integer.toString(expendedAmount));
        }
    }

    /**
     * Write the children of this object to a stream.
     *
     * @param out The target stream.
     * @throws XMLStreamException if there are any problems writing to
     *     the stream.
     */
    @Override
    protected void writeChildren(XMLStreamWriter out)
        throws XMLStreamException {
        super.writeChildren(out);

        if (scopes != null) {
            for (Scope scope : scopes) {
                scope.toXMLImpl(out);
            }
        }
        if (allowedWorkers != null) {
            for (String id : allowedWorkers) {
                out.writeStartElement("worker");
                out.writeAttribute(ID_ATTRIBUTE_TAG, id);
                out.writeEndElement();
            }
        }
        if (tileTypeChanges != null) {
            for (TileTypeChange change : tileTypeChanges.values()) {
                change.toXML(out);
            }
        }
    }

    /**
     * Reads the attributes of this object from an XML stream.
     *
     * @param in The XML input stream.
     * @throws XMLStreamException if a problem was encountered
     *     during parsing.
     */
    @Override
    protected void readAttributes(XMLStreamReader in)
        throws XMLStreamException {
        super.readAttributes(in);

        natural = getAttribute(in, "natural", false);
        addWorkTurns = getAttribute(in, "add-work-turns", 0);
        movementCost = getAttribute(in, "movement-cost", 0);
        movementCostFactor = -1;
        magnitude = getAttribute(in, "magnitude", 1);

        requiredImprovementType = getSpecification().getType(in,
            "required-improvement", TileImprovementType.class, null);

        zIndex = getAttribute(in, "zIndex", 0);
        exposeResourcePercent = getAttribute(in, "exposeResourcePercent", 0);

        expendedEquipmentType = getSpecification().getType(in,
            "expended-equipment-type", EquipmentType.class, null);
        expendedAmount = getAttribute(in, "expended-amount", 0);

        // @compat 0.10.4
        deliverGoodsType = getSpecification().getType(in,
            "deliver-goods-type", GoodsType.class, null);
        deliverAmount = getAttribute(in, "deliver-amount", 0);
        // end @compat
    }

    /**
     * Reads a child object.
     *
     * @param in The XML stream to read.
     * @exception XMLStreamException if an error occurs
     */
    @Override
    protected void readChild(XMLStreamReader in) throws XMLStreamException {
        String childName = in.getLocalName();
        if ("scope".equals(childName)) {
            scopes.add(new Scope(in));
        } else if ("worker".equals(childName)) {
            allowedWorkers.add(in.getAttributeValue(null, ID_ATTRIBUTE_TAG));
            in.nextTag(); // close this element
        } else if ("change".equals(childName)) {
            TileTypeChange change = new TileTypeChange();
            if (deliverGoodsType == null) {
                change.readFromXML(in, getSpecification());
            } else {
                // @compat 0.10.4
                change.setFrom(getSpecification().getTileType(in.getAttributeValue(null, "from")));
                change.setTo(getSpecification().getTileType(in.getAttributeValue(null, "to")));
                change.setProduction(new AbstractGoods(deliverGoodsType, deliverAmount));
                // end @compat
                in.nextTag(); // close this element
            }
            tileTypeChanges.put(change.getFrom(), change);
        } else {
            super.readChild(in);
        }
    }

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