package mil.nga.geopackage.test.features.index;

import java.io.File;
import java.sql.SQLException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import junit.framework.TestCase;
import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.core.srs.SpatialReferenceSystem;
import mil.nga.geopackage.db.GeoPackageDataType;
import mil.nga.geopackage.features.columns.GeometryColumns;
import mil.nga.geopackage.features.index.FeatureIndexManager;
import mil.nga.geopackage.features.index.FeatureIndexResults;
import mil.nga.geopackage.features.index.FeatureIndexType;
import mil.nga.geopackage.features.user.FeatureColumn;
import mil.nga.geopackage.features.user.FeatureDao;
import mil.nga.geopackage.features.user.FeatureResultSet;
import mil.nga.geopackage.features.user.FeatureRow;
import mil.nga.geopackage.features.user.FeatureTable;
import mil.nga.geopackage.geom.GeoPackageGeometryData;
import mil.nga.geopackage.manager.GeoPackageManager;
import mil.nga.geopackage.schema.TableColumnKey;
import mil.nga.geopackage.test.GeoPackageTestUtils;
import mil.nga.geopackage.test.TestUtils;
import mil.nga.geopackage.test.io.TestGeoPackageProgress;
import mil.nga.sf.GeometryEnvelope;
import mil.nga.sf.GeometryType;
import mil.nga.sf.Point;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionConstants;
import mil.nga.sf.proj.ProjectionFactory;
import mil.nga.sf.proj.ProjectionTransform;
import mil.nga.sf.util.GeometryEnvelopeBuilder;

/**
 * Feature Index Manager Utility test methods
 *
 * @author osbornb
 */
public class FeatureIndexManagerUtils {

	/**
	 * Test index
	 *
	 * @param geoPackage
	 *            GeoPackage
	 * @throws SQLException
	 *             upon error
	 */
	public static void testIndex(GeoPackage geoPackage) throws SQLException {
		testIndex(geoPackage, FeatureIndexType.GEOPACKAGE, false);
		testIndex(geoPackage, FeatureIndexType.RTREE, true);
	}

