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

import static nz.org.riskscape.engine.GeoHelper.*;
import static org.junit.Assert.*;

import java.util.List;

import org.geotools.coverage.grid.GridEnvelope2D;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;

import it.geosolutions.jaiext.jts.CoordinateSequence2D;
import nz.org.riskscape.engine.CrsHelper;
import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.SRIDSet;
import nz.org.riskscape.engine.GeoHelper;

public class CutGeometryByGridOpTest extends ProjectTest implements CrsHelper {


  SRIDSet sridSet = project.getSridSet();
  CutGeometryByGridOp op;
  GeometryFactory gf = sridSet.getGeometryFactory(nzMapGrid());
  Polygon boxPolygon = box(0, 0, 1, 1);

  @Before
  public void setupDefaultOp() {
    op = new CutGeometryByGridOp(sridSet);
  }

  @Test
  public void gridAPolygonThatFitsPerfectly() {
    // grid geometry uses an inclusive range
    assertGridSize(boxPolygon, 1, 0, 0);

    List<Geometry> list = op.apply(boxPolygon, 1);

    list.forEach(Geometry::normalize);
    assertThat(
        list,
        Matchers.contains(boxPolygon)
    );
  }

  @Test
  public void gridAPolygonThatFitsPerfectlyOffsetFromAxis() {
    // grid geometry uses an inclusive range
    boxPolygon = box(1, 1, 2, 2);
    assertGridSize(boxPolygon, 1, 0, 0);

    List<Geometry> list = op.apply(boxPolygon, 1);

    list.forEach(Geometry::normalize);
    assertThat(
        list,
        Matchers.contains(GeoHelper.geometryMatch(boxPolygon, 0.1))
    );
  }

  @Test
  public void gridAPolygonThatFitsPerfectlyOffsetNegativelyFromAxis() {
    // grid geometry uses an inclusive range
    boxPolygon = box(-2, -2, -1, -1);
    assertGridSize(boxPolygon, 1, 0, 0);

    List<Geometry> list = op.apply(boxPolygon, 1);

    assertThat(
        list,
        Matchers.contains(GeoHelper.geometryMatch(boxPolygon, 0.1))
    );
  }

  @Test
  public void gridAPolygonThatFitsPerfectlySpreadAcrossAxis() {
    // grid geometry uses an inclusive range
    boxPolygon = box(-2, -2, 2, 2);

    List<Geometry> list = op.apply(boxPolygon, 4);

    assertThat(
        list,
        Matchers.contains(GeoHelper.geometryMatch(boxPolygon, 0.1))
    );
  }

  @Test
  public void gridAPolygonThatFitsWithinDistanceSpreadAcrossAxes() {
    // grid geometry uses an inclusive range
    boxPolygon = box(-2, -2, 2, 2);

    List<Geometry> list = op.apply(boxPolygon, 8);

    assertThat(
        list,
        Matchers.contains(GeoHelper.geometryMatch(boxPolygon, 0.1))
    );
  }

  @Test
  public void gridAPolygonThatCleavesNicelyInToFour() {

    List<Geometry> list = op.apply(boxPolygon, 0.5D);

    list.forEach(Geometry::normalize);
    assertThat(
        list,
        Matchers.containsInAnyOrder(
            box(0.0, 0.0, 0.5, 0.5),
            box(0.0, 0.5, 0.5, 1.0),
            box(0.5, 0.0, 1.0, 0.5),
            box(0.5, 0.5, 1.0, 1.0)
        )
    );
  }

  @Test
  public void gridAPolygonThatCleavesNicelyInToFourSpreadAcrossAxes() throws Exception {

    boxPolygon = box(-0.5, -0.5, 0.5, 0.5);

    List<Geometry> list = op.apply(boxPolygon, 0.5D);

    list.forEach(Geometry::normalize);
    assertThat(
        list,
        Matchers.containsInAnyOrder(
            box(-0.5, -0.5, 0.0, 0.0),
            box(-0.5, 0.0, 0.0, 0.5),
            box(0.0, -0.5, 0.5, 0.0),
            box(0.0, 0.0, 0.5, 0.5)
        )
    );
  }

