/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2002-2006, GeoTools Project Managment Committee (PMC)
 *    
 *    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.gml;

import java.util.StringTokenizer;
import java.util.logging.Logger;

import org.xml.sax.SAXException;


/**
 * LEVEL1 saxGML4j GML filter: Sends basic alerts for GML types to
 * GMLFilterGeometry.
 * 
 * <p>
 * This filter separates and passes GML events to a GMLHandlerGeometry. The
 * main simplification that it performs is to pass along coordinates as an
 * abstracted method call, regardless of their notation in the GML (Coord vs.
 * Coordinates).  This call turns the coordinates into doubles and makes sure
 * that it distinguishes between 2 and 3 value coordinates.
 * </p>
 * 
 * <p>
 * The filter also handles some more subtle processing, including handling
 * different delimiters (decimal, coordinate, tuple) that may be used by more
 * outlandish GML generators.
 * </p>
 * 
 * <p></p>
 *
 * @author Rob Hranac, Vision for New York
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/gml/GMLFilterDocument.java $
 * @version $Id: GMLFilterDocument.java 27862 2007-11-12 19:51:19Z desruisseaux $
 */
public class GMLFilterDocument extends org.xml.sax.helpers.XMLFilterImpl {
    /** The logger for the GML module */
    private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.gml");

    // Static Globals to handle some expected elements

    /** GML namespace string */
    private static final String GML_NAMESPACE = "http://www.opengis.net/gml";

    /** Coord name */
    private static final String COORD_NAME = "coord";

    /** Coordinates name */
    private static final String COORDINATES_NAME = "coordinates";

    /** X Coordinate name */
    private static final String X_NAME = "X";

    /** Y Coordinate name */
    private static final String Y_NAME = "Y";

    /** Z Coordinate name */
    private static final String Z_NAME = "Z";

    /** Sub geometry elements that may be passed in GML */
    private static final java.util.Collection SUB_GEOMETRY_TYPES = new java.util.Vector(java.util.Arrays
            .asList(new String[] { "outerBoundaryIs", "innerBoundaryIs" }));

    /** Base geometry elements that may be passed in GML */
    private static final java.util.Collection BASE_GEOMETRY_TYPES = new java.util.Vector(java.util.Arrays
            .asList(new String[] {
                    "Point", "LineString", "Polygon", "LinearRing", "Box",
                    "MultiPoint", "MultiLineString", "MultiPolygon",
                    "GeometryCollection"
                }));

    /** Added by Sean Geoghegan to store character data chunks */
    private StringBuffer buffer = new StringBuffer();

    /** Parent of the filter: must implement GMLHandlerGeometry. */
    private GMLHandlerGeometry parent;

    /** Handles all coordinate parsing. */
    private CoordinateReader coordinateReader = new CoordinateReader();

    /** Whether or not this parser should consider namespaces. */
    private boolean namespaceAware = true;

    /**
     * Constructor with parent.
     *
     * @param parent Parent of the filter: must implement GMLHandlerGeometry.
     */
    public GMLFilterDocument(GMLHandlerGeometry parent) {
        super();
        LOGGER.entering("GMLFilterDocument", "new", parent);
        this.parent = parent;
        LOGGER.exiting("GMLFilterDocument", "new");
    }

    /**
     * Checks for GML element start and - if not a coordinates element - sends
     * it directly on down the chain to the appropriate parent handler.  If it
     * is a coordinates (or coord) element, it uses internal methods to set
     * the current state of the coordinates reader appropriately.
     * 
     * <p>
     * Modified by Sean Geoghegan to create new StringBuffers when  entering a
     * coord or coordinate element.
     * </p>
     *
     * @param namespaceURI The namespace of the element.
     * @param localName The local name of the element.
     * @param qName The full name of the element, including namespace prefix.
     * @param atts The element attributes.
     *
     * @throws SAXException Some parsing error occurred while reading
     *         coordinates.
     */
    public void startElement(String namespaceURI, String localName,
        String qName, org.xml.sax.Attributes atts) throws SAXException {
        LOGGER.entering("GMLFilterDocument", "startElement",
            new Object[] { namespaceURI, localName, qName, atts });

        /* if at a GML element, do some checks to determine
         * how to handle the element
         */
        if (namespaceURI != null && namespaceURI.equals(GML_NAMESPACE)) {
            // if geometry, pass it on down the filter chain
            if (BASE_GEOMETRY_TYPES.contains(localName)) {
                parent.geometryStart(localName, atts);
            } else if (SUB_GEOMETRY_TYPES.contains(localName)) {
                parent.geometrySub(localName);
            } else if (COORDINATES_NAME.equals(localName)) {
                // if coordinate, set one of the internal coordinate methods
                coordinateReader.insideCoordinates(true, atts);
                buffer = new StringBuffer();
            } else if (COORD_NAME.equals(localName)) {
                coordinateReader.insideCoord(true);
                buffer = new StringBuffer();
            } else if (X_NAME.equals(localName)) {
                buffer = new StringBuffer();
                coordinateReader.insideX(true);
            } else if (Y_NAME.equals(localName)) {
                buffer = new StringBuffer();
                coordinateReader.insideY(true);
            } else if (Z_NAME.equals(localName)) {
                buffer = new StringBuffer();
                coordinateReader.insideZ(true);
            } else {
                parent.startElement(namespaceURI, localName, qName, atts);
            }
        } else {
            /* all non-GML elements passed on down the filter chain without
             * modification
             */
            parent.startElement(namespaceURI, localName, qName, atts);
        }

        LOGGER.exiting("GMLFilterDocument", "startElement");
    }

