package uk.me.nxg.unity;

import java.util.List;
import java.util.Iterator;


/**
 * A parsed unit expression.
 *
 * <p>This class has no public constructors.  To obtain instances of
 * this class, parse a unit string using a {@link UnitParser}.
 */
public class UnitExpr implements Iterable<OneUnit>, Comparable<UnitExpr> {
    private final double logFactor;
    private final List<OneUnit> unitsList;
    private java.util.SortedSet<OneUnit> unitsSet; // not final; cached
    private final UnitDefinitionMap.Resolver resolver;
    
    /**
     * Construct a UnitExpr instance from a single list of units
     * @param logFactor the base-ten log of the factor at the head of the unit specification
     * @param l the list of units
     */
    UnitExpr(double logFactor, List<OneUnit> l) {
        this(logFactor, l, null);
    }

    /**
     * Construct a UnitExpr instance from a pair of lists of units.
     * Divide the numerator by the denominator.
     * @param logFactor the base-ten log of the factor at the head of the unit specification
     * @param num the list of units in the numerator
     * @param den the list of units in the numerator
     */
    UnitExpr(double logFactor, List<OneUnit> num, List<OneUnit> den) {
        if (num == null || num.isEmpty()) {
            throw new IllegalArgumentException("UnitExpr must have non-empty numerator");
        }
        this.logFactor = logFactor;
        if (den == null) {
            unitsList = num;
        } else {
            unitsList = OneUnit.divide(num, den);
        }
        assert unitsList != null && unitsList.size() > 0;

        resolver = unitsList.get(0).getUnitResolver();
        assert resolver != null;

        unitsSet = null;
    }

    /**
     * Returns a new UnitExpr, representing the same expression, but
     * with the units in some canonical order (not specified here, but
     * consistent within a library release).
     */
    public UnitExpr canonicalize() {
        List<OneUnit> l = new java.util.ArrayList<OneUnit>(getSortedSet());
        return new UnitExpr(logFactor, l);
    }

    /**
     * Obtain the base-10 log of the factor multiplying this expression
     */
    public double getLogFactor() {
        return logFactor;
    }

    /**
     * Obtain the factor multiplying this expression
     */
    public double getFactor() {
        return Math.pow(10, logFactor);
    }

    private java.util.SortedSet<OneUnit> getSortedSet() {
        if (unitsSet == null) {
            unitsSet = new java.util.TreeSet<OneUnit>(unitsList);
        }
        return unitsSet;
    }

    /**
     * Return a representation of the parsed expression as an iterator.
     */
    public Iterator<OneUnit>iterator() {
        return unitsList.iterator();
    }

    /**
     * Return a representation of the parsed expression as an
     * iterator, with the units in a canonical order.  The order is
     * not specified here, but it is consistent in any library release.
     */
    public Iterator<OneUnit>sortedIterator() {
        return getSortedSet().iterator();
    }

    /**
     * Extracts the unit information from the expression, keyed by an
     * abstract unit instance.  Returns null if the unit is not
     * present in the expression.
     *
     * <p>At present, this won't fully work if there is more than one
     * occurrence of a unit in an expression, for example in the case
     * "m^3/mm".  It's not clear if that should be disallowed, or if
     * not how it should be manipulated, so this part of the interface
     * will quite possibly change in some way in future.
     * @param reqUnit a non-null unit definition
     */
    public OneUnit getUnit(UnitDefinition reqUnit) {
        if (reqUnit == null) {
            throw new IllegalArgumentException("getUnit(UnitDefinition) argument may not be null");
        }
        for (OneUnit u : unitsList) {
            UnitDefinition ud;
            if (u instanceof SimpleUnit) {
                ud = u.getBaseUnitDefinition();
                if (ud != null && ud.equals(reqUnit)) {
                    return u;       // JUMP OUT
                }
            } else {
                assert u instanceof FunctionOfUnit;
                OneUnit result = ((FunctionOfUnit)u).getUnit(reqUnit);
                if (result != null) {
                    return u;   // JUMP OUT
                }
            }
        }
        // not found
        return null;
    }

    /**
     * Extracts the unit information from the expression, keyed by a
     * symbolic name for the unit, as defined by the syntax which was
     * used to parse the expression.  For example, metres are
     * represented by the symbol ‘m’.  Returns null if the unit is not
     * present in the expression.  If there is more than one symbolic
     * representation for a unit in a given syntax – for example,
     * some syntaxes permit both ‘yr’ and ‘a’ for year – then either
     * may be used to look up the unit in an expression which was
     * parsed using that syntax.
     *
     * <p>If the argument is not a recognised unit in the parsing
     * system, then this returns the unit which has a matching
     * symbol.  For example, ‘erg’ is not recognised symbol in the CDS
     * syntax, so if the string ‘erg/s’ were parsed using the CDS
     * parser, then the call <code>getUnit("erg")</code> would find the (unknown) unit
     * ‘erg’, but using {@link #getUnit(UnitDefinition)} with an ‘Erg’
     * UnitDefinition would retrieve nothing.
     * @param reqUnit a non-null string indicating a unit symbol
     * @return a unit, or null if the specified unit is not in the expression
     */
    public OneUnit getUnit(String reqUnit) {
        if (reqUnit == null) {
            throw new IllegalArgumentException("getUnit(String) argument may not be null");
        }
        UnitDefinition reqUnitDef = resolver.lookupSymbol(reqUnit);
        if (reqUnitDef != null) {
            return getUnit(reqUnitDef);
        } else {
            // this wasn't a recognised unit in this resolver's syntax
            for (OneUnit u : unitsList) {
                String us = u.getBaseUnitString();
                if (us != null && us.equals(reqUnit)) {
                    return u;  // JUMP OUT
                }
            }
            // not found
            return null;
        }
    }

