/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *   
 *   (C) 2003-2006, Geotools Project Managment Committee (PMC)
 *   (C) 2002, 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; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    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.referencing.wkt;

// J2SE dependencies
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Iterator;

// OpenGIS dependencies
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.parameter.ParameterValue;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.NoSuchIdentifierException;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.opengis.referencing.operation.OperationMethod;

// Geotools dependencies
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.AbstractIdentifiedObject;
import org.geotools.referencing.operation.DefaultMathTransformFactory;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;


/**
 * Parser for {@linkplain MathTransform math transform}
 * <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well
 * Known Text</cite> (WKT)</A> of math transform.
 * 
 * @since 2.0
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/wkt/MathTransformParser.java $
 * @version $Id: MathTransformParser.java 25050 2007-04-06 00:41:49Z jgarnett $
 * @author Remi Eve
 * @author Martin Desruisseaux
 * @author Rueben Schulz
 */
public class MathTransformParser extends AbstractParser {
    /**
     * The factory to use for creating math transforms.
     */   
    protected final MathTransformFactory mtFactory;

    /**
     * The classification of the last math transform or projection parsed,
     * or {@code null} if none.
     */
    private String classification;

    /**
     * The method for the last math transform passed, or {@code null} if none.
     *
     * @see #getOperationMethod
     */
    private OperationMethod lastMethod;
    
    /**
     * Constructs a parser using the default set of symbols.
     */
    public MathTransformParser() {
        this(Symbols.DEFAULT);
    }
    
    /**
     * Constructs a parser using the specified set of symbols
     * and the default factories.
     *
     * @param symbols The symbols for parsing and formatting numbers.
     *
     * @todo Pass hints in argument.
     */
    public MathTransformParser(final Symbols symbols) {
        this(symbols, ReferencingFactoryFinder.getMathTransformFactory(null));
    }
    
    /**
     * Constructs a parser for the specified set of symbols and factory.
     *
     * @param symbols   The symbols for parsing and formatting numbers.
     * @param mtFactory The factory to use to create {@link MathTransform} objects.
     */
    public MathTransformParser(final Symbols symbols, final MathTransformFactory mtFactory) {
        super(symbols);
        this.mtFactory = mtFactory;
    }

    /**
     * Parses a math transform element.
     *
     * @param  text The text to be parsed.
     * @return The math transform.
     * @throws ParseException if the string can't be parsed.
     */
    public MathTransform parseMathTransform(final String text) throws ParseException {
        final Element element = getTree(text, new ParsePosition(0));
        final MathTransform mt = parseMathTransform(element, true);
        element.close();
        return mt;
    }

    /**
     * Parses the next element in the specified <cite>Well Know Text</cite> (WKT) tree.
     *
     * @param  element The element to be parsed.
     * @return The object.
     * @throws ParseException if the element can't be parsed.
     */
    protected Object parse(final Element element) throws ParseException {
        return parseMathTransform(element, true);
    }
    
    /**
     * Parses the next element (a {@link MathTransform}) in the specified
     * <cite>Well Know Text</cite> (WKT) tree.
     *
     * @param  element The parent element.
     * @param  required True if parameter is required and false in other case.
     * @return The next element as a {@link MathTransform} object.
     * @throws ParseException if the next element can't be parsed.
     */
    final MathTransform parseMathTransform(final Element element, final boolean required)
            throws ParseException
    {
        lastMethod = null;
        classification = null;
        final Object key = element.peek();
        if (key instanceof Element) {
            final String keyword = ((Element) key).keyword.trim().toUpperCase(symbols.locale);
            if ("PARAM_MT"      .equals(keyword))  return parseParamMT      (element);
            if ("CONCAT_MT"     .equals(keyword))  return parseConcatMT     (element);
            if ("INVERSE_MT"    .equals(keyword))  return parseInverseMT    (element);
            if ("PASSTHROUGH_MT".equals(keyword))  return parsePassThroughMT(element);
        }
        if (required) {
            throw element.parseFailed(null, Errors.format(ErrorKeys.UNKNOW_TYPE_$1, key));
        }
        return null;
    }
    
