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

import org.geotools.data.jdbc.DefaultSQLBuilder;
import org.geotools.data.jdbc.JDBCDataStoreConfig;
import org.geotools.data.jdbc.fidmapper.FIDMapper;
import org.geotools.feature.AttributeType;
import org.geotools.feature.FeatureType;
import org.geotools.feature.GeometryAttributeType;
import org.geotools.filter.Filter;
import org.geotools.filter.SQLEncoder;
import org.geotools.filter.SQLEncoderException;
import org.geotools.filter.SQLEncoderPostgis;
import org.opengis.referencing.crs.CoordinateReferenceSystem;

/**
 * Builds sql for postgis.
 *
 * @author Chris Holmes
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/plugin/postgis/src/main/java/org/geotools/data/postgis/PostgisSQLBuilder.java $
 */
public class PostgisSQLBuilder extends DefaultSQLBuilder {
	
    /** If true, WKB format is used instead of WKT */
    protected boolean WKBEnabled = false;
    
    /** If true, ByteA function is used to transfer WKB data*/
    protected boolean byteaEnabled = false;

    /** If true, tables are qualified with a schema **/
    protected boolean schemaEnabled = true;
    
    /** The DataStore we are building SQL for **/
    protected JDBCDataStoreConfig config;
    
    /**
     * Strategy used to encode column information into SQL.
     * <p>
     * Different stratagies are defined according to:
     * <ul>
     * <li>how they handle CoordinateReferenceSystem information with 3D data
     * <li>if the geometry information is requested using WKB
     * <li>if "Byte A" needs to be enabled
     * </ul>
     */
    private class ColumnStratagy {    	
    	/**
    	 * The base Column Stratagy does nothing special.
    	 * 
    	 * @param sql
    	 * @param attribute
    	 */
    	public StringBuffer sqlColumn(StringBuffer sql, AttributeType attribute){
    		sql.append( "\"" );
    		sql.append( attribute.getLocalName() );
    		sql.append( "\"" );
    		return sql;
    	}
    }
    /** Used when "plain" WKB is enabled */
    private class GeometryStratagy extends ColumnStratagy {
    	public StringBuffer sqlColumn(StringBuffer sql, AttributeType attribute) {
    		GeometryAttributeType geometryType = (GeometryAttributeType) attribute;
    		CoordinateReferenceSystem crs = geometryType.getCoordinateSystem();
			final int D = crs == null ? 2 : crs.getCoordinateSystem().getDimension();
			if( D == 3 && !isForce2D()){
    			sql.append("asEWKT(");
    		}
    		else {
    			sql.append("asText(");
    		}
    		sqlGeometry(sql, geometryType);
    		sql.append(")");
    		return sql;
    	}
    	/**
    	 * Request the Geometry (with the correct number of dimensions based
    	 * on CoordinateReferenceSystem).
    	 * <p>
    	 * We may need to shut this functionality off (and force2d all the time
    	 * when rendering a 2D map - why request Z values if we are going to
    	 * throw them away?).
    	 * 
    	 * @param sql
    	 * @param attribute
    	 * @param geometryType
    	 */
		protected void sqlGeometry(StringBuffer sql,GeometryAttributeType geometryType) {
			CoordinateReferenceSystem crs = geometryType.getCoordinateSystem();
			final int D = crs == null ? 2 : crs.getCoordinateSystem().getDimension();
			
			String geomName = geometryType.getLocalName();
    		
    		if( D == 3 && !isForce2D()){
    			sql.append("force_3d(\"" + geomName + "\")");
    		}
    		else {
    			sql.append("force_2d(\"" + geomName + "\")");
    		}
		}
    }
    
    /** Used when "plain" WKB is enabled */
    private class WKBGeometryStratagy extends GeometryStratagy {
    	public StringBuffer sqlColumn(StringBuffer sql, AttributeType attribute) {
    		GeometryAttributeType geometryType = (GeometryAttributeType) attribute;
			CoordinateReferenceSystem crs = geometryType.getCoordinateSystem();
			final int D = crs == null ? 2 : crs.getCoordinateSystem().getDimension();
			
			if( D == 3 && !isForce2D()){
				sql.append("asEWKB(");
			}
			else {
				sql.append("AsBinary(");
			}
    		sqlGeometry(sql, geometryType);
    		sql.append(",'XDR')");
    		return sql;
    	}
    }
    /** Used when "ByteA" WKB is enabled */
    private class ByteaWKBGeometryStratagy extends GeometryStratagy {
    	public StringBuffer sqlColumn(StringBuffer sql, AttributeType attribute) {
    		GeometryAttributeType geometryType = (GeometryAttributeType) attribute;
    		CoordinateReferenceSystem crs = geometryType.getCoordinateSystem();
			final int D = crs == null ? 2 : crs.getCoordinateSystem().getDimension();
			
    		sql.append("encode(");			
			if( D == 3 ){
				sql.append("asEWKB(");
			}
			else {
				sql.append("asBinary(");
			}
    		sqlGeometry(sql, geometryType);
    		sql.append(",'XDR'),'base64')");
    		return sql;
    	}
    }
    