  @Test
  public void gridAPolygonThatSplitsInToNineEvenPieces() {
    boxPolygon = box(0, 0, 1.5, 1.5);
    List<Geometry> list = op.apply(boxPolygon, 0.5D);

    list.forEach(Geometry::normalize);
    assertThat(
        list,
        Matchers.containsInAnyOrder(
            box(0.0, 0.0, 0.5, 0.5), // TL
            box(0.5, 0.0, 1.0, 0.5), // TC
            box(1.0, 0.0, 1.5, 0.5), // TR
            box(0.0, 0.5, 0.5, 1.0), // ML
            box(0.5, 0.5, 1.0, 1.0), // MC
            box(1.0, 0.5, 1.5, 1.0), // MR
            box(0.0, 1.0, 0.5, 1.5), // BL
            box(0.5, 1.0, 1.0, 1.5), // BC
            box(1.0, 1.0, 1.5, 1.5)  // BR
        )
    );
  }


  @Test
  public void gridAPolygonThatSplitsUnevenlyInToNineEvenPieces() {
    boxPolygon = box(0.1, 0.1, 1.4, 1.4);
    List<Geometry> list = op.apply(boxPolygon, 0.5D);

    assertThat(
        list,
        Matchers.containsInAnyOrder(
            GeoHelper.geometryMatch(box(0.1, 0.1, 0.5, 0.5), 2), // TL
            GeoHelper.geometryMatch(box(0.5, 0.1, 1.0, 0.5), 2), // TC
            GeoHelper.geometryMatch(box(1.0, 0.1, 1.4, 0.5), 2), // TR
            GeoHelper.geometryMatch(box(0.1, 0.5, 0.5, 1.0), 2), // ML
            GeoHelper.geometryMatch(box(0.5, 0.5, 1.0, 1.0), 2), // MC
            GeoHelper.geometryMatch(box(1.0, 0.5, 1.4, 1.0), 2), // MR
            GeoHelper.geometryMatch(box(0.1, 1.0, 0.5, 1.4), 2), // BL
            GeoHelper.geometryMatch(box(0.5, 1.0, 1.0, 1.4), 2), // BC
            GeoHelper.geometryMatch(box(1.0, 1.0, 1.4, 1.4), 2)  // BR
        )
    );
  }

  @Test
  public void gridsALessRegularPolygonCorrectly() throws Exception {
    // a four pointed star with a hole in the middle
    Polygon star = (Polygon) gf.createPolygon(gf.createLinearRing(new CoordinateSequence2D(
        0, 5,
        2, 2,
        5, 0,
        2, -2,
        0, -5,
        -2, -2,
        -5, 0,
        -2, 2,
        0, 5
    )), new LinearRing[] {
        gf.createLinearRing(new CoordinateSequence2D(
            0, 2,
            2, 0,
            0, -2,
            -2, 0,
            0, 2
        ))
    }).norm();

    List<Geometry> list = op.apply(star, 2);

    assertEquals(20, list.size());

    assertThat(
        list,
        // just going to sample a few intersections - too gruelling to match them all
        Matchers.allOf(
            Matchers.hasItem(
            // the tip of the star
            GeoHelper.geometryMatch(gf.createPolygon(new CoordinateSequence2D(
                5, 0,
                3.5, -1,
                3, -1,
                3, 1,
                3.5, 1,
                5, 0
            )), GeoHelper.METER_TOLERANCE_NEAREST_MM)
            ),
            Matchers.hasItem(
            // piece that intersects the hole
            GeoHelper.geometryMatch(gf.createPolygon(new CoordinateSequence2D(
                1, -1,
                3, -1,
                3, -1.333333333333,
                2, -2,
                1.33333333333, -3,
                1, -3,
                1, -1
            )), GeoHelper.METER_TOLERANCE_NEAREST_MM)
          )
        )
    );
  }

