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

import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.geotools.factory.Hints;
import org.opengis.filter.Filter;
import org.opengis.filter.sort.SortBy;
import org.opengis.referencing.crs.CoordinateReferenceSystem;


/**
 * The query object is used by the {@link FeatureSource#getFeatures()} method of
 * the DataSource interface, to encapsulate a request.  It defines which
 * feature type  to query, what properties to retrieve and what constraints
 * (spatial and non-spatial) to apply to those properties.  It is designed to
 * closesly match a WFS Query element of a <code>getFeatures</code> request.
 * The only difference is the addition of the maxFeatures element, which
 * allows more control over the features selected.  It allows a full
 * <code>getFeatures</code> request to properly control how many features it
 * gets from each query, instead of requesting and discarding when the max is
 * reached.
 *
 * @author Chris Holmes
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/data/DefaultQuery.java $
 */
public class DefaultQuery implements Query {
    /** The properties to fetch */
    private String[] properties;

    /** The maximum numbers of features to fetch */
    private int maxFeatures = Query.DEFAULT_MAX;

    /** The filter to constrain the request. */
    private Filter filter = Filter.INCLUDE;

    /** The typeName to get */
    private String typeName;

    /** The namespace to get */
    private URI namespace =Query.NO_NAMESPACE;

    /** The handle associated with this query. */
    private String handle;

    /** Coordinate System associated with this query */
    private CoordinateReferenceSystem coordinateSystem;
    
    /** Reprojection associated associated with this query */
    private CoordinateReferenceSystem coordinateSystemReproject;
    
    /** Sorting for the query */
    private SortBy[] sortBy;
    
    /** The version according to WFS 1.0 and 1.1 specs */
    private String version;
    
    /** The hints to be used during query execution */
    private Hints hints;
    
    /** 
    /**
     * No argument constructor.
     */
    public DefaultQuery() {
        // no arg
    }

    /**
     * Query with typeName.
     * <p>
     * </p>
     * @param typeName the name of the featureType to retrieve
     */
    public DefaultQuery( String typeName ){
        this( typeName, Filter.INCLUDE );
    }

    /**
     * Constructor with typeName and filter.  Note that current datasource
     * implementations only have one type per datasource, so the typeName
     * field will likely be ignored.
     *
     * @param typeName the name of the featureType to retrieve.
     * @param filter the OGC filter to constrain the request.
     */
    public DefaultQuery(String typeName, Filter filter) {
        this( typeName, filter, Query.ALL_NAMES );        
    }
    
    /**
     * Constructor that sets the filter and properties
     * @param typeName 
     *
     * @param filter the OGC filter to constrain the request.
     * @param properties an array of the properties to fetch.
     */
    public DefaultQuery(String typeName, Filter filter, String[] properties) {
        this( typeName, null, filter, Query.DEFAULT_MAX, properties, null );        
    }    
    /**
     * Constructor that sets all fields.
     *
     * @param typeName the name of the featureType to retrieve.
     * @param filter the OGC filter to constrain the request.
     * @param maxFeatures the maximum number of features to be returned.
     * @param propNames an array of the properties to fetch.
     * @param handle the name to associate with the query.
     */
    public DefaultQuery(String typeName, Filter filter, int maxFeatures,
        String[] propNames, String handle) {
        this(typeName, null, filter, maxFeatures, propNames, handle );
    }
    
    /**
     * Constructor that sets all fields.
     *
     * @param typeName the name of the featureType to retrieve.
     * @param namespace Namespace for provided typeName, or null if unspecified
     * @param filter the OGC filter to constrain the request.
     * @param maxFeatures the maximum number of features to be returned.
     * @param propNames an array of the properties to fetch.
     * @param handle the name to associate with the query.
     */
    public DefaultQuery(String typeName, URI namespace, Filter filter, int maxFeatures,
        String[] propNames, String handle) {
        this.typeName = typeName;
        this.filter = filter;
        this.namespace = namespace;
        this.properties = propNames;
        this.maxFeatures = maxFeatures;
        this.handle = handle;
    }
    
