package mil.nga.geopackage.test.extension.coverage;

import java.sql.SQLException;
import java.util.List;

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.core.srs.SpatialReferenceSystemDao;
import mil.nga.geopackage.extension.coverage.CoverageData;
import mil.nga.geopackage.extension.coverage.CoverageDataAlgorithm;
import mil.nga.geopackage.extension.coverage.CoverageDataResults;
import mil.nga.geopackage.extension.coverage.GriddedCoverage;
import mil.nga.geopackage.extension.coverage.GriddedCoverageEncodingType;
import mil.nga.geopackage.extension.coverage.GriddedTile;
import mil.nga.geopackage.tiles.TileBoundingBoxUtils;
import mil.nga.geopackage.tiles.matrix.TileMatrix;
import mil.nga.geopackage.tiles.matrix.TileMatrixDao;
import mil.nga.geopackage.tiles.matrixset.TileMatrixSet;
import mil.nga.geopackage.tiles.matrixset.TileMatrixSetDao;
import mil.nga.geopackage.tiles.user.TileDao;
import mil.nga.geopackage.tiles.user.TileResultSet;
import mil.nga.geopackage.tiles.user.TileRow;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionConstants;
import mil.nga.sf.proj.ProjectionFactory;
import mil.nga.sf.proj.ProjectionTransform;

/**
 * Coverage Data test utils
 * 
 * @author osbornb
 */
public class CoverageDataTestUtils {

	/**
	 * Test performing coverage data queries
	 * 
	 * @param geoPackage
	 * @param coverageData
	 * @param tileMatrixSet
	 * @param algorithm
	 * @param allowNulls
	 * @throws SQLException
	 */
	public static void testCoverageDataQueries(GeoPackage geoPackage,
			CoverageData<?> coverageData, TileMatrixSet tileMatrixSet,
			CoverageDataAlgorithm algorithm, boolean allowNulls)
			throws SQLException {

		// Determine an alternate projection
		BoundingBox boundingBox = tileMatrixSet.getBoundingBox();
		SpatialReferenceSystemDao srsDao = geoPackage
				.getSpatialReferenceSystemDao();
		long srsId = tileMatrixSet.getSrsId();
		SpatialReferenceSystem srs = srsDao.queryForId(srsId);

		long epsg = srs.getOrganizationCoordsysId();
		Projection projection = srs.getProjection();
		long requestEpsg = -1;
		if (epsg == ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM) {
			requestEpsg = ProjectionConstants.EPSG_WEB_MERCATOR;
		} else {
			requestEpsg = ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM;
		}
		Projection requestProjection = ProjectionFactory
				.getProjection(requestEpsg);
		ProjectionTransform coverageToRequest = projection
				.getTransformation(requestProjection);
		BoundingBox projectedBoundingBox = boundingBox
				.transform(coverageToRequest);

		// Get a random coordinate
		double latDistance = projectedBoundingBox.getMaxLatitude()
				- projectedBoundingBox.getMinLatitude();
		double latitude = latDistance * .9 * Math.random()
				+ projectedBoundingBox.getMinLatitude() + (.05 * latDistance);
		double lonDistance = projectedBoundingBox.getMaxLongitude()
				- projectedBoundingBox.getMinLongitude();
		double longitude = lonDistance * .9 * Math.random()
				+ projectedBoundingBox.getMinLongitude() + (.05 * lonDistance);

		// Test getting the coverage data value of a single coordinate
		CoverageData<?> coverageData2 = CoverageData.getCoverageData(
				geoPackage, coverageData.getTileDao(), requestProjection);
		coverageData2.setAlgorithm(algorithm);
		Double value = coverageData2.getValue(latitude, longitude);
		if (!allowNulls) {
			TestCase.assertNotNull(value);
		}

		// Build a random bounding box
		double minLatitude = (projectedBoundingBox.getMaxLatitude() - projectedBoundingBox
				.getMinLatitude())
				* Math.random()
				+ projectedBoundingBox.getMinLatitude();
		double minLongitude = (projectedBoundingBox.getMaxLongitude() - projectedBoundingBox
				.getMinLongitude())
				* Math.random()
				+ projectedBoundingBox.getMinLongitude();
		double maxLatitude = (projectedBoundingBox.getMaxLatitude() - minLatitude)
				* Math.random() + minLatitude;
		double maxLongitude = (projectedBoundingBox.getMaxLongitude() - minLongitude)
				* Math.random() + minLongitude;

		BoundingBox requestBoundingBox = new BoundingBox(minLongitude,
				minLatitude, maxLongitude, maxLatitude);
		CoverageDataResults values = coverageData2
				.getValues(requestBoundingBox);
		TestCase.assertNotNull(values);
		TestCase.assertNotNull(values.getValues());
		TestCase.assertEquals(values.getValues()[0].length, values.getWidth());
		TestCase.assertEquals(values.getValues().length, values.getHeight());
		TestCase.assertNotNull(values.getTileMatrix());
		TestCase.assertTrue(values.getZoomLevel() >= 0);
		TestCase.assertTrue(values.getValues().length > 0);
		TestCase.assertTrue(values.getValues()[0].length > 0);
		for (int y = 0; y < values.getValues().length; y++) {
			for (int x = 0; x < values.getValues()[y].length; x++) {
				TestCase.assertEquals(values.getValues()[y][x],
						values.getValue(y, x));
			}
		}

		int specifiedWidth = 50;
		int specifiedHeight = 100;
		coverageData2.setWidth(specifiedWidth);
		coverageData2.setHeight(specifiedHeight);

		values = coverageData2.getValues(requestBoundingBox);
		TestCase.assertNotNull(values);
		TestCase.assertNotNull(values.getValues());
		TestCase.assertEquals(values.getValues()[0].length, values.getWidth());
		TestCase.assertEquals(values.getValues().length, values.getHeight());
		TestCase.assertNotNull(values.getTileMatrix());
		TestCase.assertTrue(values.getZoomLevel() >= 0);
		TestCase.assertTrue(values.getValues().length > 0);
		TestCase.assertTrue(values.getValues()[0].length > 0);
		TestCase.assertEquals(specifiedHeight, values.getHeight());
		TestCase.assertEquals(specifiedWidth, values.getWidth());
		for (int y = 0; y < specifiedHeight; y++) {
			for (int x = 0; x < specifiedWidth; x++) {
				TestCase.assertEquals(values.getValues()[y][x],
						values.getValue(y, x));
			}
		}

		values = coverageData2.getValuesUnbounded(requestBoundingBox);
		TestCase.assertNotNull(values);
		TestCase.assertNotNull(values.getValues());
		TestCase.assertEquals(values.getValues()[0].length, values.getWidth());
		TestCase.assertEquals(values.getValues().length, values.getHeight());
		TestCase.assertNotNull(values.getTileMatrix());
		TestCase.assertTrue(values.getZoomLevel() >= 0);
		TestCase.assertTrue(values.getValues().length > 0);
		TestCase.assertTrue(values.getValues()[0].length > 0);
		TestCase.assertEquals(values.getValues()[0].length,
				values.getValues()[values.getValues().length - 1].length);
		for (int y = 0; y < values.getValues().length; y++) {
			for (int x = 0; x < values.getValues()[y].length; x++) {
				TestCase.assertEquals(values.getValues()[y][x],
						values.getValue(y, x));
			}
		}
	}

