/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 * 
 *    (C) 2001-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.
 */
package org.geotools.parameter;

import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.GeneralParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.ParameterValue;
import org.opengis.referencing.ReferenceIdentifier;
import org.opengis.referencing.operation.Matrix;
import org.opengis.util.InternationalString;
import org.opengis.util.GenericName;

import org.geotools.io.TableWriter;
import org.geotools.referencing.operation.matrix.MatrixFactory;
import org.geotools.resources.UnmodifiableArrayList;
import org.geotools.resources.XArray;
import org.geotools.util.Utilities;


/**
 * The values for a group of {@linkplain MatrixParameterDescriptors matrix parameters}. This value
 * group is extensible, i.e. the number of <code>"elt_<var>row</var>_<var>col</var>"</code>
 * parameters depends on the <code>"num_row"</code> and <code>"num_col"</code> parameter values.
 * Consequently, this {@linkplain ParameterGroup parameter value group} is also its own mutable
 * {@linkplain ParameterDescriptorGroup operation parameter group}.
 *
 * @since 2.1
 *
 * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/modules/library/referencing/src/main/java/org/geotools/parameter/MatrixParameters.java $
 * @version $Id: MatrixParameters.java 37299 2011-05-25 05:21:24Z mbedward $
 * @author Martin Desruisseaux (IRD)
 *
 * @see MatrixParameterDescriptors
 */
public class MatrixParameters extends ParameterGroup implements ParameterDescriptorGroup {
    /**
     * Serial number for interoperability with different versions.
     */
    private static final long serialVersionUID = -7747712999115044943L;

    /**
     * The parameter values. Will be constructed only when first requested.
     */
    private ParameterValue<Double>[][] matrixValues;

    /**
     * The value for the {@link MatrixParameterDescriptors#numRow} parameter.
     * Consider this field as final. It is not only for {@link #clone} implementation.
     */
    private ParameterValue<Integer> numRow;

    /**
     * The value for the {@link MatrixParameterDescriptors#numCol} parameter.
     * Consider this field as final. It is not only for {@link #clone} implementation.
     */
    private ParameterValue<Integer> numCol;

    /**
     * Constructs default values for the specified
     * {@linkplain MatrixParameterDescriptors matrix parameter descriptors}.
     *
     * @param descriptor The descriptor for this group of parameters.
     */
    public MatrixParameters(final MatrixParameterDescriptors descriptor) {
        super(descriptor);
        numRow = Parameters.cast((ParameterValue) parameter(0), Integer.class);
        numCol = Parameters.cast((ParameterValue) parameter(1), Integer.class);
    }

    /**
     * Returns a description of this parameter value group. Returns always {@code this},
     * since the description depends on <code>"num_row"</code> and <code>"num_col"</code>
     * parameter values.
     */
    @Override
    public ParameterDescriptorGroup getDescriptor() {
        return this;
    }

    /**
     * Forward the call to the {@linkplain MatrixParameterDescriptors matrix parameter descriptors}
     * specified at construction time.
     */
    public ReferenceIdentifier getName() {
        return descriptor.getName();
    }

    /**
     * Forward the call to the {@linkplain MatrixParameterDescriptors matrix parameter descriptors}
     * specified at construction time.
     */
    public Collection<GenericName> getAlias() {
        return descriptor.getAlias();
    }

    /**
     * Forward the call to the {@linkplain MatrixParameterDescriptors matrix parameter descriptors}
     * specified at construction time.
     */
    public Set<ReferenceIdentifier> getIdentifiers() {
        return descriptor.getIdentifiers();
    }

    /**
     * Forward the call to the {@linkplain MatrixParameterDescriptors matrix parameter descriptors}
     * specified at construction time.
     */
    public InternationalString getRemarks() {
        return descriptor.getRemarks();
    }

    /**
     * Forward the call to the {@linkplain MatrixParameterDescriptors matrix parameter descriptors}
     * specified at construction time.
     */
    public int getMinimumOccurs() {
        return descriptor.getMinimumOccurs();
    }

    /**
     * Forward the call to the {@linkplain MatrixParameterDescriptors matrix parameter descriptors}
     * specified at construction time.
     */
    public int getMaximumOccurs() {
        return descriptor.getMaximumOccurs();
    }

    /**
     * Returns the parameter in this group for the specified name. The name can be a matrix element
     * if it uses the following syntax: <code>"elt_<var>row</var>_<var>col</var>"</code> where
     * <code>"elt_"</code> is the {@linkplain MatrixParameterDescriptors#prefix prefix} for all
     * matrix elements, and <var>row</var> and <var>col</var> are row and column indices
     * respectively. For example <code>"elt_2_1"</code> is the element name for the value at line 2
     * and row 1. The row and column index are 0 based.
     *
     * @param  name The case insensitive name of the parameter to search for.
     * @return The parameter for the given name.
     * @throws ParameterNotFoundException if there is no parameter for the given name.
     */
    public GeneralParameterDescriptor descriptor(final String name)
            throws ParameterNotFoundException
    {
        return ((MatrixParameterDescriptors) descriptor).descriptor(name,
                numRow.intValue(), numCol.intValue());
    }

