package mil.nga.geopackage.features.user;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.db.CoreSQLUtils;
import mil.nga.geopackage.features.index.FeatureIndexManager;
import mil.nga.sf.GeometryEnvelope;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionTransform;

/**
 * Performs manual brute force queries against feature rows. See
 * {@link FeatureIndexManager} for performing indexed queries.
 * 
 * @author osbornb
 * @since 3.1.0
 */
public class ManualFeatureQuery {

	/**
	 * Feature DAO
	 */
	private final FeatureDao featureDao;

	/**
	 * Query single chunk limit
	 */
	protected int chunkLimit = 1000;

	/**
	 * Query range tolerance
	 */
	protected double tolerance = .00000000000001;

	/**
	 * Constructor
	 *
	 * @param featureDao
	 *            feature DAO
	 */
	public ManualFeatureQuery(FeatureDao featureDao) {
		this.featureDao = featureDao;
	}

	/**
	 * Get the feature DAO
	 * 
	 * @return feature DAO
	 */
	public FeatureDao getFeatureDao() {
		return featureDao;
	}

	/**
	 * Get the SQL query chunk limit
	 * 
	 * @return chunk limit
	 */
	public int getChunkLimit() {
		return chunkLimit;
	}

	/**
	 * Set the SQL query chunk limit
	 * 
	 * @param chunkLimit
	 *            chunk limit
	 */
	public void setChunkLimit(int chunkLimit) {
		this.chunkLimit = chunkLimit;
	}

	/**
	 * Get the query range tolerance
	 * 
	 * @return query range tolerance
	 */
	public double getTolerance() {
		return tolerance;
	}

	/**
	 * Set the query range tolerance
	 * 
	 * @param tolerance
	 *            query range tolerance
	 */
	public void setTolerance(double tolerance) {
		this.tolerance = tolerance;
	}

	/**
	 * Query for features
	 * 
	 * @return feature results
	 * @since 3.4.0
	 */
	public FeatureResultSet query() {
		return featureDao.query();
	}

	/**
	 * Query for features
	 * 
	 * @param columns
	 *            columns
	 * 
	 * @return feature results
	 * @since 3.5.0
	 */
	public FeatureResultSet query(String[] columns) {
		return featureDao.query(columns);
	}

	/**
	 * Get the count of features
	 *
	 * @return count
	 */
	public int count() {
		return featureDao.count();
	}

	/**
	 * Get the count of features with non null geometries
	 *
	 * @return count
	 */
	public int countWithGeometries() {
		return featureDao.count(
				CoreSQLUtils.quoteWrap(featureDao.getGeometryColumnName())
						+ " IS NOT NULL",
				null);
	}

	/**
	 * Query for features
	 * 
	 * @param fieldValues
	 *            field values
	 * 
	 * @return feature results
	 * @since 3.4.0
	 */
	public FeatureResultSet query(Map<String, Object> fieldValues) {
		String where = featureDao.buildWhere(fieldValues.entrySet());
		String[] whereArgs = featureDao.buildWhereArgs(fieldValues.values());
		return featureDao.query(where, whereArgs);
	}

	/**
	 * Query for features
	 * 
	 * @param columns
	 *            columns
	 * @param fieldValues
	 *            field values
	 * 
	 * @return feature results
	 * @since 3.5.0
	 */
	public FeatureResultSet query(String[] columns,
			Map<String, Object> fieldValues) {
		String where = featureDao.buildWhere(fieldValues.entrySet());
		String[] whereArgs = featureDao.buildWhereArgs(fieldValues.values());
		return featureDao.query(columns, where, whereArgs);
	}

	/**
	 * Count features
	 * 
	 * @param fieldValues
	 *            field values
	 * 
	 * @return count
	 * @since 3.4.0
	 */
	public int count(Map<String, Object> fieldValues) {
		String where = featureDao.buildWhere(fieldValues.entrySet());
		String[] whereArgs = featureDao.buildWhereArgs(fieldValues.values());
		return featureDao.count(where, whereArgs);
	}

	/**
	 * Query for features
	 * 
	 * @param where
	 *            where clause
	 * 
	 * @return feature results
	 * @since 3.4.0
	 */
	public FeatureResultSet query(String where) {
		return featureDao.query(where);
	}