	/**
	 * Test a random bounding box query
	 * 
	 * @param geoPackage
	 *            GeoPackage
	 * @param algorithm
	 *            algorithm
	 * @param allowNulls
	 *            allow null coverage data values
	 * @throws Exception
	 */
	public static void testRandomBoundingBox(GeoPackage geoPackage,
			CoverageDataAlgorithm algorithm, boolean allowNulls)
			throws Exception {

		// Verify the coverage data shows up as an coverage data table and not a
		// tile table
		List<String> tilesTables = geoPackage.getTileTables();
		List<String> coverageDataTables = CoverageData.getTables(geoPackage);
		TestCase.assertFalse(coverageDataTables.isEmpty());
		for (String tilesTable : tilesTables) {
			TestCase.assertFalse(coverageDataTables.contains(tilesTable));
		}

		TileMatrixSetDao dao = geoPackage.getTileMatrixSetDao();
		TestCase.assertTrue(dao.isTableExists());

		for (String coverageTable : coverageDataTables) {

			TileMatrixSet tileMatrixSet = dao.queryForId(coverageTable);

			TileDao tileDao = geoPackage.getTileDao(tileMatrixSet);
			CoverageData<?> coverageData = CoverageData.getCoverageData(
					geoPackage, tileDao);
			coverageData.setAlgorithm(algorithm);

			int specifiedWidth = (int) (Math.random() * 100.0) + 1;
			int specifiedHeight = (int) (Math.random() * 100.0) + 1;
			coverageData.setWidth(specifiedWidth);
			coverageData.setHeight(specifiedHeight);

			BoundingBox boundingBox = tileMatrixSet.getBoundingBox();

			// Build a random bounding box
			double minLatitude = (boundingBox.getMaxLatitude() - boundingBox
					.getMinLatitude())
					* Math.random()
					+ boundingBox.getMinLatitude();
			double minLongitude = (boundingBox.getMaxLongitude() - boundingBox
					.getMinLongitude())
					* Math.random()
					+ boundingBox.getMinLongitude();
			double maxLatitude = (boundingBox.getMaxLatitude() - minLatitude)
					* Math.random() + minLatitude;
			double maxLongitude = (boundingBox.getMaxLongitude() - minLongitude)
					* Math.random() + minLongitude;

			BoundingBox requestBoundingBox = new BoundingBox(minLongitude,
					minLatitude, maxLongitude, maxLatitude);

			CoverageDataResults values = coverageData
					.getValues(requestBoundingBox);

			TestCase.assertNotNull(values);
			TestCase.assertNotNull(values.getValues());
			TestCase.assertEquals(values.getValues()[0].length,
					values.getWidth());
			TestCase.assertEquals(values.getValues().length, values.getHeight());
			TestCase.assertNotNull(values.getTileMatrix());
			TestCase.assertTrue(values.getZoomLevel() >= 0);
			TestCase.assertTrue(values.getValues().length > 0);
			TestCase.assertTrue(values.getValues()[0].length > 0);
			TestCase.assertEquals(specifiedHeight, values.getHeight());
			TestCase.assertEquals(specifiedWidth, values.getWidth());

			for (int y = 0; y < specifiedHeight; y++) {
				boolean nonNullFound = false;
				boolean secondNullsFound = false;
				for (int x = 0; x < specifiedWidth; x++) {
					TestCase.assertEquals(values.getValues()[y][x],
							values.getValue(y, x));
					if (!allowNulls) {
						if (values.getValues()[y][x] != null) {
							TestCase.assertFalse(secondNullsFound);
							nonNullFound = true;
						} else if (nonNullFound) {
							secondNullsFound = true;
						}
					}
				}
			}

			for (int x = 0; x < specifiedWidth; x++) {
				boolean nonNullFound = false;
				boolean secondNullsFound = false;
				for (int y = 0; y < specifiedHeight; y++) {
					TestCase.assertEquals(values.getValues()[y][x],
							values.getValue(y, x));
					if (!allowNulls) {
						if (values.getValues()[y][x] != null) {
							TestCase.assertFalse(secondNullsFound);
							nonNullFound = true;
						} else if (nonNullFound) {
							secondNullsFound = true;
						}
					}
				}
			}

		}

	}

