/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *   
 *   (C) 2004-2006, Geotools Project Managment Committee (PMC)
 *   (C) 2004, 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.io.IOException;
import java.io.Serializable;
import java.io.Writer;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Collections;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

// OpenGIS dependencies
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.NoSuchIdentifierException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;

// Geotools dependencies
import org.geotools.io.TableWriter;
import org.geotools.referencing.Console;
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;


/**
 * A parser that performs string replacements before to delegate the work to an other parser.
 * String replacements are specified through calls to the {@link #addDefinition addDefinition}
 * method. In the example below, the {@code WGS84} string in the {@linkplain #parseObject
 * parseObject} call is expanded into the full <code>GEOGCS["WGS84", ...</code> string before
 * to be parsed.
 *
 * <blockquote><code>
 * {@linkplain #addDefinition addDefinition}("WGS84", "GEOGCS[\"WGS84\", DATUM[</code> ...<i>etc</i>... <code>]]<BR>
 * {@linkplain #parseObject parseObject}("PROJCS[\"Mercator_1SP\", <strong>WGS84</strong>, PROJECTION[</code> ...<i>etc</i>... <code>]]")</code>
 * </blockquote>
 *
 * @since 2.1
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/wkt/Preprocessor.java $
 * @version $Id: Preprocessor.java 22295 2006-10-20 00:58:17Z desruisseaux $
 * @author Martin Desruisseaux
 */
public class Preprocessor extends Format {
    /**
     * The WKT parser, usually a {@link Parser} object.
     */
    protected final Format parser;
    
    /**
     * The set of objects defined by calls to {@link #addDefinition}.
     */
    private final Map definitions/*<String,Definition>*/ = new TreeMap();

    /**
     * The unmodifiable set of keys in the {@link #definitions} map. Will be constructed
     * only when first needed.
     */
    private transient Set names;

    /**
     * A linked list of informations about the replacements performed by {@link #substitutes}.
     * Those informations are used by {@link #parseObject(String,Class)} in order to adjust
     * {@linkplain ParseException#getErrorOffset error offset} in case of failure.
     */
    private transient Replacement replacements;

    /**
     * The initial offset of the line in process of being parsed. This is a helper field
     * for use by {@link AbstractConsole} only, in order to produce more accurate information in
     * case of {@link ParseException}. This field has no impact on the object returned as a result
     * of successful parsing.
     */
    transient int offset = 0;
    
    /**
     * Creates a new preprocessor that delegates the work to the specified parser.
     *
     * @param parser The WKT parser, usually a {@link Parser} object.
     */
    public Preprocessor(final Format parser) {
        this.parser = parser;
    }

    /**
     * Formats the specified object. This method delegates the work to the
     * {@linkplain #parser parser} given at construction time.
     *
     * @param object     The object to format.
     * @param toAppendTo Where the text is to be appended.
     * @param position   Identification of a field in the formatted text.
     * @return The string buffer passed in as {@code toAppendTo},
     *         with formatted text appended
     */
    public StringBuffer format(final Object        object,
                               final StringBuffer  toAppendTo,
                               final FieldPosition position)
    {
        return parser.format(object, toAppendTo, position);
    }

    /**
     * Parses the specified Well Know Text starting at the specified position.
     * The default implementation delegates the work to
     * <code>{@link #parseObject(String) parseObject}(wkt.substring(position.getIndex()))</code>.
     *
     * @param  wkt The text to parse.
     * @param  position The index of the first character to parse.
     * @return The parsed object, or {@code null} in case of failure.
     */
    public Object parseObject(final String wkt, final ParsePosition position) {
        /*
         * NOTE:  the other way around (parseObject(String) invoking
         * parseObject(String,ParsePosition) like the default Format
         * implementation) is not pratical. Among other problems, it
         * doesn't provide any accurate error message.
         */
        final int start = position.getIndex();
        try {
            return parseObject(wkt.substring(start));
        } catch (ParseException exception) {
            position.setIndex(start);
            position.setErrorIndex(exception.getErrorOffset() + start);
            return null;
        }
    }

