/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-2006, GeoTools Project Managment Committee (PMC)
 *    (C) 2001, Institut de Recherche pour le Développement
 *   
 *    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.referencing.operation;

// J2SE dependencies
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

// OpenGIS dependencies
import org.opengis.parameter.GeneralParameterDescriptor;
import org.opengis.parameter.ParameterDescriptorGroup;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.Projection;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.util.InternationalString;

// Geotools dependencies
import org.geotools.parameter.Parameters;
import org.geotools.parameter.DefaultParameterDescriptorGroup;
import org.geotools.referencing.AbstractIdentifiedObject;
import org.geotools.referencing.operation.LinearTransform;
import org.geotools.referencing.operation.transform.AbstractMathTransform;
import org.geotools.referencing.operation.transform.ConcatenatedTransform;
import org.geotools.referencing.operation.transform.PassThroughTransform;
import org.geotools.referencing.wkt.Formatter;
import org.geotools.resources.Utilities;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;


/**
 * Definition of an algorithm used to perform a coordinate operation. Most operation
 * methods use a number of operation parameters, although some coordinate conversions
 * use none. Each coordinate operation using the method assigns values to these parameters.
 *  
 * @since 2.1
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/operation/DefaultOperationMethod.java $
 * @version $Id: DefaultOperationMethod.java 24925 2007-03-27 20:12:08Z jgarnett $
 * @author Martin Desruisseaux
 *
 * @see DefaultOperation
 */
public class DefaultOperationMethod extends AbstractIdentifiedObject implements OperationMethod {
    /**
     * Serial number for interoperability with different versions.
     */
    private static final long serialVersionUID = -98032729598205972L;

    /**
     * List of localizable properties. To be given to {@link AbstractIdentifiedObject} constructor.
     */
    private static final String[] LOCALIZABLES = {FORMULA_KEY};

    /**
     * Formula(s) or procedure used by this operation method. This may be a reference to a
     * publication. Note that the operation method may not be analytic, in which case this
     * attribute references or contains the procedure, not an analytic formula.
     */
    private final InternationalString formula;

    /**
     * Number of dimensions in the source CRS of this operation method.
     */
    protected final int sourceDimensions;

    /**
     * Number of dimensions in the target CRS of this operation method.
     */
    protected final int targetDimensions;

    /**
     * The set of parameters, or {@code null} if none.
     */
    private final ParameterDescriptorGroup parameters;

    /**
     * Convenience constructor that creates an operation method from a math transform.
     * The information provided in the newly created object are approximative, and
     * usually acceptable only as a fallback when no other information are available.
     *
     * @param transform The math transform to describe.
     */
    public DefaultOperationMethod(final MathTransform transform) {
        this(getProperties(transform),
             transform.getSourceDimensions(),
             transform.getTargetDimensions(),
             getDescriptor(transform));
    }

    /**
     * Work around for RFE #4093999 in Sun's bug database
     * ("Relax constraint on placement of this()/super() call in constructors").
     */
    private static Map getProperties(final MathTransform transform) {
        ensureNonNull("transform", transform);
        final Map properties;
        if (transform instanceof AbstractMathTransform) {
            final AbstractMathTransform mt = (AbstractMathTransform) transform;
            properties = getProperties(mt.getParameterDescriptors(), null);
        } else {
            properties = Collections.singletonMap(NAME_KEY, Vocabulary.format(VocabularyKeys.UNKNOW));
        }
        return properties;
    }

    /**
     * Work around for RFE #4093999 in Sun's bug database
     * ("Relax constraint on placement of this()/super() call in constructors").
     * This code should have been merged with {@code getProperties} above.
     */
    private static ParameterDescriptorGroup getDescriptor(final MathTransform transform) {
        ParameterDescriptorGroup descriptor = null;
        if (transform instanceof AbstractMathTransform) {
            descriptor = ((AbstractMathTransform) transform).getParameterDescriptors();
        }
        return descriptor;
    }

    /**
     * Constructs a new operation method with the same values than the specified one.
     * This copy constructor provides a way to wrap an arbitrary implementation into a
     * Geotools one or a user-defined one (as a subclass), usually in order to leverage
     * some implementation-specific API. This constructor performs a shallow copy,
     * i.e. the properties are not cloned.
     */
    public DefaultOperationMethod(final OperationMethod method) {
        super(method);
        formula          = method.getFormula();
        parameters       = method.getParameters();
        sourceDimensions = method.getSourceDimensions();
        targetDimensions = method.getTargetDimensions();
    }