	/**
	 * Get the coverage data value at the coordinate
	 * 
	 * @param geoPackage
	 *            GeoPackage
	 * @param algorithm
	 *            algorithm
	 * @param latitude
	 *            latitude
	 * @param longitude
	 *            longitude
	 * @param epsg
	 *            epsg
	 * @return coverage data value
	 * @throws Exception
	 */
	public static Double getValue(GeoPackage geoPackage,
			CoverageDataAlgorithm algorithm, double latitude, double longitude,
			long epsg) throws Exception {

		Double value = null;

		List<String> coverageDataTables = CoverageData.getTables(geoPackage);
		TileMatrixSetDao dao = geoPackage.getTileMatrixSetDao();

		for (String coverageTable : coverageDataTables) {

			TileMatrixSet tileMatrixSet = dao.queryForId(coverageTable);
			TileDao tileDao = geoPackage.getTileDao(tileMatrixSet);

			Projection requestProjection = ProjectionFactory
					.getProjection(epsg);

			// Test getting the coverage data value of a single coordinate
			CoverageData<?> coverageData = CoverageData.getCoverageData(
					geoPackage, tileDao, requestProjection);
			coverageData.setAlgorithm(algorithm);
			value = coverageData.getValue(latitude, longitude);
		}

		return value;
	}

	/**
	 * Get the coverage data for the bounding box
	 * 
	 * @param geoPackage
	 *            GeoPackage
	 * @param algorithm
	 *            algorithm
	 * @param boundingBox
	 *            bounding box
	 * @param width
	 *            results width
	 * @param height
	 *            results height
	 * @param epsg
	 *            epsg
	 * @return coverage data results
	 * @throws Exception
	 */
	public static CoverageDataResults getValues(GeoPackage geoPackage,
			CoverageDataAlgorithm algorithm, BoundingBox boundingBox,
			int width, int height, long epsg) throws Exception {

		CoverageDataResults values = null;

		List<String> coverageDataTables = CoverageData.getTables(geoPackage);
		TileMatrixSetDao dao = geoPackage.getTileMatrixSetDao();

		for (String coverageTable : coverageDataTables) {

			TileMatrixSet tileMatrixSet = dao.queryForId(coverageTable);
			TileDao tileDao = geoPackage.getTileDao(tileMatrixSet);

			Projection requestProjection = ProjectionFactory
					.getProjection(epsg);

			// Test getting the coverage data value of a single coordinate
			CoverageData<?> coverageData = CoverageData.getCoverageData(
					geoPackage, tileDao, requestProjection);
			coverageData.setAlgorithm(algorithm);
			coverageData.setWidth(width);
			coverageData.setHeight(height);
			values = coverageData.getValues(boundingBox);
		}

		return values;
	}

