/**
 *  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.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

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 static net.sf.freecol.common.util.CollectionUtils.*;


/**
 * A type of goods, derived from the specification.
 */
public final class GoodsType extends FreeColSpecObjectType {

    public static final String TAG = "goods-type";

    private static final float DEFAULT_PRODUCTION_WEIGHT = 1.0f;
    private static final float DEFAULT_LOW_PRODUCTION_THRESHOLD = 0.0f;
    private static final float DEFAULT_ZERO_PRODUCTION_FACTOR = 1.0f;

    /** A comparator to impose a useful order on goods types. */
    public static final Comparator<GoodsType> goodsTypeComparator
        = Comparator.comparingInt(GoodsType::getRank)
            .thenComparing(Function.<FreeColObject>identity());

    /** Is this a farmed goods type. */
    private boolean isFarmed;

    /** Is this a food type. */
    private boolean isFood;

    /** Does this goods type ignore warehouse limits. */
    private boolean ignoreLimit;

    /** Is this goods type native to the New World. */
    private boolean newWorldGoods;

    /**
     * Whether this type of goods is required for building equipment
     * that grants an offence bonus or defence bonus.
     */
    private boolean isMilitary = false;

    /**
     * Whether this type of goods is required for building. (Derived
     * attribute)
     */
    private boolean buildingMaterial = false;

    /** Whether these are trade goods that can only be obtained in Europe. */
    private boolean tradeGoods;

    /** Whether this type of goods can be stored in a warehouse. */
    private boolean storable;

    /** What this goods type is stored as. */
    private GoodsType storedAs;

    /** What this goods type is made from. */
    private GoodsType madeFrom;

    /** What this goods type can make.  (Derived attribute) */
    private GoodsType makes = null;

    /** The initial amount of this goods type in a market. */
    private int initialAmount;

    /** The initial <em>minimum</em> sales price for this type of goods. */
    private int initialPrice;

    /** The initial market price difference for this type of goods. */
    private int priceDiff;

    /**
     * The number of units required to breed this type of goods. This
     * obviously only applies to animals.
     */
    private int breedingNumber = INFINITY;

    /**
     * The price of this type of goods.  This is only used for goods
     * that can not be traded in the market, such as hammers.
     */
    private int price = INFINITY;

    /**
     * A weight for the potential production of this goods type at a
     * colony site.
     */
    private float productionWeight = DEFAULT_PRODUCTION_WEIGHT;

    /**
     * A threshold amount of potential production of this goods type
     * at a colony site, below which the score for the site is reduced.
     */
    private float lowProductionThreshold = DEFAULT_LOW_PRODUCTION_THRESHOLD;

    /**
     * The multiplicative factor with which to penalize a colony site
     * with zero production of this goods type, scaling linearly to
     * unity when the amount reaches lowResourceThreshold.
     */
    private float zeroProductionFactor = DEFAULT_ZERO_PRODUCTION_FACTOR;


    /**
     * Create a new goods type.
     *
     * @param id The object identifier.
     * @param specification The {@code Specification} to refer to.
     */
    public GoodsType(String id, Specification specification) {
        super(id, specification);
    }


    /**
     * Is this a farmed goods type?
     *
     * @return True if this is a farmed goods type.
     */
    public boolean isFarmed() {
        return isFarmed;
    }

    /**
     * Is this an edible goods type?
     *
     * @return True if this is a food type.
     */
    public boolean isFoodType() {
        return isFood;
    }

    /**
     * Is this goods type a military goods type?
     *
     * @return True if this is a military goods type.
     */
    public boolean getMilitary() {
        return this.isMilitary;
    }

    // @compat 0.10.x/0.11.x
    // Needed by Specification.fixSpec()
    public void setMilitary() {
        this.isMilitary = true;
    }
    // end @compat 0.10.x/0.11.x