	/**
	 * Query for features
	 * 
	 * @param columns
	 *            columns
	 * @param where
	 *            where clause
	 * 
	 * @return feature results
	 * @since 3.5.0
	 */
	public FeatureResultSet query(String[] columns, String where) {
		return featureDao.query(columns, where);
	}

	/**
	 * Count features
	 * 
	 * @param where
	 *            where clause
	 * 
	 * @return count
	 * @since 3.4.0
	 */
	public int count(String where) {
		return featureDao.count(where);
	}

	/**
	 * Query for features
	 * 
	 * @param where
	 *            where clause
	 * @param whereArgs
	 *            where arguments
	 * 
	 * @return feature results
	 * @since 3.4.0
	 */
	public FeatureResultSet query(String where, String[] whereArgs) {
		return featureDao.query(where, whereArgs);
	}

	/**
	 * Query for features
	 * 
	 * @param columns
	 *            columns
	 * @param where
	 *            where clause
	 * @param whereArgs
	 *            where arguments
	 * 
	 * @return feature results
	 * @since 3.5.0
	 */
	public FeatureResultSet query(String[] columns, String where,
			String[] whereArgs) {
		return featureDao.query(columns, where, whereArgs);
	}

	/**
	 * Count features
	 * 
	 * @param where
	 *            where clause
	 * @param whereArgs
	 *            where arguments
	 * 
	 * @return count
	 * @since 3.4.0
	 */
	public int count(String where, String[] whereArgs) {
		return featureDao.count(where, whereArgs);
	}

	/**
	 * Manually build the bounds of the feature table
	 * 
	 * @return bounding box
	 */
	public BoundingBox getBoundingBox() {

		GeometryEnvelope envelope = null;

		long offset = 0;
		boolean hasResults = true;

		String[] columns = new String[] { featureDao.getGeometryColumnName() };

		while (hasResults) {

			hasResults = false;

			FeatureResultSet resultSet = featureDao.queryForChunk(columns,
					chunkLimit, offset);
			try {
				while (resultSet.moveToNext()) {
					hasResults = true;

					FeatureRow featureRow = resultSet.getRow();
					GeometryEnvelope featureEnvelope = featureRow
							.getGeometryEnvelope();
					if (featureEnvelope != null) {

						if (envelope == null) {
							envelope = featureEnvelope;
						} else {
							envelope = envelope.union(featureEnvelope);
						}

					}
				}
			} finally {
				resultSet.close();
			}

			offset += chunkLimit;
		}

		BoundingBox boundingBox = null;
		if (envelope != null) {
			boundingBox = new BoundingBox(envelope);
		}

		return boundingBox;
	}

	/**
	 * Manually build the bounds of the feature table in the provided projection
	 * 
	 * @param projection
	 *            desired projection
	 * @return bounding box
	 */
	public BoundingBox getBoundingBox(Projection projection) {
		BoundingBox boundingBox = getBoundingBox();
		if (boundingBox != null && projection != null) {
			ProjectionTransform projectionTransform = featureDao.getProjection()
					.getTransformation(projection);
			boundingBox = boundingBox.transform(projectionTransform);
		}
		return boundingBox;
	}

	/**
	 * Manually query for rows within the bounding box
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @return results
	 */
	public ManualFeatureQueryResults query(BoundingBox boundingBox) {
		return query(boundingBox.buildEnvelope());
	}

	/**
	 * Manually query for rows within the bounding box
	 * 
	 * @param columns
	 *            columns
	 * @param boundingBox
	 *            bounding box
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns,
			BoundingBox boundingBox) {
		return query(columns, boundingBox.buildEnvelope());
	}

	/**
	 * Manually count the rows within the bounding box
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @return count
	 */
	public long count(BoundingBox boundingBox) {
		return count(boundingBox.buildEnvelope());
	}

	/**
	 * Manually query for rows within the bounding box
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param fieldValues
	 *            field values
	 * @return results
	 * @since 3.4.0
	 */
	public ManualFeatureQueryResults query(BoundingBox boundingBox,
			Map<String, Object> fieldValues) {
		return query(boundingBox.buildEnvelope(), fieldValues);
	}