    /**
     * Parses the specified Well Know Text without restriction on the expected type.
     * The default implementation delegates the work to
     * <code>{@link #parseObject(String,Class) parseObject}(wkt, Object.class)</code>.
     *
     * @param  wkt The text to parse.
     * @return The parsed object.
     * @throws ParseException if the text can't be parsed.
     */
    public Object parseObject(final String wkt) throws ParseException {
        try {
            return parseObject(wkt, Object.class);
        } catch (FactoryException cause) {
            final ParseException e = new ParseException(cause.getLocalizedMessage(), 0);
            e.initCause(cause);
            throw e;
        }
    }

    /**
     * Parses the specified text and ensure that the resulting object is of the specified type.
     * The text can be any of the following:
     * <BR>
     * <UL>
     *   <LI>A name declared in some previous call to
     *       <code>{@linkplain #addDefinition addDefinition}(name, ...)</code>.</LI>
     *   <LI>A Well Know Text, which may contains itself shortcuts declared in
     *       previous call to {@code addDefinition}. This text is given to
     *       the underlying {@link #parser}.</LI>
     *   <LI>Any services provided by subclasses. For example a subclass way recognize
     *       some authority code like {@code EPSG:6326}.</LI>
     * </UL>
     *
     * @param  text The text, as a name, a WKT to parse, or an authority code.
     * @param  type The expected type for the object to be parsed (usually a
     *         <code>{@linkplain CoordinateReferenceSystem}.class</code> or
     *         <code>{@linkplain MathTransform}.class</code>).
     * @return The object.
     * @throws ParseException if parsing the specified WKT failed.
     * @throws FactoryException if the object is not of the expected type.
     */
    public Object parseObject(String text, final Class type)
            throws ParseException, FactoryException
    {
        Object value;
        final Definition def = (Definition) definitions.get(text);
        if (def != null) {
            value = def.asObject;
            if (type.isAssignableFrom(value.getClass())) {
                return value;
            }
        } else if (!isIdentifier(text)) {
            /*
             * The specified string was not found in the definitions map. Try to parse it as a
             * WKT, but only if it contains more than a single word. This later condition exists
             * only in order to produces a more accurate error message (WKT parsing of a single
             * word is garantee to fail). In any case, the definitions map is not updated since
             * this method is not invoked from the SET instruction.
             */
            text  = substitute  (text);
            value = forwardParse(text);
            final Class actualType = value.getClass();
            if (type.isAssignableFrom(actualType)) {
                return value;
            }
            throw new FactoryException(Errors.format(ErrorKeys.ILLEGAL_CLASS_$2,
                      Utilities.getShortName(actualType), Utilities.getShortName(type)));
        }
        throw new NoSuchIdentifierException(Errors.format(ErrorKeys.NO_SUCH_AUTHORITY_CODE_$2,
                  Utilities.getShortName(type), text), text);
    }

    /**
     * Parses a WKT. This method delegates the work to the {@link #parser}, but
     * catch the exception in case of failure. The exception is rethrown with the
     * {@linkplain ParseException#getErrorIndex error index} adjusted in order to
     * point to the character in the original text (before substitutions).
     *
     * @param  text The WKT to parse.
     * @return The object.
     * @throws ParseException if the parsing failed.
     */
    private Object forwardParse(final String text) throws ParseException {
        try {
            return parser.parseObject(text);
        } catch (ParseException exception) {
            int shift = 0;
            int errorOffset = exception.getErrorOffset();
            for (Replacement r=replacements; r!=null; r=r.next) {
                if (errorOffset < r.lower) {
                    break;
                }
                if (errorOffset < r.upper) {
                    errorOffset = r.lower;
                    break;
                }
                shift += r.shift;
            }
            final ParseException adjusted = new ParseException(exception.getLocalizedMessage(),
                                                               errorOffset - shift);
            adjusted.setStackTrace(exception.getStackTrace());
            adjusted.initCause(exception.getCause());
            throw adjusted;
        }
    }