    /**
     * Do warehouse storage limits not apply to this goods type?
     *
     * @return True if unlimited amounts of this goods type can be stored.
     */
    public boolean limitIgnored() {
        return ignoreLimit;
    }

    /**
     * Is this a goods type native to the New World?
     *
     * @return True if this goods type is native to the New World.
     */
    public boolean isNewWorldGoodsType() {
        return newWorldGoods;
    }

    /**
     * Is this goods type made from a New World goods type?
     *
     * @return True if this goods type is made from New World goods.
     */
    public boolean isNewWorldLuxuryType() {
        return madeFrom != null && madeFrom.isNewWorldGoodsType();
    }

    /**
     * Is this type of goods is required for building a BuildableType?
     *
     * @return True if this is a simple building material.
     * @see BuildableType
     */
    public boolean isBuildingMaterial() {
        return buildingMaterial;
    }

    /**
     * Is this a trade goods type?
     *
     * @return True if this goods type is trade goods.
     */
    public boolean isTradeGoods() {
        return tradeGoods;
    }

    /**
     * Does this type of goods produce liberty?
     *
     * @return True if this goods type produces liberty.
     */
    public boolean isLibertyType() {
        return containsModifierKey(Modifier.LIBERTY);
    }

    /**
     * Does this type of goods produce immigration?
     *
     * @return True if this goods type produces immigration.
     */
    public boolean isImmigrationType() {
        return containsModifierKey(Modifier.IMMIGRATION);
    }

    /**
     * Is this goods type storable?
     *
     * @return True if this goods type is storable.
     */
    public boolean isStorable() {
        return storable;
    }

    /**
     * Is this type of goods stored as something else?
     *
     * @return True if this type of goods is stored as another type.
     */
    public boolean isStoredAs() {
        return storedAs != null;
    }

    /**
     * What type of goods is this goods type stored as?
     *
     * @return The {@code GoodsType} this is stored as, usually itself.
     */
    public GoodsType getStoredAs() {
        return (storedAs == null) ? this : storedAs;
    }

    /**
     * Can this goods type be made into somthing?
     *
     * @return True if this {@code GoodsType} can be made into something.
     */
    public boolean isRawMaterial() {
        return makes != null;
    }

    /**
     * Is this goods type made from somthing?
     *
     * @return True if this {@code GoodsType} is made from something.
     */
    public boolean isRefined() {
        return madeFrom != null;
    }

    /**
     * What is this goods made into?
     *
     * @return The {@code GoodsType} this is made into, if anything.
     */
    public GoodsType getOutputType() {
        return makes;
    }

    /**
     * What is this goods type made from?
     *
     * @return The {@code GoodsType} this is made from, if anything.
     */
    public GoodsType getInputType() {
        return madeFrom;
    }

    /**
     * Get the default initial amount of this goods type in a market.
     *
     * @return The default initial amount.
     */
    public int getInitialAmount() {
        return initialAmount;
    }

    /**
     * Get the initial <em>minimum</em> sales price for this type
     * of goods.  The actual initial sales price in a particular
     * Market may be higher.  This method is only used for initializing
     * Markets.
     *
     * @return The initial sell price.
     * @see Market
     */
    int getInitialSellPrice() {
        return initialPrice;
    }

    /**
     * The default initial purchase price for this goods type.
     *
     * @return The default initial purchase price.
     */
    int getInitialBuyPrice() {
        return initialPrice + priceDiff;
    }

    /**
     * The default initial price difference (between purchase and sale price)
     * for this type of goods.
     *
     * @return The default initial price difference.
     */
    int getPriceDifference() {
        return priceDiff;
    }

    /**
     * Get the breeding number for this goods type.
     *
     * @return The breeding number.
     */
    public int getBreedingNumber() {
        return breedingNumber;
    }

    /**
     * Is this type of goods breedable?
     *
     * @return True if this {@code GoodsType} is breedable.
     */
    public boolean isBreedable() {
        return breedingNumber != INFINITY;
    }

