/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-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; 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.filter;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import org.geotools.data.oracle.sdo.SDO;
import org.geotools.data.DataSourceException;
import org.opengis.filter.ExcludeFilter;
import org.opengis.filter.Id;
import org.opengis.filter.IncludeFilter;
import org.opengis.filter.PropertyIsBetween;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.PropertyIsNull;
import org.opengis.filter.spatial.BBOX;
import org.opengis.filter.spatial.Beyond;
import org.opengis.filter.spatial.Contains;
import org.opengis.filter.spatial.Crosses;
import org.opengis.filter.spatial.DWithin;
import org.opengis.filter.spatial.Disjoint;
import org.opengis.filter.spatial.Equals;
import org.opengis.filter.spatial.Intersects;
import org.opengis.filter.spatial.Overlaps;
import org.opengis.filter.spatial.Touches;
import org.opengis.filter.spatial.Within;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateSequence;
import com.vividsolutions.jts.geom.CoordinateSequenceFactory;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;

/**
 * Encodes Geometry filters into valid oracle SDO statements.
 * 
 * <p>
 * At this stage it only supports the GEOMETRY_BBOX types.
 * </p>
 * 
 * <p>
 * Encoded filters get written to the protected Writer called <code>out</code>
 * </p>
 *
 * @author $Author: seangeo $
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/oracle-spatial/src/main/java/org/geotools/filter/SQLEncoderOracle.java $
 * @version $Id: SQLEncoderOracle.java 27862 2007-11-12 19:51:19Z desruisseaux $
 */
public class SQLEncoderOracle extends SQLEncoder {

    /** Logger - for logging */
    private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(
            "org.geotools.filter.SQLEncoderOracle");

    /** Contains filter type to SDO_RELATE mask type mappings */
    private static final Map SDO_RELATE_MASK_MAP = new HashMap();

    /** The standard SQL multicharacter wild card. */
    private static final String SQL_WILD_MULTI = "%";

    /** The standard SQL single character wild card. */
    private static final String SQL_WILD_SINGLE = "_";

    /** Default tolerance for spatial queries. */
    private static final String TOLERANCE = "0.001";

    static {
        SDO_RELATE_MASK_MAP.put(new Short(AbstractFilter.GEOMETRY_CONTAINS),
            "contains");
        SDO_RELATE_MASK_MAP.put(new Short(AbstractFilter.GEOMETRY_CROSSES),
            "overlapbydisjoint");
        SDO_RELATE_MASK_MAP.put(new Short(AbstractFilter.GEOMETRY_EQUALS),
            "equal");
        SDO_RELATE_MASK_MAP.put(new Short(AbstractFilter.GEOMETRY_OVERLAPS),
            "overlapbyintersect");
        SDO_RELATE_MASK_MAP.put(new Short(AbstractFilter.GEOMETRY_TOUCHES),
            "touch");
        SDO_RELATE_MASK_MAP.put(new Short(AbstractFilter.GEOMETRY_WITHIN),
            "inside");
        SDO_RELATE_MASK_MAP.put(new Short(AbstractFilter.GEOMETRY_DISJOINT),
            "anyinteract");
        
        //Ok, back to using these, as the not disjoint turned out to be a big
        //performance hit.  I would really like to see some solid testing on 
        //these though, as with a trivial case it really did not seem to work
        //right, not disjoint was giving different answers than anyinteract.
        SDO_RELATE_MASK_MAP.put(new Short(AbstractFilter.GEOMETRY_BBOX),
            "anyinteract");
        
        SDO_RELATE_MASK_MAP.put(new Short(AbstractFilter.GEOMETRY_INTERSECTS),
            "anyinteract");
    }

    /** The escaped version of the multiple wildcard for the REGEXP pattern. */
    //private String escapedWildcardMulti = "\W.\\*"; // "\\*"

    /** The escaped version of the single wildcard for the REGEXP pattern. */
    //private String escapedWildcardSingle = "\\.\\?";

