/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2004-2006, GeoTools Project Managment Committee (PMC)
 *   
 *    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.
 */
package org.geotools.parameter;

// J2SE dependencies and extensions
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import javax.units.Unit;

// OpenGIS dependencies
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.InvalidParameterTypeException;

// Geotools dependencies
import org.geotools.referencing.AbstractIdentifiedObject;
import org.geotools.util.logging.Logging;


/**
 * Utility class for methods helping implementing, and working with the
 * parameter API from {@link org.opengis.parameter} package.
 * <p>
 * <h3>Design note</h3>
 * This class contains some methods working on a specific parameter in a group (e.g.
 * {@linkplain #search searching}, {@linkplain #ensureSet setting a value}, <cite>etc.</cite>).
 * Parameters are identified by their {@linkplain ParameterDescriptor#getName name} instead of
 * their full {@linkplain ParameterDescriptor descriptor} object, because:
 * <ul>
 *   <li>The parameter descriptor may not be always available. For example a user may looks for
 *       the {@code "semi_major"} axis length (because it is documented in OGC specification under
 *       that name) but doesn't know and doesn't care about who is providing the implementation. In
 *       such case, he doesn't have the parameter's descriptor. He only have the parameter's name,
 *       and creating a descriptor from that name (a descriptor independent of any implementation)
 *       is tedious.</li>.
 *   <li>Parameter descriptors are implementation-dependent. For example if a user searchs for
 *       the above-cited {@code "semi_major"} axis length using the {@linkplain
 *       org.geotools.referencing.operation.projection.MapProjection.AbstractProvider#SEMI_MAJOR
 *       Geotools's descriptor} for this parameter, we will fail to find this parameter in any
 *       alternative {@link ParameterValueGroup} implementations. This is against GeoAPI's
 *       inter-operability goal.</li>
 * </ul>
 * <p>
 * The above doesn't mean that parameter's descriptor should not be used. They are used for
 * inspecting meta-data about parameters, not as a key for searching parameters in a group.
 * Since each parameter's name should be unique in a given parameter group (because
 * {@linkplain ParameterDescriptor#getMaximumOccurs maximum occurs} is always 1 for single
 * parameter), the parameter name is a suffisient key.
 *
 * @since 2.1
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/parameter/Parameters.java $
 * @version $Id: Parameters.java 27862 2007-11-12 19:51:19Z desruisseaux $
 * @author Jody Garnett (Refractions Research)
 * @author Martin Desruisseaux
 */
public class Parameters {
    /**
     * Small number for floating point comparaisons.
     */
    private static final double EPS = 1E-8;

    /**
     * An empty parameter group. This group contains no parameters.
     */
    public static ParameterDescriptorGroup EMPTY_GROUP =
            new DefaultParameterDescriptorGroup("empty", // TODO: localize
            new GeneralParameterDescriptor[0]);

    /**
     * Checks a parameter value against its {@linkplain ParameterDescriptor parameter descriptor}.
     * This method takes care of handling checking arrays and collections against parameter
     * descriptor.
     * <br><br>
     * When the {@linkplain ParameterDescriptor#getValueClass value class} is an array (like
     * {@code double[].class}) or a {@linkplain Collection collection} (like {@code List.class}),
     * the descriptor
     * {@linkplain ParameterDescriptor#getMinimumValue minimum value},
     * {@linkplain ParameterDescriptor#getMaximumValue maximum value} and
     * {@linkplain ParameterDescriptor#getValidValues valid values}
     * will be used to check the elements.
     *
     * @param parameter The parameter to test.
     * @return true if parameter is valid.
     *
     * @see Parameter#ensureValidValue
     */
    public static boolean isValid(final ParameterValue parameter) {
        final ParameterDescriptor descriptor = (ParameterDescriptor) parameter.getDescriptor();
        final Object value = parameter.getValue();
        final Class  type  = (value == null) ? Void.TYPE : value.getClass();                
        final Class  kind  = descriptor.getValueClass();

        if (kind.isInstance(value)) {
            return false; // value not of the correct type
        }
        if (type.isArray()) {
            // handle checking elements in an aray
            final int length = Array.getLength(value);
            for (int i=0; i<length; i++) {
                if (!isValidValue(Array.get(value, i), descriptor)) {
                    return false;
                }
            }
        } else if (value instanceof Collection) {
            // handle checking elements in a collection
            final Collection collection = (Collection) value;
            for (final Iterator i=collection.iterator(); i.hasNext();) {
                if (!isValidValue(i.next(), descriptor)) {
                    return false;
                }
            }
        } else {
            isValidValue(value, descriptor);
        }
        return true;
    }

