/*
 * 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.GeoHelper.*;
import static org.junit.Assert.*;

import java.io.File;
import java.nio.file.Files;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

import org.junit.Test;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.precision.CommonBitsRemover;

import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.gt.GeometryHelper;

public class OverlayOperationsTest extends ProjectTest {

  private final GeometryFactory geometryFactory = new GeometryFactory();
  private final WKTReader wktReader = new WKTReader(geometryFactory);

  private final GeometryHelper nzHelper = GeometryHelper.nzMapGridWithOriginShift(project.getSridSet());
  private final OverlayOperations subject = OverlayOperations.get();

  @Test
  public void intersectionWithPointIsAPoint() {
    // an intersection with a point (to a point, line or polygon) can only ever result in a single or
    // empty point.
    Geometry point = nzHelper.point(100, 100);

    assertThat(subject.intersection(point, nzHelper.line(0, 100, 200, 100)),
        geometryTopoMatch(point, Point.class));

    assertThat(subject.intersection(nzHelper.line(0, 100, 200, 100), point),
        geometryTopoMatch(point, Point.class));

    assertThat(subject.intersection(point, nzHelper.box(0, 0, 200, 200)),
        geometryTopoMatch(point, Point.class));

    assertThat(subject.intersection(nzHelper.box(0, 0, 200, 200), point),
        geometryTopoMatch(point, Point.class));

    assertThat(subject.intersection(point, nzHelper.line(0, 80, 200, 80)),
        geometryMatch(nzHelper.emptyPoint(), 0));

    assertThat(subject.intersection(point, nzHelper.box(0, 0, 20, 20)),
        geometryMatch(nzHelper.emptyPoint(), 0));
  }

  @Test
  public void intersectionOfLines() {
    Geometry straightLine = nzHelper.line(0, 100, 100, 100);
    Geometry zigZagLine = nzHelper.line(
        nzHelper.toCoordinate(0, 100),
        nzHelper.toCoordinate(30, 100),
        nzHelper.toCoordinate(30, 120),
        nzHelper.toCoordinate(50, 120),
        nzHelper.toCoordinate(50, 100),
        nzHelper.toCoordinate(80, 100)
    );

    assertThat(subject.intersection(straightLine, nzHelper.line(20, 100, 80, 100)),
        geometryTopoMatch(nzHelper.line(20, 100, 80, 100), LineString.class));

    // two lines that cross have a point intersection
    assertThat(subject.intersection(straightLine, nzHelper.line(50, 0, 50, 200)),
        geometryTopoMatch(nzHelper.point(50, 100), Point.class));

    assertThat(subject.intersection(nzHelper.line(20, 100, 80, 100), straightLine),
        geometryTopoMatch(nzHelper.line(20, 100, 80, 100), LineString.class));

    assertThat(subject.intersection(straightLine, zigZagLine),
        geometryTopoMatch(nzHelper.multiLine(
            nzHelper.line(0, 100, 30, 100),
            nzHelper.line(50, 100, 80, 100)), MultiLineString.class)
    );

    assertThat(subject.intersection(zigZagLine, straightLine),
        geometryTopoMatch(nzHelper.multiLine(
            nzHelper.line(0, 100, 30, 100),
            nzHelper.line(50, 100, 80, 100)), MultiLineString.class)
    );

    assertThat(subject.intersection(zigZagLine, nzHelper.box(10, 90, 80, 110)),
        geometryTopoMatch(nzHelper.multiLine(
            nzHelper.line(
                nzHelper.toCoordinate(10, 100),
                nzHelper.toCoordinate(30, 100),
                nzHelper.toCoordinate(30, 110)
            ),
            nzHelper.line(
                nzHelper.toCoordinate(50, 110),
                nzHelper.toCoordinate(50, 100),
                nzHelper.toCoordinate(80, 100)
            )), MultiLineString.class)
    );

    assertThat(subject.intersection(straightLine, nzHelper.line(200, 200, 300, 200)),
        geometryMatch(nzHelper.emptyLine(), 0));

    assertThat(subject.intersection(straightLine, nzHelper.box(200, 200, 300, 300)),
        geometryMatch(nzHelper.emptyLine(), 0));
  }

  @Test
  public void intersectionOfPolygons() {
    Geometry simpleBox1 = nzHelper.box(0, 0, 100, 100);
    Geometry simpleBox2 = nzHelper.box(0, 50, 150, 100);
    Geometry nBox = nzHelper.box(
        nzHelper.toCoordinate(0, 50),
        nzHelper.toCoordinate(0, 120),
        nzHelper.toCoordinate(80, 120),
        nzHelper.toCoordinate(80, 50),
        nzHelper.toCoordinate(70, 50),
        nzHelper.toCoordinate(70, 110),
        nzHelper.toCoordinate(10, 110),
        nzHelper.toCoordinate(10, 50),
        nzHelper.toCoordinate(0, 50)
    );

    assertThat(subject.intersection(simpleBox1, simpleBox2),
        geometryTopoMatch(nzHelper.box(0, 50, 100, 100), Polygon.class));

    assertThat(subject.intersection(simpleBox1, nBox),
        geometryTopoMatch(nzHelper.multiBox(
            nzHelper.box(0, 50, 10, 100),
            nzHelper.box(70, 50, 80, 100)
        ), MultiPolygon.class));

    assertThat(subject.intersection(nBox, simpleBox1),
        geometryTopoMatch(nzHelper.multiBox(
            nzHelper.box(0, 50, 10, 100),
            nzHelper.box(70, 50, 80, 100)
        ), MultiPolygon.class));

    assertThat(subject.intersection(simpleBox1, nzHelper.box(500, 500, 1000, 1000)),
        geometryMatch(nzHelper.emptyBox(), 0));

    assertThat(subject.intersection(nzHelper.box(500, 500, 1000, 1000), simpleBox1),
        geometryMatch(nzHelper.emptyBox(), 0));
  }

  @Test
  public void canGetDifference() {
    Geometry lhs = nzHelper.box(0, 0, 100, 100);

    assertThat(subject.difference(lhs, nzHelper.box(0, 50, 150, 100)),
        geometryTopoMatch(nzHelper.box(0, 0, 100, 50)));

    assertThat(subject.difference(lhs, nzHelper.box(20, 0, 80, 100)),
        geometryTopoMatch(nzHelper.multiBox(nzHelper.box(0, 0, 20, 100), nzHelper.box(80, 0, 100, 100))));
  }

  @Test
  public void checkTopologyExceptionCauses() {
    // This test is for data that has caused TopologyException's in the past. Some of the data in
    // these tests has been obfuscated if that data wasn't known to be openly available.

    // difference1 causes a TopologyExcecption with OverlayNG.overlay(lhs, rhs, OverlayOp.DIFFERENCE)
    // which is part of the reason for using OverlayRobust. These shapes are from buffering road centre
    // lines then removing overlaps
    doDifference("difference1lhs.txt", "difference1rhs.txt");

    // from https://gitlab.catalyst.net.nz/riskscape/riskscape/-/issues/279
    doIntersection("intersection1lhs.txt", "intersection1rhs.txt");
    doIntersection("intersection2lhs.txt", "intersection2rhs.txt");
  }

  private Geometry doIntersection(String lhsFile, String rhsFile) {
    return doGeometryOp(lhsFile, rhsFile, (lhs, rhs) -> subject.intersection(lhs, rhs));
  }

  private Geometry doDifference(String lhsFile, String rhsFile) {
    return doGeometryOp(lhsFile, rhsFile, (lhs, rhs) -> subject.difference(lhs, rhs));
  }

  private Geometry doGeometryOp(String lhsFile, String rhsFile, BiFunction<Geometry, Geometry, Geometry> op) {
    Geometry lhs = fromWkt(lhsFile);
    if (! lhs.isValid()) {
      System.err.format("LHS geom is not valid. This may cause later failures%n");
    }
    Geometry rhs = fromWkt(rhsFile);
    if (! rhs.isValid()) {
      System.err.format("RHS geom is not valid. This may cause later failures%n");
    }
    return op.apply(lhs, rhs);
  }

  /**
   * @param filename name of file containing wkt to load, relative to resources package for this class
   */
  private Geometry fromWkt(String filename) {
    File wktFile = new File(OverlayOperationsTest.class.getResource(filename).getFile());

    try {
      return wktReader.read(Files.readAllLines(wktFile.toPath()).stream().collect(Collectors.joining()));
    } catch (Throwable t) {
      throw new RuntimeException(t);
    }
  }

  /**
   * Obfuscates geometries by removing the most significant digits that are common to both.
   *
   * Useful to create obfuscated test data from original data that may not be public domain.
   *
   * @param g1
   * @param g2
   */
  private void obfuscate(Geometry g1, Geometry g2) {
    CommonBitsRemover cbr = new CommonBitsRemover();
    cbr.add(g1);
    cbr.add(g2);

    CommonBitsRemover cbrObfuscater = new CommonBitsRemover();
    cbrObfuscater.add(geometryFactory.createPoint(new Coordinate(1000000999D, 1000000999D)));
    cbrObfuscater.add(geometryFactory.createPoint(new Coordinate(1000000899D, 1000000899D)));

    // we strip all the common bits off the geoms, then add some other common bits in
    Geometry og1 = cbr.removeCommonBits(g1.copy());
    cbrObfuscater.addCommonBits(og1);
    Geometry og2 = cbr.removeCommonBits(g2.copy());
    cbrObfuscater.addCommonBits(og2);


    System.out.format("g1: %s%n", og1);
    System.out.format("g2: %s%n", og2);
  }

}