    /**
     * The Spatial Reference System IDs  Keyed by ColumnName, value is Integer
     * SRID number
     */
    private Map srids;
    private String fidColumn;
    private String currentGeomColumnName = null;
    boolean inGeomFilter = false;

    public SQLEncoderOracle(String fidColumn, int defaultSRID) {
        this(new HashMap());
        this.fidColumn = fidColumn;
        srids.put(null, new Integer(defaultSRID));

        setSqlNameEscape("\"");
    }

    public SQLEncoderOracle(int defaultSRID) {
        this(null, new HashMap());
        srids.put(null, new Integer(defaultSRID));
    }

    /**
     * Creates a new SQLEncoderOracle with a specified SRID.
     *
     * @param fidColumn DOCUMENT ME!
     * @param srids The Spatial Reference ID to use when generating SDO SQL
     *        statements.
     */
    public SQLEncoderOracle(String fidColumn, Map srids) {
        currentGeomColumnName = null;
        this.fidColumn = fidColumn;
        this.srids = srids;

        Set geomCols = srids.keySet();

        if (geomCols.size() > 0) {
            currentGeomColumnName = (String) geomCols.iterator().next();
        }

        LOGGER.fine("SQLEncoderOracle: Geometric Column is: "
            + currentGeomColumnName);

        setSqlNameEscape("\"");
    }

    /**
     * Creates a new SQLEncoderOracle with a specified SRID.
     *
     * @param srids The Spatial Reference ID to use when generating SDO SQL
     *        statements.
     */
    public SQLEncoderOracle(Map srids) {
        this(null, srids);
    }

    /**
     * Sets the capabilities of this filter.
     *
     * @return FilterCapabilities for this Filter
     */
    protected FilterCapabilities createFilterCapabilities() {
        FilterCapabilities capabilities = new FilterCapabilities();

        capabilities.addType(FilterCapabilities.LOGICAL);
        capabilities.addAll(FilterCapabilities.LOGICAL_OPENGIS);
        capabilities.addType(FilterCapabilities.SIMPLE_COMPARISONS);
        capabilities.addAll(FilterCapabilities.SIMPLE_COMPARISONS_OPENGIS);
        capabilities.addType(FilterCapabilities.NULL_CHECK);
        capabilities.addType(PropertyIsNull.class);
        capabilities.addType(FilterCapabilities.BETWEEN);
        capabilities.addType(PropertyIsBetween.class);
        capabilities.addType(FilterCapabilities.FID);
        capabilities.addType(Id.class);
        capabilities.addType(FilterCapabilities.NONE);
        capabilities.addType(IncludeFilter.class);
        capabilities.addType(FilterCapabilities.ALL);
        capabilities.addType(ExcludeFilter.class);
        capabilities.addType(FilterCapabilities.LIKE);
        capabilities.addType(PropertyIsLike.class);
        capabilities.addType(FilterCapabilities.SPATIAL_BBOX);
        capabilities.addType(BBOX.class);
        capabilities.addType(FilterCapabilities.SPATIAL_CONTAINS);
        capabilities.addType(Contains.class);
        capabilities.addType(FilterCapabilities.SPATIAL_CROSSES);
        capabilities.addType(Crosses.class);
        capabilities.addType(FilterCapabilities.SPATIAL_DISJOINT);
        capabilities.addType(Disjoint.class);
        capabilities.addType(FilterCapabilities.SPATIAL_EQUALS);
        capabilities.addType(Equals.class);
        capabilities.addType(FilterCapabilities.SPATIAL_INTERSECT);
        capabilities.addType(Intersects.class);
        capabilities.addType(FilterCapabilities.SPATIAL_OVERLAPS);
        capabilities.addType(Overlaps.class);
        capabilities.addType(FilterCapabilities.SPATIAL_TOUCHES);
        capabilities.addType(Touches.class);
        capabilities.addType(FilterCapabilities.SPATIAL_WITHIN);
        capabilities.addType(Within.class);
        capabilities.addType(FilterCapabilities.SPATIAL_DWITHIN);
        capabilities.addType(DWithin.class);
        capabilities.addType(FilterCapabilities.SPATIAL_BEYOND);
        capabilities.addType(Beyond.class);

        return capabilities;
    }

