/*
 *    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 java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.geotools.referencing.AbstractIdentifiedObject;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.util.Utilities;
import org.opengis.metadata.Identifier;
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.InvalidParameterCardinalityException;
import org.opengis.parameter.InvalidParameterTypeException;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;


/**
 * A group of related parameter values. The same group can be repeated more than once in an
 * {@linkplain org.opengis.referencing.operation.Operation operation} or higher level
 * {@link ParameterValueGroup}, if those instances contain different
 * values of one or more {@link ParameterValue}s which suitably distinquish among
 * those groups.
 *
 * @since 2.1
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/library/referencing/src/main/java/org/geotools/parameter/ParameterGroup.java $
 * @version $Id: ParameterGroup.java 37299 2011-05-25 05:21:24Z mbedward $
 * @author Martin Desruisseaux (IRD)
 * @author Jody Garnett (Refractions Research)
 *
 * @see DefaultParameterDescriptorGroup
 * @see Parameter
 */
public class ParameterGroup extends AbstractParameter implements ParameterValueGroup {
    /**
     * Serial number for interoperability with different versions.
     */
    private static final long serialVersionUID = -1985309386356545126L;

    /**
     * The {@linkplain #values() parameter values} for this group.
     * Note: consider as final. This field is not final only in order
     * to allows {@link #clone} to work.
     */
    private ArrayList<GeneralParameterValue> values;

    /**
     * A view of {@link #values} as an immutable list. Will be constructed only when first
     * needed. Note that while this list may be immutable, <strong>elements</strong> in this
     * list stay modifiable. The goal is to allows the following idiom:
     *
     * <blockquote><pre>
     * values().get(i).setValue(myValue);
     * </pre></blockquote>
     */
    private transient List<GeneralParameterValue> asList;

    /**
     * Constructs a parameter group from the specified descriptor.
     * All {@linkplain #values parameter values} will be initialized
     * to their default value.
     *
     * @param descriptor The descriptor for this group.
     */
    public ParameterGroup(final ParameterDescriptorGroup descriptor) {
        super(descriptor);
        final List<GeneralParameterDescriptor> parameters = descriptor.descriptors();
        values = new ArrayList<GeneralParameterValue>(parameters.size());
        for (final GeneralParameterDescriptor element : parameters) {
            for (int count=element.getMinimumOccurs(); --count>=0;) {
                final GeneralParameterValue value = element.createValue();
                ensureNonNull("createValue", value);
                values.add(value);
            }
        }
    }

    /**
     * Constructs a parameter group from the specified descriptor and list of parameters.
     *
     * @param descriptor The descriptor for this group.
     * @param values The list of parameter values.
     *
     * @throws IllegalStateException if the number of {@linkplain ParameterValue parameter}
     *         occurences doesn't matches the number declared in the
     *         {@linkplain ParameterDescriptorGroup descriptor}.
     */
    public ParameterGroup(final ParameterDescriptorGroup descriptor,
                          final GeneralParameterValue[]  values)
    {
        super(descriptor);
        ensureNonNull("values", values);
        this.values = new ArrayList<GeneralParameterValue>(values.length);
        for (int i=0; i<values.length; i++) {
            this.values.add(values[i]);
        }
        final List<GeneralParameterDescriptor> parameters = descriptor.descriptors();
        final Map<GeneralParameterDescriptor,int[]> occurences =
                new LinkedHashMap<GeneralParameterDescriptor,int[]>(Math.round(parameters.size()/0.75f)+1, 0.75f);
        for (final GeneralParameterDescriptor param : parameters) {
            ensureNonNull("parameters", param);
            occurences.put(param, new int[1]);
            // The value 'int[1]' will be used by 'ensureValidOccurs'
        }
        ensureValidOccurs(values, occurences);
    }