  @Test
  public void gridAPolygonThatLeavesTwoPartsInOneTile() {
    // A 16x16 grid a bit like:
    // xxx
    // x  x
    // x  x
    // xxxx
    boxPolygon = gf.createPolygon(new CoordinateSequence2D(
        0, 0,
        0, 16,
        10, 16,
        10, 12,
        4, 12,
        4, 4,
        12, 4,
        12, 10,
        16, 10,
        16, 0,
        0, 0
    ));
    assertTrue(boxPolygon.isValid());

    List<Geometry> list = op.apply(boxPolygon, 8);
    assertThat(list, Matchers.containsInAnyOrder(
        geometryTopoMatch(gf.createPolygon(new CoordinateSequence2D(
            0, 0,
            0, 8,
            4, 8,
            4, 4,
            8, 4,
            8, 0,
            0, 0
        ))),
        geometryTopoMatch(gf.createPolygon(new CoordinateSequence2D(
            0, 8,
            0, 16,
            8, 16,
            8, 12,
            4, 12,
            4, 8,
            0, 8
        ))),
        // The next two boxes are in the top right corner
        geometryTopoMatch(gf.createPolygon(new CoordinateSequence2D(
            8, 16,
            10, 16,
            10, 12,
            8, 12,
            8, 16
        ))),
        geometryTopoMatch(gf.createPolygon(new CoordinateSequence2D(
            16, 10,
            16, 8,
            12, 8,
            12, 10,
            16, 10
        ))),
        geometryTopoMatch(gf.createPolygon(new CoordinateSequence2D(
            8, 0,
            8, 4,
            12, 4,
            12, 8,
            16, 8,
            16, 0,
            8, 0
        )))
    ));
  }

  @Test
  public void canAlignGridToOrigin() {

    boxPolygon = box(13, 0, 25, 20);
    List<Geometry> list = op.apply(boxPolygon, 20, point(0, 0));
    assertThat(list, Matchers.containsInAnyOrder(
        geometryTopoMatch(box(13, 0, 20, 20)),
        geometryTopoMatch(box(20, 0, 25, 20))
    ));

    // moving the origin to the other side of the box shouldn't change the results
    list = op.apply(boxPolygon, 20, point(100, 100));
    assertThat(list, Matchers.containsInAnyOrder(
        geometryTopoMatch(box(13, 0, 20, 20)),
        geometryTopoMatch(box(20, 0, 25, 20))
    ));

    list = op.apply(boxPolygon, 20, point(100, 0));
    assertThat(list, Matchers.containsInAnyOrder(
        geometryTopoMatch(box(13, 0, 20, 20)),
        geometryTopoMatch(box(20, 0, 25, 20))
    ));

    list = op.apply(boxPolygon, 20, point(0, 100));
    assertThat(list, Matchers.containsInAnyOrder(
        geometryTopoMatch(box(13, 0, 20, 20)),
        geometryTopoMatch(box(20, 0, 25, 20))
    ));
  }

  @Test
  public void canCutLinesToGrid() {
    LineString line = line(0, 20, 200, 80);
    List<Geometry> list = op.apply(line, 100, point(0, 0));
    assertThat(list, Matchers.containsInAnyOrder(
        geometryTopoMatch(line(0, 20, 100, 50)),
        geometryTopoMatch(line(100, 50, 200, 80))
    ));
  }

  @Test
  public void buildsGridsOfExpectedSize() {
    assertGridSize(box(0, 0, 4, 6), 2D, 1, 2);
    assertGridSize(box(0, 0, 6, 4), 2D, 2, 1);
  }

  private Point point(double x, double y) {
    return gf.createPoint(new Coordinate(x, y));
  }

  private Polygon box(double x1, double y1, double x2, double y2) {
    return (Polygon) gf.createPolygon(new CoordinateSequence2D(
        x1, y1,
        x1, y2,
        x2, y2,
        x2, y1,
        x1, y1
    )).norm();
  }

  private LineString line(double x1, double y1, double x2, double y2) {
    return gf.createLineString(new CoordinateSequence2D(x1, y1, x2, y2));
  }

  private void assertGridSize(Polygon polygon, double distance, int maxGridOrdinateX, int maxGridOrdinateY) {
    Envelope geomEnv = polygon.getEnvelopeInternal();
    GridEnvelope2D gridEnvelope =
        CutGeometryByGridOp.constructGridGeometry(sridSet, polygon, distance,
          new Coordinate(geomEnv.getMinX(), geomEnv.getMinY())).getGridRange2D();

    // gridEnvelope.getHigh(ordinate) returns the maximal inclusive grid position which is what we are
    // looking for here, refer to caution in:
    // https://docs.geotools.org/latest/javadocs/org/geotools/coverage/grid/GridEnvelope2D.html
    assertEquals(maxGridOrdinateX, gridEnvelope.getHigh(0));
    assertEquals(maxGridOrdinateY, gridEnvelope.getHigh(1));
  }
}