    /**
     * Reverting back to just using anyinteract, as Thijs says this is a bad
     * performance hit
     * This is a special case for bbox and intersects filters, as the former
     * using of 'anyinteract' does not seem to be exactly a not disjoint, 
     * which is what is needed according to ogc specs.  
     *
     * @param geomFilter DOCUMENT ME!
     *
     * @throws IOException DOCUMENT ME!
     */
    /* private void doNotDisjointFilter(GeometryFilter geomFilter)
        throws IOException {
        //String mask = (String) SDO_RELATE_MASK_MAP.get(new Short(
        //            geomFilter.getFilterType()));
        Expression left = geomFilter.getLeftGeometry();
        Expression right = geomFilter.getRightGeometry();

        if (((left != null) || (currentGeomColumnName != null))
                && (right != null)) {
            inGeomFilter = true;
            out.write("NOT SDO_RELATE(");

            if (left != null) {
                left.accept(this);
            } else {
                out.write(currentGeomColumnName);
            }

            out.write(",");
            right.accept(this);
            out.write(",'mask=disjoint querytype=WINDOW') = 'TRUE' ");
            inGeomFilter = false;
        } else {
            LOGGER.warning("Invalid filter. Cannot have a Geometry filter "
                + "with only one expression.");
        }        
	}*/

    private void doSdoRelate(GeometryFilter geomFilter)
        throws IOException {
        String mask = (String) SDO_RELATE_MASK_MAP.get(new Short(
                    geomFilter.getFilterType()));
        AttributeExpression attExpr;
        LiteralExpression geomExpr;
        Expression left = geomFilter.getLeftGeometry();
        Expression right = geomFilter.getRightGeometry();
        if (left instanceof AttributeExpression &&
            right instanceof LiteralExpression) {
            attExpr = (AttributeExpression) left;
            geomExpr = (LiteralExpression) right;
        } else if (right instanceof AttributeExpression &&
                   left instanceof LiteralExpression) {
            attExpr = (AttributeExpression) right;
            geomExpr = (LiteralExpression) left;
        } else {
            String err = "Oracle currently supports one geometry and one " +
                "attribute expr.  You gave: " + left + ", " + right;
            throw new DataSourceException(err);
	}

        if (((attExpr != null) || (currentGeomColumnName != null))
                && (geomExpr != null) && (mask != null)) {
            inGeomFilter = true;
            out.write("SDO_RELATE(");

            if (attExpr != null) {
                attExpr.accept(this);
            } else {
                out.write("\"" + currentGeomColumnName + "\"");
            }

            out.write(",");
            geomExpr.accept(this);
            // for disjoint we ask for no interaction, anyinteract == false
            if(geomFilter.getFilterType() == AbstractFilter.GEOMETRY_DISJOINT) {
                out.write(",'mask=" + mask + " querytype=WINDOW') <> 'TRUE' ");
            } else {
                out.write(",'mask=" + mask + " querytype=WINDOW') = 'TRUE' ");
            }
            
            inGeomFilter = false;
        } else {
            LOGGER.warning("Invalid filter. Cannot have a Geometry filter "
                + "with only one expression.");
        }
    }

