package mil.nga.geopackage.test.geom;

import junit.framework.TestCase;

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

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.FeatureCursor;
import mil.nga.geopackage.features.user.FeatureDao;
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);

                FeatureCursor 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);

                FeatureCursor 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 attribiutes 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;
    }

}