    /**
     * Copy contructor, clones the state of a generic Query into a DefaultQuery
     * @param query
     */
    public DefaultQuery(Query query) {
      this(query.getTypeName(), query.getNamespace(), query.getFilter(), query.getMaxFeatures(),
          query.getPropertyNames(), query.getHandle());
      this.sortBy = query.getSortBy();
      this.coordinateSystem = query.getCoordinateSystem();
      this.coordinateSystemReproject = query.getCoordinateSystemReproject();
      this.version = query.getVersion();
      this.hints = query.getHints();
    }
    
    /**
     * The property names is used to specify the attributes that should be
     * selected for the return feature collection.  If the property array is
     * null, then the datasource should return all available properties, its
     * full schema.  If an array of  specified then the full schema should be
     * used (all property names). The property names can be determined with a
     * getSchema call from the DataSource interface.
     * 
     * <p>
     * This replaces our funky setSchema method of retrieving select
     * properties.  I think it makes it easier to understand how to get
     * certain properties out of the datasource, instead of having users get
     * the  schema and then compose a new schema using the attributes that
     * they want.  The old way was also giving me problems because I couldn't
     * have multiple object reuse the same datasource object, since some other
     * object could come along and change its schema, and would then return
     * the wrong properties.
     * </p>
     * 
     * <p></p>
     *
     * @return the property names to be used in the returned FeatureCollection.
     */
    public String[] getPropertyNames() {
        return properties;
    }

    /**
     * Sets the properties to retrieve from the db.  If the boolean to load all
     * properties is set to true then the AttributeTypes that are not in the
     * database's schema will just be filled with null values.
     *
     * @param propNames The names of attributes to load from the datasouce.
     */
    public void setPropertyNames(String[] propNames) {
        this.properties = propNames;
    }

    /**
     * Sets the proper attributeTypes constructed from a schema and a  list of
     * propertyNames.
     *
     * @param propNames the names of the properties to check against the
     *        schema. If null then all attributes will be returned.  If a List
     *        of size 0 is used then only the featureIDs should be used.
     *
     * @task REVISIT: This syntax is really obscure.  Consider having an fid or
     *       featureID propertyName that datasource implementors look for
     *       instead of looking to see if the list size is 0.
     */
    public void setPropertyNames(List propNames) {
        if (propNames == null) {
            this.properties = null;

            return;
        }

        String[] stringArr = new String[propNames.size()];
        this.properties = (String[]) propNames.toArray(stringArr);
    }

    /**
     * Convenience method to determine if the query should use the full schema
     * (all properties) of the data source for the features returned.  This
     * method is equivalent to if (query.getProperties() == null), but allows
     * for more clarity on the part of datasource implementors, so they do not
     * need to examine and use null values.  All Query implementations should
     * return true for this function if getProperties returns null.
     *
     * @return if all datasource attributes should be included in the  schema
     *         of the returned FeatureCollection.
     */
    public boolean retrieveAllProperties() {
        return properties == null;
    }

    /**
     * The optional maxFeatures can be used to limit the number of features
     * that a query request retrieves.  If no maxFeatures is specified then
     * all features should be returned.
     * 
     * <p>
     * This is the only method that is not directly out of the Query element in
     * the WFS spec.  It is instead a part of a <code>getFeatures</code>
     * request, which can hold one or more queries.  But each of those in turn
     * will need a maxFeatures, so it is needed here.
     * </p>
     *
     * @return the max features the getFeature call should return.
     */
    public int getMaxFeatures() {
        return this.maxFeatures;
    }

    /**
     * Sets the max features to retrieved by this query.
     *
     * @param maxFeatures the maximum number of features the getFeature call
     *        should return.
     */
    public void setMaxFeatures(int maxFeatures) {
        this.maxFeatures = maxFeatures;
    }