    /**
     * Performs a geometry distance filter, must be either a dwithin or a 
     * a beyond filter.  Uses the SDO_WITHIN_DISTANCE function, dwithin matches
     * for true, beyond for false.  
     * 
     * @param geomFilter the filter to use, must be a dwithin or beyond
     */
    private void doSdoDistance(GeometryDistanceFilter geomFilter)
        throws IOException {
       //  String mask = (String) SDO_RELATE_MASK_MAP.get(new Short(
       //             geomFilter.getFilterType()));
        //TODO: don't assume left and right are correct order, see doSdoRelate
        Expression left = geomFilter.getLeftGeometry();
        Expression right = geomFilter.getRightGeometry();
        double distance = geomFilter.getDistance();
	//only dwithin and beyond, dwithin matches true for sdo_within_distance
        boolean isDWithin = geomFilter.getFilterType() == 
            AbstractFilter.GEOMETRY_DWITHIN;
	String boolValue = isDWithin ? "TRUE" : "FALSE";

        if ((left != null) && (right != null)) {
            inGeomFilter = true;
            out.write("SDO_WITHIN_DISTANCE(");
            left.accept(this);
            out.write(",");
            right.accept(this);
            out.write(",'distance=" + distance + "') = '" + boolValue + "' ");
            inGeomFilter = false;
        } else {
            LOGGER.warning("Invalid filter for DWithin. Cannot have a Geometry filter "
                + "with only one expression.");
        }
    }

    /**
     * Converts JTS Geometry to a String version of a SDO Geometry.  This
     * should move to a utility class, as we now have more than  one class
     * using this (which is why it changed to public static)
     * 
     * TODO: Multi eometries
     *
     * @param geometry The JTS Geometry to convert.
     * @param srid DOCUMENT ME!
     *
     * @return A String representation of the SDO Geometry.
     */
    public static String toSDOGeom(Geometry geometry, int srid) {
        if (Point.class.isAssignableFrom(geometry.getClass())) {
            return toSDOGeom((Point) geometry, srid);
        } else if (LineString.class.isAssignableFrom(geometry.getClass())) {
            return toSDOGeom((LineString) geometry, srid);
        } else if (Polygon.class.isAssignableFrom(geometry.getClass())) {
            if(geometry.equals(geometry.getEnvelope())) {
                return toSDOGeom(geometry.getEnvelopeInternal(), srid);
            } else {
                return toSDOGeom((Polygon) geometry, srid);
            }
        } else if (MultiLineString.class.isAssignableFrom(geometry.getClass())) {
            return toSDOGeom((MultiLineString) geometry, srid);
        }
        else if (MultiPolygon.class.isAssignableFrom(geometry.getClass())) {
            return toSDOGeom((MultiPolygon) geometry, srid);
        }
        else {
            LOGGER.warning("Got a literal geometry that I can't handle: "
                + geometry.getClass().getName());

            return "";
        }
    }

    /**
     * TODO: Encode more then 1
     * @param line
     * @param srid
     */
    private static String toSDOGeom(MultiLineString line, int srid) {
         if( line.getNumGeometries() == 1 ){
        	 return toSDOGeom( line.getGeometryN(0), srid);        	 
         }
         throw new UnsupportedOperationException("Cannot encode MultiLineString (yet)");
    }
    /**
     * TODO: Encode more then 1
     * @param line
     * @param srid
     */
    private static String toSDOGeom(MultiPolygon polygon, int srid) {
        if( polygon.getNumGeometries() == 1 ){
       	 return toSDOGeom( polygon.getGeometryN(0), srid);        	 
        }
        throw new UnsupportedOperationException("Cannot encode MultiPolygon (yet)");
   }
    /**
     * Converts a LineString Geometry in an SDO SQL geometry construction
     * statement.
     * 
     * <p>
     * 2D geometries is assumed. If higher dimensional geometries are used the
     * query will be encoded as a 2D geometry.
     * </p>
     *
     * @param line The line to encode.
     * @param srid DOCUMENT ME!
     *
     * @return An SDO SQL geometry object construction statement
     */
    private static String toSDOGeom(LineString line, int srid) {
        if (SDO.D(line) > 2) {
            LOGGER.warning("" + SDO.D(line)
                + " dimensioned geometry provided."
                + " This encoder only supports 2D geometries. The query will be constructed as"
                + " a 2D query.");
        }

        StringBuffer buffer = new StringBuffer("MDSYS.SDO_GEOMETRY(");

        buffer.append(SDO.D(line));
        buffer.append("002,");

        if (srid > 0) {
            LOGGER.fine("Using layer SRID: " + srid);
            buffer.append(srid);
        } else {
            LOGGER.fine("Using NULL SRID: ");
            buffer.append("NULL");
        }

        buffer.append(",NULL,MDSYS.SDO_ELEM_INFO_ARRAY(1,2,1),");
        buffer.append("MDSYS.SDO_ORDINATE_ARRAY(");

        Coordinate[] coordinates = line.getCoordinates();

        for (int i = 0; i < coordinates.length; i++) {
            buffer.append(coordinates[i].x);
            buffer.append(",");
            buffer.append(coordinates[i].y);

            if (i != (coordinates.length - 1)) {
                buffer.append(",");
            }
        }

        buffer.append("))");

        return buffer.toString();
    }