    /**
     * Make sure that the number of occurences of each values is inside the expected range.
     *
     * @param values The list of parameter values.
     * @param occurences A map of the number of occurences of a value for each descriptor.
     *        The key must be {@link GeneralParameterDescriptor} instances and the values
     *        must be {@code int[]} array of length 1 initialized with the 0 value.
     *
     * @throws IllegalStateException if the number of {@linkplain ParameterValue parameter}
     *         occurences doesn't matches the number declared in the
     *         {@linkplain ParameterDescriptorGroup descriptor}.
     */
    private static void ensureValidOccurs(final GeneralParameterValue[] values,
                                          final Map<GeneralParameterDescriptor,int[]> occurences)
    {
        /*
         * Count the parameters occurences.
         */
        for (int i=0; i<values.length; i++) {
            ensureNonNull("values", values, i);
            final GeneralParameterDescriptor descriptor = values[i].getDescriptor();
            final int[] count = occurences.get(descriptor);
            if (count == null) {
                final String name = getName(descriptor);
                throw new InvalidParameterTypeException(Errors.format(
                          ErrorKeys.ILLEGAL_DESCRIPTOR_FOR_PARAMETER_$1, name), name);
            }
            count[0]++;
        }
        /*
         * Now check if the occurences are in the expected ranges.
         */
        for (final Map.Entry<GeneralParameterDescriptor,int[]> entry : occurences.entrySet()) {
            final GeneralParameterDescriptor descriptor = entry.getKey();
            final int count = entry.getValue()[0];
            final int min   = descriptor.getMinimumOccurs();
            final int max   = descriptor.getMaximumOccurs();
            if (!(count>=min && count<=max)) {
                final String name = getName(descriptor);
                throw new InvalidParameterCardinalityException(Errors.format(
                          ErrorKeys.ILLEGAL_OCCURS_FOR_PARAMETER_$4, name, count, min, max), name);
            }
        }
    }

    /**
     * Returns the abstract definition of this group of parameters.
     */
    @Override
    public ParameterDescriptorGroup getDescriptor() {
        return (ParameterDescriptorGroup) super.getDescriptor();
    }

    /**
     * Returns the values in this group. Changes in this list are reflected on this
     * {@code ParameterValueGroup}. The returned list supports the
     * {@link List#add(Object) add} operation.
     */
    public List<GeneralParameterValue> values() {
        if (asList == null) {
            asList = new ParameterValueList((ParameterDescriptorGroup) descriptor, values);
        }
        return asList;
    }

    /**
     * Returns the parameter value at the specified index.
     *
     * @param  index The zero-based index.
     * @return The parameter value at the specified index.
     * @throws IndexOutOfBoundsException if the specified index is out of bounds.
     */
    final GeneralParameterValue parameter(final int index) throws IndexOutOfBoundsException {
        return values.get(index);
    }

