package uk.me.nxg.unity;

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


/**
 * A dimensions specification is a record of the measurement
 * dimensions of a quantity.
 *
 * <p>The dimensions here match those in the ‘International System of
 * Quantities’ (ISQ), ISO 80000-1:2009.
 *
 * <p>They also match the dimensions in the <a href='http://qudt.org/'>QUDT</a>
 * framework of quantities, units and dimensions, although it is not
 * necessary to be familiar with that framework in order to use the
 * Dimensions class.  The methods here are primarily intended to be
 * used to query the dimensions of an existing object.
 *
 * <p>There is a parser, {@link #parse}, which can create new dimension
 * quantities, should that be necessary.  That parser is rather
 * ad-hoc, however, and the syntax it handles is not really specified
 * here, but it is made visible in case there is some use for it,
 * without at present any commitments that the syntax or the interface is
 * stable.  The constructor is not public: the parse method is
 * the only public way of constructing Dimension objects.
 *
 * <p>The QUDT dimension string is a sequence of letters [ULMTΘINJ]
 * and powers.  These match the ‘quantities’ in ISO 80000-1, with the
 * addition of ‘U’, which the QUDT authors use when describing the
 * Lumen as Candela-steradian, with string {@code U J}, and the Lux, with
 * string {@code U L^-2 J}.  Here, we ignore the U specifiers, and insist
 * that they are never followed by any power.  These ‘U’ specifiers do
 * not appear in any output.
 *
 * <p>The dimensions in the ISQ are
 * <table>
 * <caption>SI base units</caption>
 * <tr><th>Symbol<th>Name<th>SI base unit
 * <tr><td>L<td>Length<td>Metre
 * <tr><td>M<td>Mass<td>Kilogramme
 * <tr><td>T<td>Time<td>Second
 * <tr><td>I<td>Electric current<td>Ampère
 * <tr><td>Θ<td>Thermodynamic temperature<td>Kelvin
 * <tr><td>N<td>Amount of substance<td>Mole
 * <tr><td>J<td>Luminous intensity<td>Candela
 * </table>
 */
public class Dimensions {

    // The number of base quantities
    private static final int ndimensions = 7;

    // the singleton unity object
    private static Dimensions unityObject = null;

    /**
     * The base quantities in the International System of Quantites.
     * See ISO 80000-1:2009.
     */
    public static enum BaseQuantity {
        LENGTH(0),
        MASS(1),
        TIME(2),
        CURRENT(3),
        TEMPERATURE(4),
        AMOUNT(5),
        LUMINOUSINTENSITY(6);
        BaseQuantity(int value) { this.value = value; };
        private final int value;
    };

    // It would be neat to merge this array with the enum above
    // (perhaps BaseQuantity.lookup(int) and BaseQuantity.symbol()?).
    // But it would be no more than neat, and it's not as if the list
    // of quantities is subject to change.
    static final char[] dimensionIndicators = { 'L', 'M', 'T', 'I', 'Θ', 'N', 'J' };

    private final float[] dimensions;

    /**
     * Construct a Dimensions object from an array of dimensions.
     *
     * <p>This has package access, so that only package methods
     * can construct these objects, which means that we don't have to
     * make any (implicit) external commitments about the
     * implementation of these objedts.
     *
     * @param dimensions a a float[ndimensions], which is copied into the {@link #dimensions} instance data
     */
    Dimensions(float[] dimensions) {
        assert dimensions != null && dimensions.length == ndimensions;
        this.dimensions = dimensions;
    }

    /**
     * Returns a new Dimensions object representing 1.
     * @return a new Dimensions object
     */
    public static final Dimensions unity() {
        if (unityObject == null) {
            float unitExponents[] = { 0, 0, 0, 0, 0, 0, 0 };
            unityObject = new Dimensions(unitExponents);
        }
        return unityObject;
    }

    /**
     * Respond with a Dimensions object which is the result of
     * multiplying the dimensions of this object by the dimensions of
     * another.  This will be a new object, if the resulting dimensions are different.
     * @param d the dimensions object we want to multiply by
     * @return a new Dimensions object
     */
    public Dimensions multiply(Dimensions d) {
        if (d == null) {
            throw new IllegalArgumentException("Dimension must not be null");
        }
        float[] multiplication = new float[ndimensions];
        for (int i=0; i<ndimensions; i++) {
            multiplication[i] = dimensions[i] + d.dimensions[i];
        }
        return new Dimensions(multiplication);
    }

    /**
     * Respond with a Dimensions object which is the result of
     * multiplying the dimensions of this object by the dimensions of
     * another raised to the given power.
     * This will be a new object, if the resulting dimensions are different.
     * @param d a Dimensions object which is to multiply this one
     * @param pow the power to which the dimensions argument is raised
     * @return a new Dimensions object
     */
    public Dimensions multiply(Dimensions d, float pow) {
        if (d == null) {
            throw new IllegalArgumentException("Dimension must not be null");
        }
        float[] multiplication = new float[ndimensions];
        for (int i=0; i<ndimensions; i++) {
            multiplication[i] = dimensions[i] + pow * d.dimensions[i];
        }
        return new Dimensions(multiplication);
    }

