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

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

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

import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.coverage.TypedCoverage;
import nz.org.riskscape.engine.data.coverage.NearestNeighbourCoverage;
import nz.org.riskscape.engine.data.coverage.SpatialRelationTypedCoverage;
import nz.org.riskscape.engine.geo.NearestNeighbourIndex;
import nz.org.riskscape.engine.gt.GeometryHelper;
import nz.org.riskscape.engine.relation.ListRelation;
import nz.org.riskscape.engine.rl.BaseExpressionRealizerTest;
import nz.org.riskscape.engine.types.CoverageType;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.Referenced;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.ProblemSink;

public class CombineCoveragesIntegrationTest extends BaseExpressionRealizerTest {

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

  Struct sampleType = Struct.of("geometry", Referenced.of(Types.GEOMETRY, nzGeometryHelper.getCrs()),
      "name", Types.TEXT, "value", Types.INTEGER);
  Polygon g1 = nzGeometryHelper.box(0, 0, 100, 100);
  Polygon g2 = nzGeometryHelper.box(0, 100, 100, 200);
  Polygon g3 = nzGeometryHelper.box(100, 0, 200, 100);
  Polygon g4 = nzGeometryHelper.box(100, 100, 200, 200);

  Point pointG1 = nzGeometryHelper.point(50, 50);
  Point pointG2 = nzGeometryHelper.point(50, 150);
  Point pointG3 = nzGeometryHelper.point(150, 50);
  Point pointG4 = nzGeometryHelper.point(150, 150);

  ListRelation fooRelation = new ListRelation(sampleType,
      Tuple.ofValues(sampleType, g1, "foo-g1", 11L),
      Tuple.ofValues(sampleType, g2, "foo-g2", 12L));

  ListRelation barRelation = new ListRelation(sampleType,
      Tuple.ofValues(sampleType, g1, "bar-g1", 21L),
      Tuple.ofValues(sampleType, g3, "bar-g3", 23L));

  ListRelation pointRelation = new ListRelation(sampleType,
      Tuple.ofValues(sampleType, pointG1, "point-g1", 31L),
      Tuple.ofValues(sampleType, pointG3, "point-g3", 33L));

  TypedCoverage fooCoverage = SpatialRelationTypedCoverage.of(fooRelation, project.getSridSet(), ProblemSink.DEVNULL)
      .get();
  TypedCoverage barCoverage = SpatialRelationTypedCoverage.of(barRelation, project.getSridSet(), ProblemSink.DEVNULL)
      .get();
  TypedCoverage pointCoverage = new NearestNeighbourCoverage(
      () -> {
        NearestNeighbourIndex nni = NearestNeighbourIndex.metricMaxDistance(sampleType.getEntry("geometry"),
            project.getSridSet(), nzGeometryHelper.getCrs(), 65D);

        pointRelation.rawIterator().forEachRemaining(t -> nni.insert(t));
        return nni;
      }, sampleType, nzGeometryHelper.getCrs(), project.getSridSet());

  Tuple inputTuple = Tuple.EMPTY_TUPLE;
  MultiCoverage coverageResult;

  @Before
  public void setup() {
    project.getFunctionSet().add(new CombineCoverages().identified("combine_coverages"));
    inputStruct = Struct.EMPTY_STRUCT;
  }

  @Test
  public void canCombineASingleCoverages() {
    // kind of silly to combine a single coverage but it still should work
    addCoverage("foo", fooCoverage);

    evaluate("combine_coverages({foo}, 100)");
    assertSuccessful();

    Struct expectedReturnType = Struct.of("foo_name", Nullable.TEXT, "foo_value", Nullable.INTEGER);
    assertThat(coverageResult.getType(), is(expectedReturnType));

    // now lets do some sampling
    assertThat(coverageResult.evaluate(pointG1),
        is(Tuple.ofValues(expectedReturnType, "foo-g1", 11L)));

    assertThat(coverageResult.evaluate(pointG2),
        is(Tuple.ofValues(expectedReturnType, "foo-g2", 12L)));

    assertThat(coverageResult.evaluate(pointG3), nullValue());

    assertThat(coverageResult.evaluate(pointG4), nullValue());
  }

  @Test
  public void canCombineTwoComplexCoverages() {
    addCoverage("foo", fooCoverage);
    addCoverage("bar", barCoverage);

    evaluate("combine_coverages({foo, bar}, 100)");
    assertSuccessful();

    Struct expectedReturnType = Struct.of("foo_name", Nullable.TEXT, "foo_value", Nullable.INTEGER,
        "bar_name", Nullable.TEXT, "bar_value", Nullable.INTEGER);
    assertThat(coverageResult.getType(), is(expectedReturnType));

    // now lets do some sampling
    assertThat(coverageResult.evaluate(pointG1),
        is(Tuple.ofValues(expectedReturnType, "foo-g1", 11L, "bar-g1", 21L)));

    assertThat(coverageResult.evaluate(pointG2),
        is(Tuple.ofValues(expectedReturnType, "foo-g2", 12L, null)));

    assertThat(coverageResult.evaluate(pointG3),
        is(Tuple.ofValues(expectedReturnType, null, null, "bar-g3", 23L)));

    assertThat(coverageResult.evaluate(pointG4), nullValue());
  }

