package uk.me.nxg.unity;

import java.util.List;


/**
 * A single unit.
 * <p>The class's instances are immutable.
 *
 * <p>This class has no public constructors.  To obtain instances of
 * this class, parse a unit string using a {@link UnitParser}.
 *
 * <p>A unit can be ‘quoted’, indicating that it is to be parsed
 * as an ‘unknown’ unit even if its name matches that of a ‘known’ unit
 * (for example, <code>'B'</code> is a unit of ‘B’, and neither byte
 * nor Bel, and the <code>'furlong'</code> is a ‘furlong’ and not, as
 * it would otherwise be parsed, a femto-urlong).
 * This mechanism is syntactically permitted only in the VOUnits syntax,
 * and is used only for output and validity checks
 * (see {@link #isRecognisedUnit}), and not for processing.
 * All ‘quoted’ units are classed as not ‘recognised’.
 */
public abstract class OneUnit implements Comparable<OneUnit> {
    private final float exponent;

    private final boolean isQuoted;

    /**
     * OneUnit constructor is protected – it should only be invoked
     * by subclass constructors, and they may be invoked from the
     * {@link Maker} class.
     *
     * @param exponent the power of the base unit
     * @param isQuoted true, if this is an explicitly quoted unit (VOUnits syntax only)
     */
    protected OneUnit(float exponent, boolean isQuoted) {
        if (exponent == 0.0) {
            throw new IllegalArgumentException("Unit exponent cannot be zero");
        }
        this.exponent = exponent;
        this.isQuoted = isQuoted;
    }

    protected OneUnit(float exponent) {
        this(exponent, false);
    }

    /**
     * Is this a ‘quoted’ unit?
     * @return true if this unit is quoted
     */
    public boolean isQuoted() {
        return isQuoted;
    }

    /**
     * Produce the reciprocal of the unit, which is a unit which has
     * the negative of the exponent of this one.
     * @return a new instance
     */
    abstract OneUnit reciprocate();

    /**
     * Produce an instance of this unit raised to the given power.
     * @param exponent the power to which the base unit is to be raised
     * @return a new instance
     */
    abstract OneUnit pow(double exponent);

    /**
     * Returns the prefix of the unit, as a base-ten log.  Thus a
     * prefix of <code>"m"</code>, for ‘milli’, would produce a prefix of -3.
     * @return the power of ten which multiplies the unit
     */
    public abstract int getPrefix();

    /**
     * Returns the known base unit.  If the unit wasn't recognised as a
     * known unit in the syntax in which the string was parsed, then
     * this returns null, though {@link #getBaseUnitName} will not.
     *
     * <p>Note that the ‘base unit’ is simply the unit without the prefix, and
     * doesn't refer to the fundamental SI base units.  Thus in the
     * expression <code>"MW"</code>, it is ‘W’, Watt, that is the base unit.
     * @return a base unit
     */
    public abstract UnitDefinition getBaseUnitDefinition();

    /**
     * Returns the name of this unit.  If the unit is a known one,
     * then this will return a name for the unit such as ‘Metre’, or
     * ‘Julian year’; if it is not, then this can do no more than
     * return the unit symbol.
     *
     * <p>This should be used for identification or similar purposes.
     * To write out the unit you should generally use {@link UnitExpr#toString}.
     *
     * @return the string name of this unit
     */
    public abstract String getBaseUnitName();

    /**
     * Returns the base unit string, which will only be non-null if
     * this unit was an unrecognised one.  This is non-private
     * because UnitExpr needs to know this when it's searching for
     * units in the expression; it's package-only because this is not
     * how clients should format units – they should use the UnitExpr
     * formatting facilities instead.
     * @return the name of the base unit
     */
    abstract String getBaseUnitString();

    /**
     * Return the dimensions of the unit, if it is a recognised one.
     * If this <em>isn't</em> a recognised unit, return null.
     * @return the dimensions of the unit, or null if these aren't avaiable
     */
    public abstract Dimensions getDimensions();

    /**
     * Obtains the power the unit is raised to.  For example, for the
     * unit <code>mm^2</code>, return 2.
     * @return the power the unit is raised to
     */
    public float getExponent() {
        return exponent;
    }