    /**
     * Converts a Point Geometry in an SDO SQL geometry construction statement.
     * 
     * <p>
     * 2D geometries is assumed. If higher dimensional geometries are used the
     * query will be encoded as a 2D geometry.
     * </p>
     *
     * @param point The point to encode.
     * @param srid DOCUMENT ME!
     *
     * @return An SDO SQL geometry object construction statement
     */
    private static String toSDOGeom(Point point, int srid) {
        if (SDO.D(point) > 2) {
            LOGGER.warning("" + SDO.D(point)
                + " dimensioned geometry provided."
                + " This encoder only supports 2D geometries. The query will be constructed as"
                + " a 2D query.");
        }

        StringBuffer buffer = new StringBuffer("MDSYS.SDO_GEOMETRY(");

        buffer.append(SDO.D(point));
        buffer.append("001,");

        if (srid > 0) {
            LOGGER.fine("Using layer SRID: " + srid);
            buffer.append(srid);
        } else {
            LOGGER.fine("Using NULL SRID: ");
            buffer.append("NULL");
        }

        buffer.append(",MDSYS.SDO_POINT_TYPE(");
        buffer.append(point.getX());
        buffer.append(",");
        buffer.append(point.getY());
        buffer.append(",NULL),NULL,NULL)");

        return buffer.toString();
    }

    /**
     * Converts a Polygon Geometry in an SDO SQL geometry construction
     * statement.
     * 
     * <p>
     * 2D geometries is assumed. If higher dimensional geometries are used the
     * query will be encoded as a 2D geometry.
     * </p>
     *
     * @param polygon The polygon to encode.
     * @param srid DOCUMENT ME!
     *
     * @return An SDO SQL geometry object construction statement
     */
    private static String toSDOGeom(Polygon polygon, int srid) {
        StringBuffer buffer = new StringBuffer();

        if (SDO.D(polygon) > 2) {
            LOGGER.warning("" + SDO.D(polygon)
                + " dimensioned geometry provided."
                + " This encoder only supports 2D geometries. The query will be constructed as"
                + " a 2D query.");
        }

        if (polygon.getExteriorRing() != null) {
            buffer.append("MDSYS.SDO_GEOMETRY(");
            buffer.append(SDO.D(polygon));
            buffer.append("003,");

            if (srid > 0) {
                LOGGER.fine("Using layer SRID: " + srid);
                buffer.append(srid);
            } else {
                LOGGER.fine("Using NULL SRID: ");
                buffer.append("NULL");
            }

            buffer.append(",NULL,MDSYS.SDO_ELEM_INFO_ARRAY(1,1003,1),");
            buffer.append("MDSYS.SDO_ORDINATE_ARRAY(");

	    CoordinateSequenceFactory fact = polygon.getFactory().getCoordinateSequenceFactory();
	    CoordinateSequence exterior = polygon.getExteriorRing().getCoordinateSequence();
	    CoordinateSequence coordSeq = SDO.counterClockWise(fact, exterior);

            for (int i = 0, size = coordSeq.size(); i < size; i++) {
		Coordinate cur = coordSeq.getCoordinate(i);
                buffer.append(cur.x);
                buffer.append(",");
                buffer.append(cur.y);

                if (i != (size - 1)) {
                    buffer.append(",");
                }
	    }

	    /* This could be expensive if coordSeq implementation is not an
	       an array.  Leaving in for now as I can't test, and this is
	       more likely to work right.
            Coordinate[] coordinates = coordSeq.toCoordinateArray();
            for (int i = 0; i < coordinates.length; i++) {
                buffer.append(coordinates[i].x);
                buffer.append(",");
                buffer.append(coordinates[i].y);

                if (i != (coordinates.length - 1)) {
                    buffer.append(",");
                }
		}*/

            buffer.append("))");
        } else {
            LOGGER.warning("No Exterior ring on polygon.  "
                + "This encode only supports Polygons with exterior rings.");
        }

        if (polygon.getNumInteriorRing() > 0) {
            LOGGER.warning("Polygon contains Interior Rings. "
                + "These rings will not be included in the query.");
        }

        return buffer.toString();
    }
    