	/**
	 * Manually query for rows within the bounding box
	 * 
	 * @param columns
	 *            columns
	 * @param boundingBox
	 *            bounding box
	 * @param fieldValues
	 *            field values
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns,
			BoundingBox boundingBox, Map<String, Object> fieldValues) {
		return query(columns, boundingBox.buildEnvelope(), fieldValues);
	}

	/**
	 * Manually count the rows within the bounding box
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param fieldValues
	 *            field values
	 * @return count
	 * @since 3.4.0
	 */
	public long count(BoundingBox boundingBox,
			Map<String, Object> fieldValues) {
		return count(boundingBox.buildEnvelope(), fieldValues);
	}

	/**
	 * Manually query for rows within the bounding box
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param where
	 *            were clause
	 * @return results
	 * @since 3.4.0
	 */
	public ManualFeatureQueryResults query(BoundingBox boundingBox,
			String where) {
		return query(boundingBox, where, null);
	}

	/**
	 * Manually query for rows within the bounding box
	 * 
	 * @param columns
	 *            columns
	 * @param boundingBox
	 *            bounding box
	 * @param where
	 *            were clause
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns,
			BoundingBox boundingBox, String where) {
		return query(columns, boundingBox, where, null);
	}

	/**
	 * Manually count the rows within the bounding box
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param where
	 *            were clause
	 * @return count
	 * @since 3.4.0
	 */
	public long count(BoundingBox boundingBox, String where) {
		return count(boundingBox, where, null);
	}

	/**
	 * Manually query for rows within the bounding box
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param where
	 *            were clause
	 * @param whereArgs
	 *            where arguments
	 * @return results
	 * @since 3.4.0
	 */
	public ManualFeatureQueryResults query(BoundingBox boundingBox,
			String where, String[] whereArgs) {
		return query(boundingBox.buildEnvelope(), where, whereArgs);
	}

	/**
	 * Manually query for rows within the bounding box
	 * 
	 * @param columns
	 *            columns
	 * @param boundingBox
	 *            bounding box
	 * @param where
	 *            were clause
	 * @param whereArgs
	 *            where arguments
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns,
			BoundingBox boundingBox, String where, String[] whereArgs) {
		return query(columns, boundingBox.buildEnvelope(), where, whereArgs);
	}

	/**
	 * Manually count the rows within the bounding box
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param where
	 *            were clause
	 * @param whereArgs
	 *            where arguments
	 * @return count
	 * @since 3.4.0
	 */
	public long count(BoundingBox boundingBox, String where,
			String[] whereArgs) {
		return count(boundingBox.buildEnvelope(), where, whereArgs);
	}

	/**
	 * Manually query for rows within the bounding box in the provided
	 * projection
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param projection
	 *            projection
	 * @return results
	 */
	public ManualFeatureQueryResults query(BoundingBox boundingBox,
			Projection projection) {
		BoundingBox featureBoundingBox = featureDao
				.projectBoundingBox(boundingBox, projection);
		return query(featureBoundingBox);
	}

	/**
	 * Manually query for rows within the bounding box in the provided
	 * projection
	 * 
	 * @param columns
	 *            columns
	 * @param boundingBox
	 *            bounding box
	 * @param projection
	 *            projection
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns,
			BoundingBox boundingBox, Projection projection) {
		BoundingBox featureBoundingBox = featureDao
				.projectBoundingBox(boundingBox, projection);
		return query(columns, featureBoundingBox);
	}

	/**
	 * Manually count the rows within the bounding box in the provided
	 * projection
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param projection
	 *            projection
	 * @return count
	 */
	public long count(BoundingBox boundingBox, Projection projection) {
		BoundingBox featureBoundingBox = featureDao
				.projectBoundingBox(boundingBox, projection);
		return count(featureBoundingBox);
	}

