/*
 * 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 nz.org.riskscape.engine.TupleMatchers.*;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Polygon;

import nz.org.riskscape.engine.ProjectTest;
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.engine.util.Pair;

/**
 * A variation of IntersectionIndexTest that only tests with polygons. But this test is parameterized
 * to it is run with and without cutting geometries before they are added to the index
 */
@RunWith(Parameterized.class)
public class IntersectionIndexPolygonTest extends ProjectTest {

  @Parameters
  public static List<IntersectionIndex.Options> getParameters() {
    // we explicity turn cutting on
    IntersectionIndex.Options withCutting = new IntersectionIndex.Options();
    withCutting.setCutBeforeAdding(Optional.of(true));

    // setting cutting to empty will default to cutting for polygons
    IntersectionIndex.Options withDefaultCutting = new IntersectionIndex.Options();
    withDefaultCutting.setCutBeforeAdding(Optional.empty());

    return Arrays.asList(
      IntersectionIndex.defaultOptions(),
      withCutting,
      withDefaultCutting
    );
  }

  private IntersectionIndex.Options options;

  public IntersectionIndexPolygonTest(IntersectionIndex.Options testOptions) {
    this.options = testOptions;
  }

  GeometryHelper nzGeometryHelper = GeometryHelper.nzTransverseMercatorWithOriginShift(project.getSridSet());
  GeometryHelper latLongGeometryHelper = GeometryHelper.latLong(project.getSridSet());