    /**
     * The Filter can be used to define constraints on a query.  If no Filter
     * is present then the query is unconstrained and all feature instances
     * should be retrieved.
     *
     * @return The filter that defines constraints on the query.
     */
    public Filter getFilter() {
        return this.filter;
    }

    /**
     * Sets the filter to constrain the query.
     *
     * @param filter the OGC filter to limit the datasource getFeatures
     *        request.
     */
    public void setFilter(Filter filter) {
        this.filter = filter;
    }

    /**
     * The typeName attribute is used to indicate the name of the feature type
     * to be queried.
     * 
     * <p>
     * The DataStore API does not assume one feature type per datastore.
     * It currently makes use of this field to to specify with each request
     * what type to get.
     * </p>
     * @return the name of the feature type to be returned with this query.
     */
    public String getTypeName() {
        return this.typeName;
    }

    /* (non-Javadoc)
     * @see org.geotools.data.Query#getNamespace()
     */
    public URI getNamespace() {
        return namespace;
    }
    /**
     * Sets the typename.
     *
     * @param typeName the name of the featureType to retrieve.
     */
    public void setTypeName(String typeName) {
        this.typeName = typeName;
    }

    /**
     * The handle attribute is included to allow a client to associate  a
     * mnemonic name to the Query request. The purpose of the handle attribute
     * is to provide an error handling mechanism for locating  a statement
     * that might fail.
     *
     * @return the mnemonic name of the query request.
     */
    public String getHandle() {
        return this.handle;
    }

    /**
     * Sets a mnemonic name for the query request.
     *
     * @param handle the name to refer to this query.
     */
    public void setHandle(String handle) {
        this.handle = handle;
    }

    /**
     * From WFS Spec:  The version attribute is included in order to
     * accommodate systems that  support feature versioning. A value of ALL
     * indicates that all versions of a feature should be fetched. Otherwise
     * an integer, n, can be specified  to return the n th version of a
     * feature. The version numbers start at '1'  which is the oldest version.
     * If a version value larger than the largest version is specified then
     * the latest version is return. The default action shall be for the query
     * to return the latest version. Systems that do not support versioning
     * can ignore the parameter and return the only version  that they have.
     * 
     * <p>
     * This is ready for use, it will be up to data store implementors to
     * support it.
     * </p>
     *
     * @return the version of the feature to return, or null for latest. 
     */
    public String getVersion() {
        return version; 
    }
    
    /**
     * @see #getVersion()
     * @param version
     * @since 2.4
     */
    public void setVersion(String version) {
        this.version = version;
    }

    /**
     * Hashcode based on propertyName, maxFeatures and filter.
     *
     * @return hascode for filter
     */
    public int hashCode() {
        String[] n = getPropertyNames();

        return ((n == null) ? (-1)
                                    : ((n.length == 0) ? 0 : (n.length
                | n[0].hashCode()))) | getMaxFeatures()
                | ((getFilter() == null) ? 0 : getFilter().hashCode())
                | ((getTypeName() == null) ? 0 : getTypeName().hashCode())
                | ((getVersion() == null) ? 0 : getVersion().hashCode())
                | ((getCoordinateSystem() == null) ? 0 : getCoordinateSystem().hashCode())
                | ((getCoordinateSystemReproject() == null) ? 0 : getCoordinateSystemReproject().hashCode());
    }

