/*
 *    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.filter;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import org.xml.sax.Attributes;


/**
 * Creates filters from FilterFilter, which reads in a SAX stream and passes
 * the appropriate messages here.
 *
 * @author Rob Hranac, Vision for New York<br>
 * @author Chris Holmes, TOPP
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/filter/FilterSAXParser.java $
 * @version $Id: FilterSAXParser.java 27862 2007-11-12 19:51:19Z desruisseaux $
 */
public class FilterSAXParser {
    /** The logger for the filter module. */
    private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.filter");
    
    /** The number of attributes to be found in a like filter */
    private static final int NUM_LIKE_ATTS = 3;

    /** The filter being currently constructed */
    private Filter curFilter = null;

    /** The current completion state of the filter */
    private String curState = "uninitialized";

    /** The short representation of this type of filter */
    private short filterType;

    /** factory for creating filters. */
    private FilterFactory ff;

    /**
     * the Attributes of the filter (only applicable to LIKE filters, I think)
     */
    private Map attributes = new HashMap();

    /**
     * Constructor which flags the operator as between.
     */
    public FilterSAXParser() {
    	this( FilterFactoryFinder.createFilterFactory() );
    }
    /** Constructor injdection */
    public FilterSAXParser( FilterFactory factory ){
    	ff = factory;
    }

    /** Setter injection */
    public void setFilterFactory( FilterFactory factory ){
    	ff = factory;
    }
    /**
     * Handles all incoming generic string 'messages,' including a message to
     * create the filter, based on the XML tag that represents the start of
     * the filter.
     *
     * @param filterType The string from the SAX filter.
     *
     * @throws IllegalFilterException Filter is illegal.
     */
    public void start(short filterType) throws IllegalFilterException {
        LOGGER.finest("starting filter type " + filterType);

        if ((filterType == AbstractFilter.FID) && !curState.equals("fid")) {
            LOGGER.finer("creating the FID filter");
            curFilter = ff.createFidFilter();
        } else if (AbstractFilter.isGeometryDistanceFilter(filterType)) {
            curFilter = ff.createGeometryDistanceFilter(filterType);
        } else if (AbstractFilter.isGeometryFilter(filterType)) {
            curFilter = ff.createGeometryFilter(filterType);
        } else if (filterType == AbstractFilter.BETWEEN) {
            curFilter = ff.createBetweenFilter();
        } else if (filterType == AbstractFilter.NULL) {
            curFilter = ff.createNullFilter();
        } else if (filterType == AbstractFilter.LIKE) {
            curFilter = ff.createLikeFilter();
        } else if (AbstractFilter.isCompareFilter(filterType)) {
            curFilter = ff.createCompareFilter(filterType);
        } else {
            throw new IllegalFilterException(
                "Attempted to start a new filter with invalid type: "
                + filterType);
        }

        curState = setInitialState(filterType);
        this.filterType = filterType;

        attributes = new HashMap();
    }

    /**
     * Handles all incoming generic string 'messages,' including a message to
     * create the filter, based on the XML tag that represents the start of
     * the filter.
     *
     * @param message The string from the SAX filter.
     *
     * @throws IllegalFilterException Filter is illegal.
     */
    public void value(String message) throws IllegalFilterException {
    }

    /**
     * Adds the passed in expression to the current filter.  Generally  created
     * by the ExpressionSAXParser.
     *
     * @param expression The value of the attribute for comparison.
     *
     * @throws IllegalFilterException if the expression does not match what the
     *         current filter is expecting.
     *
     * @task REVISIT: split this method up.
     */
    public void expression(Expression expression) throws IllegalFilterException {
        // Handle all filter compare states and expressions
        if (filterType == AbstractFilter.BETWEEN) {
            if (curState.equals("attribute")) {
                ((BetweenFilter) curFilter).addMiddleValue(expression);
                curState = "LowerBoundary";
            } else if (curState.equals("LowerBoundary")) {
                ((BetweenFilter) curFilter).addLeftValue(expression);
                curState = "UpperBoundary";
            } else if (curState.equals("UpperBoundary")) {
                ((BetweenFilter) curFilter).addRightValue(expression);
                curState = "complete";
            } else {
                throw new IllegalFilterException(
                    "Got expression for Between Filter in illegal state: "
                    + curState);
            }
        } else if (AbstractFilter.isCompareFilter(filterType)) {
            if (curState.equals("leftValue")) {
                ((CompareFilter) curFilter).addLeftValue(expression);
                curState = "rightValue";
            } else if (curState.equals("rightValue")) {
                ((CompareFilter) curFilter).addRightValue(expression);
                curState = "complete";
            } else {
                throw new IllegalFilterException(
                    "Got expression for Compare Filter in illegal state: "
                    + curState);
            }
        } else if (filterType == AbstractFilter.NULL) {
            if (curState.equals("attribute")) {
                ((NullFilter) curFilter).nullCheckValue(expression);
                curState = "complete";
            } else {
                throw new IllegalFilterException(
                    "Got expression for Null Filter in illegal state: "
                    + curState);
            }
        } else if (AbstractFilter.isGeometryFilter(filterType)) {
            if (curState.equals("leftValue")) {
                ((GeometryFilter) curFilter).addLeftGeometry(expression);
                curState = "rightValue";
            } else if (curState.equals("rightValue")) {
                ((GeometryFilter) curFilter).addRightGeometry(expression);

                if (AbstractFilter.isGeometryDistanceFilter(filterType)) {
                    curState = "distance";
                } else {
                    curState = "complete";
                }

                LOGGER.finer("expression called on geometry, curState = "
                    + curState);
            } else {
                throw new IllegalFilterException(
                    "Got expression for Geometry Filter in illegal state: "
                    + curState);
            }
        } else if (filterType == AbstractFilter.LIKE) {
            if (curState.equals("attribute")) {
                ((LikeFilter) curFilter).setValue(expression);
                curState = "pattern";
            } else if (curState.equals("pattern")) {
                if (attributes.size() != NUM_LIKE_ATTS) {
                    throw new IllegalFilterException(
                        "Got wrong number of attributes (expecting 3): "
                        + attributes.size() + "\n" + attributes);
                }

                String wildcard = (String) attributes.get("wildCard");
                
                if ( (wildcard == null) || (wildcard.length() != 1) )
                {
                	throw new IllegalFilterException("like filter -- required attribute 'wildCard' missing or not exactly 1 char long.  Capitalization?");                	
                }
                String singleChar = (String) attributes.get("singleChar");
                
                if ( (singleChar == null)|| (singleChar.length() != 1) )
                {
                   	throw new IllegalFilterException("like filter -- required attribute 'singleChar' missing  or not exactly 1 char long.  Capitalization?");                	
                }
                
                String escapeChar = (String) attributes.get("escape");
                if (escapeChar == null) //totally against spec, but...
                	 escapeChar = (String) attributes.get("escapeChar");
                
                if ( (escapeChar == null)|| (escapeChar.length() != 1) )
                {
                   	throw new IllegalFilterException("like filter -- required attribute 'escape' missing  or not exactly 1 char long.  Capitalization?");                	
                }
                
                LOGGER.fine("escape char is " + escapeChar);

              
                ((LikeFilter) curFilter).setPattern(expression, wildcard,
                    singleChar, escapeChar);
                curState = "complete";
            } else {
                throw new IllegalFilterException(
                    "Got expression for Like Filter in illegal state: "
                    + curState);
            }
        }

        LOGGER.finer("current state (end): " + curState);
    }

