package uk.me.nxg.unity;

/**
 * Describes a unit.
 *
 * <p>A ‘unit’is a notion like ‘metre’, or ‘pixel’, and is not
 * dependent on a particular syntax.  The information here includes a
 * readable name for a quantity (such as ‘Metre’ or ‘Julian year’), a
 * URI uniquely naming it, and its dimensions.
 *
 * <p>The URI unique name is derived from the <a href='http://qudt.org/'>QUDT</a>
 * framework of quantities, units and dimensions, though it is not
 * restricted to the set of units and quantities defined there.
 *
 * <p>The syntax-specific aspects of describing units concern how the
 * unit is abbreviated, and indeed whether it is permitted or
 * recommended in a particular syntax, and this is described by the
 * class {@link UnitRepresentation}.
 */
public class UnitDefinition
        implements java.io.Serializable, Comparable<UnitDefinition> {
    private final String uri; // the URI name of the unit, such as http://qudt.org/vocab/unit#Meter
    private final String name; // the name of the unit, such as 'Metre' -- not the abbreviation 'm'
    private final String label; // a unique label for the unit; typically == name, but without spaces
    private final String type; // type such as 'length'
    private final Dimensions dimensions;
    private final String description;   // for example 'Hz = s-1'
    private final String latexForm;   // special LaTeX form, if different from abbrev

    // The following is unlikely to change, but its presence keeps the compiler happy
    private static final long serialVersionUID = 42L;

    /**
     * Construct a UnitDefinition object.
     * <p>The code calling this will typically be generated by the buildtools ParseUnits program.
     *
     * @param uri		the URI identifying this unit (eg {@code http://qudt.org/...})
     * @param name		the name of the unit, such as 'Metre' -- not the abbreviation 'm'
     * @param label		a unique label for the unit; typically == name, but without spaces
     * @param type		such as 'length'
     * @param dimensions	the M/L/T/... dimensions of the object
     * @param description	a textual description of the object, or null
     * @param latexForm		a LaTeX version of the unit, or null if the default is acceptable
     */
    UnitDefinition(String uri,
                   String name,
                   String label,
                   String type,
                   Dimensions dimensions,
                   String description,
                   String latexForm) {
        if (uri == null || name == null || type == null) {
            throw new IllegalArgumentException("UnitDefinition must have a non-null uri, name and type");
        }
        this.uri = uri;
        this.name = name;
        this.label = label;
        this.type = type;
        this.dimensions = dimensions;
        this.description = (description == null || description.length() == 0 ? null : description);
        this.latexForm = (latexForm == null || latexForm.length()==0) ? null : latexForm;

        // class invariant
        assert this.uri != null && this.name != null && this.type != null
                && (this.description == null || this.description.length() > 0)
                && (this.latexForm == null || this.latexForm.length() > 0);
    }

    /**
     * Construct a UnitDefinition object.
     * <p>The code calling this will typically be generated by the buildtools ParseUnits program.
     *
     * @param uri		the URI identifying this unit (eg {@code http://qudt.org/...})
     * @param name		the name of the unit, such as 'Metre' -- not the abbreviation 'm'
     * @param label		a unique label for the unit; typically == name, but without spaces
     * @param type		such as 'length'
     * @param dimensionString	the M/L/T/... dimensions of the object, as a string
     * @param description	a textual description of the object, or null
     * @param latexForm		a LaTeX version of the unit, or null if the default is acceptable
     * @throws UnitParserException if the {@code dimensionString} cannot be parsed
     * @see Dimensions#parse
     */
    UnitDefinition(String uri,
                   String name,
                   String label,
                   String type,
                   String dimensionString,
                   String description,
                   String latexForm)
            throws UnitParserException {
        this(uri, name, label, type,
             Dimensions.parse(dimensionString),
             description, latexForm);
    }

    // accessors
    /**
     * The (human-readable) name of this unit, for example ‘Metre’ or ‘Second’
     * @return a string unit name (not null)
     */
    public String name()  {
        return name;
    }
    /**
     * A label for this unit, for example ‘Meter’.
     * It is (currently) formed from the URI of the unit, but this
     * version of the library doesn't commit to the label being completely stable.
     *
     * This label will typically  be the same as the name, but will not, for
     * example, have spaces in it, and will not necessarily be spelled
     * the same way.  For example the URI for the metre has fragment
     * `#Meter`, and that for the second `#SecondTime`.
     *
     * @return a string denoting the unit, without spaces and expected to be unique (not null)
     */
    public String label() {
        return label;
    }
    /**
     * A label for the type of this unit, as a QUDT (or similar) ‘quantity’ URI.
     * For example the metre and the light year are both
     * ‘http://qudt.org/vocab/quantity#Length’
     * @return a string unit description (not null)
     */
    public String type()  {
        return type;
    }
    /**
     * Further remarks about this unit, or other comments
     * @return comments about the unit, or null if there is nothing more to say
     */
    public String description() {
        return description;
    }
    /**
     * A LaTeX version of the unit symbol, if there is one defined
     * @return a string representation of the unit in LaTeX form, or null if there is no special form
     */
    public String latexForm() {
        return latexForm;
    }
    /**
     * The dimensions of this unit
     * For example, the joule has dimensions ‘L^2 M T^-2’.
     * @return a Dimensions object representing the unit dimensions
     */
    public Dimensions dimensions() {
        return dimensions;
    }
    /**
     * The Kind of this unit, named by a URI
     * @return a string representing the unit's URI
     */
    public String getURI() {
        return uri;
    }

    /**
     * Return the syntax-specific information about this unit.
     * Returns null if the syntax is unknown for this unit, meaning
     * that the given syntax does not recognise this unit as a
     * recommended one.
     *
     * @param syntax a non-null string name for the syntax, which should be one
     * of the syntaxes of {@link Syntax}
     * @return the syntax details for this unit in this syntax, or null if the syntax is unknown
     */
    public UnitRepresentation getRepresentation(Syntax syntax) {
        if (syntax == null) {
            throw new IllegalArgumentException("Must specify a syntax for getRepresentation");
        }
        UnitDefinitionMap.Resolver r = UnitDefinitionMap.getInstance().getResolver(syntax);
        if (r == null) {
            return null;
        } else {
            return r.lookupUnit(this);
        }
    }

    /**
     * Return a representation of this unit, from any syntax that
     * knows of one.  Since every known unit appears in at least one
     * map, this will never return null.
     * @return a representation of this unit
     */
    public UnitRepresentation getRepresentation() {
        for (Syntax s : Syntax.values()) {
            UnitRepresentation r = getRepresentation(s);
            if (r != null) {
                return r;
            }
        }
        // ooops!
        assert false;
        return null;            // redundant
    }

    @Override public int compareTo(UnitDefinition o) {
        assert uri != null;
        assert o.uri != null;
        return uri.compareTo(o.uri);
    }

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

    // We override equals, so are obliged (by a warning) to override hashCode, trivially
    @Override public int hashCode() {
        return super.hashCode();
    }

    /**
     * Produces a representation of this unit as a string, combining
     * the name of the unit and a URI referring to it unambiguously.  This is
     * occasionally useful, but not for formatting expressions into
     * unit strings: for that, use {@link UnitExpr#toString}.
     * If you wish to get a human-readable representation of the unit,
     * then use {@link #getRepresentation}.
     *
     * @return a string representation of the unit
     */
    public String toString() {
        return name + "(" + type + ")";
    }
}
