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>This is derived from 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, but there is a
 * parser to create new dimension quantities, should that be necessary.
 *
 * <p>The known dimensions are
 * <table>
 * <tr><th>Symbol<th>Name<th>SI base unit
 * <tr><td>U<td>Dimensionless<td>Unity
 * <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 {

    static final char[] dimensionIndicators = { 'U', 'L', 'M', 'T', 'I', 'Θ', 'N', 'J' };

    private final float[] dimensions;
    
    private Dimensions(float[] dimensions) {
        assert dimensions != null && dimensions.length == 8;
        this.dimensions = dimensions;
    }

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

    /**
     * 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.
     */
    public Dimensions multiply(Dimensions d) {
        if (d == null) {
            throw new IllegalArgumentException("Dimension must not be null");
        }
        float[] multiplication = new float[8];
        for (int i=0; i<8; 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.
     */
    public Dimensions multiply(Dimensions d, float pow) {
        if (d == null) {
            throw new IllegalArgumentException("Dimension must not be null");
        }
        float[] multiplication = new float[8];
        for (int i=0; i<8; 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)
     */
    public static Dimensions multiply(java.lang.Iterable<Dimensions> dimlist) {
        float[] multiplication = { 0, 0, 0, 0, 0, 0, 0, 0 };
        
        for (Dimensions d : dimlist) {
            if (d == null) {
                return null;    // JUMP OUT
            }
            for (int i=0; i<8; 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.
     */
    public String getURI() {
        StringBuilder sb = new StringBuilder("http://qudt.org/vocab/dimension#Dimension_SI_");
        for (int i=0; i<8; 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.
     */
    public String toString() {
        StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (int i=0; i<8; 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.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 'U', 'L', 'M', 'T', 'I', 'Θ', 'N', 'J'.
     */
    public float[] exponents() {
        return dimensions;
    }

    /**
     * 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.
     *
     * <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
     */
    public static Dimensions parse(String dimensionString) 
            throws UnitParserException {
        Matcher m = tokenizer.matcher(dimensionString);
        float dims[] = { 0, 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;
                    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': nextIndex = 0; break;
                  case 'L': nextIndex = 1; break;
                  case 'M': nextIndex = 2; break;
                  case 'T': nextIndex = 3; break;
                  case 'I': nextIndex = 4; break;
                  case 'Θ': nextIndex = 5; break;
                  case 'N': nextIndex = 6; break;
                  case 'J': nextIndex = 7; 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);
    //     }
    // }
}

        