	private static void testIndex(GeoPackage geoPackage, FeatureIndexType type,
			boolean includeEmpty) throws SQLException {

		// Test indexing each feature table
		List<String> featureTables = geoPackage.getFeatureTables();
		for (String featureTable : featureTables) {

			FeatureDao featureDao = geoPackage.getFeatureDao(featureTable);
			FeatureIndexManager featureIndexManager = new FeatureIndexManager(
					geoPackage, featureDao);
			featureIndexManager.setContinueOnError(false);
			featureIndexManager.setIndexLocation(type);
			featureIndexManager.deleteAllIndexes();

			// Determine how many features have geometry envelopes or geometries
			int expectedCount = 0;
			FeatureRow testFeatureRow = null;
			FeatureResultSet featureResultSet = featureDao.query();
			while (featureResultSet.moveToNext()) {
				FeatureRow featureRow = featureResultSet.getRow();
				if (featureRow.getGeometryEnvelope() != null) {
					expectedCount++;
					// Randomly choose a feature row with Geometry for testing
					// queries later
					if (testFeatureRow == null) {
						testFeatureRow = featureRow;
					} else if (Math
							.random() < (1.0 / featureResultSet.getCount())) {
						testFeatureRow = featureRow;
					}
				} else if (includeEmpty) {
					expectedCount++;
				}
			}
			featureResultSet.close();

			TestCase.assertFalse(featureIndexManager.isIndexed());
			TestCase.assertNull(featureIndexManager.getLastIndexed());
			Date currentDate = new Date();

			// Test indexing
			TestGeoPackageProgress progress = new TestGeoPackageProgress();
			featureIndexManager.setProgress(progress);
			int indexCount = featureIndexManager.index();
			TestCase.assertEquals(expectedCount, indexCount);
			TestCase.assertEquals(featureDao.count(), progress.getProgress());
			TestCase.assertNotNull(featureIndexManager.getLastIndexed());
			Date lastIndexed = featureIndexManager.getLastIndexed();
			TestCase.assertTrue(lastIndexed.getTime() > currentDate.getTime());

			TestCase.assertTrue(featureIndexManager.isIndexed());
			TestCase.assertEquals(expectedCount, featureIndexManager.count());

			// Test re-indexing, both ignored and forced
			TestCase.assertEquals(0, featureIndexManager.index());
			TestCase.assertEquals(expectedCount,
					featureIndexManager.index(true));
			TestCase.assertTrue(featureIndexManager.getLastIndexed()
					.getTime() > lastIndexed.getTime());

			// Query for all indexed geometries
			int resultCount = 0;
			FeatureIndexResults featureIndexResults = featureIndexManager
					.query();
			for (FeatureRow featureRow : featureIndexResults) {
				validateFeatureRow(featureIndexManager, featureRow, null,
						includeEmpty);
				resultCount++;
			}
			featureIndexResults.close();
			TestCase.assertEquals(expectedCount, resultCount);

			// Query for all indexed geometries with columns
			resultCount = 0;
			featureIndexResults = featureIndexManager
					.query(featureDao.getIdAndGeometryColumnNames());
			for (FeatureRow featureRow : featureIndexResults) {
				validateFeatureRow(featureIndexManager, featureRow, null,
						includeEmpty);
				resultCount++;
			}
			featureIndexResults.close();
			TestCase.assertEquals(expectedCount, resultCount);

			// Test the query by envelope
			GeometryEnvelope envelope = testFeatureRow.getGeometryEnvelope();
			final double difference = .000001;
			envelope.setMinX(envelope.getMinX() - difference);
			envelope.setMaxX(envelope.getMaxX() + difference);
			envelope.setMinY(envelope.getMinY() - difference);
			envelope.setMaxY(envelope.getMaxY() + difference);
			if (envelope.hasZ()) {
				envelope.setMinZ(envelope.getMinZ() - difference);
				envelope.setMaxZ(envelope.getMaxZ() + difference);
			}
			if (envelope.hasM()) {
				envelope.setMinM(envelope.getMinM() - difference);
				envelope.setMaxM(envelope.getMaxM() + difference);
			}
			resultCount = 0;
			boolean featureFound = false;
			TestCase.assertTrue(featureIndexManager.count(envelope) >= 1);
			featureIndexResults = featureIndexManager.query(envelope);
			for (FeatureRow featureRow : featureIndexResults) {
				validateFeatureRow(featureIndexManager, featureRow, envelope,
						includeEmpty);
				if (featureRow.getId() == testFeatureRow.getId()) {
					featureFound = true;
				}
				resultCount++;
			}
			featureIndexResults.close();
			TestCase.assertTrue(featureFound);
			TestCase.assertTrue(resultCount >= 1);
			resultCount = 0;
			featureFound = false;
			featureIndexResults = featureIndexManager
					.query(featureDao.getIdAndGeometryColumnNames(), envelope);
			for (FeatureRow featureRow : featureIndexResults) {
				validateFeatureRow(featureIndexManager, featureRow, envelope,
						includeEmpty);
				if (featureRow.getId() == testFeatureRow.getId()) {
					featureFound = true;
				}
				resultCount++;
			}
			featureIndexResults.close();
			TestCase.assertTrue(featureFound);
			TestCase.assertTrue(resultCount >= 1);

			// Test the query by envelope with id iteration
			resultCount = 0;
			featureFound = false;
			TestCase.assertTrue(featureIndexManager.count(envelope) >= 1);
			featureIndexResults = featureIndexManager.query(envelope);
			for (long featureRowId : featureIndexResults.ids()) {
				FeatureRow featureRow = featureDao.queryForIdRow(featureRowId);
				validateFeatureRow(featureIndexManager, featureRow, envelope,
						includeEmpty);
				if (featureRowId == testFeatureRow.getId()) {
					featureFound = true;
				}
				resultCount++;
			}
			featureIndexResults.close();
			TestCase.assertTrue(featureFound);
			TestCase.assertTrue(resultCount >= 1);

			// Pick a projection different from the feature dao and project the
			// bounding box
			BoundingBox boundingBox = new BoundingBox(envelope.getMinX() - 1,
					envelope.getMinY() - 1, envelope.getMaxX() + 1,
					envelope.getMaxY() + 1);
			Projection projection = null;
			if (!featureDao.getProjection().equals(
					ProjectionConstants.AUTHORITY_EPSG,
					ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM)) {
				projection = ProjectionFactory.getProjection(
						ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM);
			} else {
				projection = ProjectionFactory
						.getProjection(ProjectionConstants.EPSG_WEB_MERCATOR);
			}
			ProjectionTransform transform = featureDao.getProjection()
					.getTransformation(projection);
			BoundingBox transformedBoundingBox = boundingBox
					.transform(transform);

			// Test the query by projected bounding box
			resultCount = 0;
			featureFound = false;
			TestCase.assertTrue(featureIndexManager
					.count(transformedBoundingBox, projection) >= 1);
			featureIndexResults = featureIndexManager
					.query(transformedBoundingBox, projection);
			for (FeatureRow featureRow : featureIndexResults) {
				validateFeatureRow(featureIndexManager, featureRow,
						boundingBox.buildEnvelope(), includeEmpty);
				if (featureRow.getId() == testFeatureRow.getId()) {
					featureFound = true;
				}
				resultCount++;
			}
			featureIndexResults.close();
			TestCase.assertTrue(featureFound);
			TestCase.assertTrue(resultCount >= 1);
			resultCount = 0;
			featureFound = false;
			featureIndexResults = featureIndexManager.query(
					featureDao.getIdAndGeometryColumnNames(),
					transformedBoundingBox, projection);
			for (FeatureRow featureRow : featureIndexResults) {
				validateFeatureRow(featureIndexManager, featureRow,
						boundingBox.buildEnvelope(), includeEmpty);
				if (featureRow.getId() == testFeatureRow.getId()) {
					featureFound = true;
				}
				resultCount++;
			}
			featureIndexResults.close();
			TestCase.assertTrue(featureFound);
			TestCase.assertTrue(resultCount >= 1);

			// Test query by criteria
			FeatureTable table = featureDao.getTable();
			List<FeatureColumn> columns = table.getColumns();

			Map<String, Number> numbers = new HashMap<>();
			Map<String, String> strings = new HashMap<>();

			for (FeatureColumn column : columns) {
				if (column.isPrimaryKey() || column.isGeometry()) {
					continue;
				}
				GeoPackageDataType dataType = column.getDataType();
				switch (dataType) {
				case DOUBLE:
				case FLOAT:
				case INT:
				case INTEGER:
				case TINYINT:
				case SMALLINT:
				case MEDIUMINT:
				case REAL:
					numbers.put(column.getName(), null);
					break;
				case TEXT:
					strings.put(column.getName(), null);
					break;
				default:

				}
			}

			for (String number : numbers.keySet()) {
				Object value = testFeatureRow.getValue(number);
				numbers.put(number, (Number) value);
			}

			for (String string : strings.keySet()) {
				String value = testFeatureRow.getValueString(string);
				strings.put(string, value);
			}

			for (Entry<String, Number> number : numbers.entrySet()) {

				String column = number.getKey();
				double value = number.getValue().doubleValue();

				String where = column + " >= ? AND " + column + " <= ?";
				String[] whereArgs = new String[] {
						String.valueOf(value - 0.001),
						String.valueOf(value + 0.001) };

				long count = featureIndexManager.count(where, whereArgs);
				TestCase.assertTrue(count >= 1);
				featureIndexResults = featureIndexManager.query(where,
						whereArgs);
				TestCase.assertEquals(count, featureIndexResults.count());
				for (FeatureRow featureRow : featureIndexResults) {
					TestCase.assertEquals(value,
							((Number) featureRow.getValue(column))
									.doubleValue());
				}
				featureIndexResults.close();
				featureIndexResults = featureIndexManager
						.query(new String[] { column }, where, whereArgs);
				TestCase.assertEquals(count, featureIndexResults.count());
				for (FeatureRow featureRow : featureIndexResults) {
					TestCase.assertEquals(value,
							((Number) featureRow.getValue(column))
									.doubleValue());
				}
				featureIndexResults.close();

				resultCount = 0;
				featureFound = false;

				count = featureIndexManager.count(transformedBoundingBox,
						projection, where, whereArgs);
				TestCase.assertTrue(count >= 1);
				featureIndexResults = featureIndexManager.query(
						transformedBoundingBox, projection, where, whereArgs);
				TestCase.assertEquals(count, featureIndexResults.count());
				for (FeatureRow featureRow : featureIndexResults) {
					TestCase.assertEquals(value,
							((Number) featureRow.getValue(column))
									.doubleValue());
					validateFeatureRow(featureIndexManager, featureRow,
							boundingBox.buildEnvelope(), includeEmpty);
					if (featureRow.getId() == testFeatureRow.getId()) {
						featureFound = true;
					}
					resultCount++;
				}
				featureIndexResults.close();
				TestCase.assertTrue(featureFound);
				TestCase.assertTrue(resultCount >= 1);

				resultCount = 0;
				featureFound = false;

				featureIndexResults = featureIndexManager.query(
						new String[] { featureDao.getGeometryColumnName(),
								column, featureDao.getIdColumnName() },
						transformedBoundingBox, projection, where, whereArgs);
				TestCase.assertEquals(count, featureIndexResults.count());
				for (FeatureRow featureRow : featureIndexResults) {
					TestCase.assertEquals(value,
							((Number) featureRow.getValue(column))
									.doubleValue());
					validateFeatureRow(featureIndexManager, featureRow,
							boundingBox.buildEnvelope(), includeEmpty);
					if (featureRow.getId() == testFeatureRow.getId()) {
						featureFound = true;
					}
					resultCount++;
				}
				featureIndexResults.close();
				TestCase.assertTrue(featureFound);
				TestCase.assertTrue(resultCount >= 1);
			}

			for (Entry<String, String> string : strings.entrySet()) {

				String column = string.getKey();
				String value = string.getValue();

				Map<String, Object> fieldValues = new HashMap<>();
				fieldValues.put(column, value);

				long count = featureIndexManager.count(fieldValues);
				TestCase.assertTrue(count >= 1);
				featureIndexResults = featureIndexManager.query(fieldValues);
				TestCase.assertEquals(count, featureIndexResults.count());
				for (FeatureRow featureRow : featureIndexResults) {
					TestCase.assertEquals(value,
							featureRow.getValueString(column));
				}
				featureIndexResults.close();
				featureIndexResults = featureIndexManager
						.query(new String[] { column }, fieldValues);
				TestCase.assertEquals(count, featureIndexResults.count());
				for (FeatureRow featureRow : featureIndexResults) {
					TestCase.assertEquals(value,
							featureRow.getValueString(column));
				}
				featureIndexResults.close();

				resultCount = 0;
				featureFound = false;

				count = featureIndexManager.count(transformedBoundingBox,
						projection, fieldValues);
				TestCase.assertTrue(count >= 1);
				featureIndexResults = featureIndexManager
						.query(transformedBoundingBox, projection, fieldValues);
				TestCase.assertEquals(count, featureIndexResults.count());
				for (FeatureRow featureRow : featureIndexResults) {
					TestCase.assertEquals(value,
							featureRow.getValueString(column));
					validateFeatureRow(featureIndexManager, featureRow,
							boundingBox.buildEnvelope(), includeEmpty);
					if (featureRow.getId() == testFeatureRow.getId()) {
						featureFound = true;
					}
					resultCount++;
				}
				featureIndexResults.close();
				TestCase.assertTrue(featureFound);
				TestCase.assertTrue(resultCount >= 1);

				resultCount = 0;
				featureFound = false;

				featureIndexResults = featureIndexManager.query(
						new String[] { column, featureDao.getIdColumnName(),
								featureDao.getGeometryColumnName() },
						transformedBoundingBox, projection, fieldValues);
				TestCase.assertEquals(count, featureIndexResults.count());
				for (FeatureRow featureRow : featureIndexResults) {
					TestCase.assertEquals(value,
							featureRow.getValueString(column));
					validateFeatureRow(featureIndexManager, featureRow,
							boundingBox.buildEnvelope(), includeEmpty);
					if (featureRow.getId() == testFeatureRow.getId()) {
						featureFound = true;
					}
					resultCount++;
				}
				featureIndexResults.close();
				TestCase.assertTrue(featureFound);
				TestCase.assertTrue(resultCount >= 1);

			}

			// Update a Geometry and update the index of a single feature row
			GeoPackageGeometryData geometryData = new GeoPackageGeometryData(
					featureDao.getGeometryColumns().getSrsId());
			Point point = new Point(5, 5);
			geometryData.setGeometry(point);
			testFeatureRow.setGeometry(geometryData);
			TestCase.assertEquals(1, featureDao.update(testFeatureRow));
			Date lastIndexedBefore = featureIndexManager.getLastIndexed();
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
			}
			TestCase.assertTrue(featureIndexManager.index(testFeatureRow));
			Date lastIndexedAfter = featureIndexManager.getLastIndexed();
			TestCase.assertTrue(lastIndexedAfter.after(lastIndexedBefore));

			// Verify the index was updated for the feature row
			envelope = GeometryEnvelopeBuilder.buildEnvelope(point);
			resultCount = 0;
			featureFound = false;
			TestCase.assertTrue(featureIndexManager.count(envelope) >= 1);
			featureIndexResults = featureIndexManager.query(envelope);
			for (FeatureRow featureRow : featureIndexResults) {
				validateFeatureRow(featureIndexManager, featureRow, envelope,
						includeEmpty);
				if (featureRow.getId() == testFeatureRow.getId()) {
					featureFound = true;
				}
				resultCount++;
			}
			featureIndexResults.close();
			TestCase.assertTrue(featureFound);
			TestCase.assertTrue(resultCount >= 1);

			featureIndexManager.close();
		}

