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.
     */
    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?
     */
    public boolean isQuoted() {
        return this.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.
     * @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.
     */
    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 #getBaseUnitString} 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.
     */
    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.
     */
    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.
     */
    public 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.
     */
    public float getExponent() {
        return exponent;
    }

    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
     * 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).
     * @param syntax one of the syntaxes of {@link Syntax}
     * @see #isRecommendedUnit
     */
    public abstract boolean isRecognisedUnit(Syntax syntax);

    /**
     * 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).
     * @param syntax one of the syntaxes of {@link Syntax}
     * @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}
     */
    public abstract boolean satisfiesUsageConstraints(Syntax syntax);

    /**
     * Format this unit in some sort of canonical form.
     * The form is unspecified.
     * This should not generally be used for formatted output – for that, it will
     * generally be more appropriate to use {@link UnitExpr#toString}.
     */
    public abstract String toString();

    /**
     * Format this unit in some sort of canonical form appropriate to the given syntax.
     * The form is unspecified.
     * This should not generally be used for formatted output – for that, it will
     * generally be more appropriate to use {@link UnitExpr#toString}.
     */
    public abstract String toString(Syntax syntax);

    /**
     * Obtains the string representation of the unit, including
     * prefix, in the given syntax.  That is, return ‘mm’ not ‘m’ or ‘metre’.
     * @param syntax one of the syntaxes of {@link Syntax}
     * @return a non-null string representation of the unit
     * @throws UnitParserException if the syntax is unrecognised
     */
    public abstract String unitString(Syntax syntax) throws UnitParserException;

    /**
     * Write out the unit in a testable format.
     * This is for debugging and testing.
     */
    public abstract String toDebugString();
    
    /**
     * Produce the reciprocal of the unit, which is a unit which has
     * the negative of the exponent of this one.
     * @return a new instance
     */
    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
     */
    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;
         
        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.
         */
        public SimpleUnit make(String s, int e) {
            return SimpleUnit.makeSimpleUnit(unitResolver, s, (float)e);
        }
        /** Construct a SimpleUnit from a string unit and a real
         * exponent.  For example, cubic millimetres would have
         * arguments "mm" and 3.0.
         */
        public SimpleUnit make(String s, double e) {
            return SimpleUnit.makeSimpleUnit(unitResolver, s, (float)e);
        }
        /**
         * Construct a ‘quoted’ 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;
            }
        }
    }
}