    /**
     * For every definition key found in the given string, substitute
     * the key by its value. The replacement will not be performed if
     * the key was found between two quotation marks.
     *
     * @param  text The string to process.
     * @return The string with all keys replaced by their values.
     */
    private String substitute(final String text) {
        Replacement last;
        replacements = last = new Replacement(0, 0, offset);
        StringBuffer buffer = null;
        for (final Iterator it=definitions.entrySet().iterator(); it.hasNext();) {
            final Map.Entry entry = (Map.Entry)  it.next();
            final String     name = (String)     entry.getKey();
            final Definition def  = (Definition) entry.getValue();
            int index = (buffer!=null) ? buffer.indexOf(name) : text.indexOf(name);
            while (index >= 0) {
                /*
                 * An occurence of the text to substitute was found. First, make sure
                 * that the occurence found is a full word  (e.g. if the occurence to
                 * search is "WGS84", do not accept "TOWGS84").
                 */
                final int upper = index + name.length();
                final CharSequence cs = (buffer!=null) ? (CharSequence)buffer : (CharSequence)text;
                if ((index==0           || !Character.isJavaIdentifierPart(cs.charAt(index-1))) &&
                    (upper==cs.length() || !Character.isJavaIdentifierPart(cs.charAt(upper))))
                {
                    /*
                     * Count the number of quotes before the text to substitute. If this
                     * number is odd, then the text is between quotes and should not be
                     * substituted.
                     */
                    int count = 0;
                    for (int scan=index; --scan>=0;) {
                        scan = (buffer!=null) ? buffer.lastIndexOf("\"", scan)
                                              :   text.lastIndexOf( '"', scan);
                        if (scan < 0) {
                            break;
                        }
                        count++;
                    }
                    if ((count & 1) == 0) {
                        /*
                         * An even number of quotes was found before the text to substitute.
                         * Performs the substitution and keep trace of this replacement in a
                         * chained list of 'Replacement' objects.
                         */
                        if (buffer == null) {
                            buffer = new StringBuffer(text);
                            assert buffer.indexOf(name, index) == index;
                        }
                        final String value = def.asString;
                        buffer.replace(index, upper, value);
                        final int change = value.length() - name.length();
                        last = last.next = new Replacement(index, index+value.length(), change);
                        index = buffer.indexOf(name, index + change);
                        // Note: it is okay to skip the text we just replaced, since the
                        //       'definitions' map do not contains nested definitions.
                        continue;
                    }
                }
                /*
                 * The substitution was not performed because the text found was not a word,
                 * or was between quotes. Search the next occurence.
                 */
                index += name.length();
                index = (buffer!=null) ? buffer.indexOf(name, index)
                                       : text  .indexOf(name, index);
            }
        }
        return (buffer!=null) ? buffer.toString() : text;
    }

