/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.engine.geo;

import static nz.org.riskscape.engine.Matchers.*;
import static nz.org.riskscape.engine.geo.GeometryUtils.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;

import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.junit.Test;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;

import nz.org.riskscape.engine.CrsHelper;
import nz.org.riskscape.engine.SRIDSet;
import nz.org.riskscape.engine.gt.GeometryHelper;
import nz.org.riskscape.engine.types.Referenced;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.Types;

public class GeometryUtilsTest implements CrsHelper {

  SRIDSet sridSet = new SRIDSet();
  GeometryHelper latLongHelper = GeometryHelper.latLong(sridSet);
  GeometryHelper nzGeomHelper = GeometryHelper.nzMapGridWithOriginShift(sridSet);
  GeometryHelper newYorkGeomHelper = GeometryHelper.newYorkFoot(sridSet);

  @Test
  public void haveFactsCorrect() {
    assertThat(METRES_PER_NAUTICAL_MILE, is(1852D));
    assertThat(METRES_PER_DEGREE, is(1852 * 60D));
  }

  @Test
  public void canProcessesGeometry() {
    AtomicInteger counter = new AtomicInteger();
    Consumer<Geometry> processor = g -> counter.incrementAndGet();

    GeometryUtils.processPerPart(latLongHelper.point(10, 10), processor);
    assertThat(counter.get(), is(1));

    counter.set(0);
    GeometryUtils.processPerPart(latLongHelper.line(10, 10, 20, 20), processor);
    assertThat(counter.get(), is(1));

    counter.set(0);
    GeometryUtils.processPerPart(latLongHelper.box(10, 10, 20, 20), processor);
    assertThat(counter.get(), is(1));
  }

  @Test
  public void canProcessesGeometryCollections() {
    AtomicInteger counter = new AtomicInteger();
    Consumer<Geometry> processor = g -> counter.incrementAndGet();

    GeometryUtils.processPerPart(latLongHelper.multiPoint(latLongHelper.point(10, 10)), processor);
    assertThat(counter.get(), is(1));

    counter.set(0);
    GeometryUtils.processPerPart(latLongHelper.multiPoint(
        latLongHelper.point(10, 10),
        latLongHelper.point(20, 10)
    ), processor);
    assertThat(counter.get(), is(2));

    counter.set(0);
    GeometryUtils.processPerPart(latLongHelper.multiLine(), processor);
    assertThat(counter.get(), is(0));

    counter.set(0);
    GeometryUtils.processPerPart(latLongHelper.multiLine(
        latLongHelper.line(10, 10, 20, 20)
    ), processor);
    assertThat(counter.get(), is(1));

    counter.set(0);
    GeometryUtils.processPerPart(latLongHelper.multiLine(
        latLongHelper.line(10, 10, 20, 20),
        latLongHelper.line(10, 10, 20, 30)
    ), processor);
    assertThat(counter.get(), is(2));
  }

  @Test
  public void testToCrsUnits() {
    // no conversion on a metric CRS
    assertThat(GeometryUtils.toCrsUnits(20D, nzGeomHelper.getCrs()), is(20D));

    // non metric projected CRS needs a convertion
    // 30.48m is approx 100 us-ft
    assertThat(GeometryUtils.toCrsUnits(30.48006D, newYorkGeomHelper.getCrs()), closeTo(100D, 0.1D));

    // WSG84 need to convert meters to degrees
    // 1852m is 1 nautical mile, which is 1/60 of a degree
    assertThat(GeometryUtils.toCrsUnits(1852, latLongHelper.getCrs()), closeTo(1 / 60D, 0.0001));
    assertThat(GeometryUtils.toCrsUnits(926, latLongHelper.getCrs()), closeTo(1 / 120D, 0.0001));
  }

  @Test
  public void testEquivalentTypes() {
    Point point1 = latLongHelper.point(1, 1);
    Point point2 = latLongHelper.point(2, 1);

    LineString line1 = latLongHelper.line(1, 1, 2, 2);
    LineString line2 = latLongHelper.line(2, 1, 3, 2);

    Polygon box1 = latLongHelper.box(1, 1, 2, 2);
    Polygon box2 = latLongHelper.box(10, 10, 20, 20);

    assertTrue(equivalentTypes(point1, point2));
    assertTrue(equivalentTypes(point1, latLongHelper.multiPoint(point1, point2)));

    assertTrue(equivalentTypes(line1, line2));
    assertTrue(equivalentTypes(line1, latLongHelper.multiLine(line1, line2)));

    assertTrue(equivalentTypes(box1, box2));
    assertTrue(equivalentTypes(box1, latLongHelper.multiBox(box1, box2)));

    assertFalse(equivalentTypes(point1, line1));
    assertFalse(equivalentTypes(point1, box2));
    assertFalse(equivalentTypes(line1, point1));
    assertFalse(equivalentTypes(line1, box1));
    assertFalse(equivalentTypes(box1, point1));
    assertFalse(equivalentTypes(box1, line1));
  }