    /**
     * Return true if this unit was guessed.  That is, if it was
     * identified heuristically, by using a parser configured with
     * {@link UnitParser#setGuessing}
     *
     * @return true if the unit was guessed
     */
    public boolean wasGuessed() {
        return false;
    }

    abstract UnitDefinitionMap.Resolver getUnitResolver(); // package only

    /**
     * Indicates whether the base unit is one of those recognised
     * within the specification of the given syntax.  In this context,
     * ‘recognised’ means ‘mentioned in the specification’, so deprecated units
     * count as recognised ones.
     *
     * <p>Note that this checks that the <em>unit</em> is a
     * recognised one: we don't (currently) check whether the
     * abbreviation that got us here is a recommended one (for
     * example, ‘pixel’ is a valid FITS/CDS name for pixels, and ‘pix’
     * is a FITS and OGIP one).  This also means that if we
     * <em>guessed</em> a unit, and that unit is a recognised one in
     * this syntax, then this method returns true.
     *
     * <p>If the syntax is Syntax.ALL, then this checks whether this
     * is a recognised unit in any syntax.
     *
     * @param syntax one of the syntaxes of {@link Syntax}
     * @return true if the unit is recognised
     * @see #isRecommendedUnit
     */
    public abstract boolean isRecognisedUnit(Syntax syntax);

    /**
     * Indicates whether the base unit is one of those recognised in
     * any syntax.
     *
     * <p>This is equivalent to {@code isRecognisedUnit(Syntax.ALL)}.
     * If this method returns true, then
     * {@link #getBaseUnitDefinition()} would return non-null, and
     * <em>vice versa</em> if this method returns false.
     *
     * @return true if the unit is recognised
     */
    public abstract boolean isRecognisedUnit();

    /**
     * Indicates whether the base unit is one of those recommended
     * within the specification of the given syntax.  In this context,
     * ‘recommended’ means ‘mentioned in the specification’
     * <em>and</em> not deprecated.  Thus all ‘recommended’ units are
     * also <em>a fortiori</em> ‘recognised’.
     *
     * <p>Note that this checks that the <em>unit</em> is a
     * recommended one: we don't (currently) check whether the
     * abbreviation that got us here is a recommended one (for
     * example, "pixel" is a valid FITS/CDS name for pixels, and "pix"
     * is a FITS and OGIP one).  This also means that if we
     * <em>guessed</em> a unit, and that unit is a recommended one in
     * this syntax, then this method returns true.
     * @param syntax one of the syntaxes of {@link Syntax}
     * @return true if the unit is a recommended one
     * @see #isRecognisedUnit
     */
    public abstract boolean isRecommendedUnit(Syntax syntax);

    /**
     * Indicates whether the unit is being used in a way which
     * satisfies any usage constraints.  Principally, this tests
     * whether a unit which may not be used with SI prefixes was
     * provided with a prefix, but there may be other constraints
     * present.
     *
     * <p>An unrecognised unit has no constraints, and so will always
     * satisfy them; this extends to units which are unrecognised in a
     * particular syntax.
     * @param syntax one of the syntaxes of {@link Syntax}
     * @return true if the unit satisfies its usage constraints
     */
    public abstract boolean satisfiesUsageConstraints(Syntax syntax);

    /**
     * Format this unit in readable form.
     * The form is unspecified.
     *
     * <p>This should not generally be used for formatted output – for that, it will
     * more often be better to use {@link UnitExpr#toString}.
     * @return a string representation of the unit
     */
    public abstract String toString();

    /**
     * Format this unit in readable form, appropriate to the given syntax.
     * The form is unspecified.
     *
     * <p>This should not generally be used for formatted output – for that, it will
     * more often be better to use {@link UnitExpr#toString}.
     *
     * @param syntax one of the known syntaxes
     * @return a string representation of the unit
     * @throws UnwritableExpression if the unit can't be written in
     *     the given syntax
     */
    public abstract String toString(Syntax syntax)
            throws UnwritableExpression;