    /**
     * Constructs a new operation method with the same values than the specified one
     * except the dimensions.
     *
     * @param method The operation method to copy.
     * @param sourceDimensions Number of dimensions in the source CRS of this operation method.
     * @param targetDimensions Number of dimensions in the target CRS of this operation method.
     */
    public DefaultOperationMethod(final OperationMethod method,
                                  final int sourceDimensions,
                                  final int targetDimensions)
    {
        super(method);
        this.formula    = method.getFormula();
        this.parameters = method.getParameters();
        this.sourceDimensions = sourceDimensions;
        this.targetDimensions = targetDimensions;
        ensurePositive("sourceDimensions", sourceDimensions);
        ensurePositive("targetDimensions", targetDimensions);
    }

    /**
     * Utility method used to kludge {@code GeneralParameterDescriptor[]}
     * into a {@code ParameterDescriptorGroup}.
     * This is a work around for RFE #4093999 in Sun's bug database
     * ("Relax constraint on placement of this()/super() call in constructors").
     */
    private static ParameterDescriptorGroup toGroup(final Map properties,
                                                    final GeneralParameterDescriptor[] parameters)
    {
        return (parameters==null || parameters.length==0) ? null :
               new DefaultParameterDescriptorGroup(properties, parameters);
    }

    /**
     * Constructs an operation method from a set of properties and a descriptor group.
     * The properties given in argument follow the same rules than for the
     * {@linkplain AbstractIdentifiedObject#AbstractIdentifiedObject(Map) super-class constructor}.
     * Additionally, the following properties are understood by this construtor:
     * <br><br>
     * <table border='1'>
     *   <tr bgcolor="#CCCCFF" class="TableHeadingColor">
     *     <th nowrap>Property name</th>
     *     <th nowrap>Value type</th>
     *     <th nowrap>Value given to</th>
     *   </tr>
     *   <tr>
     *     <td nowrap>&nbsp;{@link #FORMULA_KEY "formula"}&nbsp;</td>
     *     <td nowrap>&nbsp;{@link String} or {@link InternationalString}&nbsp;</td>
     *     <td nowrap>&nbsp;{@link #getFormula}</td>
     *   </tr>
     * </table>
     *
     * @param properties Set of properties. Should contains at least <code>"name"</code>.
     * @param sourceDimensions Number of dimensions in the source CRS of this operation method.
     * @param targetDimensions Number of dimensions in the target CRS of this operation method.
     * @param parameters The set of parameters, or {@code null} if none.
     */
    public DefaultOperationMethod(final Map properties,
                                  final int sourceDimensions,
                                  final int targetDimensions,
                                  final ParameterDescriptorGroup parameters)
    {
        this(properties, new HashMap(), sourceDimensions, targetDimensions, parameters);
    }

    /**
     * Work around for RFE #4093999 in Sun's bug database
     * ("Relax constraint on placement of this()/super() call in constructors").
     */
    private DefaultOperationMethod(final Map properties,
                                   final Map subProperties,
                                   final int sourceDimensions,
                                   final int targetDimensions,
                                   ParameterDescriptorGroup parameters)
    {
        super(properties, subProperties, LOCALIZABLES);
        formula = (InternationalString) subProperties.get(FORMULA_KEY);
        // 'parameters' may be null, which is okay. A null value will
        // make serialization smaller and faster than an empty object.
        this.parameters       = parameters;
        this.sourceDimensions = sourceDimensions;
        this.targetDimensions = targetDimensions;
        ensurePositive("sourceDimensions", sourceDimensions);
        ensurePositive("targetDimensions", targetDimensions);
    }

    /**
     * Ensure that the specified value is positive.
     * An {@link IllegalArgumentException} is throws if it is not.
     *
     * @param name  The parameter name.
     * @param value The parameter value.
     * @throws IllegalArgumentException if the specified value is not positive.
     */
    private static void ensurePositive(final String name, final int value)
            throws IllegalArgumentException
    {
        if (value < 0) {
            throw new IllegalArgumentException(Errors.format(
                      ErrorKeys.ILLEGAL_ARGUMENT_$2, name, new Integer(value)));
        }
    }

    /**
     * Formula(s) or procedure used by this operation method. This may be a reference to a
     * publication. Note that the operation method may not be analytic, in which case this
     * attribute references or contains the procedure, not an analytic formula.
     */
    public InternationalString getFormula() {
        return formula;
    }

    /**
     * Number of dimensions in the source CRS of this operation method.
     *
     * @return The dimension of source CRS.
     */
    public int getSourceDimensions() {
        return sourceDimensions;
    }

    /**
     * Number of dimensions in the target CRS of this operation method.
     */
    public int getTargetDimensions() {
        return targetDimensions;
    }

    /**
     * Returns the set of parameters.
     */
    public ParameterDescriptorGroup getParameters() {
        return (parameters!=null) ? parameters : Parameters.EMPTY_GROUP;
    }