	/**
	 * Manually query for rows within the bounding box in the provided
	 * projection
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param projection
	 *            projection
	 * @param fieldValues
	 *            field values
	 * @return results
	 * @since 3.4.0
	 */
	public ManualFeatureQueryResults query(BoundingBox boundingBox,
			Projection projection, Map<String, Object> fieldValues) {
		BoundingBox featureBoundingBox = featureDao
				.projectBoundingBox(boundingBox, projection);
		return query(featureBoundingBox, fieldValues);
	}

	/**
	 * Manually query for rows within the bounding box in the provided
	 * projection
	 * 
	 * @param columns
	 *            columns
	 * @param boundingBox
	 *            bounding box
	 * @param projection
	 *            projection
	 * @param fieldValues
	 *            field values
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns,
			BoundingBox boundingBox, Projection projection,
			Map<String, Object> fieldValues) {
		BoundingBox featureBoundingBox = featureDao
				.projectBoundingBox(boundingBox, projection);
		return query(columns, featureBoundingBox, fieldValues);
	}

	/**
	 * Manually count the rows within the bounding box in the provided
	 * projection
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param projection
	 *            projection
	 * @param fieldValues
	 *            field values
	 * @return count
	 * @since 3.4.0
	 */
	public long count(BoundingBox boundingBox, Projection projection,
			Map<String, Object> fieldValues) {
		BoundingBox featureBoundingBox = featureDao
				.projectBoundingBox(boundingBox, projection);
		return count(featureBoundingBox, fieldValues);
	}

	/**
	 * Manually query for rows within the bounding box in the provided
	 * projection
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param projection
	 *            projection
	 * @param where
	 *            were clause
	 * @return results
	 * @since 3.4.0
	 */
	public ManualFeatureQueryResults query(BoundingBox boundingBox,
			Projection projection, String where) {
		return query(boundingBox, projection, where, null);
	}

	/**
	 * Manually query for rows within the bounding box in the provided
	 * projection
	 * 
	 * @param columns
	 *            columns
	 * @param boundingBox
	 *            bounding box
	 * @param projection
	 *            projection
	 * @param where
	 *            were clause
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns,
			BoundingBox boundingBox, Projection projection, String where) {
		return query(columns, boundingBox, projection, where, null);
	}

	/**
	 * Manually count the rows within the bounding box in the provided
	 * projection
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param projection
	 *            projection
	 * @param where
	 *            were clause
	 * @return count
	 * @since 3.4.0
	 */
	public long count(BoundingBox boundingBox, Projection projection,
			String where) {
		return count(boundingBox, projection, where, null);
	}

	/**
	 * Manually query for rows within the bounding box in the provided
	 * projection
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param projection
	 *            projection
	 * @param where
	 *            were clause
	 * @param whereArgs
	 *            where arguments
	 * @return results
	 * @since 3.4.0
	 */
	public ManualFeatureQueryResults query(BoundingBox boundingBox,
			Projection projection, String where, String[] whereArgs) {
		BoundingBox featureBoundingBox = featureDao
				.projectBoundingBox(boundingBox, projection);
		return query(featureBoundingBox, where, whereArgs);
	}

	/**
	 * Manually query for rows within the bounding box in the provided
	 * projection
	 * 
	 * @param columns
	 *            columns
	 * @param boundingBox
	 *            bounding box
	 * @param projection
	 *            projection
	 * @param where
	 *            were clause
	 * @param whereArgs
	 *            where arguments
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns,
			BoundingBox boundingBox, Projection projection, String where,
			String[] whereArgs) {
		BoundingBox featureBoundingBox = featureDao
				.projectBoundingBox(boundingBox, projection);
		return query(columns, featureBoundingBox, where, whereArgs);
	}

	/**
	 * Manually count the rows within the bounding box in the provided
	 * projection
	 * 
	 * @param boundingBox
	 *            bounding box
	 * @param projection
	 *            projection
	 * @param where
	 *            were clause
	 * @param whereArgs
	 *            where arguments
	 * @return count
	 * @since 3.4.0
	 */
	public long count(BoundingBox boundingBox, Projection projection,
			String where, String[] whereArgs) {
		BoundingBox featureBoundingBox = featureDao
				.projectBoundingBox(boundingBox, projection);
		return count(featureBoundingBox, where, whereArgs);
	}

