package mil.nga.geopackage.test.geom;

import java.io.IOException;
import java.nio.ByteOrder;
import java.sql.SQLException;
import java.util.List;

import junit.framework.TestCase;
import mil.nga.geopackage.GeoPackage;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.core.srs.SpatialReferenceSystem;
import mil.nga.geopackage.core.srs.SpatialReferenceSystemDao;
import mil.nga.geopackage.features.columns.GeometryColumns;
import mil.nga.geopackage.features.columns.GeometryColumnsDao;
import mil.nga.geopackage.features.user.FeatureDao;
import mil.nga.geopackage.features.user.FeatureResultSet;
import mil.nga.geopackage.geom.GeoPackageGeometryData;
import mil.nga.sf.CircularString;
import mil.nga.sf.CompoundCurve;
import mil.nga.sf.CurvePolygon;
import mil.nga.sf.Geometry;
import mil.nga.sf.GeometryCollection;
import mil.nga.sf.GeometryEnvelope;
import mil.nga.sf.GeometryType;
import mil.nga.sf.LineString;
import mil.nga.sf.MultiLineString;
import mil.nga.sf.MultiPoint;
import mil.nga.sf.MultiPolygon;
import mil.nga.sf.Point;
import mil.nga.sf.Polygon;
import mil.nga.sf.PolyhedralSurface;
import mil.nga.sf.TIN;
import mil.nga.sf.Triangle;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionConstants;
import mil.nga.sf.proj.ProjectionTransform;
import mil.nga.sf.wkb.GeometryCodes;

/**
 * GeoPackage Geometry Data test utils
 * 
 * @author osbornb
 */
public class GeoPackageGeometryDataUtils {

	/**
	 * Test reading and writing (and comparing) geometry bytes
	 * 
	 * @param geoPackage
	 * @throws SQLException
	 * @throws IOException
	 */
	public static void testReadWriteBytes(GeoPackage geoPackage)
			throws SQLException, IOException {

		GeometryColumnsDao geometryColumnsDao = geoPackage
				.getGeometryColumnsDao();

		if (geometryColumnsDao.isTableExists()) {
			List<GeometryColumns> results = geometryColumnsDao.queryForAll();

			for (GeometryColumns geometryColumns : results) {

				FeatureDao dao = geoPackage.getFeatureDao(geometryColumns);
				TestCase.assertNotNull(dao);

				FeatureResultSet cursor = dao.queryForAll();

				while (cursor.moveToNext()) {

					GeoPackageGeometryData geometryData = cursor.getGeometry();
					if (geometryData != null) {

						byte[] geometryDataToBytes = geometryData.toBytes();
						compareByteArrays(geometryDataToBytes,
								geometryData.getBytes());

						GeoPackageGeometryData geometryDataAfterToBytes = geometryData;

						// Re-retrieve the original geometry data
						geometryData = cursor.getGeometry();

						// Compare the original with the toBytes geometry data
						compareGeometryData(geometryData,
								geometryDataAfterToBytes);

						// Create a new geometry data from the bytes and compare
						// with original
						GeoPackageGeometryData geometryDataFromBytes = new GeoPackageGeometryData(
								geometryDataToBytes);
						compareGeometryData(geometryData, geometryDataFromBytes);

						// Set the geometry empty flag and verify the geometry
						// was not written / read
						geometryDataAfterToBytes = cursor.getGeometry();
						geometryDataAfterToBytes.setEmpty(true);
						geometryDataToBytes = geometryDataAfterToBytes
								.toBytes();
						geometryDataFromBytes = new GeoPackageGeometryData(
								geometryDataToBytes);
						TestCase.assertNull(geometryDataFromBytes.getGeometry());
						compareByteArrays(
								geometryDataAfterToBytes.getHeaderBytes(),
								geometryDataFromBytes.getHeaderBytes());

						// Flip the byte order and verify the header and bytes
						// no longer matches the original, but the geometries
						// still do
						geometryDataAfterToBytes = cursor.getGeometry();
						geometryDataAfterToBytes
								.setByteOrder(geometryDataAfterToBytes
										.getByteOrder() == ByteOrder.BIG_ENDIAN ? ByteOrder.LITTLE_ENDIAN
										: ByteOrder.BIG_ENDIAN);
						geometryDataToBytes = geometryDataAfterToBytes
								.toBytes();
						geometryDataFromBytes = new GeoPackageGeometryData(
								geometryDataToBytes);
						compareGeometryData(geometryDataAfterToBytes,
								geometryDataFromBytes);
						TestCase.assertFalse(equalByteArrays(
								geometryDataAfterToBytes.getHeaderBytes(),
								geometryData.getHeaderBytes()));
						TestCase.assertFalse(equalByteArrays(
								geometryDataAfterToBytes.getWkbBytes(),
								geometryData.getWkbBytes()));
						TestCase.assertFalse(equalByteArrays(
								geometryDataAfterToBytes.getBytes(),
								geometryData.getBytes()));
						compareGeometries(geometryData.getGeometry(),
								geometryDataAfterToBytes.getGeometry());
					}

				}
				cursor.close();
			}
		}

	}