    /**
     * Returns the operation type. Current implementation returns {@code Projection.class} for
     * proper WKT formatting using an unknow implementation. But the {@link MathTransformProvider}
     * subclass (with protected access) will overrides this method with a more conservative default
     * value.
     */
    Class getOperationType() {
        return Projection.class;
    }

    /**
     * Compare this operation method with the specified object for equality.
     * If {@code compareMetadata} is {@code true}, then all available
     * properties are compared including {@linkplain #getFormula formula}.
     *
     * @param  object The object to compare to {@code this}.
     * @param  compareMetadata {@code true} for performing a strict comparaison, or
     *         {@code false} for comparing only properties relevant to transformations.
     * @return {@code true} if both objects are equal.
     */
    public boolean equals(final AbstractIdentifiedObject object, final boolean compareMetadata) {
        if (object == this) {
            return true; // Slight optimization.
        }
        if (super.equals(object, compareMetadata)) {
            final DefaultOperationMethod that = (DefaultOperationMethod) object;
            if (this.sourceDimensions == that.sourceDimensions &&
                this.targetDimensions == that.targetDimensions &&
                equals(this.parameters,  that.parameters, compareMetadata))
            {
                return !compareMetadata || Utilities.equals(this.formula, that.formula);
            }
        }
        return false;
    }

    /**
     * Returns a hash code value for this operation method.
     */
    public int hashCode() {
        int code = (int)serialVersionUID + sourceDimensions + 37*targetDimensions;
        if (parameters != null) {
            code = code * 37 + parameters.hashCode();
        }
        return code;
    }
    
    /**
     * Format the inner part of a
     * <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well
     * Known Text</cite> (WKT)</A> element.
     *
     * @param  formatter The formatter to use.
     * @return The WKT element name.
     */
    protected String formatWKT(final Formatter formatter) {
        if (Projection.class.isAssignableFrom(getOperationType())) {
            return "PROJECTION";
        }
        return super.formatWKT(formatter);
    }

    /**
     * Returns {@code true} if the specified transform is likely to exists only for axis switch
     * and/or unit conversions. The heuristic rule checks if the transform is backed by a square
     * matrix with exactly one non-null value in each row and each column. This method is used
     * for implementation of the {@link #checkDimensions} method only.
     */
    private static boolean isTrivial(final MathTransform transform) {
        if (transform instanceof LinearTransform) {
            final Matrix matrix = ((LinearTransform) transform).getMatrix();
            final int size = matrix.getNumRow();
            if (matrix.getNumCol() == size) {
                for (int j=0; j<size; j++) {
                    int n1=0, n2=0;
                    for (int i=0; i<size; i++) {
                        if (matrix.getElement(j,i) != 0) n1++;
                        if (matrix.getElement(i,j) != 0) n2++;
                    }
                    if (n1!=1 || n2!=1) {
                        return false;
                    }
                }
                return true;
            }
        }
        return false;
    }

    /**
     * Checks if an operation method and a math transform have a compatible number of source
     * and target dimensions. In the special case of a {@linkplain PassThroughTransform pass
     * through transform}, the method's dimensions may be checked against the
     * {@linkplain PassThroughTransform#getSubTransform sub transform}'s dimensions.
     * This convenience method is provided for argument checking.
     *
     * @param  method    The operation method to compare to the math transform, or {@code null}.
     * @param  transform The math transform to compare to the operation method, or {@code null}.
     * @throws MismatchedDimensionException if the number of dimensions are incompatibles.
     *
     * @todo The check for {@link ConcatenatedTransform} and {@link PassThroughTransform} works
     *       only for Geotools implementation.
     */
    public static void checkDimensions(final OperationMethod method, MathTransform transform)
            throws MismatchedDimensionException
    {
        if (method!=null && transform!=null) {
            int actual, expected=method.getSourceDimensions();
            while ((actual=transform.getSourceDimensions()) > expected) {
                if (transform instanceof ConcatenatedTransform) {
                    // Ignore axis switch and unit conversions.
                    final ConcatenatedTransform c = (ConcatenatedTransform) transform;
                    if (isTrivial(c.transform1)) {
                        transform = c.transform2;
                    } else if (isTrivial(c.transform2)) {
                        transform = c.transform1;
                    } else {
                        break;
                    }
                } else if (transform instanceof PassThroughTransform) {
                    transform = ((PassThroughTransform) transform).getSubTransform();
                } else {
                    break;
                }
            }
            final String name;
            if (actual != expected) {
                name = "sourceDimensions";
            } else {
                actual   = transform.getTargetDimensions();
                expected =    method.getTargetDimensions();
                if (actual != expected) {
                    name = "targetDimensions";
                } else {
                    return;
                }
            }
            throw new IllegalArgumentException(Errors.format(ErrorKeys.MISMATCHED_DIMENSION_$3,
                                               name, new Integer(actual), new Integer(expected)));
        }
    }
}