  @Test
  public void canGetIntersections() {
    Struct type = Struct.of("geom", Types.GEOMETRY, "value", Types.TEXT);

    IntersectionIndex index = new IntersectionIndex(type.getEntry("geom"), project.getSridSet(), options);

    Tuple foo = Tuple.ofValues(type, nzGeometryHelper.box(0, 0, 100, 100), "foo");
    Tuple bar = Tuple.ofValues(type, nzGeometryHelper.box(100, 0, 200, 100), "bar");
    index.insert(foo);
    index.insert(bar);

    // line that goes up through foo then turns around to come back and touch foo. This results in
    // a line and point intersection, but the point part should be removed from the result.
    LineString line = nzGeometryHelper.line(
        nzGeometryHelper.toCoordinate(20, 0),
        nzGeometryHelper.toCoordinate(20, 120),
        nzGeometryHelper.toCoordinate(40, 120),
        nzGeometryHelper.toCoordinate(40, 100)
    );

    assertThat(index.findIntersections(line), contains(
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.line(20, 0, 20, 100))),
            hasProperty("right", is(foo))
        )
    ));

    assertThat(index.findIntersectingValues(line), contains(
        is(foo)
    ));

    // box with a line border on foo, part in bar and another part that then touches bar on a line edge.
    Polygon box = nzGeometryHelper.box(
        nzGeometryHelper.toCoordinate(100, 100),
        nzGeometryHelper.toCoordinate(250, 100),
        nzGeometryHelper.toCoordinate(250, 20),
        nzGeometryHelper.toCoordinate(200, 20),
        nzGeometryHelper.toCoordinate(200, 40),
        nzGeometryHelper.toCoordinate(220, 40),
        nzGeometryHelper.toCoordinate(220, 80),
        nzGeometryHelper.toCoordinate(100, 80),
        nzGeometryHelper.toCoordinate(100, 100)
    );
    assertThat(index.findIntersections(box), contains(
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.box(100, 80, 200, 100))),
            hasProperty("right", is(bar))
        )
    ));

    assertThat(index.findIntersectingValues(box), containsInAnyOrder(
        is(foo),  // foo is found because of a glancing intersection
        is(bar)
    ));
  }

  @Test @Ignore("See GL1105")
  public void canGetConsistentIntersectionGeometryTypeReturned() {
    Struct type = Struct.of("geom", Types.GEOMETRY, "value", Types.TEXT);

    IntersectionIndex index = new IntersectionIndex(type.getEntry("geom"), project.getSridSet(), options);

    Tuple foo = Tuple.ofValues(type, nzGeometryHelper.box(0, 0, 100, 100), "foo");
    index.insert(foo);

    LineString line = nzGeometryHelper.line(
        nzGeometryHelper.toCoordinate(10, 0), nzGeometryHelper.toCoordinate(10, 110));

    assertThat(index.findIntersections(line),
        contains(allOf(hasProperty("left", geometryTopoMatch(nzGeometryHelper.line(10, 0, 10, 100))),
            hasProperty("right", is(foo)))));

    // line that goes up through foo and then turns around and comes back
    LineString zigzag = nzGeometryHelper.line(
        nzGeometryHelper.toCoordinate(10, 0),
        nzGeometryHelper.toCoordinate(10, 110),
        nzGeometryHelper.toCoordinate(20, 110),
        nzGeometryHelper.toCoordinate(20, 90)
    );
    // TODO we get a single MULTILINESTRING here, but really we should get *two* LINESTRINGs instead
    // (that's what sampling a GeoTIFF would do)
    assertThat(index.findIntersections(zigzag), contains(
            allOf(hasProperty("left", geometryTopoMatch(nzGeometryHelper.line(10, 0, 10, 100))),
                hasProperty("right", is(foo))),
            allOf(hasProperty("left", geometryTopoMatch(nzGeometryHelper.line(20, 100, 20, 90))),
                hasProperty("right", is(foo))
    )));
  }

  @Test
  public void canGetIntersectionsWithOverlappingPolygons() {
    Struct type = Struct.of("geom", Types.GEOMETRY, "value", Types.TEXT);

    IntersectionIndex index = new IntersectionIndex(type.getEntry("geom"), project.getSridSet(), options);

    Tuple foo = Tuple.ofValues(type, nzGeometryHelper.box(0, 0, 100, 100), "foo");
    Tuple bar = Tuple.ofValues(type, nzGeometryHelper.box(10, 10, 110, 110), "bar");
    index.insert(foo);
    index.insert(bar);

    LineString overlappingLine = nzGeometryHelper.line(
        nzGeometryHelper.toCoordinate(20, 20),
        nzGeometryHelper.toCoordinate(40, 40)
    );

    // line fully overlaps both polygons, so we should get both foo and bar back
    assertThat(index.findIntersections(overlappingLine), containsInAnyOrder(
        allOf(
            hasProperty("left", geometryTopoMatch(overlappingLine)),
            hasProperty("right", is(foo))
        ),
        allOf(
            hasProperty("left", geometryTopoMatch(overlappingLine)),
            hasProperty("right", is(bar))
        )
    ));

    // repeat with a box that intersects both polygons with a partial overlap
    Polygon box = nzGeometryHelper.box(90, 90, 120, 120);
    assertThat(index.findIntersections(box), containsInAnyOrder(
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.box(90, 90, 100, 100))),
            hasProperty("right", is(foo))
        ),
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.box(90, 90, 110, 110))),
            hasProperty("right", is(bar))
        )
    ));

    assertThat(index.findIntersectingValues(box), containsInAnyOrder(
        is(foo),
        is(bar)
    ));
  }

  @Test
  public void canGetIntersectionsFromPolygonsWithHoles() {
    // set cut points low to ensure our geometry gets cut up.
    options.setCutPoints(10);

    Struct type = Struct.of("geom", Types.GEOMETRY, "value", Types.TEXT);

    IntersectionIndex index = new IntersectionIndex(type.getEntry("geom"), project.getSridSet(), options);

    // build up some linear rings that will become our polygon with holes
    // a square to keep things simple
    LinearRing outterRing = nzGeometryHelper.ring(
        nzGeometryHelper.toCoordinate(0, 0),
        nzGeometryHelper.toCoordinate(0, 1000),
        nzGeometryHelper.toCoordinate(1000, 1000),
        nzGeometryHelper.toCoordinate(1000, 0),
        nzGeometryHelper.toCoordinate(0, 0)
    );
    // an upside down L
    LinearRing upSideDownLHole = nzGeometryHelper.ring(
        nzGeometryHelper.toCoordinate(100, 100),
        nzGeometryHelper.toCoordinate(100, 900),
        nzGeometryHelper.toCoordinate(900, 900),
        nzGeometryHelper.toCoordinate(900, 800),
        nzGeometryHelper.toCoordinate(200, 800),
        nzGeometryHelper.toCoordinate(200, 100),
        nzGeometryHelper.toCoordinate(100, 100)
    );
    LinearRing centreHole = nzGeometryHelper.ring(
        nzGeometryHelper.toCoordinate(400, 400),
        nzGeometryHelper.toCoordinate(400, 600),
        nzGeometryHelper.toCoordinate(600, 600),
        nzGeometryHelper.toCoordinate(600, 400),
        nzGeometryHelper.toCoordinate(400, 400)
    );
    LinearRing lowerRightHole = nzGeometryHelper.ring(
        nzGeometryHelper.toCoordinate(700, 100),
        nzGeometryHelper.toCoordinate(700, 300),
        nzGeometryHelper.toCoordinate(900, 300),
        nzGeometryHelper.toCoordinate(900, 100),
        nzGeometryHelper.toCoordinate(700, 100)
    );
    Polygon polygonWithHoles = nzGeometryHelper.box(outterRing, upSideDownLHole, centreHole, lowerRightHole);
    Tuple foo = Tuple.ofValues(type, polygonWithHoles, "foo");
    index.insert(foo);

    if (options.getCutBeforeAdding().get()) {
      // sanity check that our geometry has been cut up. the exact number isn't important we just want to
      // know that the scissors have come out.
      assertThat(index.size(), greaterThan(10));
    } else {
      // no cutting here so should only have one item
      assertThat(index.size(), is(1));
    }

    // a horizontal line
    assertThat(index.findIntersections(nzGeometryHelper.line(0, 700, 1000, 700)), containsInAnyOrder(
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.multiLine(
                nzGeometryHelper.line(0, 700, 100, 700),
                nzGeometryHelper.line(200, 700, 1000, 700)
            ))),
            hasProperty("right", is(foo))
        )
    ));

    // diagonal line
    assertThat(index.findIntersections(nzGeometryHelper.line(0, 0, 1000, 1000)), containsInAnyOrder(
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.multiLine(
                nzGeometryHelper.line(0, 0, 100, 100),
                nzGeometryHelper.line(200, 200, 400, 400),
                nzGeometryHelper.line(600, 600, 800, 800),
                nzGeometryHelper.line(900, 900, 1000, 1000)
            ))),
            hasProperty("right", is(foo))
        )
    ));

    assertThat(index.findIntersections(nzGeometryHelper.line(0, 550, 1000, 550)), contains(
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.multiLine(
                nzGeometryHelper.line(0, 550, 100, 550),
                nzGeometryHelper.line(200, 550, 400, 550),
                nzGeometryHelper.line(600, 550, 1000, 550)
            ))),
            hasProperty("right", is(foo))
        )
    ));

    // lines on the edge of a hole does intersect
    assertThat(index.findIntersections(nzGeometryHelper.line(400, 400, 400, 600)), contains(
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.line(400, 400, 400, 600))),
            hasProperty("right", is(foo))
        )
    ));

    // lines completely inside the holes don't intersect
    // line in the centreHole
    assertThat(index.findIntersections(nzGeometryHelper.line(450, 550, 550, 550)), hasSize(0));
    // line in upSideDownLHole
    assertThat(index.findIntersections(nzGeometryHelper.line(
        nzGeometryHelper.toCoordinate(150, 150),
        nzGeometryHelper.toCoordinate(150, 850),
        nzGeometryHelper.toCoordinate(850, 850)
    )), hasSize(0));
    // line in lowerRightHole
    assertThat(index.findIntersections(nzGeometryHelper.line(720, 120, 880, 280)), hasSize(0));

    // lines across the holes that touch (but not cross) polygon don't intersect
    assertThat(index.findIntersections(nzGeometryHelper.line(100, 600, 200, 600)), hasSize(0));
    assertThat(index.findIntersections(nzGeometryHelper.line(400, 400, 600, 600)), hasSize(0));

    // better check polygon a intersection
    LinearRing expectedOutter = nzGeometryHelper.ring(
        nzGeometryHelper.toCoordinate(200, 150),
        nzGeometryHelper.toCoordinate(200, 800),
        nzGeometryHelper.toCoordinate(850, 800),
        nzGeometryHelper.toCoordinate(850, 300),
        nzGeometryHelper.toCoordinate(700, 300),
        nzGeometryHelper.toCoordinate(700, 150),
        nzGeometryHelper.toCoordinate(200, 150)
    );
    assertThat(index.findIntersections(nzGeometryHelper.box(150, 150, 850, 850)), contains(
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.box(expectedOutter, centreHole))),
            hasProperty("right", is(foo))
        )
    ));
  }

  @Test
  public void canGetDifferenceAndIntersections() {
    Struct type = Struct.of("geom", Types.GEOMETRY, "value", Types.TEXT);

    IntersectionIndex index = new IntersectionIndex(type.getEntry("geom"), project.getSridSet(), options);

    index.insert(Tuple.ofValues(type, nzGeometryHelper.box(0, 0, 100, 100), "foo"));
    index.insert(Tuple.ofValues(type, nzGeometryHelper.box(100, 0, 200, 100), "bar"));

    Pair<Optional<Geometry>,List<Pair<Geometry,Tuple>>> results =
        index.findDifferenceAndIntersections(nzGeometryHelper.box(50, 50, 150, 150));

    assertThat(results.getLeft().get(), geometryTopoMatch(nzGeometryHelper.box(50, 100, 150, 150)));
    assertThat(results.getRight(), containsInAnyOrder(
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.box(50, 50, 100, 100))),
            hasProperty("right", tupleWithValue("value", is("foo")))
        ),
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.box(100, 50, 150, 100))),
            hasProperty("right", tupleWithValue("value", is("bar")))
        )
    ));

    // Now with no difference
    results =
        index.findDifferenceAndIntersections(nzGeometryHelper.box(50, 50, 150, 90));

    assertThat(results.getLeft().isPresent(), is(false));
    assertThat(results.getRight(), containsInAnyOrder(
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.box(50, 50, 100, 90))),
            hasProperty("right", tupleWithValue("value", is("foo")))
        ),
        allOf(
            hasProperty("left", geometryTopoMatch(nzGeometryHelper.box(100, 50, 150, 90))),
            hasProperty("right", tupleWithValue("value", is("bar")))
        )
    ));
  }

  @Test
  public void intersectionIsNotReprojectedIfCoversAllOfInput() {
    Struct type = Struct.of("geom", Types.GEOMETRY, "value", Types.TEXT);

    IntersectionIndex index = new IntersectionIndex(type.getEntry("geom"), project.getSridSet(), options);

    index.insert(Tuple.ofValues(type, latLongGeometryHelper.box(-60, -30, 170, 179), "foo"));

    // The first x/y are super precise, as it would be very unlikely that such precise vaules would
    // remain the same if they went through the reprojection loop.
    Polygon nzBox = nzGeometryHelper.box(50.787878787, 50.65656565666, 150, 150);
    Pair<Optional<Geometry>,List<Pair<Geometry,Tuple>>> results =
        index.findDifferenceAndIntersections(nzBox);

    // If there are no intersections then the difference is the input geometry. But reprojecting
    // nz -> lat-long -> nz would create errors. So in this case the input geom gets returned without
    // the needless reprojections.
    assertThat(results.getLeft().isPresent(), is(false));
    assertThat(results.getRight(), contains(
        allOf(
            hasProperty("left", is(nzBox)),
            hasProperty("right", tupleWithValue("value", is("foo")))
        ))
    );
  }

  @Test
  public void differenceIsNotReprojectedIfNoIntersections() {
    Struct type = Struct.of("geom", Types.GEOMETRY, "value", Types.TEXT);

    IntersectionIndex index = new IntersectionIndex(type.getEntry("geom"), project.getSridSet(), options);

    index.insert(Tuple.ofValues(type, latLongGeometryHelper.box(0, 0, 10, 10), "foo"));

    Polygon nzBox = nzGeometryHelper.box(50.787878787, 50.65656565666, 150, 150);
    Pair<Optional<Geometry>,List<Pair<Geometry,Tuple>>> results =
        index.findDifferenceAndIntersections(nzBox);

    // If there are no intersections then the difference is the input geometry. But reprojecting
    // nz -> lat-long -> nz would create errors. So in this case the input geom gets returned without
    // the needless reprojections.
    assertThat(results.getLeft().get(), is(nzBox));
    assertThat(results.getRight(), empty());
  }

}