    public ColumnStratagy getStratagy( AttributeType attribute ){
    	if (attribute instanceof GeometryAttributeType) {            
            if (WKBEnabled) {
                if(byteaEnabled) {
                	return new ByteaWKBGeometryStratagy();
                } else {
                    return new WKBGeometryStratagy();
                }
            } else {
                return new GeometryStratagy();
            }
        } else {
            return new ColumnStratagy();
        }
    }
    
    /**
     *
     */
    public PostgisSQLBuilder(int srid, JDBCDataStoreConfig config) {
        this((SQLEncoder) new SQLEncoderPostgis(srid),config);
    }

    /**
     * Constructor with encoder.  Use PostgisSQLBuilder(encoder, config, ft) if possible.
     * 
     * @param encoder
     */
    public PostgisSQLBuilder(SQLEncoder encoder, JDBCDataStoreConfig config) {
        super(encoder);
        this.config = config;
    }
    
    public PostgisSQLBuilder(SQLEncoder encoder, JDBCDataStoreConfig config, FeatureType ft) {
    	super(encoder);
        this.config = config;
        this.ft = ft;
        encoder.setFeatureType( ft );
    }

    /**
     * Produces the select information required.
     * 
     * <p>
     * The featureType, if known, is always requested.
     * </p>
     * 
     * <p>
     * sql: <code>featureID (,attributeColumn)</code>
     * </p>
     * 
     * <p>
     * We may need to provide AttributeReaders with a hook so they can request
     * a wrapper function.
     * </p>
     *
     * @param sql
     * @param mapper
     * @param attributes
     */
    public void sqlColumns(StringBuffer sql, FIDMapper mapper,
        AttributeType[] attributes) {
    	
        for (int i = 0; i < mapper.getColumnCount(); i++) {
            sql.append("\""+mapper.getColumnName(i)+"\"");
            //DJB: add quotes in.  NOTE: if FID mapper isnt oid (ie. PK - Primary Key),
            // you could be requesting PK columns multiple times             
            if ((attributes.length > 0) || (i < (mapper.getColumnCount() - 1))) {
                sql.append(", ");
            }
        }

        for (int i = 0; i < attributes.length; i++) {
        	AttributeType attributeType = attributes[i];

        	ColumnStratagy stratagy = getStratagy( attributeType );			
        	stratagy.sqlColumn( sql, attributeType );

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

    /**
     * Consutrcts FROM clause for featureType
     * 
     * <p>
     * sql: <code>FROM typeName</code>
     * </p>
     *
     * @param sql
     * @param typeName
     */
    public void sqlFrom(StringBuffer sql, String typeName) {
        sql.append(" FROM ");
        sql.append(encodeTableName(typeName));
    }

    /**
     * Constructs WHERE clause, if needed, for FILTER.
     * 
     * <p>
     * sql: <code>WHERE filter encoding</code>
     * </p>
     *
     * @param sql DOCUMENT ME!
     * @param preFilter DOCUMENT ME!
     *
     * @throws SQLEncoderException DOCUMENT ME!
     */
    public void sqlWhere(StringBuffer sql, Filter preFilter)
        throws SQLEncoderException {
        if ((preFilter != null) || (preFilter == org.geotools.filter.Filter.NONE)) {
            String where = encoder.encode(preFilter);
            sql.append(" ");
            sql.append(where);
        }
    }

    /**
     * Returns true if the WKB format is used to transfer geometries, false
     * otherwise
     *
     */
    public boolean isWKBEnabled() {
        return WKBEnabled;
    }

    /**
     * If turned on, WKB will be used to transfer geometry data instead of  WKT
     *
     * @param enabled
     */
    public void setWKBEnabled(boolean enabled) {
        WKBEnabled = enabled;
    }
    
    /**
     * Enables the use of the bytea function to transfer faster WKB geometries
     */
    public boolean isByteaEnabled() {
        return byteaEnabled;
    }
    /**
     * Enables/disables the use of the bytea function
     * @param byteaEnable
     */
    public void setByteaEnabled(boolean byteaEnable) {
        byteaEnabled = byteaEnable;
    }
    /**
     * Enables/disables schema name qualification.
     */
    public void setSchemaEnabled(boolean schemaEnabled) {
		this.schemaEnabled = schemaEnabled;
	}
    /**
     * @return true if table names are prefixed with the containing schema.
     */
    public boolean isSchemaEnabled() {
		return schemaEnabled;
	}
    
    /**
     * Encode the table name (if schemaEnabled is true the schema name
     * will be prepended).
     * 
     * @param tableName
     * @return "tableName" or "schema"."tableName"
     */
    public String encodeTableName(String tableName) {
    	return schemaEnabled ? 
			"\"" + config.getDatabaseSchemaName() + "\".\"" + tableName + "\"" : 
			"\"" + tableName + "\""; 
    }
    /**
     * Surround columnName with quotes.
     * @param columnName
     * @return "columnName"
     */
    public String encodeColumnName(String columnName) {
    	return "\"" + columnName + "\""; 
    }

}