  @Test
  public void testCanReprojectSafely() throws Exception {
    Type nz = Referenced.of(Types.POINT, nzGeomHelper.getCrs());
    Type wsg84 = Referenced.of(Types.POINT, latLongHelper.getCrs());

    assertTrue(canReprojectSafely(nz, wsg84));

    // we don't know this is safe as the bounds aren't known.
    assertFalse(canReprojectSafely(wsg84, nz));

    // Add in bounds that are slightly within projection
    wsg84 = Referenced.of(Types.POINT, latLongHelper.getCrs(),
        new ReferencedEnvelope(-47.2, -34.2, 166.4, 178.4, latLongHelper.getCrs()));
    assertTrue(canReprojectSafely(wsg84, nz));

    // These CRS's are parsed from a wkt projection file. Just like a shapefile reader will do.
    // CRS's that are parsed from WKT don't tend to include the domain of validity which means that the
    // toBounds() needs to do a lookup to get the bounds.
    assertTrue(canReprojectSafely(wsg84, Referenced.of(Types.POINT, nzMapGrid())));
    assertTrue(canReprojectSafely(wsg84, Referenced.of(Types.POINT, nzTransverseMercator())));

    // slightly outside of bounds
    wsg84 = Referenced.of(Types.POINT, latLongHelper.getCrs(),
        new ReferencedEnvelope(-47.2, -30, 166.4, 178.4, latLongHelper.getCrs()));
    assertFalse(canReprojectSafely(wsg84, nz));

    // These CRS's are parsed from a wkt projection file. Just like a shapefile reader will do
    assertFalse(canReprojectSafely(wsg84, Referenced.of(Types.POINT, nzMapGrid())));
    assertFalse(canReprojectSafely(wsg84, Referenced.of(Types.POINT, nzTransverseMercator())));

    // move the latitude down out of projection
    wsg84 = Referenced.of(Types.POINT, latLongHelper.getCrs(),
        new ReferencedEnvelope(-47.2, -25, 166.4, 178.4, latLongHelper.getCrs()));
    assertFalse(canReprojectSafely(wsg84, nz));

    // move the latitude right out of projection
    wsg84 = Referenced.of(Types.POINT, latLongHelper.getCrs(),
        new ReferencedEnvelope(-41, -33, 151, 175.5, latLongHelper.getCrs()));
    assertFalse(canReprojectSafely(wsg84, nz));

    // returns false if projections are the same
    assertFalse(canReprojectSafely(wsg84, wsg84));
    assertFalse(canReprojectSafely(nz, nz));

    CoordinateReferenceSystem antarcticPolar = CRS.decode("EPSG:3031");
    wsg84 = Referenced.of(Types.POINT, latLongHelper.getCrs(),
        new ReferencedEnvelope(-89, -80, -179, -160, latLongHelper.getCrs()));
    assertTrue(canReprojectSafely(wsg84, Referenced.of(Types.POINT, antarcticPolar)));
  }