    /**
     * Converts an Envelope in an SDO SQL geometry construction statement.
     *
     * @param envelope The envelope to encode.
     * @param srid DOCUMENT ME!
     *
     * @return An SDO SQL geometry object construction statement
     */
    private static String toSDOGeom(Envelope envelope, int srid) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("MDSYS.SDO_GEOMETRY(");
        buffer.append("2003,");

        if (srid > 0) {
            LOGGER.fine("Using layer SRID: " + srid);
            buffer.append(srid);
        } else {
            LOGGER.fine("Using NULL SRID: ");
            buffer.append("NULL");
        }

        buffer.append(",NULL,MDSYS.SDO_ELEM_INFO_ARRAY(1,1003,3),");
        buffer.append("MDSYS.SDO_ORDINATE_ARRAY(");
        buffer.append(envelope.getMinX());
        buffer.append(",");
        buffer.append(envelope.getMinY());
        buffer.append(",");
        buffer.append(envelope.getMaxX());
        buffer.append(",");
        buffer.append(envelope.getMaxY());
        buffer.append("))");

        return buffer.toString();
    }

    /**
     * Handles Geometry Filter encoding. Currently only supports the encoding
     * of GEOMETRY_BBOX filters. If a GEOMETRY_BBOX filter is encounter it
     * will be converted into an SDO_RELATE() function. If another filter is
     * found, nothing will happen.
     *
     * @param geomFilter The geometry filter to encode.
     *
     * @see org.geotools.filter.FilterVisitor#visit(org.geotools.filter.GeometryFilter)
     */
    public void visit(GeometryFilter geomFilter) {
        LOGGER.finer("Visiting a Geometry filter");

        try {
	    short filterType = geomFilter.getFilterType();
	    if ((filterType == AbstractFilter.GEOMETRY_DWITHIN)
	    || (filterType == AbstractFilter.GEOMETRY_BEYOND)){
                doSdoDistance((GeometryDistanceFilter)geomFilter);
		//} else if (filterType == AbstractFilter.GEOMETRY_INTERSECTS
		//|| filterType == AbstractFilter.GEOMETRY_BBOX) {
                //doNotDisjointFilter(geomFilter);
            } else 
            if (SDO_RELATE_MASK_MAP.get(new Short(geomFilter.getFilterType())) != null) {
                doSdoRelate(geomFilter);
            } else {
                LOGGER.warning("Unknown filter type: "
                    + geomFilter.getFilterType());
            }
        } catch (IOException e) {
            LOGGER.warning("IO Error exporting geometry filter");
        }
    }

    /**
     * Writes the SQL for the Like Filter.  Assumes the current java
     * implemented wildcards for the Like Filter: . for multi and .? for
     * single. And replaces them with the SQL % and _, respectively. Currently
     * does nothing, and should not be called, not included in the
     * capabilities.
     *
     * @param filter the Like Filter to be visited.
     *
     * @task TODO: LikeFilter doesn't work right...revisit this when it does.
     *       Need to think through the escape char, so it works right when
     *       Java uses one, and escapes correctly with an '_'.
     */
    public void visit(LikeFilter filter) {
        try {
            String pattern = filter.getPattern();
            
            String multi = "\\Q"+filter.getWildcardMulti()+"\\E"; 
            pattern = pattern.replaceAll( multi, SQL_WILD_MULTI);
            
            String single = "\\Q"+filter.getWildcardSingle()+"\\E";
            pattern = pattern.replaceAll( single, SQL_WILD_SINGLE);

            //pattern = pattern.replace('\\', ''); //get rid of java escapes.
            out.write("UPPER(");
            ((Expression) filter.getValue()).accept(this);
            out.write(") LIKE ");
            out.write("UPPER('" + pattern + "')");

            String esc = filter.getEscape();

            if (pattern.indexOf(esc) != -1) { //if it uses the escape char
                out.write(" ESCAPE " + "'" + esc + "'"); //this needs testing
            }

            //TODO figure out when to add ESCAPE clause, probably just for the
            // '_' char.
        } catch (java.io.IOException ioe) {
            LOGGER.warning("Unable to export filter" + ioe);
        }
    }

    /**
     * Converts a literal expression into a valid SDO object. Only handles
     * Literal Geometries, all other literals are passed up to the parent.
     *
     * @param literal The Literal expression to encode.
     *
     * @see org.geotools.filter.FilterVisitor#visit(org.geotools.filter.LiteralExpression)
     */
    public void visit(LiteralExpression literal) {
        if (literal.getType() == DefaultExpression.LITERAL_GEOMETRY) {
            Geometry geometry = (Geometry) literal.getLiteral();

            try {
                int srid = -1;
                Integer sridO = (Integer) srids.get(currentGeomColumnName);

                if (sridO == null) {
                    // try for default
                    sridO = (Integer) srids.get(null);
                }

                if (sridO != null) {
                    srid = sridO.intValue();
                }

                out.write(toSDOGeom(geometry, srid));
            } catch (IOException e) {
                LOGGER.warning("IO Error exporting Literal Geometry");
            }
        } else {
            // can't do it, send it off to the parent
            super.visit(literal);
        }
    }

    /**
     * DOCUMENT ME!
     *
     * @param filter
     *
     * @see org.geotools.filter.SQLEncoder#visit(org.geotools.filter.FidFilter)
     */
    public void visit(FidFilter filter) {
        if (fidColumn != null) {
            String[] fids = filter.getFids();
            LOGGER.finer("Exporting FID=" + Arrays.asList(fids));

            for (int i = 0; i < fids.length; i++) {
                try {
                    out.write(fidColumn);
                    out.write(" = '");

                    int pos;

                    if ((pos = fids[i].indexOf('.')) != -1) {
                        out.write(fids[i].substring(pos + 1));
                    } else {
                        out.write(fids[i]);
                    }

                    out.write("'");

                    if (i < (fids.length - 1)) {
                        out.write(" OR ");
                    }
                } catch (IOException e) {
                    LOGGER.warning("IO Error exporting FID Filter.");
                }
            }
        } else {
            super.visit(filter);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.geotools.filter.SQLEncoder#visit(org.geotools.filter.AttributeExpression)
     */
    public void visit(AttributeExpression ae) throws RuntimeException {
        super.visit(ae);

        if (inGeomFilter) {
            currentGeomColumnName = ae.getAttributePath();
        }
    }
}
