/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2004-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 *
 *    This package contains documentation from OpenGIS specifications.
 *    OpenGIS consortium's work is fully acknowledged here.
 */
package org.geotools.parameter;

import javax.measure.unit.Unit;

import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.opengis.parameter.InvalidParameterTypeException;
import org.opengis.parameter.InvalidParameterValueException;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterValue;


/**
 * A parameter value as a floating point (double precision) number.
 * This class provides the same functionalities than {@link Parameter}, except that:
 * <ul>
 *   <li>Values are always floating point numbers of type {@code double}.</li>
 *   <li>Units are the same than the {@linkplain ParameterDescriptor#getUnit default units}.</li>
 * </ul>
 * When those conditions are meet, {@code ParameterRealValue} is slightly more efficient
 * than {@code ParameterValue} since it avoid the creation of {@link Double} objects.
 *
 * @since 2.1
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/library/referencing/src/main/java/org/geotools/parameter/FloatParameter.java $
 * @version $Id: FloatParameter.java 37299 2011-05-25 05:21:24Z mbedward $
 * @author Martin Desruisseaux (IRD)
 *
 * @see DefaultParameterDescriptor
 * @see ParameterGroup
 */
public class FloatParameter extends AbstractParameter implements ParameterValue<Double> {
    /**
     * Serial number for interoperability with different versions.
     */
    private static final long serialVersionUID = 9027797654033417816L;

    /**
     * The value.
     */
    private double value;

    /**
     * Constructs a parameter from the specified descriptor. The descriptor
     * {@linkplain ParameterDescriptor#getValueClass() value class}
     * must be <code>{@linkplain Double}.class</code>.
     *
     * @param  descriptor The abstract definition of this parameter.
     * @throws IllegalArgumentException if the value class is not {@code Double.class}.
     */
    public FloatParameter(final ParameterDescriptor<Double> descriptor) {
        super(descriptor);
        final Class type = descriptor.getValueClass();
        final Class expected = Double.class;
        if (!expected.equals(type) && !Double.TYPE.equals(type)) {
            throw new IllegalArgumentException(Errors.format(
                    ErrorKeys.ILLEGAL_CLASS_$2, type, expected));
        }
        final Number value = descriptor.getDefaultValue();
        this.value = (value!=null) ? value.doubleValue() : Double.NaN;
    }

    /**
     * Constructs a parameter from the specified descriptor and value. This convenience
     * constructor is equivalents to the one-argument constructor followed by a call to
     * {@link #setValue(double)}.
     *
     * @param  descriptor The abstract definition of this parameter.
     * @param  value The parameter value.
     * @throws IllegalArgumentException if the value class is not {@code Double.class}.
     */
    public FloatParameter(final ParameterDescriptor<Double> descriptor, final double value) {
        this(descriptor);
        setValue(value);
    }

    /**
     * Returns the abstract definition of this parameter.
     */
    @Override
    @SuppressWarnings("unchecked") // Type should has been checked by the constructor.
    public ParameterDescriptor<Double> getDescriptor() {
        return (ParameterDescriptor) super.getDescriptor();
    }

    /**
     * Returns the unit of measure of the {@linkplain #doubleValue() parameter value}. The default
     * implementation always delegates to {@link ParameterDescriptor#getUnit}.
     *
     * @return The unit of measure, or {@code null} if none.
     */
    public Unit<?> getUnit() {
        return ((ParameterDescriptor) descriptor).getUnit();
    }

    /**
     * Returns the numeric value of the coordinate operation parameter in the specified unit
     * of measure. This convenience method apply unit conversion on the fly as needed.
     *
     * @param  unit The unit of measure for the value to be returned.
     * @return The numeric value represented by this parameter after conversion to type
     *         {@code double} and conversion to {@code unit}.
     * @throws IllegalArgumentException if the specified unit is invalid for this parameter.
     */
    public double doubleValue(final Unit<?> unit) throws IllegalArgumentException {
        ensureNonNull("unit", unit);
        final Unit<?> thisUnit = getUnit();
        if (thisUnit == null) {
            throw unitlessParameter(descriptor);
        }
        final int expectedID = Parameter.getUnitMessageID(thisUnit);
        if (Parameter.getUnitMessageID(unit) != expectedID) {
            throw new IllegalArgumentException(Errors.format(expectedID, unit));
        }
        return thisUnit.getConverterTo(unit).convert(value);
    }