    /**
     * Get the price of a non-tradeable goods type.
     *
     * @return The price.
     */
    public int getPrice() {
        return price;
    }

    public void setPrice(final int newPrice) {
        this.price = newPrice;
    }

    /**
     * Get the production weight.
     *
     * @return The production weight.
     */
    public float getProductionWeight() {
        return productionWeight;
    }

    /**
     * Get the low production threshold.
     *
     * @return The low production threshold.
     */
    public float getLowProductionThreshold() {
        return lowProductionThreshold;
    }

    /**
     * Get the zero production factor.
     *
     * @return The zero production factor.
     */
    public float getZeroProductionFactor() {
        return zeroProductionFactor;
    }


    /**
     * Gets the i18n-ed name for this goods type.
     *
     * @return The name of this {@code GoodsType}.
     */
    public StringTemplate getLabel() {
        return StringTemplate.key(getNameKey());
    }

    /**
     * Gets the "workingAs" key for the profession that makes this goods type.
     *
     * @return The "workingAs" message key.
     */
    public final String getWorkingAsKey() {
        return getId() + ".workingAs";
    }

    /**
     * Gets the production chain of the goods type, beginning with a
     * raw material that can not be produced from any other.  The last
     * member of the production chain is the goods type itself.
     *
     * Note that this does not really handle goods that are stored as
     * something else as is the case for grain etc.
     * FIXME: fix or retire this routine?
     *
     * @return The production chain of this {@code GoodsType} as
     *     a list.
     */
    public List<GoodsType> getProductionChain() {
        List<GoodsType> result = new ArrayList<>();
        GoodsType currentGoods = this;
        while (currentGoods != null) {
            result.add(0, currentGoods);
            currentGoods = currentGoods.madeFrom;
        }
        return result;
    }

    /**
     * Is this type of goods required somewhere in the chain for
     * producing a BuildableType, and is not itself buildable.
     *
     * @return True if a raw building type.
     * @see BuildableType
     */
    public boolean isRawBuildingMaterial() {
        if (this.madeFrom != null) return false;

        GoodsType refinedType = makes;
        while (refinedType != null) {
            if (refinedType.isBuildingMaterial()) return true;
            refinedType = refinedType.makes;
        }
        return false;
    }
    
    /**
     * Is this type of goods required somewhere in the chain for
     * producing a BuildableType, and is not itself buildable.
     *
     * @return True if a raw building type.
     * @see BuildableType
     */
    public boolean isRawMaterialForUnstorableBuildingMaterial() {
        if (this.madeFrom != null) return false;

        GoodsType refinedType = makes;
        while (refinedType != null) {
            if (refinedType.isBuildingMaterial() && !refinedType.isStorable()) {
                return true;
            }
            refinedType = refinedType.makes;
        }
        return false;
    }

    /**
     * Get all the equivalent goods types, in the sense that they are
     * stored as this type.
     *
     * @return A set of equivalent {@code GoodsType}s, which
     *     must include this one.
     */
    public Set<GoodsType> getEquivalentTypes() {
        return transform(getSpecification().getGoodsTypeList(),
                         gt -> gt == this || gt.getStoredAs() == this,
                         Function.<GoodsType>identity(), Collectors.toSet());
    }
        