    /**
     * Reads the only internal characters read by pure GML parsers, which are
     * coordinates.  These coordinates are sent to the coordinates reader
     * class, which interprets them appropriately, depending on its current
     * state.
     * 
     * <p>
     * Modified by Sean Geoghegan to append character data to buffer when
     * inside a coordinate or coord element.  SAX doesn't guarentee that all
     * the character data of an element will be passed to the character method
     * in one call, it may be split up into chunks.
     * </p>
     *
     * @param ch Raw coordinate string from the GML document.
     * @param start Beginning character position of raw coordinate string.
     * @param length Length of the character string.
     *
     * @throws SAXException Some parsing error occurred while reading
     *         coordinates.
     */
    public void characters(char[] ch, int start, int length)
        throws SAXException {
        LOGGER.entering("GMLFilterDocument", "characters",
            new Object[] { ch, new Integer(start), new Integer(length) });

        /* the methods here read in both coordinates and coords and
         * take the grunt-work out of this task for geometry handlers
         * see the documentation for CoordinatesReader to see what this entails
         */
        String rawCoordinates = new String(ch, start, length);

        /* determines how to read coordinates, depending on
         * what element we are currently inside
         */
        if (coordinateReader.insideCoordinates()) {
            buffer.append(rawCoordinates);

            //coordinateReader.readCoordinates(rawCoordinates);
        } else if (coordinateReader.insideCoord()) {
            buffer.append(rawCoordinates);

            //coordinateReader.readCoord(rawCoordinates);
        } else {
            /* all non-coordinate data passed on down the
             * filter chain without modification
             */
            parent.characters(ch, start, length);
        }

        LOGGER.exiting("GMLFilterDocument", "characters");
    }

    /**
     * Checks for GML element end and - if not a coordinates element -  sends
     * it directly on down the chain to the appropriate parent handler.  If it
     * is a coordinates (or coord) element,  it uses internal methods to set
     * the current state of the coordinates reader appropriately.
     * 
     * <p>
     * Modified by Sean Geoghegan. When we reach the end of a coord or
     * coordinate element, then the buffer is passed to the handler for
     * processing.
     * </p>
     *
     * @param namespaceURI The namespace of the element.
     * @param localName The local name of the element.
     * @param qName The full name of the element, including namespace prefix.
     *
     * @throws SAXException Some parsing error occurred while  reading
     *         coordinates.
     */
    public void endElement(String namespaceURI, String localName, String qName)
        throws SAXException {
        LOGGER.entering("GMLFilterDocument", "endElement",
            new Object[] { namespaceURI, localName, qName });

        /* if leaving a GML element, handle and pass to appropriate
         * internal or external method
         */
        if (namespaceURI.equals(GML_NAMESPACE) || !namespaceAware) {
            // if geometry, pass on down the chain to appropriate handlers
            if (BASE_GEOMETRY_TYPES.contains(localName)) {
                parent.geometryEnd(localName);
            } else if (SUB_GEOMETRY_TYPES.contains(localName)) {
                parent.geometrySub(localName);
            } else if (COORDINATES_NAME.equals(localName)) {
                // Convert the string buffer to a string and process the
                // coordinate, then end the coordinate status in the handler.
                coordinateReader.readCoordinates(buffer.toString());
                coordinateReader.insideCoordinates(false);
            } else if (COORD_NAME.equals(localName)) {
                coordinateReader.readCoord(buffer.toString());
                coordinateReader.insideCoord(false);
            } else if (X_NAME.equals(localName)) {
                coordinateReader.readCoord(buffer.toString());
                coordinateReader.insideX(false);
            } else if (Y_NAME.equals(localName)) {
                coordinateReader.readCoord(buffer.toString());
                coordinateReader.insideY(false);
            } else if (Z_NAME.equals(localName)) {
                coordinateReader.readCoord(buffer.toString());
                coordinateReader.insideZ(false);
            } else if (!namespaceAware) {
                /* if not namespace aware, then just pass element through;
                 * otherwise, there is some error in the GML
                 */
                parent.endElement(namespaceURI, localName, qName);
            } else {
                parent.endElement(namespaceURI, localName, qName);
            }

            //else { throw new SAXException("Unrecognized GML element."); }
        } else {
            /* all non-GML elements passed on down the filter chain
             * without modification
             */
            parent.endElement(namespaceURI, localName, qName);
        }

        LOGGER.exiting("GMLFilterDocument", "endElement");
    }

