package uk.me.nxg.unity;

import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.Matcher;


/**
 * A single simple unit, such as 'kg'.
 * This is represented as a <em>prefix</em>,
 * a <em>base unit</em> and an <em>exponent</em>.  For example, the
 * string "mm s^-2" will result in two SimpleUnit instances, one of which
 * will have a base unit of "m" and a prefix power of -3, and the
 * other of which will have a base unit of "s" and an exponent of -2.
 *
 * <p>This represents the abstract unit, shorn of the symbols which
 * were parsed to obtain it.  Thus if, in some odd syntax, the symbol
 * 'x' were used to denote metres, then the SimpleUnit instance which
 * resulted would be the same as the OneUnit which resulted from a
 * more conventional syntax.
 *
 * <p>Also, there is a potential ambiguity if a symbol is recognised
 * in one syntax, but not in another.  Thus if the string 'erg' were
 * parsed in a syntax which didn't recognise that, then it would be
 * stored as just that, an unrecognised symbol, not associated with
 * the {@link UnitDefinition} for the erg.  Similarly, if a unit is
 * parsed with guessing, then we may encounter a unit which is recognised
 * in the sense of {@link OneUnit#isRecognisedUnit()}, but not
 * in the sense of {@link OneUnit#isRecognisedUnit(Syntax)}.
 *
 * <p>The class's instances are immutable.
 */