    /**
     * Returns the value in this group for the specified
     * {@linkplain Identifier#getCode identifier code}.
     * If no {@linkplain ParameterValue parameter value} is found but
     * a {@linkplain ParameterDescriptor parameter descriptor} is found
     * (which may occurs if the parameter is optional, i.e.
     * <code>{@linkplain ParameterDescriptor#getMinimumOccurs minimumOccurs} == 0</code>),
     * then a {@linkplain ParameterValue parameter value} is
     * automatically created and initialized to its
     * {@linkplain ParameterDescriptor#getDefaultValue default value} (if any).
     *
     * <P>This convenience method provides a way to get and set parameter values by name. For
     * example the following idiom fetches a floating point value for the
     * <code>"false_easting"</code> parameter:</P>
     *
     * <blockquote><code>
     * double value =
     * parameter("false_easting").{@linkplain ParameterValue#doubleValue() doubleValue()};
     * </code></blockquote>
     *
     * <P>This method do not search recursively in subgroups. This is because more than one
     * subgroup may exist for the same {@linkplain ParameterDescriptorGroup descriptor}. The
     * user must {@linkplain #groups query all subgroups} and select explicitly
     * the appropriate one to use.</P>
     *
     * @param  name The case insensitive {@linkplain Identifier#getCode identifier code} of the
     *              parameter to search for.
     * @return The parameter value for the given identifier code.
     * @throws ParameterNotFoundException if there is no parameter value for the given identifier
     *         code.
     */
    public ParameterValue parameter(String name) throws ParameterNotFoundException {
        ensureNonNull("name", name);
        name = name.trim();
        for (final GeneralParameterValue value : values) {
            if (value instanceof ParameterValue) {
                if (AbstractIdentifiedObject.nameMatches(value.getDescriptor(), name)) {
                    return (ParameterValue) value;
                }
            }
        }
        /*
         * No existing parameter found. Check if an optional parameter exists.
         * If such a descriptor is found, create it, add it to the list of values
         * and returns it.
         */
        for (final GeneralParameterDescriptor descriptor : getDescriptor().descriptors()) {
            if (descriptor instanceof ParameterDescriptor) {
                if (AbstractIdentifiedObject.nameMatches(descriptor, name)) {
                    final ParameterValue value = ((ParameterDescriptor) descriptor).createValue();
                    values.add(value);
                    return value;
                }
            }
        }
        throw new ParameterNotFoundException(Errors.format(
                  ErrorKeys.MISSING_PARAMETER_$1, name), name);
    }

    /**
     * Returns all subgroups with the specified name. This method do not create new groups.
     * If the requested group is optional (i.e.
     * <code>{@linkplain ParameterDescriptor#getMinimumOccurs minimumOccurs} == 0</code>)
     * and no value were set, then this method returns an empty set.
     *
     * @param  name The case insensitive {@linkplain Identifier#getCode identifier code}
     *         of the parameter group to search for.
     * @return The set of all parameter group for the given identifier code.
     * @throws ParameterNotFoundException if no {@linkplain ParameterDescriptorGroup descriptor}
     *         was found for the given name.
     */
    public List<ParameterValueGroup> groups(String name) throws ParameterNotFoundException {
        ensureNonNull("name", name);
        name = name.trim();
        final List<ParameterValueGroup> groups =
                new ArrayList<ParameterValueGroup>(Math.min(values.size(), 10));
        for (final GeneralParameterValue value : values) {
            if (value instanceof ParameterValueGroup) {
                if (AbstractIdentifiedObject.nameMatches(value.getDescriptor(), name)) {
                    groups.add((ParameterValueGroup) value);
                }
            }
        }
        /*
         * No groups were found. Check if the group actually exists (i.e. is declared in the
         * descriptor). If it doesn't exists, then an exception is thrown. If it exists (i.e.
         * it is simply an optional group not yet defined), then returns an empty list.
         */
        if (groups.isEmpty()) {
            final GeneralParameterDescriptor check =
                    ((ParameterDescriptorGroup) descriptor).descriptor(name);
            if (!(check instanceof ParameterDescriptorGroup)) {
                throw new ParameterNotFoundException(Errors.format(
                          ErrorKeys.MISSING_PARAMETER_$1, name), name);
            }
        }
        return groups;
    }

    /**
     * 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 (object == this) {
            return true;
        }
        if (super.equals(object)) {
            final ParameterGroup that = (ParameterGroup) object;
            return Utilities.equals(this.values, that.values);
        }
        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() {
        return super.hashCode() ^ values.hashCode();
    }

    /**
     * Returns a deep copy of this group of parameter values.
     * Included parameter values and subgroups are cloned recursively.
     *
     * @return A copy of this group of parameter values.
     */
    @Override
    public ParameterGroup clone() {
        final ParameterGroup copy = (ParameterGroup) super.clone();
        copy.values = (ArrayList<GeneralParameterValue>) copy.values.clone();
        for (int i=copy.values.size(); --i>=0;) {
            // TODO: remove cast with J2SE 1.5
            copy.values.set(i, copy.values.get(i).clone());
        }
        copy.asList = null;
        return copy;
    }
}
