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.
 *
 * <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.
     * Precisely one of baseUnitString and baseUnitDefinition is non-null.
     */
    private final UnitDefinition baseUnitDefinition;

    /**
     * The base unit as a string.  If this is non-null, it means that
     * the unit string wasn't recognised as a known-unit.
     */
    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
     * @param prefix the multiplier prefix
     * @param us the string form of the new unit
     * @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);
        prefixPower = prefix;
        if (us == null) {
            baseUnitString = null;
            baseUnitDefinition = ud;
        } else {
            baseUnitString = us;
            baseUnitDefinition = null;
        }

        // class invariants
        assert    (baseUnitString == null && baseUnitDefinition != null)
               || (baseUnitString != null && baseUnitDefinition == null);
        this.unitResolver = r;
        assert 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
     */
    static SimpleUnit makeSimpleUnit(UnitDefinitionMap.Resolver r,
                                     String unitString,
                                     float e) {
        // 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 which lookupUnit
        // returns null for).  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).
        UnitDefinition def = r.lookupSymbol(unitString);

        SimpleUnit rval;
        
        if (def != null) {
            rval = new SimpleDecimalUnit(r, 0, null, def, e, false);
        } else {
            PrefixSplit ps = SimpleBinaryUnit.splitUnitString(unitString);
            if (ps == null) {
                // no binary unit recognised
                rval = null;
            } else {                
                // 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 (def == null) {
                    // unknown unit, so no binary prefix should be recognised
                    rval = null;
                } else {
                    UnitRepresentation rep = def.getRepresentation(r.getSyntax());
                    if (rep != null && rep.mayHaveBinaryPrefixes()) {
                        // SimpleBinaryUnit constructor doesn't have a
                        // is_quoted? argument
                        rval = new SimpleBinaryUnit(r, ps.getPower(), null, def, e);
                    } else {
                        rval = null;
                    }
                }
            }

            if (rval == null) {
                ps = SimpleDecimalUnit.splitUnitString(unitString);
                if (ps != null) {
                    String us = ps.getUnit();
                    def = r.lookupSymbol(us);
                    if (def == null) {
                        rval = new SimpleDecimalUnit(r, ps.getPower(), us, null, e, false);
                    } else {
                        rval = new SimpleDecimalUnit(r, ps.getPower(), null, def, e, false);
                    }
                } else {
                    rval = new SimpleDecimalUnit(r, 0, unitString, null, e, false);
                }
            }
        }

        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
     */
    static SimpleUnit makeQuotedSimpleUnit(UnitDefinitionMap.Resolver r,
                                           String prefix,
                                           String unitString,
                                           float e) 
            throws UnitParserException {
        // very similar to makeSimpleUnit
        SimpleUnit rval;
        
        if (prefix == null) {
            rval = new SimpleDecimalUnit(r, 0, unitString, null, e, true);
        } 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);
            } 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.
     */
    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.
     */
    abstract String prefixPowerToStringLaTeX();

    @Override public String toString() {
        return toString(null);
    }
    
    @Override public String toString(Syntax syntax) {
        StringBuilder sb = new StringBuilder();
        try {
            sb.append(unitString(syntax));
        } catch (UnitParserException e1) {
            try {
                sb.append(unitString(Syntax.FITS));
            } catch (Exception e2) {
                // this should be impossible
                throw new AssertionError("SimpleUnit.toString with syntax FITS really should work: " + e2);
            }
        }
        if (getExponent() != 1) {
            sb.append('^').append(getExponent());
        }
        return sb.toString();
    }

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

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

    @Override public String getBaseUnitName() {
        assert baseUnitString != null || baseUnitDefinition != 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();
        }
    }
    
    /**
     * 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
     * @throws UnitParserException if the syntax is unrecognised
     */
    private String unitStringText(Syntax syntax)
            throws UnitParserException {
        StringBuilder sb = new StringBuilder();
        if (prefixPower != 0) {
            sb.append(prefixPowerToString());
        }
        if (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 UnitParserException("Unknown syntax: " + syntax);
                }
            }
            assert baseUnitDefinition != null;
            UnitRepresentation rep = r.lookupUnit(baseUnitDefinition);
            if (rep == null) {
                // this UnitDefinition is not recognised in this syntax
                rep = unitResolver.lookupUnit(baseUnitDefinition);
                // this resolver is where we originally got the
                // baseUnitDefinition from, so this must succeed
            }
            assert rep != null;
            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
     * @throws UnitParserException if the syntax is unrecognised
     */
    private String unitStringLaTeX()
            throws UnitParserException {
        StringBuilder sb = new StringBuilder();
        if (prefixPower != 0) {
            sb.append(prefixPowerToStringLaTeX()).append(' ');
        }
        if (baseUnitString != null) {
            sb.append(baseUnitString);
        } else {
            assert baseUnitDefinition != null; // from class invariant

            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;

                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.
     */
    public String unitString() {
        try {
            return unitString(null);
        } catch (UnitParserException e) {
            throw new AssertionError("Odd: SimpleUnit.unitString() should not have failed");
        }
    }
    
    @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.
        return baseUnitDefinition != null
                && baseUnitDefinition.getRepresentation(syntax) != 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.
     * @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.
        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('/');
        if (baseUnitString == null) {
            // first try this with a given syntax, for the sake of
            // consistency, but fall back to an any-syntax option
            // without qualms
            UnitRepresentation r = baseUnitDefinition.getRepresentation(Syntax.FITS);
            if (r == null) {
                // fallback
                r = baseUnitDefinition.getRepresentation();
            }
            sb.append(r.getAbbreviation());
        } else {
            sb.append(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;
            SimpleUnit u = (SimpleUnit)o;
            double e0 = getExponent();
            double e1 = u.getExponent();

            if (e0 == e1) {
                if (baseUnitDefinition != null && u.baseUnitDefinition != null) {
                    cmp = baseUnitDefinition.compareTo(u.baseUnitDefinition);
                } else if (baseUnitString != null && u.baseUnitString != null) {
                    cmp = baseUnitString.compareTo(u.baseUnitString);
                } else {
                    cmp = (baseUnitDefinition != null ? -1 : +1);
                }
                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 {
            assert baseUnitString == null;
            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();
    }
}