  @Test
  public void removesNonFamilyMembers() {
    assertThat(removeNonFamilyMembers(nzGeomHelper.point(10, 10), GeometryFamily.PUNTAL),
        is(nzGeomHelper.point(10, 10)));

    assertThat(removeNonFamilyMembers(nzGeomHelper.line(10, 10, 100, 100), GeometryFamily.PUNTAL),
        is(nzGeomHelper.emptyPoint()));

    assertThat(removeNonFamilyMembers(nzGeomHelper.line(10, 10, 100, 100), GeometryFamily.LINEAL),
        is(nzGeomHelper.line(10, 10, 100, 100)));

    assertThat(removeNonFamilyMembers(nzGeomHelper.point(10, 10), GeometryFamily.LINEAL),
        is(nzGeomHelper.emptyLine()));

    assertThat(removeNonFamilyMembers(nzGeomHelper.box(10, 10, 100, 100), GeometryFamily.POLYGONAL),
        is(nzGeomHelper.box(10, 10, 100, 100)));

    assertThat(removeNonFamilyMembers(nzGeomHelper.line(10, 10, 100, 100), GeometryFamily.POLYGONAL),
        is(nzGeomHelper.emptyBox()));

    assertThat(removeNonFamilyMembers(nzGeomHelper.collection(
        nzGeomHelper.point(10, 20),
        nzGeomHelper.box(10, 10, 100, 100),
        nzGeomHelper.line(10, 10, 100, 100)
    ), GeometryFamily.POLYGONAL),
        is(nzGeomHelper.box(10, 10, 100, 100)));

    assertThat(removeNonFamilyMembers(nzGeomHelper.collection(
        nzGeomHelper.point(10, 20),
        nzGeomHelper.box(10, 10, 100, 100),
        nzGeomHelper.line(10, 10, 100, 100),
        nzGeomHelper.box(200, 200, 300, 300)
    ), GeometryFamily.POLYGONAL),
        is(nzGeomHelper.multiBox(
            nzGeomHelper.box(10, 10, 100, 100),
            nzGeomHelper.box(200, 200, 300, 300)
        )));

    assertThat(removeNonFamilyMembers(nzGeomHelper.collection(
        nzGeomHelper.point(10, 20),
        nzGeomHelper.box(10, 10, 100, 100),
        nzGeomHelper.line(10, 10, 100, 100),
        nzGeomHelper.box(200, 200, 300, 300)
    ), GeometryFamily.LINEAL),
        is(nzGeomHelper.line(10, 10, 100, 100)));

    assertThat(removeNonFamilyMembers(nzGeomHelper.collection(
        nzGeomHelper.box(10, 10, 100, 100),
        nzGeomHelper.box(200, 200, 300, 300)
    ), GeometryFamily.PUNTAL),
        is(nzGeomHelper.emptyPoint()));

    assertThat(removeNonFamilyMembers(nzGeomHelper.collection(
        nzGeomHelper.point(10, 20),
        nzGeomHelper.box(10, 10, 100, 100),
        nzGeomHelper.box(200, 200, 300, 300)
    ), GeometryFamily.LINEAL),
        is(nzGeomHelper.emptyLine()));

    assertThat(removeNonFamilyMembers(nzGeomHelper.collection(
        nzGeomHelper.point(10, 20),
        nzGeomHelper.line(10, 10, 100, 100)
    ), GeometryFamily.POLYGONAL),
        is(nzGeomHelper.emptyBox()));
  }

  @Test
  public void midpointOfPointIsThePoint() {
    Point point = nzGeomHelper.point(10, 20);
    assertThat(GeometryUtils.getMidpoint(point), isPresent(point));
  }

  @Test
  public void midpointOfPolygonIsTheCentroid() {
    Polygon poly = nzGeomHelper.box(10, 10, 100, 100);
    assertThat(GeometryUtils.getMidpoint(poly), isPresent(poly.getCentroid()));
  }

  @Test
  public void midpointOfPolygonIsEmptyIfCentroidNotContained() {
    Polygon lShape = nzGeomHelper.box(
        nzGeomHelper.toCoordinate(0, 0),
        nzGeomHelper.toCoordinate(0, 1000),
        nzGeomHelper.toCoordinate(10, 1000),
        nzGeomHelper.toCoordinate(10, 10),
        nzGeomHelper.toCoordinate(1000, 10),
        nzGeomHelper.toCoordinate(1000, 0),
        nzGeomHelper.toCoordinate(0, 0)
    );
    // sanity check
    assertThat(lShape.contains(lShape.getCentroid()), is(false));

    assertThat(GeometryUtils.getMidpoint(lShape), isEmptyOptional());
  }

  @Test
  public void midpointOfLine() {
    assertThat(
        GeometryUtils.getMidpoint(nzGeomHelper.line(0, 0, 0, 100)),
        isPresent(nzGeomHelper.point(0, 50))
    );

    assertThat(
        GeometryUtils.getMidpoint(nzGeomHelper.line(
            nzGeomHelper.toCoordinate(0, 0),
            nzGeomHelper.toCoordinate(1000, 0),
            nzGeomHelper.toCoordinate(1000, 500)
        )),
        // midpoint in first segement
        isPresent(nzGeomHelper.point(750, 0))
    );

    assertThat(
        GeometryUtils.getMidpoint(nzGeomHelper.line(
            nzGeomHelper.toCoordinate(0, 0),
            nzGeomHelper.toCoordinate(1000, 0),
            nzGeomHelper.toCoordinate(1000, 2000)
        )),
        // midpoint in last segment
        isPresent(nzGeomHelper.point(1000, 500))
    );
  }

}