    /**
     * Simplifies the parsing process for GML coordinate elements.
     * 
     * <p>
     * One of the more annoying features of GML (from a SAX parsing
     * perspective) is the dual coord and coordinate representation of
     * coordinates. To further complicate the matter, delimiters for the
     * coordinates element are quite flexible.  This class hides all that
     * nasty complexity beneath a benign exterior and greatly reduces the
     * complexity of the GMLFilterDocument code.
     * </p>
     */
    private class CoordinateReader {
        /** Flag for indicating not inside any tag. */
        private static final int NOT_INSIDE = 0;

        /** Flag for indicating inside coord tag. */
        private static final int INSIDE_COORD = 1;

        /** Flag for indicating inside coordinates tag. */
        private static final int INSIDE_COORDINATES = 2;

        /** Flag for indicating inside X tag. */
        private static final int INSIDE_X = 1;

        /** Flag for indicating inside Y tag. */
        private static final int INSIDE_Y = 2;

        /** Flag for indicating inside Z tag. */
        private static final int INSIDE_Z = 3;

        /** Remembers where we are inside the GML coordinate stream. */
        private int insideOuterFlag = NOT_INSIDE;

        /** Remembers where we are inside the GML coordinate stream. */
        private int insideInnerFlag = NOT_INSIDE;

        /** Remembers last X coordinate read. */
        private Double x = new Double(Double.NaN);

        /** Remembers last Y coordinate read. */
        private Double y = new Double(Double.NaN);

        /** Remembers last Z coordinate read. */
        private Double z = new Double(Double.NaN);

        /**
         * Stores requested delimiter for coordinate  separation; default = ','
         */
        private String coordinateDelimeter = ",";

        /** Stores requested delimiter for tuple separation; default = ' ' */
        private String tupleDelimeter = " ";

        /** Stores requested delimiter for decimal separation; default = '.' */
        private StringBuffer decimalDelimeter = new StringBuffer(".");

        /**
         * Remembers whether or not the standard decimal is used, to speed up
         * parsing.
         */
        private boolean standardDecimalFlag = true;

        /**
         * Empty constructor.
         */
        public CoordinateReader() {
        }

        /**
         * Reads raw coordinates from the GML and returns them to the parent as
         * neat functions.
         *
         * @param coordinateString Raw coordinate string from the GML document.
         *
         * @throws SAXException Some parsing error occurred while reading
         *         coordinates.
         */
        public void readCoordinates(String coordinateString)
            throws SAXException {
            /* if non-standard delimiter, replace it with
             * standard ',' through the entire string
             */
            if (!standardDecimalFlag) {
                coordinateString = coordinateString.replace(decimalDelimeter
                        .charAt(0), '.');
            }

            // separate tuples and loop through the set
            StringTokenizer coordinateSets = new StringTokenizer(coordinateString
                    .trim(), tupleDelimeter);
            StringTokenizer coordinates;

            // loop through each of the coordinate sets.
            // Depending on the number of coordinates found,
            // call the correct parent coordinate class
            while (coordinateSets.hasMoreElements()) {
                coordinates = new StringTokenizer(coordinateSets.nextToken(),
                        coordinateDelimeter);
                x = new Double(coordinates.nextToken().trim());
                y = new Double(coordinates.nextToken().trim());

                if (coordinates.hasMoreElements()) {
                    z = new Double(coordinates.nextToken().trim());
                    parent.gmlCoordinates(x.doubleValue(), y.doubleValue(),
                        z.doubleValue());
                } else {
                    parent.gmlCoordinates(x.doubleValue(), y.doubleValue());
                }
            }
        }