		// Delete the extensions
		boolean everyOther = false;
		for (String featureTable : featureTables) {
			FeatureDao featureDao = geoPackage.getFeatureDao(featureTable);
			FeatureIndexManager featureIndexManager = new FeatureIndexManager(
					geoPackage, featureDao);
			featureIndexManager.setIndexLocation(type);
			TestCase.assertTrue(featureIndexManager.isIndexed());

			// Test deleting a single geometry index
			if (everyOther) {
				FeatureResultSet featureResultSet = featureDao.query();
				while (featureResultSet.moveToNext()) {
					FeatureRow featureRow = featureResultSet.getRow();
					if (featureRow.getGeometryEnvelope() != null) {
						featureResultSet.close();
						TestCase.assertTrue(
								featureIndexManager.deleteIndex(featureRow));
						break;
					}
				}
				featureResultSet.close();
			}

			featureIndexManager.deleteIndex();

			TestCase.assertFalse(featureIndexManager.isIndexed());
			everyOther = !everyOther;

			featureIndexManager.close();
		}

	}

	/**
	 * Validate a Feature Row result
	 *
	 * @param featureIndexManager
	 * @param featureRow
	 * @param queryEnvelope
	 */
	private static void validateFeatureRow(
			FeatureIndexManager featureIndexManager, FeatureRow featureRow,
			GeometryEnvelope queryEnvelope, boolean includeEmpty) {
		TestCase.assertNotNull(featureRow);
		GeometryEnvelope envelope = featureRow.getGeometryEnvelope();

		if (!includeEmpty) {
			TestCase.assertNotNull(envelope);

			if (queryEnvelope != null) {
				TestCase.assertTrue(
						envelope.getMinX() <= queryEnvelope.getMaxX());
				TestCase.assertTrue(
						envelope.getMaxX() >= queryEnvelope.getMinX());
				TestCase.assertTrue(
						envelope.getMinY() <= queryEnvelope.getMaxY());
				TestCase.assertTrue(
						envelope.getMaxY() >= queryEnvelope.getMinY());
				if (envelope.isHasZ()) {
					if (queryEnvelope.hasZ()) {
						TestCase.assertTrue(
								envelope.getMinZ() <= queryEnvelope.getMaxZ());
						TestCase.assertTrue(
								envelope.getMaxZ() >= queryEnvelope.getMinZ());
					}
				}
				if (envelope.isHasM()) {
					if (queryEnvelope.hasM()) {
						TestCase.assertTrue(
								envelope.getMinM() <= queryEnvelope.getMaxM());
						TestCase.assertTrue(
								envelope.getMaxM() >= queryEnvelope.getMinM());
					}
				}
			}
		}
	}

	/**
	 * Test large index
	 *
	 * @param geoPackage
	 *            GeoPackage
	 * @param numFeatures
	 *            num features
	 * @param verbose
	 *            verbose printing
	 * @throws SQLException
	 *             upon error
	 */
	public static void testLargeIndex(GeoPackage geoPackage, int numFeatures,
			boolean verbose) throws SQLException {

		String featureTable = "large_index";

		GeometryColumns geometryColumns = new GeometryColumns();
		geometryColumns.setId(new TableColumnKey(featureTable, "geom"));
		geometryColumns.setGeometryType(GeometryType.POLYGON);
		geometryColumns.setZ((byte) 0);
		geometryColumns.setM((byte) 0);

		BoundingBox boundingBox = new BoundingBox(-180, -90, 180, 90);

		SpatialReferenceSystem srs = geoPackage.getSpatialReferenceSystemDao()
				.getOrCreateCode(ProjectionConstants.AUTHORITY_EPSG,
						ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM);
		List<FeatureColumn> additionalColumns = GeoPackageTestUtils
				.getFeatureColumns();
		geometryColumns = geoPackage.createFeatureTableWithMetadata(
				geometryColumns, additionalColumns, boundingBox, srs.getId());

		FeatureDao featureDao = geoPackage.getFeatureDao(geometryColumns);

		System.out.println();
		System.out.println("Inserting Feature Rows: " + numFeatures);
		TestUtils.addRowsToFeatureTable(geoPackage, geometryColumns,
				featureDao.getTable(), numFeatures, false, false, false);

		testTimedIndex(geoPackage, featureTable, true, verbose);
	}

	/**
	 * Main method to test a GeoPackage file for query times
	 * 
	 * @param args
	 *            arguments
	 * @throws Exception
	 *             upon error
	 */
	public static void main(String[] args) throws Exception {
		File file = new File("/path/name.gpkg");
		GeoPackage geoPackage = GeoPackageManager.open(file);
		boolean compareProjectionCounts = true;
		boolean verbose = true;
		testTimedIndex(geoPackage, compareProjectionCounts, verbose);
	}

	/**
	 * Test large index
	 *
	 * @param geoPackage
	 *            GeoPackage
	 * @param compareProjectionCounts
	 *            compare projection counts and query counts
	 * @param verbose
	 *            verbose printing
	 * @throws SQLException
	 *             upon error
	 */
	public static void testTimedIndex(GeoPackage geoPackage,
			boolean compareProjectionCounts, boolean verbose)
			throws SQLException {
		for (String featureTable : geoPackage.getFeatureTables()) {
			testTimedIndex(geoPackage, featureTable, compareProjectionCounts,
					verbose);
		}
	}

	/**
	 * Test large index
	 *
	 * @param geoPackage
	 *            GeoPackage
	 * @param featureTable
	 *            feature table
	 * @param compareProjectionCounts
	 *            compare projection counts and query counts
	 * @param verbose
	 *            verbose printing
	 * @throws SQLException
	 *             upon error
	 */
	public static void testTimedIndex(GeoPackage geoPackage,
			String featureTable, boolean compareProjectionCounts,
			boolean verbose) throws SQLException {

		FeatureDao featureDao = geoPackage.getFeatureDao(featureTable);

		System.out.println();
		System.out.println("+++++++++++++++++++++++++++++++++++++");
		System.out.println("Timed Index Test");
		System.out.println(featureTable);
		System.out.println("Features: " + featureDao.count() + ", Columns: "
				+ featureDao.columnCount());
		System.out.println("+++++++++++++++++++++++++++++++++++++");

		GeometryEnvelope envelope = null;
		FeatureResultSet resultSet = featureDao.query();
		while (resultSet.moveToNext()) {
			FeatureRow featureRow = resultSet.getRow();
			GeometryEnvelope rowEnvelope = featureRow.getGeometryEnvelope();
			if (envelope == null) {
				envelope = rowEnvelope;
			} else if (rowEnvelope != null) {
				envelope = envelope.union(rowEnvelope);
			}
		}
		resultSet.close();

		List<FeatureIndexTestEnvelope> envelopes = createEnvelopes(envelope);

		resultSet = featureDao.query();
		while (resultSet.moveToNext()) {
			FeatureRow featureRow = resultSet.getRow();
			GeometryEnvelope rowEnvelope = featureRow.getGeometryEnvelope();
			if (rowEnvelope != null) {
				BoundingBox rowBoundingBox = new BoundingBox(rowEnvelope);
				for (FeatureIndexTestEnvelope testEnvelope : envelopes) {
					if (rowBoundingBox.intersects(
							new BoundingBox(testEnvelope.envelope), true)) {
						testEnvelope.count++;
					}
				}
			}
		}
		resultSet.close();

		testTimedIndex(geoPackage, FeatureIndexType.GEOPACKAGE, featureDao,
				envelopes, .0000000001, compareProjectionCounts, .001, verbose);
		testTimedIndex(geoPackage, FeatureIndexType.RTREE, featureDao,
				envelopes, .0000000001, .0001, compareProjectionCounts, .001,
				verbose);
		testTimedIndex(geoPackage, FeatureIndexType.NONE, featureDao, envelopes,
				.0000000001, compareProjectionCounts, .001, verbose);
	}

	private static List<FeatureIndexTestEnvelope> createEnvelopes(
			GeometryEnvelope envelope) {
		List<FeatureIndexTestEnvelope> envelopes = new ArrayList<>();
		for (int percentage = 100; percentage >= 0; percentage -= 10) {
			envelopes.add(createEnvelope(envelope, percentage));
		}
		return envelopes;
	}

	private static FeatureIndexTestEnvelope createEnvelope(
			GeometryEnvelope envelope, int percentage) {

		FeatureIndexTestEnvelope testEnvelope = new FeatureIndexTestEnvelope();

		double minX;
		double maxX;
		double minY;
		double maxY;

		if (percentage < 100) {

			float percentageRatio = percentage / 100.0f;

			double width = envelope.getMaxX() - envelope.getMinX();
			double height = envelope.getMaxY() - envelope.getMinY();

			minX = envelope.getMinX()
					+ (Math.random() * width * (1.0 - percentageRatio));
			minY = envelope.getMinY()
					+ (Math.random() * height * (1.0 - percentageRatio));

			maxX = minX + (width * percentageRatio);
			maxY = minY + (height * percentageRatio);

		} else {
			minX = envelope.getMinX();
			maxX = envelope.getMaxX();
			minY = envelope.getMinY();
			maxY = envelope.getMaxY();
		}

		testEnvelope.envelope = new GeometryEnvelope(minX, minY, maxX, maxY);
		testEnvelope.percentage = percentage;

		return testEnvelope;
	}

	private static void testTimedIndex(GeoPackage geoPackage,
			FeatureIndexType type, FeatureDao featureDao,
			List<FeatureIndexTestEnvelope> envelopes, double precision,
			boolean compareProjectionCounts, double projectionPrecision,
			boolean verbose) {
		testTimedIndex(geoPackage, type, featureDao, envelopes, precision,
				precision, compareProjectionCounts, projectionPrecision,
				verbose);
	}

	private static void testTimedIndex(GeoPackage geoPackage,
			FeatureIndexType type, FeatureDao featureDao,
			List<FeatureIndexTestEnvelope> envelopes, double innerPrecision,
			double outerPrecision, boolean compareProjectionCounts,
			double projectionPrecision, boolean verbose) {

		System.out.println();
		System.out.println("-------------------------------------");
		System.out.println("Type: " + type);
		System.out.println("-------------------------------------");
		System.out.println();

		int geometryFeatureCount = featureDao.count(
				featureDao.getGeometryColumnName() + " IS NOT NULL", null);
		int totalFeatureCount = featureDao.count();

		FeatureIndexManager featureIndexManager = new FeatureIndexManager(
				geoPackage, featureDao);
		featureIndexManager.setContinueOnError(false);
		try {

			featureIndexManager.setIndexLocation(type);
			featureIndexManager.prioritizeQueryLocation(type);

			if (type != FeatureIndexType.NONE) {
				featureIndexManager.deleteIndex(type);
				TestCase.assertFalse(featureIndexManager.isIndexed(type));
			} else {
				featureIndexManager.setIndexLocationOrder(type);
				TestCase.assertFalse(featureIndexManager.isIndexed());
			}

			TestTimer timerQuery = new FeatureIndexManagerUtils().new TestTimer();
			TestTimer timerCount = new FeatureIndexManagerUtils().new TestTimer();
			timerCount.print = verbose;

			if (type != FeatureIndexType.NONE
					&& !featureIndexManager.isIndexed(type)) {
				timerQuery.start();
				int indexCount = featureIndexManager.index();
				timerQuery.end("Index");
				TestCase.assertEquals(geometryFeatureCount, indexCount);
				TestCase.assertTrue(featureIndexManager.isIndexed());
			}

			timerCount.start();
			long queryCount = featureIndexManager.count();
			timerCount.end("Count Query");
			TestCase.assertTrue(queryCount == geometryFeatureCount
					|| queryCount == totalFeatureCount);

			Projection projection = featureDao.getProjection();
			Projection webMercatorProjection = ProjectionFactory.getProjection(
					ProjectionConstants.AUTHORITY_EPSG,
					ProjectionConstants.EPSG_WEB_MERCATOR);
			ProjectionTransform transformToWebMercator = projection
					.getTransformation(webMercatorProjection);
			ProjectionTransform transformToProjection = webMercatorProjection
					.getTransformation(projection);

			timerCount.start();
			BoundingBox bounds = featureIndexManager.getBoundingBox();
			timerCount.end("Bounds Query");
			TestCase.assertNotNull(bounds);
			FeatureIndexTestEnvelope firstEnvelope = envelopes.get(0);
			BoundingBox firstBounds = new BoundingBox(firstEnvelope.envelope);

			assertRange(firstBounds.getMinLongitude(), bounds.getMinLongitude(),
					outerPrecision, innerPrecision);
			assertRange(firstBounds.getMinLatitude(), bounds.getMinLatitude(),
					outerPrecision, innerPrecision);
			assertRange(firstBounds.getMaxLongitude(), bounds.getMaxLongitude(),
					innerPrecision, outerPrecision);
			assertRange(firstBounds.getMaxLatitude(), bounds.getMaxLatitude(),
					innerPrecision, outerPrecision);

			timerCount.start();
			BoundingBox projectedBounds = featureIndexManager
					.getBoundingBox(webMercatorProjection);
			timerCount.end("Bounds Projection Query");
			TestCase.assertNotNull(projectedBounds);
			BoundingBox reprojectedBounds = projectedBounds
					.transform(transformToProjection);

			assertRange(firstBounds.getMinLongitude(),
					reprojectedBounds.getMinLongitude(), projectionPrecision,
					projectionPrecision);
			assertRange(firstBounds.getMinLatitude(),
					reprojectedBounds.getMinLatitude(), projectionPrecision,
					projectionPrecision);
			assertRange(firstBounds.getMaxLongitude(),
					reprojectedBounds.getMaxLongitude(), projectionPrecision,
					projectionPrecision);
			assertRange(firstBounds.getMaxLatitude(),
					reprojectedBounds.getMaxLatitude(), projectionPrecision,
					projectionPrecision);

			timerQuery.reset();
			timerCount.reset();
			TestTimer timerIteration = new FeatureIndexManagerUtils().new TestTimer();
			TestTimer timerColumnsIteration = new FeatureIndexManagerUtils().new TestTimer();

			timerIteration.print = timerCount.print;
			timerColumnsIteration.print = timerCount.print;
			timerQuery.print = timerCount.print;

			String[] columns = featureDao.getIdAndGeometryColumnNames();

			for (FeatureIndexTestEnvelope testEnvelope : envelopes) {

				String percentage = Integer.toString(testEnvelope.percentage);
				GeometryEnvelope envelope = testEnvelope.envelope;
				int expectedCount = testEnvelope.count;

				if (verbose) {
					System.out.println(
							percentage + "% Feature Count: " + expectedCount);
				}

				timerCount.start();
				long fullCount = featureIndexManager.count(envelope);
				timerCount.end(percentage + "% Envelope Count Query");
				assertCounts(featureIndexManager, testEnvelope, type,
						outerPrecision, expectedCount, fullCount);

				timerQuery.start();
				FeatureIndexResults results = featureIndexManager
						.query(envelope);
				timerQuery.end(percentage + "% Envelope Query");
				iterateResults(timerIteration,
						percentage + "% Envelope Query Iteration", results);
				assertCounts(featureIndexManager, testEnvelope, type,
						outerPrecision, expectedCount, results.count());
				results.close();

				timerQuery.start();
				results = featureIndexManager.query(columns, envelope);
				timerQuery.end(percentage + "% Envelope Columns Query");
				iterateResults(timerColumnsIteration,
						percentage + "% Envelope Columns Query Iteration",
						results);
				assertCounts(featureIndexManager, testEnvelope, type,
						outerPrecision, expectedCount, results.count());
				results.close();

				BoundingBox boundingBox = new BoundingBox(envelope);
				timerCount.start();
				fullCount = featureIndexManager.count(boundingBox);
				timerCount.end(percentage + "% Bounding Box Count Query");
				assertCounts(featureIndexManager, testEnvelope, type,
						outerPrecision, expectedCount, fullCount);

				timerQuery.start();
				results = featureIndexManager.query(boundingBox);
				timerQuery.end(percentage + "% Bounding Box Query");
				iterateResults(timerIteration,
						percentage + "% Bounding Box Query Iteration", results);
				assertCounts(featureIndexManager, testEnvelope, type,
						outerPrecision, expectedCount, results.count());
				results.close();

				timerQuery.start();
				results = featureIndexManager.query(columns, boundingBox);
				timerQuery.end(percentage + "% Bounding Box Columns Query");
				iterateResults(timerColumnsIteration,
						percentage + "% Bounding Box Columns Query Iteration",
						results);
				assertCounts(featureIndexManager, testEnvelope, type,
						outerPrecision, expectedCount, results.count());
				results.close();

				BoundingBox webMercatorBoundingBox = boundingBox
						.transform(transformToWebMercator);
				timerCount.start();
				fullCount = featureIndexManager.count(webMercatorBoundingBox,
						webMercatorProjection);
				timerCount.end(
						percentage + "% Projected Bounding Box Count Query");
				if (compareProjectionCounts) {
					assertCounts(featureIndexManager, testEnvelope, type,
							outerPrecision, expectedCount, fullCount);
				}

				timerQuery.start();
				results = featureIndexManager.query(webMercatorBoundingBox,
						webMercatorProjection);
				timerQuery.end(percentage + "% Projected Bounding Box Query");
				iterateResults(timerIteration,
						percentage + "% Projected Bounding Box Query Iteration",
						results);
				if (compareProjectionCounts) {
					assertCounts(featureIndexManager, testEnvelope, type,
							outerPrecision, expectedCount, results.count());
				}
				results.close();

				timerQuery.start();
				results = featureIndexManager.query(columns,
						webMercatorBoundingBox, webMercatorProjection);
				timerQuery.end(
						percentage + "% Projected Bounding Box Columns Query");
				iterateResults(timerColumnsIteration, percentage
						+ "% Projected Bounding Box Columns Query Iteration",
						results);
				if (compareProjectionCounts) {
					assertCounts(featureIndexManager, testEnvelope, type,
							outerPrecision, expectedCount, results.count());
				}
				results.close();
			}

			System.out.println();
			System.out.println(
					"Average Count: " + timerCount.averageString() + " ms");
			System.out.println(
					"Average Query: " + timerQuery.averageString() + " ms");
			System.out.println("Average Iteration: "
					+ timerIteration.averageString() + " ms");
			System.out
					.println("Average " + columns.length + " Column Iteration: "
							+ timerColumnsIteration.averageString() + " ms");
		} finally {
			featureIndexManager.close();
		}
	}

	private static void iterateResults(TestTimer timerIteration, String message,
			FeatureIndexResults results) {
		timerIteration.start();
		for (@SuppressWarnings("unused")
		FeatureRow featureRow : results) {
		}
		timerIteration.end(message);
	}

	private static void assertCounts(FeatureIndexManager featureIndexManager,
			FeatureIndexTestEnvelope testEnvelope, FeatureIndexType type,
			double precision, int expectedCount, long fullCount) {

		switch (type) {
		case RTREE:

			if (expectedCount != fullCount) {
				int count = 0;
				FeatureIndexResults results = featureIndexManager.query(
						new String[] { featureIndexManager.getFeatureDao()
								.getGeometryColumnName() },
						testEnvelope.envelope);
				for (FeatureRow featureRow : results) {
					GeometryEnvelope envelope = featureRow
							.getGeometryEnvelope();
					if (envelope.intersects(testEnvelope.envelope, true)) {
						count++;
					} else {
						GeometryEnvelope adjustedEnvelope = new GeometryEnvelope(
								envelope.getMinX() - precision,
								envelope.getMinY() - precision,
								envelope.getMaxX() + precision,
								envelope.getMaxY() + precision);
						TestCase.assertTrue(adjustedEnvelope
								.intersects(testEnvelope.envelope, true));
					}
				}
				results.close();
				TestCase.assertEquals(expectedCount, count);
			}

			break;
		default:
			TestCase.assertEquals(expectedCount, fullCount);
		}

	}

	private static void assertRange(double expected, double actual,
			double lowPrecision, double highPrecision) {
		double low = expected - lowPrecision;
		double high = expected + highPrecision;
		TestCase.assertTrue("Value: " + actual + ", not within range: " + low
				+ " - " + high, low <= actual && actual <= high);
	}

	private class TestTimer {

		int count = 0;
		long totalTime = 0;
		Date before;
		boolean print = true;

		/**
		 * Start the timer
		 */
		public void start() {
			before = new Date();
		}

		/**
		 * End the timer and print the output
		 *
		 * @param output
		 *            output string
		 */
		public void end(String output) {
			long time = new Date().getTime() - before.getTime();
			count++;
			totalTime += time;
			before = null;
			if (print) {
				System.out.println(output + ": " + time + " ms");
			}
		}

		/**
		 * Get the average request time
		 *
		 * @return average milliseconds
		 */
		public double average() {
			return (double) totalTime / count;
		}

		/**
		 * Get the average request time as a string
		 *
		 * @return average milliseconds
		 */
		public String averageString() {
			DecimalFormat formatter = new DecimalFormat("#.00");
			return formatter.format(average());
		}

		/**
		 * Reset the timer
		 */
		public void reset() {
			count = 0;
			totalTime = 0;
			before = null;
		}

	}

}