    /**
     * Parses a "PARAM_MT" element. This element has the following pattern:
     *
     * <blockquote><code>
     * PARAM_MT["<classification-name>" {,<parameter>}* ]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "PARAM_MT" element as an {@link MathTransform} object.
     * @throws ParseException if the "PARAM_MT" element can't be parsed.
     */
    private MathTransform parseParamMT(final Element parent) throws ParseException {     
        final Element element = parent.pullElement("PARAM_MT");
        classification = element.pullString("classification");
        final ParameterValueGroup parameters;
        try {
            parameters = mtFactory.getDefaultParameters(classification);
        } catch (NoSuchIdentifierException exception) {
            throw element.parseFailed(exception, null);
        }
        /*
         * Scan over all PARAMETER["name", value] elements and
         * set the corresponding parameter in the parameter group.
         */
        Element param;
        while ((param=element.pullOptionalElement("PARAMETER")) != null) {
            final String name = param.pullString("name");
            final ParameterValue parameter = parameters.parameter(name);
            final Class type = ((ParameterDescriptor)parameter.getDescriptor()).getValueClass();
            if (Integer.class.equals(type)) {
                parameter.setValue(param.pullInteger("value"));
            } else if (Double.class.equals(type)) {
                parameter.setValue(param.pullDouble("value"));
            } else {
                parameter.setValue(param.pullString("value"));
            }
            param.close();
        }
        element.close();
        /*
         * We now have all informations for constructing the math transform. If the factory is
         * a Geotools implementation, we will use a special method that returns the operation
         * method used. Otherwise, we will use the ordinary method and will performs a slower
         * search for the operation method later if the user ask for it.
         */
        final MathTransform transform;
        try {
            transform = mtFactory.createParameterizedTransform(parameters);
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }
        if (mtFactory instanceof DefaultMathTransformFactory) {
            lastMethod = ((DefaultMathTransformFactory) mtFactory).getLastUsedMethod();
        }
        return transform;
    }    
    
    /**
     * Parses a "INVERSE_MT" element. This element has the following pattern:
     *
     * <blockquote><code>
     * INVERSE_MT[<math transform>]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "INVERSE_MT" element as an {@link MathTransform} object.
     * @throws ParseException if the "INVERSE_MT" element can't be parsed.
     */
    private MathTransform parseInverseMT(final Element parent) throws ParseException {       
        final Element element = parent.pullElement("INVERSE_MT");
        try {
            final MathTransform transform;
            transform = ((MathTransform)parseMathTransform(element, true)).inverse();
            element.close();
            return transform;
        }
        catch (NoninvertibleTransformException exception) {
            throw element.parseFailed(exception, null);
        }
    }
    
    /**
     * Parses a "PASSTHROUGH_MT" element. This element has the following pattern:
     *
     * <blockquote><code>
     * PASSTHROUGH_MT[<integer>, <math transform>]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "PASSTHROUGH_MT" element as an {@link MathTransform} object.
     * @throws ParseException if the "PASSTHROUGH_MT" element can't be parsed.
     */
    private MathTransform parsePassThroughMT(final Element parent) throws ParseException {
        final Element           element = parent.pullElement("PASSTHROUGH_MT");
        final int firstAffectedOrdinate = parent.pullInteger("firstAffectedOrdinate");
        final MathTransform   transform = parseMathTransform(element, true);
        element.close();
        try {
            return mtFactory.createPassThroughTransform(firstAffectedOrdinate, transform, 0);
        } catch (FactoryException exception) {
            throw element.parseFailed(exception, null);
        }   
    }  
        
    /**
     * Parses a "CONCAT_MT" element. This element has the following pattern:
     *
     * <blockquote><code>
     * CONCAT_MT[<math transform> {,<math transform>}*]
     * </code></blockquote>
     *
     * @param  parent The parent element.
     * @return The "CONCAT_MT" element as an {@link MathTransform} object.
     * @throws ParseException if the "CONCAT_MT" element can't be parsed.
     */
    private MathTransform parseConcatMT(final Element parent) throws ParseException {       
        final Element element = parent.pullElement("CONCAT_MT");
        MathTransform transform = parseMathTransform(element, true);
        MathTransform optionalTransform;
        while ((optionalTransform = parseMathTransform(element, false)) != null) {
            try {
                transform = mtFactory.createConcatenatedTransform(transform, optionalTransform);
            } catch (FactoryException exception) {
                throw element.parseFailed(exception, null);
            }
        }
        element.close();
        return transform;
    }

    /**
     * Returns the operation method for the last math transform parsed. This is used by
     * {@link Parser} in order to built {@link org.opengis.referencing.crs.DerivedCRS}.
     */
    final OperationMethod getOperationMethod() {
        /*
         * Information available if the math transform used was a Geotools implementation.
         */
        if (lastMethod == null) {
            /*
             * Not a Geotools implementation. Unfortunatly, there is no obvious way to get this
             * information using GeoAPI interfaces at this time. Performs a slower and less robust
             * check.
             */
            if (classification != null) {
                for (final Iterator it=mtFactory.getAvailableMethods(CoordinateOperation.class).iterator(); it.hasNext();) {
                    final OperationMethod method = (OperationMethod) it.next();
                    if (AbstractIdentifiedObject.nameMatches(method, classification)) {
                        lastMethod = method;
                        break;
                    }
                }
            }
        }
        return lastMethod;
    }
}
