/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.sis.measure;

import javax.measure.Unit;
import javax.measure.Quantity;
import javax.measure.UnitConverter;
import javax.measure.quantity.Time;
import javax.measure.quantity.Angle;
import javax.measure.quantity.Length;
import javax.measure.format.ParserException;
import org.apache.sis.util.Static;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.resources.Errors;


/**
 * Provides static methods working on {@link Quantity} instances.
 * Apache SIS implementation of quantities has the following characteristics:
 *
 * <ul>
 *   <li>Values are stored with {@code double} precision.</li>
 *   <li>All quantities implement the specific subtype (e.g. {@link Length} instead of {@code Quantity<Length>}).</li>
 *   <li>Quantities are immutable, {@link Comparable} and {@link java.io.Serializable}.</li>
 * </ul>
 *
 * @author  Martin Desruisseaux (Geomatys)
 * @version 1.0
 * @since   0.8
 * @module
 */
public final class Quantities extends Static {
    /**
     * Do not allow instantiation of this class.
     */
    private Quantities() {
    }

    /**
     * Creates a quantity for the given value and unit of measurement symbol.
     * This is a convenience method that combines a call to {@link Units#valueOf(String)}
     * with {@link #create(double, Unit)}.
     *
     * @param  value  the quantity magnitude.
     * @param  unit   symbol of the unit of measurement associated to the given value.
     * @return a quantity of the given type for the given value and unit of measurement.
     * @throws ParserException if the given symbol can not be parsed.
     */
    public static Quantity<?> create(final double value, final String unit) {
        return create(value, Units.valueOf(unit));
    }

    /**
     * Creates a quantity for the given value and unit of measurement.
     *
     * @param  <Q>    the quantity type (e.g. {@link Length}, {@link Angle}, {@link Time}, <i>etc.</i>).
     * @param  value  the quantity magnitude.
     * @param  unit   the unit of measurement associated to the given value.
     * @return a quantity of the given type for the given value and unit of measurement.
     * @throws IllegalArgumentException if the given unit class is not a supported implementation.
     *
     * @see UnitServices#getQuantityFactory(Class)
     */
    public static <Q extends Quantity<Q>> Q create(final double value, final Unit<Q> unit) {
        ArgumentChecks.ensureNonNull("unit", unit);
        final Unit<Q> system = unit.getSystemUnit();
        if (system instanceof SystemUnit<?>) {
            final UnitConverter c = unit.getConverterTo(system);
            final ScalarFactory<Q> factory = ((SystemUnit<Q>) system).factory;
            if (c.isLinear()) {
                /*
                 * We require arithmetic operations (A + B, A * 2, etc.) to be performed as if all values were
                 * converted to system unit before calculation. This is mandatory for preserving arithmetic laws
                 * like associativity, commutativity, etc.  But in the special case were the unit of measurement
                 * if related to the system unit with only a scale factor (no offset), we get equivalent results
                 * even if we skip the conversion to system unit.  Since the vast majority of units fall in this
                 * category, it is worth to do this optimization.
                 *
                 * (Note: despite its name, above 'isLinear()' method actually has an 'isScale()' behavior).
                 */
                if (factory != null) {
                    return factory.create(value, unit);
                } else {
                    return ScalarFallback.factory(value, unit, ((SystemUnit<Q>) system).quantity);
                }
            }
            /*
             * If the given unit of measurement is derived from the system unit by a more complex formula
             * than a scale factor, then we need to perform arithmetic operations using the full path
             * (convert all values to system unit before calculation).
             */
            if (factory != null) {
                final Q quantity = factory.createDerived(value, unit, system, c);
                if (quantity != null) {
                    return quantity;
                }
            }
            return DerivedScalar.Fallback.factory(value, unit, system, c, ((SystemUnit<Q>) system).quantity);
        } else {
            throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedImplementation_1, unit.getClass()));
        }
    }

    /**
     * Returns the given quantity as an instance of the specific {@code Quantity} subtype.
     * For example this method can be used for converting a {@code Quantity<Length>} to a {@link Length}.
     * If the given quantity already implements the specific interface, then it is returned as-is.
     *
     * @param  <Q>      the quantity type (e.g. {@link Length}, {@link Angle}, {@link Time}, <i>etc.</i>), or {@code null}.
     * @param  quantity the quantity to convert to the specific subtype.
     * @return the given quantity as a specific subtype (may be {@code quantity} itself), or {@code null} if the given quantity was null.
     * @throws IllegalArgumentException if the unit class associated to the given quantity is not a supported implementation.
     */
    @SuppressWarnings("unchecked")
    public static <Q extends Quantity<Q>> Q castOrCopy(final Quantity<Q> quantity) {
        if (quantity != null) {
            final Unit<Q> unit   = quantity.getUnit();
            final Unit<Q> system = unit.getSystemUnit();
            if (!(system instanceof SystemUnit<?>)) {
                throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedImplementation_1, unit.getClass()));
            }
            final Class<Q> type = ((SystemUnit<Q>) system).quantity;
            if (!type.isInstance(quantity)) {
                final ScalarFactory<Q> factory = ((SystemUnit<Q>) system).factory;
                final double value = AbstractConverter.doubleValue(quantity.getValue());
                if (factory != null) {
                    return factory.create(value, unit);
                } else {
                    return ScalarFallback.factory(value, unit, type);
                }
            }
        }
        return (Q) quantity;
    }
}