    /**
     * Multiplies several dimensions together, and returns a (new)
     * object representing the dimensions of the result.
     * If any of the Dimensions in the list are null, the result is
     * null (and not an error)
     * @param dimlist a collection of dimensions to be multiplied together
     * @return a new Dimensions object
     */
    public static Dimensions multiply(java.lang.Iterable<Dimensions> dimlist) {
        float[] multiplication = { 0, 0, 0, 0, 0, 0, 0 };

        for (Dimensions d : dimlist) {
            if (d == null) {
                return null;    // JUMP OUT
            }
            for (int i=0; i<ndimensions; i++) {
                multiplication[i] += d.dimensions[i];
            }
        }

        return new Dimensions(multiplication);
    }

    /**
     * Return a URI naming one of the dimensions in the QUDT
     * dimensions ontology.  We construct this based on the dimensions
     * of the current quantity, using the QUDT system, rather than
     * looking it up; it's therefore possible to construct a dimension
     * which isn't included in that set.
     * @return a String naming a dimension
     */
    public String getURI() {
        StringBuilder sb = new StringBuilder("http://qudt.org/vocab/dimension#Dimension_SI_");
        for (int i=0; i<ndimensions; i++) {
            if (dimensions[i] == 0) {
                // nothing
            } else if (dimensions[i] == 1) {
                sb.append(dimensionIndicators[i]);
            } else {
                //sb.append(dimensionIndicators[i]).append(Float.toString(dimensions[i]));
                if (Math.floor(dimensions[i]) == dimensions[i]) {
                    // the dimension is an integer
                    sb.append(dimensionIndicators[i]).append(String.format("%.0f", dimensions[i]));
                } else if (Math.floor(2*dimensions[i]) == 2*dimensions[i]) {
                    // it's integer+0.5
                    sb.append(dimensionIndicators[i]).append(String.format("%.1f", dimensions[i]));
                } else {
                    // I didn't think we had any of these
                    throw new AssertionError("Dimension power is an 'odd' fraction: "
                                             + dimensionIndicators[i] + '^' + dimensions[i]
                                             + " (funny, I didn't think we had any of those in practice)");
                }
            }
        }
        return sb.toString();
    }

    /**
     * Return a string representation of the dimension.  The format of
     * this string is not specified here, but it is intended to be
     * (roughly) human-readable.  A dimensionless quantity is reported
     * as having units "1".
     * @return a String representation of the dimension
     */
    public String toString() {
        StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (int i=0; i<ndimensions; i++) {
            if (dimensions[i] == 0) {
                // nothing
            } else if (dimensions[i] == 1) {
                if (first) {
                    first = false;
                } else {
                    sb.append(' ');
                }
                sb.append(dimensionIndicators[i]);
            } else {
                if (first) {
                    first = false;
                } else {
                    sb.append(' ');
                }
                if (Math.floor(dimensions[i]) == dimensions[i]) {
                    // the dimension is an integer
                    sb.append(dimensionIndicators[i]).append(String.format("^%.0f", dimensions[i]));
                } else if (Math.floor(2*dimensions[i]) == 2*dimensions[i]) {
                    // it's integer+0.5
                    sb.append(dimensionIndicators[i]).append(String.format("^%.0f/2", 2*dimensions[i]));
                } else {
                    // I didn't think we had any of these
                    throw new AssertionError("Dimension power is an 'odd' fraction: "
                                             + dimensionIndicators[i] + '^' + dimensions[i]
                                             + " (funny, I didn't think we had any of those in practice)");
                }
            }
        }
        return (sb.length() == 0 ? "1" : sb.toString());
    }

    /**
     * Write the array of dimensions in a form that will construct a new object.
     *
     * <p>This is so that other code in this package can construct
     * Dimensions objects, in particular the code generated by the
     * ParseUnits program.
     *
     * <p>This is to avoid parsing the dimensions string when the
     * objects are initialised.  This is partly for speed, but mostly
     * so that if there is a malformed dimensions string, we catch it
     * at this point rather than when the library is used.
     *
     * <p>Note that the generated code uses this class's
     * {@link #Dimensions(float[])} constructor, which has
     * package-only access, so the code generated here is usable only
     * by other code in this package.
     *
     * @return a string in the Java language, which will construct this current object
     */
    String toConstructor() {
        StringBuilder sb = new StringBuilder();
        sb.append("new Dimensions(new float[]{");
        for (int i=0; i<ndimensions; i++) {
            sb.append(String.format("%.1ff,", dimensions[i]));
        }
        sb.append("})");
        return sb.toString();
    }