    /**
     * Called on a single {@linkplain ParameterValue parameter value}, or on elements of a
     * parameter value. This method ensures that
     * {@linkplain ParameterDescriptor#getMinimumValue minimum value},
     * {@linkplain ParameterDescriptor#getMaximumValue maximum value} and
     * {@linkplain ParameterDescriptor#getValidValues valid values}
     * all think the provided value is okay.
     *
     * @param  value The value to test.
     * @param  descriptor The descriptor for the value.
     * @return true if parameter is valid.
     *
     * @see Parameter#ensureValidValue
     */
    private static boolean isValidValue(final Object value, final ParameterDescriptor descriptor) {
        final Class  type = (value == null) ? Void.TYPE : value.getClass();                
        final Class  expected = descriptor.getValueClass();
        final Set validValues = descriptor.getValidValues();
        if (validValues!=null && !validValues.contains(value)) {
            return false;
        }
        final Comparable min = descriptor.getMinimumValue();
        if (min!=null && min.compareTo(value) > 0) {
            return false;
        }
        final Comparable max = descriptor.getMaximumValue();
        if (max!=null && max.compareTo(value) < 0) {
            return false;
        }
        return true;
    }

    /**
     * Searchs all parameters with the specified name. The given {@code name} is compared against
     * parameter {@link GeneralParameterDescriptor#getName name} and
     * {@link GeneralParameterDescriptor#getAlias alias}. This method search recursively
     * in subgroups up to the specified depth:
     * <p>
     * <ul>
     *   <li>If {@code maxDepth} is equals to 0, then this method returns {@code param}
     *       if and only if it matches the specified name.</li>
     *   <li>If {@code maxDepth} is equals to 1 and {@code param} is an instance of
     *       {@link ParameterDescriptorGroup}, then this method checks all elements
     *       in this group but not in subgroups.</li>
     *   <li>...</li>
     *   <li>If {@code maxDepth} is a high number (e.g. 100), then this method checks all elements
     *       in all subgroups up to the specified depth, which is likely to be never reached. In
     *       this case, {@code maxDepth} can be seen as a safeguard against never ending loops, for
     *       example if parameters graph contains cyclic entries.</li>
     * </ul>
     *
     * @param  param The parameter to inspect.
     * @param  name  The name of the parameter to search for. See the class javadoc
     *               for a rational about the usage of name as a key instead of
     *               {@linkplain ParameterDescriptor descriptor}.
     * @return The set (possibly empty) of parameters with the given name.
     */
    public static List search(final GeneralParameterValue param, final String name, int maxDepth) {
        final List list = new ArrayList();
        search(param, name, maxDepth, list);
        return list;
    }

    /**
     * Implementation of the search algorithm. The result is stored in the supplied set.
     */
    private static void search(final GeneralParameterValue param, final String name,
                               final int maxDepth, final Collection list)
    {
        if (maxDepth >= 0) {
            if (AbstractIdentifiedObject.nameMatches(param.getDescriptor(), name)) {
                list.add(param);
            }
            if (maxDepth!=0 && param instanceof ParameterValueGroup) {
                for (Iterator it=((ParameterValueGroup)param).values().iterator(); it.hasNext();) {
                    search((GeneralParameterValue) it.next(), name, maxDepth-1, list);
                }
            }
        }
    }

    /**
     * Copies all parameter values from {@code source} to {@code target}. A typical usage of
     * this method is for transfering values from an arbitrary implementation to some specific
     * implementation (e.g. a parameter group implementation backed by a
     * {@link java.awt.image.renderable.ParameterBlock} for image processing operations).
     *
     * @since 2.2
     */
    public static void copy(final ParameterValueGroup source, final ParameterValueGroup target) {
        for (final Iterator it=source.values().iterator(); it.hasNext();) {
            final GeneralParameterValue param = (GeneralParameterValue) it.next();
            final String name = param.getDescriptor().getName().getCode();
            if (param instanceof ParameterValueGroup) {
                copy((ParameterValueGroup) param, target.addGroup(name));
            } else {
                target.parameter(name).setValue(((ParameterValue) param).getValue());
            }
        }
    }