	/**
	 * Test transforming geometries between projections
	 * 
	 * @param geoPackage
	 * @throws SQLException
	 * @throws IOException
	 */
	public static void testGeometryProjectionTransform(GeoPackage geoPackage)
			throws SQLException, IOException {

		GeometryColumnsDao geometryColumnsDao = geoPackage
				.getGeometryColumnsDao();

		if (geometryColumnsDao.isTableExists()) {
			List<GeometryColumns> results = geometryColumnsDao.queryForAll();

			for (GeometryColumns geometryColumns : results) {

				FeatureDao dao = geoPackage.getFeatureDao(geometryColumns);
				TestCase.assertNotNull(dao);

				FeatureResultSet cursor = dao.queryForAll();

				while (cursor.moveToNext()) {

					GeoPackageGeometryData geometryData = cursor.getGeometry();
					if (geometryData != null) {

						Geometry geometry = geometryData.getGeometry();

						if (geometry != null) {

							SpatialReferenceSystemDao srsDao = geoPackage
									.getSpatialReferenceSystemDao();
							long srsId = geometryData.getSrsId();
							SpatialReferenceSystem srs = srsDao
									.queryForId(srsId);

							long epsg = srs.getOrganizationCoordsysId();
							Projection projection = srs.getProjection();
							long toEpsg = -1;
							if (epsg == ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM) {
								toEpsg = ProjectionConstants.EPSG_WEB_MERCATOR;
							} else {
								toEpsg = ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM;
							}
							ProjectionTransform transformTo = projection
									.getTransformation(toEpsg);
							ProjectionTransform transformFrom = srs
									.getTransformation(transformTo
											.getToProjection());

							byte[] bytes = geometryData.getWkbBytes();

							Geometry projectedGeometry = transformTo
									.transform(geometry);
							GeoPackageGeometryData projectedGeometryData = new GeoPackageGeometryData(
									-1);
							projectedGeometryData
									.setGeometry(projectedGeometry);
							projectedGeometryData.toBytes();
							byte[] projectedBytes = projectedGeometryData
									.getWkbBytes();

							if (epsg > 0) {
								TestCase.assertFalse(equalByteArrays(bytes,
										projectedBytes));
							}

							Geometry restoredGeometry = transformFrom
									.transform(projectedGeometry);

							compareGeometries(geometry, restoredGeometry, .001);
						}
					}

				}
				cursor.close();
			}
		}
	}

	/**
	 * Compare two geometry datas and verify they are equal
	 * 
	 * @param expected
	 * @param actual
	 */
	public static void compareGeometryData(GeoPackageGeometryData expected,
			GeoPackageGeometryData actual) {

		// Compare geometry data attributes
		TestCase.assertEquals(expected.isExtended(), actual.isExtended());
		TestCase.assertEquals(expected.isEmpty(), actual.isEmpty());
		TestCase.assertEquals(expected.getByteOrder(), actual.getByteOrder());
		TestCase.assertEquals(expected.getSrsId(), actual.getSrsId());
		compareEnvelopes(expected.getEnvelope(), actual.getEnvelope());
		TestCase.assertEquals(expected.getWkbGeometryIndex(),
				actual.getWkbGeometryIndex());

		// Compare header bytes
		compareByteArrays(expected.getHeaderBytes(), actual.getHeaderBytes());

		// Compare geometries
		compareGeometries(expected.getGeometry(), actual.getGeometry());

		// Compare well-known binary geometries
		compareByteArrays(expected.getWkbBytes(), actual.getWkbBytes());

		// Compare all bytes
		compareByteArrays(expected.getBytes(), actual.getBytes());

	}