    /**
     * Returns the numeric value of the coordinate operation parameter with its
     * associated {@linkplain #getUnit unit of measure}.
     *
     * @return The numeric value represented by this parameter after conversion to type {@code double}.
     */
    public double doubleValue() {
        return value;
    }

    /**
     * Returns the numeric value rounded to the nearest integer.
     *
     * @return The numeric value represented by this parameter after conversion to type {@code int}.
     */
    public int intValue() {
        return (int) Math.round(value);
    }

    /**
     * Format an error message for illegal method call for the current value type.
     */
    private static String getClassTypeError() {
        return Errors.format(ErrorKeys.ILLEGAL_OPERATION_FOR_VALUE_CLASS_$1, Double.class);
    }

    /**
     * Returns the parameter value as {{@link Double},
     *
     * @return The parameter value as an object.
     */
    public Double getValue() {
        return Double.valueOf(value);
    }

    /**
     * Set the parameter value as a floating point and its associated unit.
     *
     * @param  value The parameter value.
     * @param  unit The unit for the specified value.
     * @throws InvalidParameterValueException if the value is illegal for some reason
     *         (for example a value out of range).
     */
    public void setValue(double value, final Unit<?> unit) throws InvalidParameterValueException {
        ensureNonNull("unit", unit);
        @SuppressWarnings("unchecked") // Checked by constructor.
        final ParameterDescriptor<Double> descriptor = (ParameterDescriptor) this.descriptor;
        final Unit<?> thisUnit = descriptor.getUnit();
        if (thisUnit == null) {
            throw unitlessParameter(descriptor);
        }
        final int expectedID = Parameter.getUnitMessageID(thisUnit);
        if (Parameter.getUnitMessageID(unit) != expectedID) {
            throw new IllegalArgumentException(Errors.format(expectedID, unit));
        }
        value = unit.getConverterTo(thisUnit).convert(value);
        this.value = Parameter.ensureValidValue(descriptor, Double.valueOf(value));
    }

    /**
     * Set the parameter value as a floating point.
     *
     * @param value The parameter value.
     * @throws InvalidParameterValueException if the value is illegal for some reason
     *         (for example a value out of range).
     */
    public void setValue(final double value) throws InvalidParameterValueException {
        @SuppressWarnings("unchecked") // Checked by constructor.
        final ParameterDescriptor<Double> descriptor = (ParameterDescriptor) this.descriptor;
        this.value = Parameter.ensureValidValue(descriptor, Double.valueOf(value));
    }

    /**
     * Set the parameter value as an integer.
     *
     * @param  value The parameter value.
     * @throws InvalidParameterValueException if the value is illegal for some reason
     *         (for example a value out of range).
     */
    public void setValue(final int value) throws InvalidParameterValueException {
        setValue((double) value);
    }

    /**
     * Set the parameter value as a {@link Double} object.
     *
     * @param  value The parameter value.
     * @throws InvalidParameterValueException if the type of {@code value} is inappropriate
     *         for this parameter, or if the value is illegal for some other reason (for example
     *         the value is numeric and out of range).
     */
    public void setValue(final Object value) throws InvalidParameterValueException {
        @SuppressWarnings("unchecked") // Checked by constructor.
        final ParameterDescriptor<Double> descriptor = (ParameterDescriptor) this.descriptor;
        this.value = Parameter.ensureValidValue(descriptor, value);
    }

    /**
     * Always throws an exception, since this parameter is not an array.
     */
    public void setValue(double[] values, final Unit<?> unit)
            throws InvalidParameterValueException
    {
        throw new InvalidParameterTypeException(getClassTypeError(),
                  Parameter.getName(descriptor));
    }

    /**
     * Compares the specified object with this parameter for equality.
     *
     * @param  object The object to compare to {@code this}.
     * @return {@code true} if both objects are equal.
     */
    @Override
    public boolean equals(final Object object) {
        if (super.equals(object)) {
            final FloatParameter that = (FloatParameter) object;
            return Double.doubleToLongBits(this.value) ==
                   Double.doubleToLongBits(that.value);
        }
        return false;
    }

    /**
     * Returns a hash value for this parameter.
     *
     * @return The hash code value. This value doesn't need to be the same
     *         in past or future versions of this class.
     */
    @Override
    public int hashCode() {
        final long code = Double.doubleToLongBits(value);
        return (int)code ^ (int)(code >>> 32) + super.hashCode()*37;
    }

    /**
     * Returns a clone of this parameter.
     */
    @Override
    public FloatParameter clone() {
        return (FloatParameter) super.clone();
    }
}
