/*
 * 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.rl;

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

import java.util.Arrays;
import java.util.HashSet;

import org.junit.Before;
import org.junit.Test;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;

import nz.org.riskscape.engine.GeoHelper;
import nz.org.riskscape.engine.GeometryProblems;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.gt.GeometryHelper;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.problem.Problem.Severity;

public class MeasureFunctionsTest extends BaseExpressionRealizerTest {

  GeometryHelper nzmgHelper = GeometryHelper.nzMapGridWithOriginShift(project.getSridSet());
  GeometryHelper latLongHelper = GeometryHelper.latLong(project.getSridSet());
  GeometryHelper newYorkFootGeomHelper = GeometryHelper.newYorkFoot(project.getSridSet());
  GeometryFunctions subject = new GeometryFunctions(engine);

  public Point point = nzmgHelper.point(1, 1);
  public Point empty = nzmgHelper.emptyPoint();

  public LineString lineString = nzmgHelper.line(new Coordinate[] {
      new Coordinate(1, 1),
      new Coordinate(2, 1),
      new Coordinate(2, 2)
  });

  public Polygon simplePolygon = nzmgHelper.box(new Coordinate[] {
      new Coordinate(1, 1),
      new Coordinate(2, 1),
      new Coordinate(2, 2),
      new Coordinate(1, 2),
      new Coordinate(1, 1)
  });

  public Polygon complexPolygon = nzmgHelper.box(
      nzmgHelper.ring(
          new Coordinate[]{
            new Coordinate(0, 0),
            new Coordinate(3, 0),
            new Coordinate(3, 3),
            new Coordinate(0, 3),
            new Coordinate(0, 0)
          }
      ),
      nzmgHelper.ring(
          new Coordinate[]{
            new Coordinate(1, 1),
            new Coordinate(2, 1),
            new Coordinate(2, 2),
            new Coordinate(1, 2),
            new Coordinate(1, 1)
          }
      )
  );

  @Before
  public void setup() {
    inputStruct = Struct.of("geom", Types.GEOMETRY);
    project.getFunctionSet().addAll(new GeometryFunctions(engine).getFunctions());
  }

  @Test
  public void measureAreaOfAPointIsZero() throws Exception {
    assertThat(evaluate("measure_area(geom)", tuple(point)), is(0.0D));
  }

  @Test
  public void measureLengthOfAPointIsZero() throws Exception {
    assertThat(evaluate("measure_length(geom)", tuple(point)), is(0.0D));
  }

  @Test
  public void measureAreaOfALineIsZero() throws Exception {
    assertThat(evaluate("measure_area(geom)", tuple(lineString)), is(0.0D));
  }

  @Test
  public void measureLengthOfALineIsDistanceBetweenAllPoints() throws Exception {
    assertThat(evaluate("measure_length(geom)", tuple(lineString)), is(2.0D));
  }

  @Test
  public void measureLengthOfAPolygonIsTheDistanceAroundThePerimeterOfShellAndRings() throws Exception {
    assertThat(evaluate("measure_length(geom)", tuple(simplePolygon)), is(4.0D));
    assertThat(evaluate("measure_length(geom)", tuple(complexPolygon)), is(16.0D));
  }

  @Test
  public void measureAreaOfAPolygonIsTheAreaOfTheShellMinusRings() throws Exception {
    assertThat(evaluate("measure_area(geom)", tuple(simplePolygon)), is(1.0D));
    assertThat(evaluate("measure_area(geom)", tuple(complexPolygon)), is(8.0D));
  }

  @Test
  public void measureGivesZeroForPointsAndEmptyGeometries() throws Exception {
    assertThat(evaluate("measure(geom)", tuple(point)), is(0D));
    assertThat(evaluate("measure(geom)", tuple(empty)), is(0D));
  }

  @Test
  public void measureOnLinesGiveLengths() throws Exception {
    assertThat(evaluate("measure(geom)", tuple(lineString)), is(2.0D));
  }

  @Test
  public void measureOnPolysGiveAreas() throws Exception {
    assertThat(evaluate("measure(geom)", tuple(complexPolygon)), is(8.0D));
  }

  @Test
  public void measureOngeometryCollectionsReturnTheSumOfTheirParts() throws Exception {
    GeometryCollection polygonCollection = nzmgHelper.multiBox(
        simplePolygon,
        simplePolygon,
        complexPolygon
    );

    assertThat(evaluate("measure(geom)", tuple(polygonCollection)), is(10.0D));

    GeometryCollection lineCollection = nzmgHelper.multiLine(
        lineString,
        lineString
    );

    assertThat(evaluate("measure(geom)", tuple(lineCollection)), is(4.0D));

    assertThat(evaluate("measure(geom)", tuple(nzmgHelper.multiLine())), is(0D));

    assertThat(evaluate("measure(geom)", tuple(nzmgHelper.multiPoint(point, point))), is(0D));
  }

  @Test
  public void cannotMeasureGeometryCollectionsOfDifferentTypes() throws Exception {
    // note that any GeometryCollection will currently throw an error, but I'm assuming
    // the only way we end up with a GeometryCollection is if it contains heterogenous types
    GeometryCollection polygonsAndLines = nzmgHelper.collection(simplePolygon, complexPolygon, lineString);

    EvalException ex = assertThrows(EvalException.class,
        () -> evaluate("measure(geom)", tuple(polygonsAndLines)));
    HashSet<String> geomTypes = new HashSet<>(Arrays.asList("LineString", "Polygon"));
    assertThat(ex.getProblem(), hasAncestorProblem(
        is(GeometryProblems.get().mixedGeometryTypes(geomTypes))
    ));

    GeometryCollection linesAndPoints = nzmgHelper.collection(lineString, point);
    ex = assertThrows(EvalException.class,
        () -> evaluate("measure(geom)", tuple(linesAndPoints)));
    geomTypes = new HashSet<>(Arrays.asList("LineString", "Point"));
    assertThat(ex.getProblem(), hasAncestorProblem(
        is(GeometryProblems.get().mixedGeometryTypes(geomTypes))
    ));
  }

  @Test
  public void canMeasureLengthInMetricFromGeodetic() {
    // we start with an NZ line, because meters are easy to understand
    LineString nzLine = nzmgHelper.line(0, 0, 0, 1000);
    assertThat(nzLine.getLength(), is(1000.0D));  //sanity check

    // now we reproject it to lat long
    LineString latLongLine = (LineString)latLongHelper.reproject(nzLine);
    // sanity check that geom has recalculated length in degrees
    assertThat(latLongLine.getLength(), closeTo(0.009D, 0.001));

    Tuple input = tuple(latLongLine);

    assertThat((Double)evaluate("measure_length(geom)", input), closeTo(1000.0D, 1.2D));
  }

  @Test
  public void canMeasureLengthInMetricFromImperial() {
    // we start with an NZ line, because meters are easy to understand
    LineString newYorkLine = newYorkFootGeomHelper.line(0, 0, 0, 1000);
    assertThat(newYorkLine.getLength(), is(1000.0D));  //sanity check

    assertThat((Double)evaluate("measure_length(geom)", tuple(newYorkLine)), closeTo(304.8D, 0.01D));
  }

  @Test
  public void measureLargeLengths() {
    // north south line height of NZ
    assertThat((Double)evaluate("measure_length(geom)", tuple(latLongHelper.box(-34, 170, -47, 170))),
        closeTo(2886270.0D, 0.1D));

    // north south line from Northern Queensland down to Southern Tasmania
    assertThat((Double)evaluate("measure_length(geom)", tuple(latLongHelper.box(-11, 142, -43, 142))),
        closeTo(7090494.2D, 0.1D));

    assertThat((Double)evaluate("measure_length(geom)", tuple(latLongHelper.box(0, 142, -43, 142))),
        closeTo(9522786.0D, 0.1D));

    assertThat((Double)evaluate("measure_length(geom)", tuple(latLongHelper.box(43, 142, -43, 142))),
        closeTo(1.9045199700896177E7D, 0.1D));


    // west east line spanning Australia
    EvalException ex = assertThrows(EvalException.class,
        () -> evaluate("measure_length(geom)", tuple(latLongHelper.box(-27, 113, -27, 153))));
    assertThat(ex.getProblem(), hasAncestorProblem(
        isProblem(Severity.ERROR, GeometryProblems.class, "cannotReproject")
    ));
  }

  @Test
  public void canMeasureAreaInMetricFromImperial() {
    Polygon newYorkBox = newYorkFootGeomHelper.box(0, 0, 1000, 1000);
    assertThat(newYorkBox.getArea(), is(1000000.0D));  //sanity check area in square feet

    // The new york box is approx 305 x 305m or 93000 square metres
    assertThat((Double)evaluate("measure_area(geom)", tuple(newYorkBox)), closeTo(92903.415D, 0.01D));
  }

  @Test
  public void measureLargeAreas() {
    // approximately a bbox around NZ
    assertThat((double)evaluate("measure_area(geom)", tuple(latLongHelper.box(-47, 166, -34, 178))),
        closeTo(1.459748057918147E12D, GeoHelper.METER_TOLERANCE_NEAREST_MM));

    // approximately a bbox around Queensland, New South Wales, Victoria and Tasmania
    assertThat((double)evaluate("measure_area(geom)", tuple(latLongHelper.box(-43, 140, -11, 153))),
        closeTo(4.416703860485376E12D, GeoHelper.METER_TOLERANCE_NEAREST_MM));

    // approximately a bbox around Australia, far too big
    EvalException ex = assertThrows(EvalException.class,
        () -> evaluate("measure_area(geom)", tuple(latLongHelper.box(-43, 113, -11, 153))));
    assertThat(ex.getProblem(), hasAncestorProblem(
        isProblem(Severity.ERROR, GeometryProblems.class, "cannotReproject")
    ));
  }

  private Tuple tuple(Object... values) {
    return Tuple.ofValues(inputStruct.find(Struct.class).get(), values);
  }

}