    /**
     * Returns the value in this group for the specified name. The name can be a matrix element
     * if it uses the following syntax: <code>"elt_<var>row</var>_<var>col</var>"</code> where
     * <code>"elt_"</code> is the {@linkplain MatrixParameterDescriptors#prefix prefix} for all
     * matrix elements, and <var>row</var> and <var>col</var> are row and column indices
     * respectively. For example <code>"elt_2_1"</code> is the element name for the value at line
     * 2 and row 1. The row and column index are 0 based.
     *
     * @param  name The case insensitive name of the parameter to search for.
     * @return The parameter value for the given name.
     * @throws ParameterNotFoundException if there is no parameter for the given name.
     */
    @Override
    public ParameterValue<?> parameter(String name) throws ParameterNotFoundException {
        ensureNonNull("name", name);
        name = name.trim();
        final MatrixParameterDescriptors descriptor = ((MatrixParameterDescriptors) this.descriptor);
        final String prefix = descriptor.prefix;
        RuntimeException cause = null;
        if (name.regionMatches(true, 0, prefix, 0, prefix.length())) {
            final int split = name.indexOf(descriptor.separator, prefix.length());
            if (split >= 0) try {
                final int row = Integer.parseInt(name.substring(prefix.length(), split));
                final int col = Integer.parseInt(name.substring(split + 1));
                return parameter(row, col);
            } catch (NumberFormatException exception) {
                cause = exception;
            } catch (IndexOutOfBoundsException exception) {
                cause = exception;
            }
        }
        /*
         * The parameter name is not a matrix element name. Search in the super
         * class for other parameters, especially "num_row" and "num_col".
         */
        try {
            return super.parameter(name);
        } catch (ParameterNotFoundException exception) {
            if (cause != null) try {
                exception.initCause(cause);
            } catch (IllegalStateException ignore) {
                // A cause has already be given to the exception. Forget the cause then.
            }
            throw exception;
        }
    }

    /**
     * Returns the value in this group for a matrix element at the specified index.
     * Row and column index are 0 based.
     *
     * @param  row    The row indice.
     * @param  column The column indice
     * @return The parameter value for the specified matrix element (never {@code null}).
     * @throws IndexOutOfBoundsException if {@code row} or {@code column} is out of bounds.
     */
    public final ParameterValue<Double> parameter(final int row, final int column)
            throws IndexOutOfBoundsException
    {
        return parameter(row, column, numRow.intValue(), numCol.intValue());
    }

    /**
     * Implementation of {@link #parameter(int,int)}.
     *
     * @param  row    The row indice.
     * @param  column The column indice
     * @param  numRow The maximum number of rows.
     * @param  numCol The maximum number of columns.
     * @return The parameter value for the specified matrix element.
     * @throws IndexOutOfBoundsException if {@code row} or {@code column} is out of bounds.
     */
    @SuppressWarnings("unchecked") // Because of array creation
    private ParameterValue<Double> parameter(final int row,    final int column,
                                             final int numRow, final int numCol)
            throws IndexOutOfBoundsException
    {
        MatrixParameterDescriptors.checkIndice("row",    row,    numRow);
        MatrixParameterDescriptors.checkIndice("column", column, numCol);
        if (matrixValues == null) {
            matrixValues = new ParameterValue[numRow][];
        }
        if (row >= matrixValues.length) {
            matrixValues = XArray.resize(matrixValues, numRow);
        }
        ParameterValue<Double>[] rowValues = matrixValues[row];
        if (rowValues == null) {
            matrixValues[row] = rowValues = new ParameterValue[numCol];
        }
        if (column >= rowValues.length) {
            matrixValues[row] = rowValues = XArray.resize(rowValues, numCol);
        }
        ParameterValue<Double> param = rowValues[column];
        if (param == null) {
            rowValues[column] = param = new FloatParameter(
                ((MatrixParameterDescriptors) descriptor).descriptor(row, column, numRow, numCol));
        }
        return param;
    }

    /**
     * Returns the parameters descriptors in this group. The amount of parameters depends
     * on the value of <code>"num_row"</code> and <code>"num_col"</code> parameters.
     */
    public List<GeneralParameterDescriptor> descriptors() {
        return ((MatrixParameterDescriptors) descriptor).descriptors(
                numRow.intValue(), numCol.intValue());
    }

    /**
     * Returns the parameters values in this group. The amount of parameters depends
     * on the value of <code>"num_row"</code> and <code>"num_col"</code> parameters.
     * The parameter array will contains only matrix elements which have been requested at
     * least once by one of {@code parameter(...)} methods. Never requested elements
     * are left to their default value and omitted from the returned array.
     */
    @Override
    public List<GeneralParameterValue> values() {
        final int numRow = this.numRow.intValue();
        final int numCol = this.numCol.intValue();
        final ParameterValue[] parameters = new ParameterValue[numRow*numCol + 2];
        int k = 0;
        parameters[k++] = this.numRow;
        parameters[k++] = this.numCol;
        if (matrixValues != null) {
            final int maxRow = Math.min(numRow, matrixValues.length);
            for (int j=0; j<maxRow; j++) {
                final ParameterValue[] rowValues = matrixValues[j];
                if (rowValues != null) {
                    final int maxCol = Math.min(numCol, rowValues.length);
                    for (int i=0; i<maxCol; i++) {
                        final ParameterValue value = rowValues[i];
                        if (value != null) {
                            parameters[k++] = value;
                        }
                    }
                }
            }
        }
        return UnmodifiableArrayList.wrap((GeneralParameterValue[]) XArray.resize(parameters, k));
    }