	/**
	 * Compare two geometry envelopes and verify they are equal
	 * 
	 * @param expected
	 * @param actual
	 */
	private static void compareEnvelopes(GeometryEnvelope expected,
			GeometryEnvelope actual) {

		if (expected == null) {
			TestCase.assertNull(actual);
		} else {
			TestCase.assertNotNull(actual);

			TestCase.assertEquals(
					GeoPackageGeometryData.getIndicator(expected),
					GeoPackageGeometryData.getIndicator(actual));
			TestCase.assertEquals(expected.getMinX(), actual.getMinX());
			TestCase.assertEquals(expected.getMaxX(), actual.getMaxX());
			TestCase.assertEquals(expected.getMinY(), actual.getMinY());
			TestCase.assertEquals(expected.getMaxY(), actual.getMaxY());
			TestCase.assertEquals(expected.hasZ(), actual.hasZ());
			TestCase.assertEquals(expected.getMinZ(), actual.getMinZ());
			TestCase.assertEquals(expected.getMaxZ(), actual.getMaxZ());
			TestCase.assertEquals(expected.hasM(), actual.hasM());
			TestCase.assertEquals(expected.getMinM(), actual.getMinM());
			TestCase.assertEquals(expected.getMaxM(), actual.getMaxM());
		}

	}

	/**
	 * Compare two geometries and verify they are equal
	 * 
	 * @param expected
	 * @param actual
	 */
	public static void compareGeometries(Geometry expected, Geometry actual) {
		compareGeometries(expected, actual, 0.0);
	}

	/**
	 * Compare two geometries and verify they are equal
	 * 
	 * @param expected
	 * @param actual
	 * @param delta
	 */
	public static void compareGeometries(Geometry expected, Geometry actual,
			double delta) {
		if (expected == null) {
			TestCase.assertNull(actual);
		} else {
			TestCase.assertNotNull(actual);

			GeometryType geometryType = expected.getGeometryType();
			switch (geometryType) {

			case GEOMETRY:
				TestCase.fail("Unexpected Geometry Type of "
						+ geometryType.name() + " which is abstract");
			case POINT:
				comparePoint((Point) expected, (Point) actual, delta);
				break;
			case LINESTRING:
				compareLineString((LineString) expected, (LineString) actual,
						delta);
				break;
			case POLYGON:
				comparePolygon((Polygon) expected, (Polygon) actual, delta);
				break;
			case MULTIPOINT:
				compareMultiPoint((MultiPoint) expected, (MultiPoint) actual,
						delta);
				break;
			case MULTILINESTRING:
				compareMultiLineString((MultiLineString) expected,
						(MultiLineString) actual, delta);
				break;
			case MULTIPOLYGON:
				compareMultiPolygon((MultiPolygon) expected,
						(MultiPolygon) actual, delta);
				break;
			case GEOMETRYCOLLECTION:
				compareGeometryCollection((GeometryCollection<?>) expected,
						(GeometryCollection<?>) actual, delta);
				break;
			case CIRCULARSTRING:
				compareCircularString((CircularString) expected,
						(CircularString) actual, delta);
				break;
			case COMPOUNDCURVE:
				compareCompoundCurve((CompoundCurve) expected,
						(CompoundCurve) actual, delta);
				break;
			case CURVEPOLYGON:
				compareCurvePolygon((CurvePolygon<?>) expected,
						(CurvePolygon<?>) actual, delta);
				break;
			case MULTICURVE:
				TestCase.fail("Unexpected Geometry Type of "
						+ geometryType.name() + " which is abstract");
			case MULTISURFACE:
				TestCase.fail("Unexpected Geometry Type of "
						+ geometryType.name() + " which is abstract");
			case CURVE:
				TestCase.fail("Unexpected Geometry Type of "
						+ geometryType.name() + " which is abstract");
			case SURFACE:
				TestCase.fail("Unexpected Geometry Type of "
						+ geometryType.name() + " which is abstract");
			case POLYHEDRALSURFACE:
				comparePolyhedralSurface((PolyhedralSurface) expected,
						(PolyhedralSurface) actual, delta);
				break;
			case TIN:
				compareTIN((TIN) expected, (TIN) actual, delta);
				break;
			case TRIANGLE:
				compareTriangle((Triangle) expected, (Triangle) actual, delta);
				break;
			default:
				throw new GeoPackageException("Geometry Type not supported: "
						+ geometryType);
			}
		}
	}