	/**
	 * Manually query for rows within the geometry envelope
	 * 
	 * @param envelope
	 *            geometry envelope
	 * @return results
	 */
	public ManualFeatureQueryResults query(GeometryEnvelope envelope) {
		return query(envelope.getMinX(), envelope.getMinY(), envelope.getMaxX(),
				envelope.getMaxY());
	}

	/**
	 * Manually query for rows within the geometry envelope
	 * 
	 * @param columns
	 *            columns
	 * @param envelope
	 *            geometry envelope
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns,
			GeometryEnvelope envelope) {
		return query(columns, envelope.getMinX(), envelope.getMinY(),
				envelope.getMaxX(), envelope.getMaxY());
	}

	/**
	 * Manually count the rows within the geometry envelope
	 * 
	 * @param envelope
	 *            geometry envelope
	 * @return count
	 */
	public long count(GeometryEnvelope envelope) {
		return count(envelope.getMinX(), envelope.getMinY(), envelope.getMaxX(),
				envelope.getMaxY());
	}

	/**
	 * Manually query for rows within the geometry envelope
	 * 
	 * @param envelope
	 *            geometry envelope
	 * @param fieldValues
	 *            field values
	 * @return results
	 * @since 3.4.0
	 */
	public ManualFeatureQueryResults query(GeometryEnvelope envelope,
			Map<String, Object> fieldValues) {
		return query(envelope.getMinX(), envelope.getMinY(), envelope.getMaxX(),
				envelope.getMaxY(), fieldValues);
	}

	/**
	 * Manually query for rows within the geometry envelope
	 * 
	 * @param columns
	 *            columns
	 * @param envelope
	 *            geometry envelope
	 * @param fieldValues
	 *            field values
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns,
			GeometryEnvelope envelope, Map<String, Object> fieldValues) {
		return query(columns, envelope.getMinX(), envelope.getMinY(),
				envelope.getMaxX(), envelope.getMaxY(), fieldValues);
	}

	/**
	 * Manually count the rows within the geometry envelope
	 * 
	 * @param envelope
	 *            geometry envelope
	 * @param fieldValues
	 *            field values
	 * @return count
	 * @since 3.4.0
	 */
	public long count(GeometryEnvelope envelope,
			Map<String, Object> fieldValues) {
		return count(envelope.getMinX(), envelope.getMinY(), envelope.getMaxX(),
				envelope.getMaxY(), fieldValues);
	}

	/**
	 * Manually query for rows within the geometry envelope
	 * 
	 * @param envelope
	 *            geometry envelope
	 * @param where
	 *            where clause
	 * @return results
	 * @since 3.4.0
	 */
	public ManualFeatureQueryResults query(GeometryEnvelope envelope,
			String where) {
		return query(envelope, where, null);
	}

	/**
	 * Manually query for rows within the geometry envelope
	 * 
	 * @param columns
	 *            columns
	 * @param envelope
	 *            geometry envelope
	 * @param where
	 *            where clause
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns,
			GeometryEnvelope envelope, String where) {
		return query(columns, envelope, where, null);
	}

	/**
	 * Manually count the rows within the geometry envelope
	 * 
	 * @param envelope
	 *            geometry envelope
	 * @param where
	 *            where clause
	 * @return count
	 * @since 3.4.0
	 */
	public long count(GeometryEnvelope envelope, String where) {
		return count(envelope, where, null);
	}

	/**
	 * Manually query for rows within the geometry envelope
	 * 
	 * @param envelope
	 *            geometry envelope
	 * @param where
	 *            where clause
	 * @param whereArgs
	 *            where arguments
	 * @return results
	 * @since 3.4.0
	 */
	public ManualFeatureQueryResults query(GeometryEnvelope envelope,
			String where, String[] whereArgs) {
		return query(envelope.getMinX(), envelope.getMinY(), envelope.getMaxX(),
				envelope.getMaxY(), where, whereArgs);
	}