    /**
     * Adds a predefined Well Know Text (WKT). The {@code value} argument given to this method
     * can contains itself other definitions specified in some previous calls to this method.
     *
     * @param  name The name for the definition to be added.
     * @param  value The Well Know Text (WKT) represented by the name.
     * @throws IllegalArgumentException if the name is invalid.
     * @throws ParseException if the WKT can't be parsed.
     */
    public void addDefinition(final String name, String value) throws ParseException {
        if (value==null || value.trim().length()==0) {
            throw new IllegalArgumentException(Errors.format(ErrorKeys.MISSING_WKT_DEFINITION));
        }
        if (!isIdentifier(name)) {
            throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_IDENTIFIER_$1, name));
        }
        value = substitute(value);
        final Definition newDef = new Definition(value, forwardParse(value));
        final Definition oldDef = (Definition) definitions.put(name, newDef);
    }

    /**
     * Removes a definition set in some previous call to
     * <code>{@linkplain #addDefinition addDefinition}(name, ...)</code>.
     *
     * @param name The name of the definition to remove.
     */
    public void removeDefinition(final String name) {
        definitions.remove(name);
    }

    /**
     * Returns an unmodifiable set which contains all definition's names given to the
     * <code>{@linkplain #addDefinition addDefinition}(name, ...)</code> method. The
     * elements in this set are sorted in alphabetical order.
     */
    public Set getDefinitionNames() {
        if (names == null) {
            names = Collections.unmodifiableSet(definitions.keySet());
        }
        return names;
    }

    /**
     * Prints to the specified stream a table of all definitions.
     * The content of this table is inferred from the values given to the
     * {@link #addDefinition} method.
     *
     * @param  out writer The output stream where to write the table.
     * @throws IOException if an error occured while writting to the output stream.
     */
    public void printDefinitions(final Writer out) throws IOException {
        final Locale locale = null;
        final Vocabulary resources = Vocabulary.getResources(locale);
        final TableWriter table = new TableWriter(out, " \u2502 ");
        table.setMultiLinesCells(true);
        table.writeHorizontalSeparator();
        table.write(resources.getString(VocabularyKeys.NAME));
        table.nextColumn();
        table.write(resources.getString(VocabularyKeys.TYPE));
        table.nextColumn();
        table.write(resources.getString(VocabularyKeys.DESCRIPTION));
        table.nextLine();
        table.writeHorizontalSeparator();
        for (final Iterator it=definitions.entrySet().iterator(); it.hasNext();) {
            final Map.Entry entry = (Map.Entry) it.next();
            final Object   object = ((Definition) entry.getValue()).asObject;
            table.write(String.valueOf(entry.getKey()));
            table.nextColumn();
            table.write(Utilities.getShortClassName(object));
            table.nextColumn();
            if (object instanceof IdentifiedObject) {
                table.write(((IdentifiedObject) object).getName().getCode());
            }
            table.nextLine();
        }
        table.writeHorizontalSeparator();
        table.flush();
    }

    /**
     * Returns {@code true} if the specified text is a valid identifier.
     */
    private static boolean isIdentifier(final String text) {
        for (int i=text.length(); --i>=0;) {
            final char c = text.charAt(i);
            if (!Character.isJavaIdentifierPart(c) && c!=':') {
                return false;
            }
        }
        return true;
    }

    /**
     * An entry for the {@link Console#definitions} map. This entry contains a definition
     * as a well know text (WKT), and the parsed value for this WKT (usually a
     * {@linkplain CoordinateReferenceSystem} or a {@linkplain MathTransform} object).
     */
    private static final class Definition implements Serializable {
        /**
         * The definition as a string. This string should not contains anymore
         * shortcut to substitute by an other WKT (i.e. compound definitions
         * must be resolved before to construct a {@code Definition} object).
         */
        public final String asString;

        /**
         * The definition as an object (usually a {@linkplain CoordinateReferenceSystem}
         * or a {@linkplain MathTransform} object).
         */
        public final Object asObject;

        /**
         * Constructs a new definition.
         */
        public Definition(final String asString, final Object asObject) {
            this.asString = asString;
            this.asObject = asObject;
        }
    }

    /**
     * Contains informations about the index changes induced by a replacement in a string.
     * All index refer to the string <strong>after</strong> the replacement. The substring
     * at index between {@link #lower} inclusive and {@link #upper} exclusive is the replacement
     * string. The {@link #shift} is the difference between the replacement substring length and
     * the replaced substring length.
     */
    private static final class Replacement {
        /** The lower index in the target string, inclusive. */ public final int  lower;
        /** The upper index in the target string, exclusive. */ public final int  upper;
        /** The shift from source string to target string.   */ public final int  shift;
        /** The next element in the linked list.             */ public Replacement next;

        /** Constructs a new index shift initialized with the given values. */
        public Replacement(final int lower, final int upper, final int shift) {
            this.lower = lower;
            this.upper = upper;
            this.shift = shift;
        }

        /**
         * Returns a string representation for debugging purpose.
         */
        public String toString() {
            final StringBuffer buffer = new StringBuffer();
            for (Replacement r=this; r!=null; r=r.next) {
                if (r != this) {
                    buffer.append(", ");
                }
                buffer.append('[')
                      .append(r.lower)
                      .append("..")
                      .append(r.upper)
                      .append("] \u2192 ")
                      .append(r.shift);
            }
            return buffer.toString();
        }
    }
}