    /**
     * Gets a flat view of
     * {@linkplain ParameterDescriptor#getName name}-{@linkplain ParameterValue#getValue value}
     * pairs. This method copies all parameter values into the supplied {@code destination} map.
     * Keys are parameter names as {@link String} objects, and values are parameter values as
     * arbitrary objects. All subgroups (if any) are extracted recursively.
     *
     * @param  parameters  The parameters to extract values from.
     * @param  destination The destination map, or {@code null} for a default one.
     * @return {@code destination}, or a new map if {@code destination} was null.
     */
    public static Map toNameValueMap(final GeneralParameterValue parameters, Map destination) {
        if (destination == null) {
            destination = new LinkedHashMap();
        }
        if (parameters instanceof ParameterValue) {
            final ParameterValue param = (ParameterValue) parameters;
            final Object value = param.getValue();
            final Object old = destination.put(param.getDescriptor().getName().getCode(), value);
            if (old!=null && !old.equals(value)) {
                // TODO: This code will fails to detect if a null value was explicitly supplied
                //       previously. We assume that this case should be uncommon and not a big deal.
                throw new IllegalArgumentException("Inconsistent value"); // TODO: localize.
            }
        }
        if (parameters instanceof ParameterValueGroup) {
            final ParameterValueGroup group = (ParameterValueGroup) parameters;
            for (final Iterator it=group.values().iterator(); it.hasNext();) {
                destination = toNameValueMap((GeneralParameterValue) it.next(), destination);
            }
        }
        return destination;
    }

    /**
     * Ensures that the specified parameter is set. The {@code value} is set if and only if
     * no value were already set by the user for the given {@code name}.
     * <p>
     * The {@code force} argument said what to do if the named parameter is already set. If the
     * value matches, nothing is done in all case. If there is a mismatch and {@code force} is
     * {@code true}, then the parameter is overridden with the specified {@code value}. Otherwise,
     * the parameter is left unchanged but a warning is logged with the {@link Level#FINE FINE}
     * level.
     *
     * @param parameters The set of projection parameters.
     * @param name       The parameter name to set.
     * @param value      The value to set, or to expect if the parameter is already set.
     * @param unit       The value unit.
     * @param force      {@code true} for forcing the parameter to the specified {@code value}
     *                   is case of mismatch.
     * @return {@code true} if the were a mismatch, or {@code false} if the parameters can be
     *         used with no change.
     */
    public static boolean ensureSet(final ParameterValueGroup parameters,
                                    final String name, final double value, final Unit unit,
                                    final boolean force)
    {
        final ParameterValue parameter;
        try {
            parameter = parameters.parameter(name);
        } catch (ParameterNotFoundException ignore) {
            /*
             * Parameter not found. This exception should not occurs most of the time.
             * If it occurs, we will not try to set the parameter here, but the same
             * exception is likely to occurs at MathTransform creation time. The later
             * is the expected place for this exception, so we will let it happen there.
             */
            return false;
        }
        try {
            if (Math.abs(parameter.doubleValue(unit)/value - 1) <= EPS) {
                return false;
            }
        } catch (InvalidParameterTypeException exception) {
            /*
             * The parameter is not a floating point value. Don't try to set it. An exception is
             * likely to be thrown at MathTransform creation time, which is the expected place.
             */
            return false;
        } catch (IllegalStateException exception) {
            /*
             * No value were set for this parameter, and there is no default value.
             */
            parameter.setValue(value, unit);
            return true;
        }
        /*
         * A value were set, but is different from the expected value.
         */
        if (force) {
            parameter.setValue(value, unit);
        } else {
            // TODO: localize
            final LogRecord record = new LogRecord(Level.FINE, "Axis length mismatch.");
            record.setSourceClassName(Parameters.class.getName());
            record.setSourceMethodName("ensureSet");
            Logging.getLogger("org.geotools.parameter").log(record);
        }
        return true;
    }
}