    /**
     * Forwards the call to the {@linkplain MatrixParameterDescriptors matrix parameter descriptors}
     * specified at construction time.
     */
    public ParameterValueGroup createValue() {
        return (ParameterValueGroup) descriptor.createValue();
    }

    /**
     * Creates a matrix from this group of parameters.
     *
     * @return A matrix created from this group of parameters.
     */
    public Matrix getMatrix() {
        final int numRow = this.numRow.intValue();
        final int numCol = this.numCol.intValue();
        final Matrix matrix = MatrixFactory.create(numRow, numCol);
        if (matrixValues != null) {
            for (int j=0; j<numRow; j++) {
                final ParameterValue[] row = matrixValues[j];
                if (row != null) {
                    for (int i=0; i<numCol; i++) {
                        final ParameterValue element = row[i];
                        if (element != null) {
                            matrix.setElement(j, i, element.doubleValue());
                        }
                    }
                }
            }
        }
        return matrix;
    }

    /**
     * Sets all parameter values to the element value in the specified matrix.
     * After this method call, {@link #values} will returns only the elements
     * different from the default value.
     *
     * @param matrix The matrix to copy in this group of parameters.
     */
    @SuppressWarnings("unchecked") // Because of array creation
    public void setMatrix(final Matrix matrix) {
        final MatrixParameterDescriptors matrixDescriptor =
            ((MatrixParameterDescriptors) this.descriptor);
        final int numRow = matrix.getNumRow();
        final int numCol = matrix.getNumCol();
        this.numRow.setValue(numRow);
        this.numCol.setValue(numCol);
        for (int row=0; row<numRow; row++) {
            for (int col=0; col<numCol; col++) {
                final double element = matrix.getElement(row,col);
                ParameterDescriptor<Double> descriptor = matrixDescriptor.descriptor(row, col);
                final double value = descriptor.getDefaultValue();
                if (Double.doubleToLongBits(element) == Double.doubleToLongBits(value)) {
                    /*
                     * Value matches the default value, so there is no need to keep it.
                     * Remove entry to keep things sparse.
                     */
                    if (matrixValues != null  &&  matrixValues[row] != null) {
                        matrixValues[row][col] = null;
                    }
                    continue;
                }
                if (matrixValues == null) {
                    matrixValues = new ParameterValue[numRow][];
                }
                if (matrixValues[row] == null ){
                    matrixValues[row] = new ParameterValue[numCol];
                }
                matrixValues[row][col] = new FloatParameter(descriptor, element);
            }
        }
    }

    /**
     * Compare this object with the specified one for equality.
     */
    @Override
    public boolean equals(final Object object) {
        if (object == this) {
            return true; // Slight optimization.
        }
        if (super.equals(object)) {
            final MatrixParameters that = (MatrixParameters) object;
            final int numRow = this.numRow.intValue();
            final int numCol = this.numCol.intValue();
            for (int j=0; j<numRow; j++) {
                for (int i=0; i<numCol; i++) {
                    if (!Utilities.equals(this.parameter(j,i, numRow, numCol),
                                          that.parameter(j,i, numRow, numCol)))
                    {
                        return false;
                    }
                }
            }
            return true;
        }
        return false;
    }

    /**
     * Returns a clone of this parameter group.
     */
    @Override
    @SuppressWarnings("unchecked")
    public MatrixParameters clone() {
        final MatrixParameters copy = (MatrixParameters) super.clone();
        if (copy.matrixValues != null) {
            copy.numRow       = (ParameterValue) copy.parameter(0);
            copy.numCol       = (ParameterValue) copy.parameter(1);
            copy.matrixValues =                  copy.matrixValues.clone();
            for (int j=0; j<copy.matrixValues.length; j++) {
                ParameterValue[] array = copy.matrixValues[j];
                if (array != null) {
                    copy.matrixValues[j] = array = array.clone();
                    for (int i=0; i<array.length; i++) {
                        if (array[i] != null) {
                            array[i] = array[i].clone();
                        }
                    }
                }
            }
        }
        return copy;
    }

    /**
     * Writes the content of this parameter to the specified table.
     *
     * @param  table The table where to format the parameter value.
     * @throws IOException if an error occurs during output operation.
     */
    @Override
    protected void write(final TableWriter table) throws IOException {
        table.write(getName(descriptor));
        table.nextColumn();
        table.write('=');
        table.nextColumn();
        table.write(getMatrix().toString());
        table.nextLine();
    }
}