	/**
	 * Manually query for rows within the geometry envelope
	 * 
	 * @param columns
	 *            columns
	 * @param envelope
	 *            geometry envelope
	 * @param where
	 *            where clause
	 * @param whereArgs
	 *            where arguments
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns,
			GeometryEnvelope envelope, String where, String[] whereArgs) {
		return query(columns, envelope.getMinX(), envelope.getMinY(),
				envelope.getMaxX(), envelope.getMaxY(), where, whereArgs);
	}

	/**
	 * Manually count the rows within the geometry envelope
	 * 
	 * @param envelope
	 *            geometry envelope
	 * @param where
	 *            where clause
	 * @param whereArgs
	 *            where arguments
	 * @return count
	 * @since 3.4.0
	 */
	public long count(GeometryEnvelope envelope, String where,
			String[] whereArgs) {
		return count(envelope.getMinX(), envelope.getMinY(), envelope.getMaxX(),
				envelope.getMaxY(), where, whereArgs);
	}

	/**
	 * Manually query for rows within the bounds
	 * 
	 * @param minX
	 *            min x
	 * @param minY
	 *            min y
	 * @param maxX
	 *            max x
	 * @param maxY
	 *            max y
	 * @return results
	 */
	public ManualFeatureQueryResults query(double minX, double minY,
			double maxX, double maxY) {
		return query(minX, minY, maxX, maxY, null, null);
	}

	/**
	 * Manually query for rows within the bounds
	 * 
	 * @param columns
	 *            columns
	 * @param minX
	 *            min x
	 * @param minY
	 *            min y
	 * @param maxX
	 *            max x
	 * @param maxY
	 *            max y
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns, double minX,
			double minY, double maxX, double maxY) {
		return query(columns, minX, minY, maxX, maxY, null, null);
	}

	/**
	 * Manually count the rows within the bounds
	 * 
	 * @param minX
	 *            min x
	 * @param minY
	 *            min y
	 * @param maxX
	 *            max x
	 * @param maxY
	 *            max y
	 * @return count
	 */
	public long count(double minX, double minY, double maxX, double maxY) {
		return query(minX, minY, maxX, maxY).count();
	}

	/**
	 * Manually query for rows within the bounds
	 * 
	 * @param minX
	 *            min x
	 * @param minY
	 *            min y
	 * @param maxX
	 *            max x
	 * @param maxY
	 *            max y
	 * @param fieldValues
	 *            field values
	 * @return results
	 * @since 3.4.0
	 */
	public ManualFeatureQueryResults query(double minX, double minY,
			double maxX, double maxY, Map<String, Object> fieldValues) {
		String where = featureDao.buildWhere(fieldValues.entrySet());
		String[] whereArgs = featureDao.buildWhereArgs(fieldValues.values());
		return query(minX, minY, maxX, maxY, where, whereArgs);
	}

	/**
	 * Manually query for rows within the bounds
	 * 
	 * @param columns
	 *            columns
	 * @param minX
	 *            min x
	 * @param minY
	 *            min y
	 * @param maxX
	 *            max x
	 * @param maxY
	 *            max y
	 * @param fieldValues
	 *            field values
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns, double minX,
			double minY, double maxX, double maxY,
			Map<String, Object> fieldValues) {
		String where = featureDao.buildWhere(fieldValues.entrySet());
		String[] whereArgs = featureDao.buildWhereArgs(fieldValues.values());
		return query(columns, minX, minY, maxX, maxY, where, whereArgs);
	}

	/**
	 * Manually count the rows within the bounds
	 * 
	 * @param minX
	 *            min x
	 * @param minY
	 *            min y
	 * @param maxX
	 *            max x
	 * @param maxY
	 *            max y
	 * @param fieldValues
	 *            field values
	 * @return count
	 * @since 3.4.0
	 */
	public long count(double minX, double minY, double maxX, double maxY,
			Map<String, Object> fieldValues) {
		String where = featureDao.buildWhere(fieldValues.entrySet());
		String[] whereArgs = featureDao.buildWhereArgs(fieldValues.values());
		return count(minX, minY, maxX, maxY, where, whereArgs);
	}

	/**
	 * Manually query for rows within the bounds
	 * 
	 * @param minX
	 *            min x
	 * @param minY
	 *            min y
	 * @param maxX
	 *            max x
	 * @param maxY
	 *            max y
	 * @param where
	 *            where clause
	 * @return results
	 * @since 3.4.0
	 */
	public ManualFeatureQueryResults query(double minX, double minY,
			double maxX, double maxY, String where) {
		return query(minX, minY, maxX, maxY, where, null);
	}

