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

import org.geotools.data.jdbc.fidmapper.FIDMapper;
import org.geotools.factory.Hints;
import org.geotools.feature.AttributeType;
import org.geotools.feature.FeatureType;
import org.geotools.feature.GeometryAttributeType;
import org.opengis.filter.Filter;
import org.geotools.filter.FilterCapabilities;
import org.geotools.filter.Filters;
import org.geotools.filter.SQLEncoder;
import org.geotools.filter.SQLEncoderException;
import org.geotools.filter.visitor.ClientTransactionAccessor;
import org.geotools.filter.visitor.PostPreProcessFilterSplittingVisitor;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;


/**
 * Builds a complete SQL query to select the specified attributes for the
 * specified feature type, using a specified filter to generate a WHERE
 * clause.
 * 
 * <p>
 * The actual WHERE clause is generated by the FilterToSQL class or appropriate
 * subclass for a particular database.  If a specific encoder is to be used,
 * it must be specified to the constructor for this class.
 * </p>
 * 
 * <p>
 * In order to implement the functionality of the application-specified Filter,
 * this is split into a 'preQueryFilter' which can be incorporated into the
 * SQL query itself and a 'postQueryFilter.  The encoder capabilities are used
 * to determine how much of the function can be performed by the database
 * directly and how much has to be performed on the result set.
 * </p>
 *
 * @author Sean Geoghegan, Defence Science and Technology Organisation.
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/jdbc/src/main/java/org/geotools/data/jdbc/GeoAPISQLBuilder.java $
 */
public class GeoAPISQLBuilder implements SQLBuilder {
    // The instance of the encoder to be used to generate the WHERE clause
    protected FilterToSQL encoder;

    protected FeatureType ft;
    
    protected ClientTransactionAccessor accessor;
    
    private Filter lastFilter = null;
    private Filter lastPreFilter = null;
    private Filter lastPostFilter = null;

	private Hints hints;
    
	/**
	 * Constructs an instance of this class with a default FilterToSQL
	 */
	 public GeoAPISQLBuilder() {
	     this(new FilterToSQL(), null, null);
	 }

    /**
     * Constructs an instance of this class using the encoder class specified.
     * This will typically be from the getSqlBuilder method of a JDBCDataStore
     * subclass.
     * 
     * @param encoder the specific encoder to be used.
     * @param featureType
     * @param accessor client-side transaction handler; may be null.
     */
    public GeoAPISQLBuilder(FilterToSQL encoder, FeatureType featureType, ClientTransactionAccessor accessor) {
    	this.encoder = encoder;
    	this.ft = featureType;
    	this.accessor = accessor;
    	
    	//set the feature type on teh encoders
    	encoder.setFeatureType( featureType );
    }
    
    public void setHints(Hints hints) {
    	this.hints = hints;
    }
    
    /** Check the hints to see if we are forced into 2D */
    public boolean isForce2D(){
    	if( hints == null ){
    		return false;
    	}
    	Boolean force2d = (Boolean) hints.get( Hints.FEATURE_2D );
    	if(force2d == null ){
    		return false;
    	}
    	return force2d.booleanValue();    	
    }
    
    /**
     * Return the postQueryFilter that must be applied to the database query
     * result set.
     *
     * @param filter the application filter which must be applied
     *
     * @return the filter representing the functionality that must be performed
     *         on the result set.
     */
    public Filter getPostQueryFilter(Filter filter) {
    	if (filter != null && ( lastFilter == null || !filter.equals(lastFilter) ) ) {
    		splitFilter(filter);
    	}
    	return lastPostFilter;
    }

    /**
     * Return the preQueryFilter that can be used to generate the WHERE clause.
     *
     * @param filter the application filter which must be applied
     *
     * @return the filter representing the functionality that can be performed
     *         by the database.
     */
    public Filter getPreQueryFilter(Filter filter) {
    	if (filter != null && ( lastFilter == null || !filter.equals(lastFilter) ) ) {
    		splitFilter(filter);
    	}
    	return lastPreFilter;
    }

    protected void splitFilter(Filter filter) {
    	lastFilter = filter;
        FilterCapabilities cap = encoder.getCapabilities();
        PostPreProcessFilterSplittingVisitor pfv = new PostPreProcessFilterSplittingVisitor(cap, ft, accessor);
        
        filter.accept(pfv, null);

        lastPreFilter = (Filter) pfv.getFilterPre();
        lastPostFilter = (Filter) pfv.getFilterPost();
    }
    
    /**
     * Constructs the FROM clause for a featureType
     * 
     * <p>
     * sql: <code>FROM typeName</code>
     * </p>
     *
     * @param sql the StringBuffer that the WHERE clause should be appended to
     * @param typeName the name of the table (feature type) to be queried
     */
    public void sqlFrom(StringBuffer sql, String typeName) {
        sql.append(" FROM ");
        sql.append(encoder.escapeName(typeName));
    }