public abstract class SimpleUnit
        extends OneUnit {
    private final UnitDefinitionMap.Resolver unitResolver;

    /**
     * Represents the power-of-ten (or power-of-two)
     * modifier in front of the base unit.
     * For example, in the string "mm", the prefix power would be "-3".
     */
    private final int prefixPower;

    /**
     * The base unit as a UnitDefinition.
     * The 'base unit' is the unit without the prefix.
     * Thus in the expression "MW", it is 'W', Watt, that is the base unit.
     * This doesn't refer to the fundamental SI base units.
     * If this is non-null, it means that the unit was a recognised one
     * in the parsing syntax.
     */
    private final UnitDefinition baseUnitDefinition;

    /**
     * The base unit as a string, as it originally appeared in the
     * parsed unit string.
     */
    private final String baseUnitString;

    /**
     * Constructor is intended to be internal, but has to be invokable
     * by subclasses.
     *
     * @param r the resolver which is used to (attempt to)
     *     resolve the unit string (non-null)
     * @param prefix the multiplier prefix
     * @param us the string form of the new unit (non-null)
     * @param ud the UnitDefinition which corresponds to this unit
     * @param e the exponent to which the unit is raised, which must not be zero
     * @param quoted whether this is a quoted unit (see {@link OneUnit} for discussion)
     */
    SimpleUnit(UnitDefinitionMap.Resolver r,
               int prefix,
               String us,
               UnitDefinition ud,
               float e,
               boolean quoted) {
        super(e, quoted);

        if (r == null) {
            throw new IllegalArgumentException("SimpleUnit constructor: resolver 'r' must be non-null");
        }
        if (us == null) {
            throw new IllegalArgumentException("SimpleUnit constructor: unit string 'us' must be non-null");
        }

        this.prefixPower = prefix;
        this.baseUnitString = us;
        this.baseUnitDefinition = ud;
        this.unitResolver = r;

        // class invariants
        assert this.baseUnitString != null;
        assert this.unitResolver != null;
    }

    /**
     * Create a new SimpleUnit.
     * @param r the resolver which is used to (attempt to) resolve the unit string
     * @param unitString the string form of the new unit
     * @param e the exponent to which the unit is raised, which must not be zero
     * @param guessUnits if true, then use some heuristics to guess a likely intended unit
     * @return a parsed unit
     */
    static SimpleUnit makeSimpleUnit(UnitDefinitionMap.Resolver r,
                                     String unitString,
                                     float e,
                                     boolean guessUnits) {
        // It would be possible/appropriate to do strict checking of
        // units in here – that is, throwing exceptions whenever we
        // encounter any non-recommended unit (ie one for which lookupUnit
        // returns null).  This would create the additional
        // possibility of being a convenient/reasonable place to check
        // whether the abbreviation that we look up is an allowed one
        // for the syntax we're parsing, as long as we were
        // additionally provided with some information within this
        // constructor, about which syntax that is (which is currently
        // not available in here).
        SimpleUnit rval = null;

        UnitDefinition def = r.lookupSymbol(unitString);

        if (def != null) {
            // that was easy!
            rval = new SimpleDecimalUnit(r, 0, unitString, def, e);
        }

        if (rval == null) {
            PrefixSplit ps = SimpleBinaryUnit.splitUnitString(unitString);
            if (ps != null) {
                // This looks very much like a binary prefix.
                // HOWEVER: we accept it as such _only_ if the base_unit which
                // we would return is a known unit which we permit to take
                // binary prefixes.
                String us = ps.getUnit();
                def = r.lookupSymbol(us);
                // if this is null, then this is an unknown unit, so
                // no binary prefix should be recognised
                if (def != null) {
                    UnitRepresentation rep = def.getRepresentation(r.getSyntax());
                    if (rep != null && rep.mayHaveBinaryPrefixes()) {
                        rval = new SimpleBinaryUnit(r, ps.getPower(), us, def, e);
                    }
                }
            }
        }

        if (rval == null) {
            PrefixSplit ps = SimpleDecimalUnit.splitUnitString(unitString);
            if (ps == null) {
                // there was no SI prefix
                if (guessUnits) {
                    rval = guessUnit(r, unitString, 0, unitString, e);
                }
                if (rval == null) {
                    rval = new SimpleDecimalUnit(r, 0, unitString, null, e);
                }
            } else {
                // this string starts with an SI prefix (ie, m, k, M, ...)
                String us = ps.getUnit();
                def = r.lookupSymbol(us);
                if (def == null) {
                    if (guessUnits) {
                        rval = guessUnit(r, unitString, ps.getPower(), us, e);
                    }
                    if (rval == null) {
                        rval = new SimpleDecimalUnit(r, ps.getPower(), us, null, e);
                    }
                } else {
                    // we've positively identified this as SI-prefix plus a known unit
                    rval = new SimpleDecimalUnit(r, ps.getPower(), us, def, e);
                }
            }
        }

        assert rval != null;
        return rval;
    }

    /**
     * Create a new quoted SimpleUnit.
     * @param r the resolver which is used to (attempt to) resolve the unit string (unused, but available to callers)
     * @param prefix the decimal or binary prefix to the unit
     * @param unitString the string form of the new unit
     * @param e the exponent to which the unit is raised, which must not be zero
     * @return a parsed unit
     * @throws UnitParserException if the multiplier prefix is unrecognised
     */
    static SimpleUnit makeQuotedSimpleUnit(UnitDefinitionMap.Resolver r,
                                           String prefix,
                                           String unitString,
                                           float e)
            throws UnitParserException {
        // very similar to makeSimpleUnit
        SimpleUnit rval;

        if (prefix == null) {
            // we deem quoted units to be 'not guessed'
            rval = new SimpleDecimalUnit(r, 0, unitString, null, e, true, false);
        } else {
            // In this case, we do not even attempt to spot binary
            // units, since these are to be accepted only where the
            // base unit is recognised and explicitly permits binary
            // units; since this is a quoted unit, it is not
            // recognised, by hypothesis.

            // make a fake unit "X", so that splitUnitString doesn't object
            String testString = prefix + "X";
            PrefixSplit ps = SimpleDecimalUnit.splitUnitString(testString);
            if (ps != null) {
                rval = new SimpleDecimalUnit(r, ps.getPower(), unitString, null, e, true, false);
            } else {
                throw new UnitParserException("Unrecognised prefix: " + prefix);
            }
        }

        assert rval != null;
        return rval;
    }

    /**
     * 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 SimpleUnit reciprocate();

    /**
     * Returns the prefix of the unit, as a base-ten log.  Thus a
     * prefix of "m", for "milli", would produce a prefix of -3.
     */
    @Override public int getPrefix() {
        return prefixPower;
    }

    /**
     * Convert a prefix power to a String.  These are returned as
     * Strings, so that we can later consider 'da' or 'Ki' (etc) as prefixes.
     * @return a string representation of the power
     */
    abstract String prefixPowerToString();

    /**
     * Convert a prefix power to a String, in a form useful within
     * siunitx.  The only real difference from {@link #prefixPowerToString}
     * is that <code>\micro</code> is formatted differently.
     * @return a string representation of the power, in LaTeX form
     */
    abstract String prefixPowerToStringLaTeX();

    @Override public String toString() {
        try {
            return toString(null);
        } catch (UnitParserException ex) {
            // this should be impossible
            throw new AssertionError("SimpleUnit.toString() really should work: " + ex);
        }
    }

    @Override public String toString(Syntax syntax)
            throws UnwritableExpression {
        if (syntax == Syntax.DEBUG) {
            return toDebugString();
        } else {
            StringBuilder sb = new StringBuilder();
            // unitString(Syntax.ALL) will throw an exception, but no other writers should
            sb.append(unitString(syntax));
            if (getExponent() != 1) {
                sb.append('^').append(getExponent());
            }
            return sb.toString();
        }
    }

    public String unitString(Syntax syntax)
            throws UnwritableExpression {
        String ret;
        if (syntax == Syntax.DEBUG) {
            ret = toDebugString();
        } else if (syntax == Syntax.LATEX) {
            ret = unitStringLaTeX();
        } else {
            ret = unitStringText(syntax);
        }
        assert ret != null : syntax;
        return ret;
    }

    @Override public UnitDefinition getBaseUnitDefinition() {
        return baseUnitDefinition;
    }

    @Override public String getBaseUnitName() {
        assert baseUnitString != null;
        return (baseUnitDefinition != null ? baseUnitDefinition.name() : baseUnitString);
    }

    @Override public String getBaseUnitString() {
        return baseUnitString;
    }

    @Override public Dimensions getDimensions() {
        if (baseUnitDefinition == null) {
            return null;
        } else {
            return baseUnitDefinition.dimensions();
        }
    }

    public String getOriginalUnitString() {
        return baseUnitString;
    }

    /**
     * Retrieve the string form of the unit in one of the text syntaxes.
     * @param syntaxName one of the syntaxes of {@link Syntax}
     * @return the text unit as, for example, "mm" for millimetres
     */
    private String unitStringText(Syntax syntax)
            throws UnwritableExpression {
        if (syntax == Syntax.ALL) {
            // this is probably redundant, since writing out the ALL
            // syntax should be caught (and an exception thrown),
            // before this
            throw new UnwritableExpression("You can't write in the ALL syntax");
        }

        StringBuilder sb = new StringBuilder();
        if (prefixPower != 0) {
            sb.append(prefixPowerToString());
        }
        if (baseUnitDefinition == null) {
            assert baseUnitString != null;

            if (isQuoted()) {
                sb.append('\'').append(baseUnitString).append('\'');
            } else {
                sb.append(baseUnitString);
            }

        } else {
            UnitDefinitionMap.Resolver r;
            if (syntax == null) {
                // special call, from unitString(void)
                r = unitResolver;
            } else {
                r = UnitDefinitionMap.getInstance().getResolver(syntax);
                if (r == null) {
                    throw new AssertionError("unitString: Unknown syntax: " + syntax);
                }
            }

            UnitRepresentation rep = r.lookupUnit(baseUnitDefinition);
            if (rep == null) {
                // this UnitDefinition is not recognised in this syntax
                rep = unitResolver.lookupUnit(baseUnitDefinition);
            }

            // This unitResolver is where we originally got the
            // baseUnitDefinition from, so this will succeed in all
            // cases _except_ where this baseUnitDefinition was
            // obtained as the result of the guessing process.  In
            // this case, we have to look further afield.
            if (rep == null) {
                rep = UnitDefinitionMap.getInstance()
                        .lookupUnitRepresentation(Syntax.ALL, baseUnitDefinition);
            }
            // by now, surely...
            assert rep != null : String.format("unitStringText: no representation for syntax [%s]",
                                               syntax);

            sb.append(rep.getAbbreviation());
        }

        return sb.toString();
    }

    /**
     * Retrieve the string form of the unit in a form suitable for use
     * within the siunitx LaTeX package.  The LaTeX serialisation
     * isn't quite a syntax like the text ones, though it appears
     * similar to one from the outside.
     * @return the text unit as, for example, "mm" for millimetres
     */
    private String unitStringLaTeX() {
        StringBuilder sb = new StringBuilder();
        if (prefixPower != 0) {
            sb.append(prefixPowerToStringLaTeX()).append(' ');
        }
        if (baseUnitDefinition == null) {
            assert baseUnitString != null;
            sb.append(baseUnitString);
        } else {
            if (baseUnitDefinition.latexForm() != null) {
                sb.append(baseUnitDefinition.latexForm());
            } else {
                UnitRepresentation rep = unitResolver.lookupUnit(baseUnitDefinition);
                // this unitResolver is where we originally got the
                // baseUnitDefinition from, so this must succeed
                assert rep != null : "unit's own representation has disappeared";

                sb.append(rep.getAbbreviation());
            }
        }
        return sb.toString();
    }

    /**
     * Obtains the string form of the unit, including prefix, with a
     * default syntax.  That is, return 'mm' not 'm'.
     *
     * <p>The default syntax is (currently) the syntax with which this
     * unit was originally read.
     * @return a string representation of the unit
     */
    public String unitString() {
        try {
            return unitString(null);
        } catch (UnitParserException e) {
            throw new AssertionError("Odd: SimpleUnit.unitString() should not have failed");
        }
    }

    /**
     * A UnitGuesser class maps a regexp pattern to a unit.
     */
    private static class UnitGuesser {
        protected static UnitDefinitionMap udm;
        static {
            udm = UnitDefinitionMap.getInstance();
        }

        /**
         * Given a putative unit string, try to guess what unit it
         * corresponds to.
         * @param unitString an up-to-now unrecognised unit string
         * @return a UnitDefinition, or null if nothing can be found
         */
        public UnitDefinition guess(String unitString) { return null; };
    }

    /**
     * A PatternUnitGuesser looks up arbitrary patterns.
     * The unit is specified in the constructor, as a URI which is
     * looked up in the UnitDefinitionMap.  If we fail to find this
     * URI, then we throw an assertion error.
     */
    private static class PatternUnitGuesser extends UnitGuesser {
        private final Pattern p;
        private final UnitDefinition response;

        PatternUnitGuesser(Pattern p, String unitUri) {
            this.p = p;
            this.response = udm.lookupUnitDefinition(unitUri);
            assert response != null : String.format("unitUri [%s] is inexplicably unknown",
                                                    unitUri);
        }

        @Override public UnitDefinition guess(String unitString) {
            Matcher m = p.matcher(unitString);
            if (m.matches()) {
                return response;
            } else {
                return null;
            }
        }
    };

    /**
     * A PluralUnitGuesser works similarly, but matches any unit
     * string which ends in 's', and tries to look up what comes before that.
     */
    private static class PluralUnitGuesser extends UnitGuesser {
        private final Pattern p;
        PluralUnitGuesser() {
            p = Pattern.compile("(.*)s");
        }

        @Override public UnitDefinition guess(String unitString) {
            Matcher m = p.matcher(unitString);
            UnitDefinition result = null;

            if (m.matches()) {
                // Look things up in any syntax that recognises this.
                // There's no guarantee that this will find something.
                result = udm.lookupUnitDefinition(Syntax.ALL, m.group(1));
            }
            return result;
        }
    }

    /**
     * Look up the unit string in <em>any</em> syntax.
     * This will catch the case where a unit string is recognised in
     * one syntax, but not in the preferred one.
     */
    private static class AnythingGuesser extends UnitGuesser {
        @Override public UnitDefinition guess(String unitString) {
            return udm.lookupUnitDefinition(Syntax.ALL, unitString);
        }
    }

    static UnitGuesser[] allGuesses = {
        new PatternUnitGuesser(Pattern.compile("deg(ree)?s?"),
                               "http://qudt.org/vocab/unit#DegreeAngle"),
        new PatternUnitGuesser(Pattern.compile("sec(ond)?s?"),
                               "http://qudt.org/vocab/unit#SecondTime"),
        new PluralUnitGuesser(),
        new AnythingGuesser()
    };

    private static SimpleUnit guessUnit(UnitDefinitionMap.Resolver r,
                                        String unitString,
                                        int power,
                                        String baseUnit,
                                        float e) {
        //System.err.println("guessUnit(" + unitString + ") : 10^" + power + " x " + baseUnit);

        // If we are trying to guess, say, 'degrees' here, then we have been given
        //    unitString = "degrees"
        //    power = -1               (because the prefix 'd' indicates 10^-1)
        //    baseUnit = "egrees"      (after the prefix 'd' has been stripped off)
        // We need to look at the baseUnit first, then the unitString as a whole
        for (int i=0; i<allGuesses.length; i++) {
            UnitDefinition u = null;
            int realPower = power;
            String realBaseString = baseUnit;
            if (power != 0) {
                u = allGuesses[i].guess(baseUnit);
            }
            if (u == null) {
                realPower = 0;      // we thought there was a prefix, but maybe there isn't
                realBaseString = unitString;
                u = allGuesses[i].guess(unitString);
            }
            if (u != null) {
                return new SimpleDecimalUnit(r, realPower, realBaseString, u, e,
                                             false, // this is not a quoted unit
                                             true); // flag that this was guessed
            }
        }
        return null;
    }

    @Override UnitDefinitionMap.Resolver getUnitResolver() {
        return unitResolver;
    }

    @Override public boolean isRecognisedUnit(Syntax syntax) {
        // We could check the validity of the abbreviation whilst we're
        // parsing the units, but that would frustrate our ability to
        // subsequently write out recommended units in the appropriate form.
        if (syntax == Syntax.ALL) {
            return baseUnitDefinition != null; // equivalent to isRecognisedUnit()
        } else {
            return baseUnitDefinition != null
                    && baseUnitDefinition.getRepresentation(syntax) != null;
        }
    }

    @Override public boolean isRecognisedUnit() {
        return baseUnitDefinition != null;
    }

    @Override public boolean isRecommendedUnit(Syntax syntax) {
        // We could check the validity of the abbreviation, when we're
        // parsing the units, but that would frustrate our ability to
        // subsequently write out recommended units in the appropriate form.
        if (baseUnitDefinition == null) {
            // not recognised, so a fortiori not recommended
            return false;
        } else {
            UnitRepresentation r = baseUnitDefinition.getRepresentation(syntax);
            return r != null && !r.isDeprecated();
        }
    }

    /**
     * Indicate whether this unit has permitted prefixes.  The only
     * constraints are for 'known' units, so we only check cases which
     * have a UnitRepresentation associated with them.
     * @param r the unit to test
     * @return true if this unit has an acceptable prefix
     */
    abstract boolean hasPermittedPrefix(UnitRepresentation r);

    @Override public boolean satisfiesUsageConstraints(Syntax syntax) {
        // Other constraints are possible: for some syntaxes, the 'Crab'
        // unit may be used only with the 'm' prefix
        // (but the 'Crab' is my go-to example of a nutty unit).
        boolean validity;
        if (baseUnitDefinition == null) {
            // there are no constraints, so this necessarily satisfies all of them
            validity = true;
        } else {
            UnitRepresentation r = baseUnitDefinition.getRepresentation(syntax);
            if (r == null) {
                // as above
                validity = true;
            } else {
                validity = hasPermittedPrefix(r);
                //validity = prefixPower == 0 || r.mayHaveSIPrefixes();
            }
        }
        return validity;
    }

    @Override public String toDebugString() {
        StringBuilder sb = new StringBuilder();
        sb.append(prefixPower);
        if (this instanceof SimpleBinaryUnit) {
            sb.append('b');
        }
        sb.append('/');
        sb.append (baseUnitDefinition != null
                   ? baseUnitDefinition.label()
                   : baseUnitString);
        sb.append('/');
        float e = getExponent();
        if (e == Math.floor(e)) { // no fractional part
            sb.append(Math.round(e)); // print an integer
        } else {
            sb.append(e);
        }
        return sb.toString();
    }

    /**
     * Two units are equal if they have the same power, units and
     * exponent.  They don't have to have been obtained from the same
     * syntax, so that in a syntax which (for example) permitted both
     * "yr" and "a" for years, two SimpleUnit instances, obtained from
     * parsing the two alternatives, would be regarded as equal.
     */
    @Override public boolean equals(Object o) {
        // Implementation note: the practical effect of the above
        // notes is that the 'resolver' field should be ignored for
        // the purposes of the equals and hashCode methods.
        if (o instanceof SimpleUnit) {
            return compareTo((SimpleUnit)o) == 0;
        } else {
            return false;
        }
    }

    @Override public int compareTo(OneUnit o) {
        // order is:
        //   SimpleUnit before FunctionOfUnit
        //   positive exponents before negative
        //   exponents nearer zero before further
        //   known units before unknown ones
        //   then in alphabetical order of the unit string
        int cmp;
        if (o == this) {
            cmp = 0;
        } else if (o instanceof FunctionOfUnit) {
            cmp = -1;           // order SimpleUnit before FunctionOfUnit
        } else {
            assert o instanceof SimpleUnit : String.format("What sort of unit is [%s]?!", o);
            SimpleUnit u = (SimpleUnit)o;
            double e0 = getExponent();
            double e1 = u.getExponent();

            if (e0 == e1) {
                if (baseUnitDefinition != null) {
                    cmp = u.baseUnitDefinition == null ? -1 : baseUnitDefinition.compareTo(u.baseUnitDefinition);
                } else if (u.baseUnitDefinition != null) {
                    cmp = +1;
                } else {
                    assert baseUnitString != null && u.baseUnitString != null;
                    cmp = baseUnitString.compareTo(u.baseUnitString);
                }

                if (cmp == 0) {
                    // still
                    if (prefixPower == u.prefixPower) {
                        cmp = 0;
                    } else {
                        cmp = (prefixPower < u.prefixPower ? -1 : +1);
                    }
                }
            } else {
                if (e0*e1 < 0) {
                    // exponents of opposite signs
                    cmp = (e0 > 0 ? -1 : +1); // positive exponents order first
                } else {
                    cmp = (Math.abs(e0) < Math.abs(e1) ? -1 : +1); // lower |exp| orders first
                }
            }
        }
        return cmp;
    }

    @Override public int hashCode() {
        // We've overridden equals(), so we have to override hashCode,
        // too (as noted in eg Bloch, Effective Java, prescription 8,
        // and using the simple approach recommended there).
        int result = 17;
        result = 37*result + prefixPower;
        if (baseUnitDefinition == null) {
            assert baseUnitString != null;
            result = 37*result + baseUnitString.hashCode();
        } else {
            result = 37*result + baseUnitDefinition.hashCode();
        }
        long de = Double.doubleToLongBits(getExponent());
        result = 37*result + (int)(de ^ (de >>> 32));
        return result;
    }

    interface PrefixSplit {
        public int getPower();
        public boolean isBinaryPrefix();
        public String getUnit();
    }
}