    /**
     * Creates the filter held in the parser.
     *
     * @return The current filter to be created by this parser.
     *
     * @throws IllegalFilterException If called before the filter is in a
     *         complete state.
     */
    public Filter create() throws IllegalFilterException {
        if (isComplete()) {
            LOGGER.finer("complete called, state = " + curState);
            curState = "complete"; //added by cholmes fid bug.

            return curFilter;
        } else {
            throw new IllegalFilterException(
                "Got to the end state of an incomplete filter, current"
                + " state is " + curState);
        }
    }

    /**
     * Sets the state that shall be expected next based on the fitlerType. So
     * if a between, null or like is the currentFilter then attribute should
     * be next, if an fid filter then fid should be next.  If it's a
     * comparison, geometry or not, then leftValue should be next.
     *
     * @param filterType An AbstractFilter short of the filter type.
     *
     * @return the string of what state should come next.
     *
     * @throws IllegalFilterException if the filter type is not recognized.
     */
    private static String setInitialState(short filterType)
        throws IllegalFilterException {
        if ((filterType == AbstractFilter.BETWEEN)
                || (filterType == AbstractFilter.NULL)
                || (filterType == AbstractFilter.LIKE)) {
            return "attribute";
        } else if ((filterType == AbstractFilter.FID)) {
            return "fid";
        } else if ((AbstractFilter.isCompareFilter(filterType))
                || (AbstractFilter.isGeometryFilter(filterType))) {
            return "leftValue";
        } else {
            throw new IllegalFilterException("Filter type: " + filterType
                + " is not recognized");
        }
    }

    /**
     * This sets the distance for a GeometryDistanceFilter.  It currently
     * ignores the units, and attempts to convert the distance to a double.
     *
     * @param distance the distance - should be a string of a double.
     * @param units a reference to a units dictionary.
     *
     * @throws IllegalFilterException if the distance string can not be
     *         converted to a double.
     *
     * @task TODO: Implement units, probably with org.geotools.units package
     *       and a special distance class in the filter package.  It would be
     *       nice if the distance class could get any type of units, like it
     *       would handle the conversion.
     */
    public void setDistance(String distance, String units)
        throws IllegalFilterException {
        LOGGER.finer("set distance called, current state is " + curState);

        if (curState.equals("distance")) {
            try {
                double distDouble = Double.parseDouble(distance);
                ((GeometryDistanceFilter) curFilter).setDistance(distDouble);
                curState = "complete";
            } catch (NumberFormatException nfe) {
                throw new IllegalFilterException("could not parse distance: "
                    + distance + " to a double");
            }
        } else {
            throw new IllegalFilterException(
                "Got distance for Geometry Distance Filter in illegal state: "
                + curState + ", geometry and property should be set first");
        }
    }

    /**
     * Sets the filter attributes.  Called when attributes are encountered by
     * the filter filter.  Puts them in a hash map by thier name and  value.
     *
     * @param atts the attributes to set.
     */
    public void setAttributes(Attributes atts) {
        LOGGER.finer("got attribute: " + atts.getLocalName(0) + ", "
            + atts.getValue(0));
        LOGGER.finer("current state: " + curState);

        if (curState.equals("fid")) {
            LOGGER.finer("is a fid");
            ((FidFilter) curFilter).addFid(atts.getValue(0));
            LOGGER.finer("added fid");
        } else {
            for (int i = 0; i < atts.getLength(); i++) {
                this.attributes.put(atts.getLocalName(i), atts.getValue(i));
            }
        }
    }

    /**
     * Indicates that the filter is in a complete state (ready to be created.)
     *
     * @return <tt>true</tt> if the current state is either complete or fid
     */
    private boolean isComplete() {
        return (curState.equals("complete") || curState.equals("fid"));
    }
}