    /**
     * Set the derived fields for the goods types in a specification.
     *
     * The "derived" fields are: buildingMaterial + makes
     * - buildingMaterial depends on whether a GoodsType is present on
     *   a BuildableType requiredGoods list
     * - makes depends on whether a GoodsType madeFrom field refers
     *   to another
     *   
     * This is called from Specification.clean() when the
     * specification is fully read.  We must wait until then as the
     * made-from field can change in extended specifications and mods.
     * The current example of which is horses, which is made-from food
     * in the classic ruleset and made-from grain in the freecol
     * ruleset.
     *
     * @param spec The {@code Specification} to operate on.
     */
    public static void setDerivedAttributes(Specification spec) {
        // Reset to default state
        for (GoodsType g : spec.getGoodsTypeList()) {
            g.buildingMaterial = false;
            g.makes = null;
        }

        // Set buildingMaterial attribute
        List<BuildableType> buildableTypes = new ArrayList<>();
        buildableTypes.addAll(spec.getBuildingTypeList());
        buildableTypes.addAll(spec.getUnitTypeList());
        buildableTypes.addAll(spec.getRolesList());
        forEach(flatten(buildableTypes, BuildableType::getRequiredGoods),
                ag -> ag.getType().buildingMaterial = true);

        // Set makes attribute
        for (GoodsType g : spec.getGoodsTypeList()) {
            if (g.madeFrom != null) g.madeFrom.makes = g;
        }
    }

    /**
     * Impose a general order on the goods types.
     *
     * @return An integer useful in comparators.
     */
    private int getRank() {
        return (!isStorable() || isTradeGoods()) ? -1
            : (isFoodType()) ? 1
            : (isNewWorldGoodsType()) ? 2
            : (isFarmed()) ? 3
            : (isRawMaterial()) ? 4
            : (isNewWorldLuxuryType()) ? 5
            : (isRefined()) ? 6
            : -1;
    }


    // Override FreeColObject

    /**
     * {@inheritDoc}
     */
    @Override
    public <T extends FreeColObject> boolean copyIn(T other) {
        GoodsType o = copyInCast(other, GoodsType.class);
        if (o == null || !super.copyIn(o)) return false;
        this.isFarmed = o.isFarmed();
        this.isFood = o.isFoodType();
        this.ignoreLimit = o.limitIgnored();
        this.newWorldGoods = o.isNewWorldGoodsType();
        this.isMilitary = o.getMilitary();
        this.buildingMaterial = o.isBuildingMaterial();
        this.tradeGoods = o.isTradeGoods();
        this.storable = o.isStorable();
        this.storedAs = o.getStoredAs();
        this.madeFrom = o.getInputType();
        this.makes = o.getOutputType();
        this.initialAmount = o.getInitialAmount();
        this.initialPrice = o.getInitialSellPrice();
        this.priceDiff = o.getPriceDifference();
        this.breedingNumber = o.getBreedingNumber();
        this.price = o.getPrice();
        this.productionWeight = o.getProductionWeight();
        this.lowProductionThreshold = o.getLowProductionThreshold();
        this.zeroProductionFactor = o.getZeroProductionFactor();
        return true;
    }


    // Serialization

    private static final String BREEDING_NUMBER_TAG = "breeding-number";
    private static final String IGNORE_LIMIT_TAG = "ignore-limit";
    private static final String INITIAL_AMOUNT_TAG = "initial-amount";
    private static final String INITIAL_PRICE_TAG = "initial-price";
    private static final String IS_FARMED_TAG = "is-farmed";
    private static final String IS_FOOD_TAG = "is-food";
    private static final String IS_MILITARY_TAG = "is-military";
    private static final String LOW_PRODUCTION_THRESHOLD_TAG = "low-production-threshold";
    private static final String MADE_FROM_TAG = "made-from";
    private static final String MARKET_TAG = "market";
    private static final String NEW_WORLD_GOODS_TAG = "new-world-goods";
    private static final String PRICE_TAG = "price";
    private static final String PRICE_DIFFERENCE_TAG = "price-difference";
    private static final String PRODUCTION_WEIGHT_TAG = "production-weight";
    private static final String STORABLE_TAG = "storable";
    private static final String STORED_AS_TAG = "stored-as";
    private static final String TRADE_GOODS_TAG = "trade-goods";
    private static final String ZERO_PRODUCTION_FACTOR_TAG = "zero-production-factor";


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

        xw.writeAttribute(IS_FARMED_TAG, isFarmed);

        xw.writeAttribute(IS_FOOD_TAG, isFood);