  @Test
  public void canCombineTwoCoveragesAndSampleAllIntersections() {
    addCoverage("foo", fooCoverage);
    addCoverage("bar", barCoverage);

    evaluate("combine_coverages({foo, bar}, 100)");
    assertSuccessful();

    Struct expectedReturnType = Struct.of("foo_name", Nullable.TEXT, "foo_value", Nullable.INTEGER,
        "bar_name", Nullable.TEXT, "bar_value", Nullable.INTEGER);
    assertThat(coverageResult.getType(), is(expectedReturnType));

    // now lets do some all intersection sampling
    assertThat(coverageResult.evaluateIntersection(nzGeometryHelper.line(20, 20, 20, 80)), contains(
        allOf(
            hasProperty("left", isGeometry(nzGeometryHelper.line(20, 20, 20, 80))),
            hasProperty("right", is(Tuple.ofValues(expectedReturnType, "foo-g1", 11L, "bar-g1", 21L)))
        )
    ));
    assertThat(coverageResult.evaluateIntersection(nzGeometryHelper.line(20, 20, 20, 180)), containsInAnyOrder(
        allOf(
            hasProperty("left", isGeometry(nzGeometryHelper.line(20, 20, 20, 100))),
            hasProperty("right", is(Tuple.ofValues(expectedReturnType, "foo-g1", 11L, "bar-g1", 21L)))
        ),
        allOf(
            hasProperty("left", isGeometry(nzGeometryHelper.line(20, 100, 20, 180))),
            hasProperty("right", is(Tuple.ofValues(expectedReturnType, "foo-g2", 12L)))
        )
    ));

    assertThat(coverageResult.evaluateIntersection(nzGeometryHelper.line(20, 120, 180, 120)), containsInAnyOrder(
        allOf(
            hasProperty("left", isGeometry(nzGeometryHelper.line(20, 120, 100, 120))),
            hasProperty("right", is(Tuple.ofValues(expectedReturnType, "foo-g2", 12L)))
        )
    ));

    assertThat(coverageResult.evaluateIntersection(nzGeometryHelper.line(120, 120, 120, 180)), hasSize(0));
  }

  @Test
  public void canSampleAllIntersectionsWithCoveragesThatDoNotSupportIt() {
    // at first look you wouldn't think that this should work. but the all intersections sample is not
    // actually done one the backing coverages. Instead multi-coverage grids up the sampled geom and does
    // a standard sample on each grid cell.
    addCoverage("foo", fooCoverage);
    addCoverage("point", pointCoverage);  // nearest neighbour coverage does not support all intersections

    evaluate("combine_coverages({foo, point}, 100)");
    assertSuccessful();

    Struct expectedReturnType = Struct.of("foo_name", Nullable.TEXT, "foo_value", Nullable.INTEGER,
        "point_name", Nullable.TEXT, "point_value", Nullable.INTEGER);
    assertThat(coverageResult.getType(), is(expectedReturnType));

    // now lets do some all intersection sampling
    assertThat(coverageResult.evaluateIntersection(nzGeometryHelper.line(20, 20, 20, 80)), contains(
        allOf(
            hasProperty("left", isGeometry(nzGeometryHelper.line(20, 20, 20, 80))),
            hasProperty("right", is(Tuple.ofValues(expectedReturnType, "foo-g1", 11L, "point-g1", 31L)))
        )
    ));
    assertThat(coverageResult.evaluateIntersection(nzGeometryHelper.line(50, 0, 50, 180)), containsInAnyOrder(
        allOf(
            hasProperty("left", isGeometry(nzGeometryHelper.line(50, 0, 50, 100))),
            hasProperty("right", is(Tuple.ofValues(expectedReturnType, "foo-g1", 11L, "point-g1", 31L)))
        ),
        allOf(
            hasProperty("left", isGeometry(nzGeometryHelper.line(50, 100, 50, 180))),
            hasProperty("right", is(Tuple.ofValues(expectedReturnType, "foo-g2", 12L)))
        )
    ));
  }

  private void assertSuccessful() {
    if (!realizationProblems.isEmpty()) {
      fail(Problem.debugString(realizationProblems));
    }
    assertThat(evaluated, instanceOf(MultiCoverage.class));
    coverageResult = (MultiCoverage) evaluated;
  }

  private void evaluate(String expr) {
    evaluate(expr, inputTuple);
  }

  private TypedCoverage addCoverage(String name, TypedCoverage coverage) {
    inputStruct = inputStruct.asStruct().add(name, new CoverageType(coverage.getType()));
    Tuple newTuple = new Tuple(inputStruct.asStruct());
    newTuple.setAll(inputTuple);
    newTuple.set(inputTuple.size(), coverage);
    inputTuple = newTuple;
    return coverage;
  }

}