    /**
     * Obtains the string representation of the unit, including
     * prefix, in the given syntax.  That is, return ‘mm’ not ‘m’ or
     * ‘metre’.  If a unit has more than one representation in a given
     * syntax, then this will produced the ‘preferred one’.
     *
     * <p>This will fail, with a UnitParserException, in the rare
     * cases where the unit string is inexpressible in the given
     * syntax: for example the CDS syntax permits only the log
     * function to be applied to a unit, and will fail if it is asked
     * to display a different function.
     *
     * @param syntax one of the syntaxes of {@link Syntax}
     * @return a non-null string representation of the unit
     * @throws UnwritableExpression if the unit string is inexpressible in the given syntax
     */
    public abstract String unitString(Syntax syntax)
            throws UnwritableExpression;

    /**
     * Obtains the representation of the unit, as it originally
     * appeared in the parsed input.  This will differ from what is
     * produced by {@code unitString} if (a) the original was a
     * dispreferred representation of a recognised unit (for example
     * ‘yr’ vs ‘a’), or (b) if the unit was (successfully) guessed
     * from an otherwise unrecognised string.
     *
     * @return the original input representation of the unit
     */
    public abstract String getOriginalUnitString();

    /**
     * Write out the unit in a testable format.
     * This is for debugging and testing.
     * @return a string representation of the unit
     */
    public abstract String toDebugString();

    /**
     * Produce the reciprocal of the unit, which is a unit which has
     * the negative of the exponent of this one.
     *
     * @param ul the expression to be reciprocated
     * @return a new instance
     */
    public static List<OneUnit> reciprocate(Iterable<OneUnit> ul) {
        List<OneUnit> res = new java.util.ArrayList<OneUnit>();
        for (OneUnit u : ul) {
            res.add(u.reciprocate());
        }
        return res;
    }

    /**
     * Divide one list of units by another
     * @param num the units in the numerator
     * @param den the units in the denominator
     * @return a list of units
     */
    public static List<OneUnit> divide(List<OneUnit> num, Iterable<OneUnit> den) {
        List<OneUnit> res = new java.util.ArrayList<OneUnit>(num);
        for (OneUnit u : den) {
            res.add(u.reciprocate());
        }
        return res;
    }

    @Override public abstract int compareTo(OneUnit o);

    static class Maker {
        final UnitDefinitionMap.Resolver unitResolver;

        private boolean guessUnits = false; // should we guess units when parsing?

        Maker(UnitDefinitionMap.Resolver unitResolver) {
            if (unitResolver == null) {
                throw new IllegalArgumentException("Can't make SimpleUnit.Maker with null resolver");
            }
            this.unitResolver = unitResolver;
        }
        /** Construct a SimpleUnit from a string unit and an integer
         * exponent.  For example, cubic millimetres would have
         * arguments "mm" and 3.
         * @param s a string representation of a unit
         * @param e the power to which is to be raised
         * @return a new simple unit
         */
        public SimpleUnit make(String s, int e) {
            return SimpleUnit.makeSimpleUnit(unitResolver, s, (float)e, guessUnits);
        }
        /** Construct a SimpleUnit from a string unit and a real
         * exponent.  For example, cubic millimetres would have
         * arguments "mm" and 3.0.
         * @param s a string representation of a unit
         * @param e the power to which is to be raised
         * @return a new simple unit
         */
        public SimpleUnit make(String s, double e) {
            return SimpleUnit.makeSimpleUnit(unitResolver, s, (float)e, guessUnits);
        }
        /**
         * Construct a ‘quoted’ unit.
         * @param pfx the prefix multiple, such as ‘k’ or ‘Ki’
         * @param u a string representation of a unit
         * @param e the power to which is to be raised
         * @return a new simple unit
         * @see SimpleUnit#makeSimpleUnit
         */
        public SimpleUnit makeQuotedUnit(String pfx, String u, double e) {
            try {
                return SimpleUnit.makeQuotedSimpleUnit(unitResolver, pfx, u, (float)e);
            } catch (UnitParserException ex) {
                // failed for one or other reason – we don't care
                return null;
            }
        }

        /**
         * When parsing units, should we try to guess units?
         * By default, we don't guess.  But if we are asked to guess, then
         * unrecognised units will be re-examined with some unspecified
         * heuristics so that, for example, ‘degrees’ will be recognised
         * as the degree of angle.
         * @param guess_p true to turn on guessing
         */
        public void setGuessing(boolean guess_p) {
            guessUnits = guess_p;
        }
    }
}