        xw.writeAttribute(IS_MILITARY_TAG, isMilitary);

        xw.writeAttribute(IGNORE_LIMIT_TAG, ignoreLimit);

        xw.writeAttribute(NEW_WORLD_GOODS_TAG, newWorldGoods);

        xw.writeAttribute(TRADE_GOODS_TAG, tradeGoods);

        xw.writeAttribute(STORABLE_TAG, storable);

        if (breedingNumber != INFINITY) {
            xw.writeAttribute(BREEDING_NUMBER_TAG, breedingNumber);
        }

        if (price != INFINITY) {
            xw.writeAttribute(PRICE_TAG, price);
        }

        if (madeFrom != null) {
            xw.writeAttribute(MADE_FROM_TAG, madeFrom);
        }

        if (storedAs != null) {
            xw.writeAttribute(STORED_AS_TAG, storedAs);
        }

        xw.writeAttribute(PRODUCTION_WEIGHT_TAG, productionWeight);

        if (lowProductionThreshold > DEFAULT_LOW_PRODUCTION_THRESHOLD) {
            xw.writeAttribute(LOW_PRODUCTION_THRESHOLD_TAG,
                              lowProductionThreshold);
        }

        if (0.0 <= zeroProductionFactor
            && zeroProductionFactor < DEFAULT_ZERO_PRODUCTION_FACTOR) {
            xw.writeAttribute(ZERO_PRODUCTION_FACTOR_TAG, zeroProductionFactor);
        }
    }

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

        if (initialAmount > 0) {
            xw.writeStartElement(MARKET_TAG);

            xw.writeAttribute(INITIAL_AMOUNT_TAG, initialAmount);

            xw.writeAttribute(INITIAL_PRICE_TAG, initialPrice);

            xw.writeAttribute(PRICE_DIFFERENCE_TAG, priceDiff);

            xw.writeEndElement();
        }
    }

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

        final Specification spec = getSpecification();

        isFarmed = xr.getAttribute(IS_FARMED_TAG, false);

        isFood = xr.getAttribute(IS_FOOD_TAG, false);

        isMilitary = xr.getAttribute(IS_MILITARY_TAG, false);

        ignoreLimit = xr.getAttribute(IGNORE_LIMIT_TAG, false);

        newWorldGoods = xr.getAttribute(NEW_WORLD_GOODS_TAG, false);

        tradeGoods = xr.getAttribute(TRADE_GOODS_TAG, false);

        breedingNumber = xr.getAttribute(BREEDING_NUMBER_TAG, INFINITY);

        price = xr.getAttribute(PRICE_TAG, INFINITY);

        madeFrom = xr.getType(spec, MADE_FROM_TAG, GoodsType.class,
                              (GoodsType)null);

        storable = xr.getAttribute(STORABLE_TAG, true);

        storedAs = xr.getType(spec, STORED_AS_TAG, GoodsType.class,
                              (GoodsType)null);

        productionWeight = xr.getAttribute(PRODUCTION_WEIGHT_TAG,
            DEFAULT_PRODUCTION_WEIGHT);

        lowProductionThreshold = xr.getAttribute(LOW_PRODUCTION_THRESHOLD_TAG,
            DEFAULT_LOW_PRODUCTION_THRESHOLD);

        zeroProductionFactor = xr.getAttribute(ZERO_PRODUCTION_FACTOR_TAG,
            DEFAULT_ZERO_PRODUCTION_FACTOR);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void readChild(FreeColXMLReader xr) throws XMLStreamException {
        final String tag = xr.getLocalName();

        if (MARKET_TAG.equals(tag)) {
            initialAmount = xr.getAttribute(INITIAL_AMOUNT_TAG, 0);

            initialPrice = xr.getAttribute(INITIAL_PRICE_TAG, 1);

            priceDiff = xr.getAttribute(PRICE_DIFFERENCE_TAG, 1);

            xr.closeTag(MARKET_TAG);

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

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