    /**
     * Return the numerical dimensions of the quantities in the
     * expression.  The result is an array of the exponents of the
     * base dimensions, in the order 'L', 'M', 'T', 'I', 'Θ', 'N', 'J'.
     * @return a list of floats, representing powers of base quantities
     * @deprecated As of v1.1, use {@link #exponent(Dimensions.BaseQuantity)}
     */
    //@Deprecated(since="1.1", forRemoval=true) : this syntax is available from JDK9
    @Deprecated public float[] exponents() {
        return dimensions;
    }

    /**
     * Return the exponent of the given base quantity.
     * For example, a unit "m.s^-1" would produce a Dimensions object
     * for which {@code exponent(BaseQuantity.LENGTH)} would be 1,
     * {@code exponent(BaseQuantity.TIME)} would be -1, and the others zero.
     *
     * @param q the quantity to inspect
     * @return the exponent of the given quantity in the set of dimensions
     */
    public float exponent(BaseQuantity q) {
        return dimensions[q.value];
    }

    /**
     * A pattern which matches either a single letter (including
     * theta) or a number with optional trailing fractional part.
     */
    static Pattern tokenizer = Pattern.compile("([\\p{Alpha}Θ])|(-?\\p{Digit}+(\\.\\p{Digit}+)?)");

    /**
     * Parse a dimension string to produce a new dimension object.  If
     * the string is not parseable, throw an exception.
     *
     * <p>A dimensions string consists of a sequence of capital letter
     * dimensions, and powers, for example "M L2T-2" for the
     * dimensions of "kg m2 s-2".  The dimensions are as discussed in
     * the class overview above.  The syntax here is slightly more
     * general than is required to handle the existing QUDT dimension strings.
     *
     * <p>Since the domain is so restricted, the parsing can be very
     * liberal, and everything which isn't a letter, or part of a
     * number, is ignored (for example '^').  That means, by the way,
     * that a string like 'ML2/T2' wouldn't have the same dimensions
     * as the example above.  If any letters appear which are not one
     * of the known dimension letters, we throw an exception.
     * @param dimensionString a string composed of a sequence of dimension letters and numbers
     * @throws UnitParserException if the string is malformed
     * @return a new Dimensions object
     */
    public static Dimensions parse(String dimensionString)
            throws UnitParserException {

        Matcher m = tokenizer.matcher(dimensionString);
        float dims[] = { 0, 0, 0, 0, 0, 0, 0 };

        int nextIndex = -1;
        while (m.find()) {
            if (m.group(1) == null) {
                String number = m.group(2);
                assert number != null;
                if (nextIndex >= 0) {
                    assert nextIndex < dims.length;
                    if (dims[nextIndex] != 0) {
                        throw new UnitParserException("Malformed dimension string: "
                                                      + dimensionString
                                                      + " (repeated base unit in dimension string)");
                    }
                    dims[nextIndex] = Float.parseFloat(number);
                    nextIndex = -1;
                } else {
                    throw new UnitParserException("Malformed dimension string: " + dimensionString
                                                  + " (number without preceding character)");
                }
                assert nextIndex < 0;
            } else {
                String letter = m.group(1);
                assert letter != null;
                if (nextIndex >= 0) {
                    dims[nextIndex] = 1;
                }
                switch (letter.charAt(0)) {
                  case 'U': /* ignore this */ break;
                  case 'L': nextIndex = 0; break;
                  case 'M': nextIndex = 1; break;
                  case 'T': nextIndex = 2; break;
                  case 'I': nextIndex = 3; break;
                  case 'Θ': nextIndex = 4; break;
                  case 'N': nextIndex = 5; break;
                  case 'J': nextIndex = 6; break;
                  default:
                    throw new UnitParserException("Unexpected character in dimension string: " + dimensionString);
                }
                //assert nextIndex >= 0;
            }
        }
        if (nextIndex >= 0) {
            dims[nextIndex] = 1;
        }

        return new Dimensions(dims);
    }

    // This is mostly a test function:
    //    java uk.me.nxg.unity.Dimensions L1 L1T-2 ...
    // public static void main(String[] args) {
    //     java.util.List<Dimensions> dimlist = new java.util.ArrayList<Dimensions>();
    //     try {

    //         for (String arg : args) {
    //             Dimensions d = parse(arg);
    //             System.out.println("URI=" + d.getURI() + ", or " + d.toString());
    //             dimlist.add(d);
    //         }
    //         if (args.length > 1) {
    //             System.out.println("First two: " + dimlist.get(0).multiply(dimlist.get(1)).getURI());
    //         }
    //         if (args.length > 2) {
    //             System.out.println("All: " + Dimensions.multiply(dimlist).getURI());
    //         }
    //     } catch (Exception e) {
    //         System.err.println("Exception: " + e);
    //     }
    // }
}