        /**
         * Reads a coord string.  Note that this string is actually inside an
         * X, Y, Z tag and is not directly returned by the parent function,
         * unlike the readCoordinates method.
         *
         * @param coordString The raw coordinate string from the XML document.
         */
        public void readCoord(String coordString) {
            // if non-standard delimiter, replace it with standard ',' 
            // through the entire string
            if (!standardDecimalFlag) {
                coordString = coordString.replace(decimalDelimeter.charAt(0),
                        '.');
            }

            // determine which coord string we are inside
            // set internal x,y,z values depending on the return
            switch (insideInnerFlag) {
            case INSIDE_X:
                x = new Double(coordString.trim());

                break;

            case INSIDE_Y:
                y = new Double(coordString.trim());

                break;

            case INSIDE_Z:
                z = new Double(coordString.trim());

                break;

            default:
                break;
            }
        }

        /**
         * Sets an entrance into a coordinates element.
         *
         * @param isInside Sets whether or not we are  inside a coordinates
         *        tag.
         * @param atts Passes the coordinates tag attributes.
         */
        public void insideCoordinates(boolean isInside,
            org.xml.sax.Attributes atts) {
            this.insideCoordinates(isInside);
        }

        /**
         * Sets an entrance into a coordinates element.
         *
         * @param isInside Sets whether or not we are inside a coordinates tag.
         */
        public void insideCoordinates(boolean isInside) {
            if (isInside) {
                insideOuterFlag = INSIDE_COORDINATES;
            } else {
                insideOuterFlag = NOT_INSIDE;
            }
        }

        /**
         * Sets an entrance into a coord element.
         *
         * @param isInside Sets whether or not we are inside a coord tag.
         *
         * @throws SAXException if error occurs in reading
         */
        public void insideCoord(boolean isInside) throws SAXException {
            // if entering coord tag, simply set our internal flag for this
            if (isInside) {
                insideOuterFlag = INSIDE_COORD;
            } else {
                // if leaving coord tag, send coordinates to parent and 
                // set all internal values to null equivalent.
                // if coordinates exist, send on down the filter chain
                // otherwise, throw an exception
                if ((!x.isNaN()) && (!y.isNaN()) && (z.isNaN())) {
                    parent.gmlCoordinates(x.doubleValue(), y.doubleValue());
                } else if ((!x.isNaN()) && (!y.isNaN()) && (!z.isNaN())) {
                    parent.gmlCoordinates(x.doubleValue(), y.doubleValue(),
                        z.doubleValue());
                }

                //else {
                x = new Double(Double.NaN);
                y = new Double(Double.NaN);
                z = new Double(Double.NaN);
                insideOuterFlag = NOT_INSIDE;
            }
        }

        /**
         * Sets an entrance into an X element.
         *
         * @param isInside Sets whether or not we are inside an X tag.
         */
        public void insideX(boolean isInside) {
            if (isInside) {
                insideInnerFlag = INSIDE_X;
            } else {
                insideInnerFlag = NOT_INSIDE;
            }
        }

        /**
         * Sets an entrance into a Y element.
         *
         * @param isInside Sets whether or not we are inside a Y tag.
         */
        public void insideY(boolean isInside) {
            if (isInside) {
                insideInnerFlag = INSIDE_Y;
            } else {
                insideInnerFlag = NOT_INSIDE;
            }
        }

        /**
         * Sets an entrance into a Z element.
         *
         * @param isInside Sets whether or not we are inside a Z tag.
         */
        public void insideZ(boolean isInside) {
            if (isInside) {
                insideInnerFlag = INSIDE_Z;
            } else {
                insideInnerFlag = NOT_INSIDE;
            }
        }

        /**
         * Queries whether or not we are inside a coordinates element.
         *
         * @return true/false
         */
        public boolean insideCoordinates() {
            return insideOuterFlag == INSIDE_COORDINATES;
        }

        /**
         * Queries whether or not we are inside a coord element.
         *
         * @return true/false
         */
        public boolean insideCoord() {
            return insideOuterFlag == INSIDE_COORD;
        }
    }
}