    /**
     * Get one of the units within the expression, by index.
     * @param idx the unit to retrieve
     * @see #size
     */
    public OneUnit getUnit(int idx) {
        return unitsList.get(idx);
    }

    /**
     * The number of units in the expression, which will always be at least one
     */
    public int size() {
        return unitsList.size();
    }

    UnitDefinitionMap.Resolver getUnitResolver() {
        return resolver;
    }

    /**
     * Indicates whether the parsed expression is composed only of
     * recommended units, in the sense of
     * {@link OneUnit#isRecommendedUnit}.  Recommended units are units which
     * are both recognised and not deprecated.
     * @param syntax the syntax with respect to which the
     * units should be checked
     */
    public boolean allUnitsRecommended(Syntax syntax) {
        for (OneUnit u : unitsList) {
            if (! u.isRecommendedUnit(syntax)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Indicates whether the parsed expression is composed only of recognised
     * units, in the sense of {@link OneUnit#isRecognisedUnit}.  That
     * is, an expression composed of only non-deprecated and unknown
     * units has no deprecated units.
     * @param syntax the syntax with respect to which the
     * units should be checked
     */
    public boolean allUnitsRecognised(Syntax syntax) {
        for (OneUnit u : unitsList) {
            if (! u.isRecognisedUnit(syntax)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Indicates whether the expression is being used in a way which
     * satisfies any usage constraints,
     * in the sense of {@link OneUnit#satisfiesUsageConstraints}.
     * 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 boolean allUsageConstraintsSatisfied(Syntax syntax) {
        for (OneUnit u : unitsList) {
            if (! u.satisfiesUsageConstraints(syntax)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Indicates whether the expression is fully conformant with the
     * appropriate recommendations.  That is, all units are
     * recommended, and are being used in a way which is conformant
     * with any usage constraints.
     * @param syntax one of the syntaxes of {@link Syntax}
     */
    public boolean isFullyConformant(Syntax syntax) {
        for (OneUnit u : unitsList) {
            if (! (u.satisfiesUsageConstraints(syntax) && u.isRecommendedUnit(syntax))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Obtains the dimensions of the complete expression.  If any of
     * the units within the expression were unrecognised, this returns null.
     */
    public Dimensions getDimensions() {
        Dimensions res = Dimensions.unity();
        for (OneUnit u : unitsList) {
            if (u.getDimensions() == null) {
                return null;    // JUMP OUT
            }
            res = res.multiply(u.getDimensions(), u.getExponent());
        }
        return res;
    }

    /**
     * Provides an ordering for UnitExpr instances.  The order is
     * consistent, but unspecified here.
     */
    @Override public int compareTo(UnitExpr ue) {
        int cmp;
        if (logFactor == ue.logFactor) {
            Iterator<OneUnit> i0 = sortedIterator();
            Iterator<OneUnit> i1 = ue.sortedIterator();
            cmp = 0;
            while(cmp == 0) {
                if (i0.hasNext() && i1.hasNext()) {
                    OneUnit u0 = i0.next();
                    OneUnit u1 = i1.next();
                    cmp = u0.compareTo(u1);
                } else if (! (i0.hasNext() || i1.hasNext())) {
                    // both sequences have ended
                    cmp = 0;
                    break;
                } else {
                    cmp = (i0.hasNext() ? +1 : -1); // shorter one is ordered earlier
                }
            }
        } else {
            cmp = (logFactor < ue.logFactor ? -1 : +1);
        }
        return cmp;
    }

    @Override public boolean equals(Object o) {
        if (o instanceof UnitExpr) {
            return compareTo((UnitExpr)o) == 0;
        } else {
            return false;
        }
    }

    /**
     * Produces a string representation of the unit expression, in a
     * form suitable for display
     */
    @Override public String toString() {
        try {
            return toString(null, null);
        } catch (UnitParserException e) {
            throw new AssertionError("UnitExpr.toString(FITS) failed: this shouldn't happen: " + e);
        }
    }

    /**
     * Produces a string representation of the unit expression, in a
     * format appropriate to the given syntax.
     * The syntaxes are those of {@link Syntax}.  
     * The output is locale-independent (which in practice means it is
     * in the <code>java.text.Locale.US</code> locale).
     * @param syntax the syntax to generate
     */
    public String toString(Syntax syntax)
            throws UnitParserException {
        return toString(syntax, null);
    }

    /**
     * Produces a string representation of the unit expression, in a
     * format appropriate to the given syntax, and with output
     * respecting the conventions of the given locale.
     * The syntaxes are those of {@link Syntax}.
     * If <code>locale</code> is null, the output is locale-independent
     * (which in practice means it is in the <code>java.text.Locale.US</code> locale);
     * note that this is distinct from the process's default locale.
     * @param syntax the syntax to generate
     * @param locale the locale to use, or null to use a ‘no-locale’ locale
     */
    public String toString(Syntax syntax, java.util.Locale locale)
            throws UnitParserException {
        UnitWriter w = SyntaxFactory.createWriter(syntax == null ? Syntax.FITS : syntax, this);
        if (locale != null) {
            w.setLocale(locale);
        }
        return w.write();
    }

    /**
     * Format the unit expression as a string, in some sort of
     * canonical/unambiguous form.  The format of the output is unspecified: the
     * {@link #toString} method is generally preferred as a
     * way of formatting expressions.
     */
    public String toDebugString() {
        try {
            return toString(Syntax.DEBUG, null);
        } catch (UnitParserException e) {
            throw new AssertionError("Debug writer failed: " + e);   
        }
    }
}

        