	/**
	 * Manually query for rows within the bounds
	 * 
	 * @param columns
	 *            columns
	 * @param minX
	 *            min x
	 * @param minY
	 *            min y
	 * @param maxX
	 *            max x
	 * @param maxY
	 *            max y
	 * @param where
	 *            where clause
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns, double minX,
			double minY, double maxX, double maxY, String where) {
		return query(columns, minX, minY, maxX, maxY, where, null);
	}

	/**
	 * Manually count the rows within the bounds
	 * 
	 * @param minX
	 *            min x
	 * @param minY
	 *            min y
	 * @param maxX
	 *            max x
	 * @param maxY
	 *            max y
	 * @param where
	 *            where clause
	 * @return count
	 * @since 3.4.0
	 */
	public long count(double minX, double minY, double maxX, double maxY,
			String where) {
		return count(minX, minY, maxX, maxY, where, null);
	}

	/**
	 * Manually query for rows within the bounds
	 * 
	 * @param minX
	 *            min x
	 * @param minY
	 *            min y
	 * @param maxX
	 *            max x
	 * @param maxY
	 *            max y
	 * @param where
	 *            where clause
	 * @param whereArgs
	 *            where args
	 * @return results
	 * @since 3.4.0
	 */
	public ManualFeatureQueryResults query(double minX, double minY,
			double maxX, double maxY, String where, String[] whereArgs) {
		return query(featureDao.getColumnNames(), minX, minY, maxX, maxY, where,
				whereArgs);
	}

	/**
	 * Manually query for rows within the bounds
	 * 
	 * @param columns
	 *            columns
	 * @param minX
	 *            min x
	 * @param minY
	 *            min y
	 * @param maxX
	 *            max x
	 * @param maxY
	 *            max y
	 * @param where
	 *            where clause
	 * @param whereArgs
	 *            where args
	 * @return results
	 * @since 3.5.0
	 */
	public ManualFeatureQueryResults query(String[] columns, double minX,
			double minY, double maxX, double maxY, String where,
			String[] whereArgs) {

		List<Long> featureIds = new ArrayList<>();

		long offset = 0;
		boolean hasResults = true;

		minX -= tolerance;
		maxX += tolerance;
		minY -= tolerance;
		maxY += tolerance;

		String[] queryColumns = featureDao.getIdAndGeometryColumnNames();

		while (hasResults) {

			hasResults = false;

			FeatureResultSet resultSet = featureDao.queryForChunk(queryColumns,
					where, whereArgs, chunkLimit, offset);
			try {
				while (resultSet.moveToNext()) {
					hasResults = true;

					FeatureRow featureRow = resultSet.getRow();
					GeometryEnvelope envelope = featureRow
							.getGeometryEnvelope();
					if (envelope != null) {

						double minXMax = Math.max(minX, envelope.getMinX());
						double maxXMin = Math.min(maxX, envelope.getMaxX());
						double minYMax = Math.max(minY, envelope.getMinY());
						double maxYMin = Math.min(maxY, envelope.getMaxY());

						if (minXMax <= maxXMin && minYMax <= maxYMin) {
							featureIds.add(featureRow.getId());
						}

					}
				}
			} finally {
				resultSet.close();
			}

			offset += chunkLimit;
		}

		ManualFeatureQueryResults results = new ManualFeatureQueryResults(
				featureDao, columns, featureIds);

		return results;
	}

	/**
	 * Manually count the rows within the bounds
	 * 
	 * @param minX
	 *            min x
	 * @param minY
	 *            min y
	 * @param maxX
	 *            max x
	 * @param maxY
	 *            max y
	 * @param where
	 *            where clause
	 * @param whereArgs
	 *            where args
	 * @return count
	 * @since 3.4.0
	 */
	public long count(double minX, double minY, double maxX, double maxY,
			String where, String[] whereArgs) {
		return query(minX, minY, maxX, maxY, where, whereArgs).count();
	}

}