    /**
     * Constructs WHERE clause, if needed, for FILTER.
     * 
     * <p>
     * sql: <code>WHERE filter encoding</code>
     * </p>
     *
     * @param sql The StringBuffer that the WHERE clause should be appended to
     * @param preFilter The filter to be used by the encoder class to generate
     *        the WHERE clause
     *
     * @throws SQLEncoderException Not thrown here but may be thrown by the
     *         encoder
     * FIXME:  Throw FilterToSQLException when the parent interface is fixed.
     */
    public void sqlWhere(StringBuffer sql, Filter preFilter)
        throws SQLEncoderException {
        if ((preFilter != null) && (preFilter != Filter.INCLUDE)) {
            try {
                String where = encoder.encodeToString(preFilter);
                sql.append(" ");
                sql.append(where);
            } catch (FilterToSQLException fse) {
                throw new SQLEncoderException("", fse);
            }
        }
    }

    /**
     * Constructs the full SQL SELECT statement for the supplied Filter.
     * 
     * <p>
     * The statement is constructed by concatenating the SELECT column list,
     * FROM table specification and WHERE clause appropriate to the supplied
     * Filter.
     * </p>
     *
     * @param typeName The name of the table (feature type) to be queried
     * @param mapper FIDMapper to identify the FID columns in the table
     * @param attrTypes The specific attribute columns to be selected
     * @param filter The Filter that will be used by the encoder to construct
     *        the WHERE clause
     *
     * @return The fully formed SQL SELECT statement
     *
     * @throws SQLEncoderException Not thrown by this method but may be thrown
     *         by the encoder class
     * FIXME:  Throw FilterToSQLException when the parent interface is fixed.
     */
    public String buildSQLQuery(String typeName, FIDMapper mapper,
        AttributeType[] attrTypes, org.opengis.filter.Filter filter) throws SQLEncoderException {
        StringBuffer sqlBuffer = new StringBuffer();

        sqlBuffer.append("SELECT ");
        sqlColumns(sqlBuffer, mapper, attrTypes);
        sqlFrom(sqlBuffer, typeName);
        encoder.setFIDMapper(mapper);
        sqlWhere(sqlBuffer, filter);

        String sqlStmt = sqlBuffer.toString();

        return sqlStmt;
    }

    /**
     * Appends the names of the columns to be selected.
     * 
     * <p>
     * sqlGeometryColumn is invoked for any special handling for geometry
     * columns.
     * </p>
     *
     * @param sql StringBuffer to be appended to
     * @param mapper FIDMapper to provide the name(s) of the FID columns
     * @param attributes Array of columns to be selected
     *
     * @see postgisDataStore.SQLBuilder#sqlColumns(java.lang.StringBuffer,
     *      postgisDataStore.FIDMapper.FIDMapper,
     *      org.geotools.feature.AttributeType[])
     */
    public void sqlColumns(StringBuffer sql, FIDMapper mapper,
        AttributeType[] attributes) {
        for (int i = 0; i < mapper.getColumnCount(); i++) {
            sql.append(encoder.escapeName(mapper.getColumnName(i)) + ", ");
        }

        for (int i = 0; i < attributes.length; i++) {
            if (attributes[i] instanceof GeometryAttributeType) {
                sqlGeometryColumn(sql, attributes[i]);
            } else {
                sql.append(encoder.escapeName(attributes[i].getName()));
            }

            if (i < (attributes.length - 1)) {
                sql.append(", ");
            }
        }
    }

    /**
     * Generates the select column specification for a geometry column.
     * 
     * <p>
     * This should typically be overridden in the subclass to return a
     * meaningful value that the attribute i/o handler can process.
     * </p>
     *
     * @param sql A StringBuffer that the column specification can be appended
     *        to
     * @param geomAttribute An AttributeType for a geometry attribute
     */
    public void sqlGeometryColumn(StringBuffer sql, AttributeType geomAttribute) {
        sql.append(encoder.escapeName(geomAttribute.getName()));
    }
    
    /**
     * Generates the order by clause.
     * <p>
     * This uses the standard ASC,DESC sql keywords to denote ascending,descending
     * sort respectivley.
     * </p>
     * 
     * FIXME:  Throw FilterToSQLException when the parent interface is fixed.
     */
    public void sqlOrderBy(StringBuffer sql, SortBy[] sortBy) throws SQLEncoderException {
    	if ( sortBy == null || sortBy.length == 0 ) 
    		return;	//nothing to sort on
    	
    	sql.append( " ORDER BY ");
    	for ( int i = 0; i < sortBy.length; i++ ) {
    		AttributeType type = (AttributeType) sortBy[i].getPropertyName().evaluate( ft );
    		if ( type != null ) {
    			sql.append( encoder.escapeName( type.getName() ) );
    		}
    		else {
    			sql.append( encoder.escapeName( sortBy[i].getPropertyName().getPropertyName() ) );	
    		}
    		
    		
    		if ( SortOrder.DESCENDING.equals( sortBy[i].getSortOrder() ) ) {
    			sql.append( " DESC");
    		}
    		else {
    			sql.append( " ASC");
    		}
    		
    		if ( i < sortBy.length-1 ) {
    			sql.append( ", ");
    		}
    	}
    	
    }
    
}