    /**
     * Equality based on propertyNames, maxFeatures, filter, typeName and
     * version.
     * 
     * <p>
     * Changing the handle does not change the meaning of the Query.
     * </p>
     *
     * @param obj Other object to compare against
     *
     * @return <code>true</code> if <code>obj</code> matches this filter
     */
    public boolean equals(Object obj) {
        if ((obj == null) || !(obj instanceof Query)) {
            return false;
        }
        if (this == obj) return true;        
        Query other = (Query) obj;
        
        return Arrays.equals(getPropertyNames(), other.getPropertyNames())
        && (retrieveAllProperties() == other.retrieveAllProperties())
        && (getMaxFeatures() == other.getMaxFeatures())
        && ((getFilter() == null) ? (other.getFilter() == null)
                                  : getFilter().equals(other.getFilter()))
        && ((getTypeName() == null) ? (other.getTypeName() == null)
                                    : getTypeName().equals(other.getTypeName()))
        && ((getVersion() == null) ? (other.getVersion() == null)
                                   : getVersion().equals(other.getVersion()))
        && ((getCoordinateSystem() == null) ? (other.getCoordinateSystem() == null)
                                           : getCoordinateSystem().equals(other.getCoordinateSystem()))
        && ((getCoordinateSystemReproject() == null) ? (other.getCoordinateSystemReproject() == null)
                                                   : getCoordinateSystemReproject().equals(other.getCoordinateSystemReproject()))                                           
        ;
    }
    /**
     * Over ride of toString
     *
     * @return a string representation of this query object.
     */
    public String toString() {
        StringBuffer returnString = new StringBuffer("Query:");

        if (handle != null) {
            returnString.append(" [" + handle + "]");
        }

        returnString.append("\n   feature type: " + typeName);

        if (filter != null) {
            returnString.append("\n   filter: " + filter.toString());
        }

        returnString.append("\n   [properties: ");

        if ((properties == null) || (properties.length == 0)) {
            returnString.append(" ALL ]");
        } else {
            for (int i = 0; i < properties.length; i++) {
                returnString.append(properties[i]);

                if (i < (properties.length - 1)) {
                    returnString.append(", ");
                }
            }

            returnString.append("]");
        }
        
        if(sortBy != null && sortBy.length > 0) {
        returnString.append("\n   [sort by: ");
            for (int i = 0; i < sortBy.length; i++) {
                returnString.append(sortBy[i].getPropertyName().getPropertyName());
                returnString.append(" ");
                returnString.append(sortBy[i].getSortOrder().name());

                if (i < (sortBy.length - 1)) {
                    returnString.append(", ");
                }
            }

            returnString.append("]");
        }
        
        return returnString.toString();
    }
    /**
     * getCoordinateSystem purpose.
     * <p>
     * Description ...
     * </p>
     */
    public CoordinateReferenceSystem getCoordinateSystem() {
        return coordinateSystem;
    }

    /**
     * getCoordinateSystemReproject purpose.
     * <p>
     * Description ...
     * </p>
     */
    public CoordinateReferenceSystem getCoordinateSystemReproject() {
        return coordinateSystemReproject;
    }

    /**
     * setCoordinateSystem purpose.
     * <p>
     * Description ...
     * </p>
     * @param system
     */
    public void setCoordinateSystem(CoordinateReferenceSystem system) {
        coordinateSystem = system;
    }

    /**
     * setCoordinateSystemReproject purpose.
     * <p>
     * Description ...
     * </p>
     * @param system
     */
    public void setCoordinateSystemReproject(CoordinateReferenceSystem system) {
        coordinateSystemReproject = system;
    }
    
    /**
     * Retrive list of SortBy information.
     * <p>
     * Note we are using SortBy2, to be standards complient
     * you may limit yourself to to SortBy information.
     * </p>
     */
    public SortBy[] getSortBy() {
        return sortBy;
    }   
    
    /**
     * Sets the sort by information.
     * 
     */
    public void setSortBy(SortBy[] sortBy) {
        this.sortBy = sortBy;
    }
    
    public Hints getHints() {
        if(hints == null)
            hints = new Hints(Collections.EMPTY_MAP);
        return hints;
    }
    
    /**
     * Sets the query hints
     * @param hints
     */
    public void setHints(Hints hints) {
        this.hints = hints;
    }
    
}