	/**
	 * Test the pixel encoding location
	 * 
	 * @param geoPackage
	 *            GeoPackage
	 * @param allowNulls
	 *            allow nulls
	 * @throws Exception
	 */
	public static void testPixelEncoding(GeoPackage geoPackage,
			boolean allowNulls) throws Exception {

		List<String> coverageDataTables = CoverageData.getTables(geoPackage);
		TestCase.assertFalse(coverageDataTables.isEmpty());

		TileMatrixSetDao tileMatrixSetDao = geoPackage.getTileMatrixSetDao();
		TestCase.assertTrue(tileMatrixSetDao.isTableExists());
		TileMatrixDao tileMatrixDao = geoPackage.getTileMatrixDao();
		TestCase.assertTrue(tileMatrixDao.isTableExists());

		for (String coverageTable : coverageDataTables) {

			TileMatrixSet tileMatrixSet = tileMatrixSetDao
					.queryForId(coverageTable);

			TileDao tileDao = geoPackage.getTileDao(tileMatrixSet);
			CoverageData<?> coverageData = CoverageData.getCoverageData(
					geoPackage, tileDao);
			GriddedCoverage griddedCoverage = coverageData.getGriddedCoverage();
			GriddedCoverageEncodingType encoding = griddedCoverage
					.getGridCellEncodingType();

			TileResultSet tileResultSet = tileDao.queryForTile(tileDao
					.getMaxZoom());
			TestCase.assertNotNull(tileResultSet);
			try {
				TestCase.assertTrue(tileResultSet.getCount() > 0);
				while (tileResultSet.moveToNext()) {
					TileRow tileRow = tileResultSet.getRow();

					TileMatrix tileMatrix = tileDao.getTileMatrix(tileRow
							.getZoomLevel());
					TestCase.assertNotNull(tileMatrix);

					GriddedTile griddedTile = coverageData
							.getGriddedTile(tileRow.getId());
					TestCase.assertNotNull(griddedTile);

					byte[] tileData = tileRow.getTileData();
					TestCase.assertNotNull(tileData);

					BoundingBox boundingBox = TileBoundingBoxUtils
							.getBoundingBox(tileMatrixSet.getBoundingBox(),
									tileMatrix, tileRow.getTileColumn(),
									tileRow.getTileRow());

					int tileHeight = (int) tileMatrix.getTileHeight();
					int tileWidth = (int) tileMatrix.getTileWidth();

					int heightChunk = Math.max(tileHeight / 10, 1);
					int widthChunk = Math.max(tileWidth / 10, 1);

					for (int y = 0; y < tileHeight; y = Math.min(y
							+ heightChunk, y == tileHeight - 1 ? tileHeight
							: tileHeight - 1)) {
						for (int x = 0; x < tileWidth; x = Math.min(x
								+ widthChunk, x == tileWidth - 1 ? tileWidth
								: tileWidth - 1)) {

							Double pixelValue = coverageData.getValue(
									griddedTile, tileData, x, y);
							double pixelLongitude = boundingBox
									.getMinLongitude()
									+ (x * tileMatrix.getPixelXSize());
							double pixelLatitude = boundingBox.getMaxLatitude()
									- (y * tileMatrix.getPixelYSize());
							switch (encoding) {
							case CENTER:
							case AREA:
								pixelLongitude += (tileMatrix.getPixelXSize() / 2.0);
								pixelLatitude -= (tileMatrix.getPixelYSize() / 2.0);
								break;
							case CORNER:
								pixelLatitude -= tileMatrix.getPixelYSize();
								break;
							}
							Double value = coverageData.getValue(pixelLatitude,
									pixelLongitude);

							if (!allowNulls || pixelValue != null) {
								TestCase.assertEquals("x: " + x + ", y: " + y
										+ ", encoding: " + encoding,
										pixelValue, value);
							}
						}
					}

					break;
				}
			} finally {
				tileResultSet.close();
			}
		}

	}

}