	/**
	 * Compare to the base attributes of two geometries
	 * 
	 * @param expected
	 * @param actual
	 */
	private static void compareBaseGeometryAttributes(Geometry expected,
			Geometry actual) {
		TestCase.assertEquals(expected.getGeometryType(),
				actual.getGeometryType());
		TestCase.assertEquals(expected.hasZ(), actual.hasZ());
		TestCase.assertEquals(expected.hasM(), actual.hasM());
		TestCase.assertEquals(GeometryCodes.getCode(expected),
				GeometryCodes.getCode(actual));
	}

	/**
	 * Compare the two points for equality
	 * 
	 * @param expected
	 * @param actual
	 */
	private static void comparePoint(Point expected, Point actual, double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.getX(), actual.getX(), delta);
		TestCase.assertEquals(expected.getY(), actual.getY(), delta);
		if (expected.getZ() == null) {
			TestCase.assertEquals(expected.getZ(), actual.getZ());
		} else {
			TestCase.assertEquals(expected.getZ(), actual.getZ(), delta);
		}
		if (expected.getM() == null) {
			TestCase.assertEquals(expected.getM(), actual.getM());
		} else {
			TestCase.assertEquals(expected.getM(), actual.getM(), delta);
		}
	}

	/**
	 * Compare the two line strings for equality
	 * 
	 * @param expected
	 * @param actual
	 * @parma delta
	 */
	private static void compareLineString(LineString expected,
			LineString actual, double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.numPoints(), actual.numPoints());
		for (int i = 0; i < expected.numPoints(); i++) {
			comparePoint(expected.getPoints().get(i),
					actual.getPoints().get(i), delta);
		}
	}

	/**
	 * Compare the two polygons for equality
	 * 
	 * @param expected
	 * @param actual
	 * @param delta
	 */
	private static void comparePolygon(Polygon expected, Polygon actual,
			double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.numRings(), actual.numRings());
		for (int i = 0; i < expected.numRings(); i++) {
			compareLineString(expected.getRings().get(i), actual.getRings()
					.get(i), delta);
		}
	}

	/**
	 * Compare the two multi points for equality
	 * 
	 * @param expected
	 * @param actual
	 * @param delta
	 */
	private static void compareMultiPoint(MultiPoint expected,
			MultiPoint actual, double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.numPoints(), actual.numPoints());
		for (int i = 0; i < expected.numPoints(); i++) {
			comparePoint(expected.getPoints().get(i),
					actual.getPoints().get(i), delta);
		}
	}

	/**
	 * Compare the two multi line strings for equality
	 * 
	 * @param expected
	 * @param actual
	 * @parma delta
	 */
	private static void compareMultiLineString(MultiLineString expected,
			MultiLineString actual, double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.numLineStrings(),
				actual.numLineStrings());
		for (int i = 0; i < expected.numLineStrings(); i++) {
			compareLineString(expected.getLineStrings().get(i), actual
					.getLineStrings().get(i), delta);
		}
	}

	/**
	 * Compare the two multi polygons for equality
	 * 
	 * @param expected
	 * @param actual
	 * @param delta
	 */
	private static void compareMultiPolygon(MultiPolygon expected,
			MultiPolygon actual, double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.numPolygons(), actual.numPolygons());
		for (int i = 0; i < expected.numPolygons(); i++) {
			comparePolygon(expected.getPolygons().get(i), actual.getPolygons()
					.get(i), delta);
		}
	}

	/**
	 * Compare the two geometry collections for equality
	 * 
	 * @param expected
	 * @param actual
	 * @param delta
	 */
	private static void compareGeometryCollection(
			GeometryCollection<?> expected, GeometryCollection<?> actual,
			double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.numGeometries(), actual.numGeometries());
		for (int i = 0; i < expected.numGeometries(); i++) {
			compareGeometries(expected.getGeometries().get(i), actual
					.getGeometries().get(i), delta);
		}
	}

	/**
	 * Compare the two circular strings for equality
	 * 
	 * @param expected
	 * @param actual
	 * @parma delta
	 */
	private static void compareCircularString(CircularString expected,
			CircularString actual, double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.numPoints(), actual.numPoints());
		for (int i = 0; i < expected.numPoints(); i++) {
			comparePoint(expected.getPoints().get(i),
					actual.getPoints().get(i), delta);
		}
	}

	/**
	 * Compare the two compound curves for equality
	 * 
	 * @param expected
	 * @param actual
	 * @parma delta
	 */
	private static void compareCompoundCurve(CompoundCurve expected,
			CompoundCurve actual, double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.numLineStrings(),
				actual.numLineStrings());
		for (int i = 0; i < expected.numLineStrings(); i++) {
			compareLineString(expected.getLineStrings().get(i), actual
					.getLineStrings().get(i), delta);
		}
	}

	/**
	 * Compare the two curve polygons for equality
	 * 
	 * @param expected
	 * @param actual
	 * @param delta
	 */
	private static void compareCurvePolygon(CurvePolygon<?> expected,
			CurvePolygon<?> actual, double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.numRings(), actual.numRings());
		for (int i = 0; i < expected.numRings(); i++) {
			compareGeometries(expected.getRings().get(i), actual.getRings()
					.get(i), delta);
		}
	}

	/**
	 * Compare the two polyhedral surfaces for equality
	 * 
	 * @param expected
	 * @param actual
	 * @param delta
	 */
	private static void comparePolyhedralSurface(PolyhedralSurface expected,
			PolyhedralSurface actual, double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.numPolygons(), actual.numPolygons());
		for (int i = 0; i < expected.numPolygons(); i++) {
			compareGeometries(expected.getPolygons().get(i), actual
					.getPolygons().get(i), delta);
		}
	}

	/**
	 * Compare the two TINs for equality
	 * 
	 * @param expected
	 * @param actual
	 * @param delta
	 */
	private static void compareTIN(TIN expected, TIN actual, double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.numPolygons(), actual.numPolygons());
		for (int i = 0; i < expected.numPolygons(); i++) {
			compareGeometries(expected.getPolygons().get(i), actual
					.getPolygons().get(i), delta);
		}
	}

	/**
	 * Compare the two triangles for equality
	 * 
	 * @param expected
	 * @param actual
	 * @param delta
	 */
	private static void compareTriangle(Triangle expected, Triangle actual,
			double delta) {

		compareBaseGeometryAttributes(expected, actual);
		TestCase.assertEquals(expected.numRings(), actual.numRings());
		for (int i = 0; i < expected.numRings(); i++) {
			compareLineString(expected.getRings().get(i), actual.getRings()
					.get(i), delta);
		}
	}

	/**
	 * Compare two byte arrays and verify they are equal
	 * 
	 * @param expected
	 * @param actual
	 */
	public static void compareByteArrays(byte[] expected, byte[] actual) {

		TestCase.assertEquals(expected.length, actual.length);

		for (int i = 0; i < expected.length; i++) {
			TestCase.assertEquals("Byte: " + i, expected[i], actual[i]);
		}

	}

	/**
	 * Compare two byte arrays and verify they are equal
	 * 
	 * @param expected
	 * @param actual
	 * @return true if equal
	 */
	public static boolean equalByteArrays(byte[] expected, byte[] actual) {

		boolean equal = expected.length == actual.length;

		for (int i = 0; equal && i < expected.length; i++) {
			equal = expected[i] == actual[i];
		}

		return equal;
	}

}