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

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

import org.geotools.geometry.jts.ReferencedEnvelope;
import org.junit.Test;
import org.locationtech.jts.geom.Point;

import nz.org.riskscape.engine.SRIDSet;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.geo.NearestNeighbourIndex;
import nz.org.riskscape.engine.gt.GeometryHelper;
import nz.org.riskscape.engine.query.TupleUtils;
import nz.org.riskscape.engine.query.TupleUtils.FindOption;
import nz.org.riskscape.engine.relation.ListRelation;
import nz.org.riskscape.engine.relation.Relation;
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.Struct.StructMember;
import nz.org.riskscape.engine.types.Types;

public class NearestNeighbourCoverageTest {

  private static final int ONE_THOUSAND_METERS = 1000;

  SRIDSet sridSet = new SRIDSet();
  GeometryHelper latLongHelper = GeometryHelper.latLong(sridSet);
  GeometryHelper nzHelper = GeometryHelper.nzMapGridWithOriginShift(sridSet);

  Point p1 = nzHelper.point(1000, 1000);
  Point p2 = nzHelper.point(200000, 200000);

  Struct nzSiteType = Struct.of(
      "point", Nullable.of(Referenced.of(Types.POINT, nzHelper.getCrs())),
      "sid", Types.INTEGER);

  Tuple nzP1 = Tuple.ofValues(nzSiteType, p1, 1L);
  Tuple nzP2 = Tuple.ofValues(nzSiteType, p2, 2L);

  NearestNeighbourCoverage nzCoverage = fromRelation(
      new ListRelation(
          nzP1,
          Tuple.ofValues(nzSiteType, null, -99L),
          nzP2
      ), ONE_THOUSAND_METERS);

  Struct latLongSiteType = Struct.of(
      "point", Referenced.of(Types.POINT, latLongHelper.getCrs()),
      "sid", Types.INTEGER);

  Tuple latLongP1 = Tuple.ofValues(latLongSiteType, latLongHelper.reproject(p1), 1L);
  Tuple latLongP2 = Tuple.ofValues(latLongSiteType, latLongHelper.reproject(p2), 2L);

  NearestNeighbourCoverage latLongCoverage = fromRelation(
      new ListRelation(latLongP1, latLongP2), ONE_THOUSAND_METERS);

  @Test
  public void findsExpectedData() {
    assertThat(nzCoverage.evaluate(p1), is(nzP1));
    assertThat(nzCoverage.evaluate(p2), is(nzP2));

    assertThat(nzCoverage.evaluate(nzHelper.point(0, 1000)), is(nzP1));
    assertThat(nzCoverage.evaluate(nzHelper.point(1000, 0)), is(nzP1));

    // too far from p1
    assertThat(nzCoverage.evaluate(nzHelper.point(0, 1001)), nullValue());
    assertThat(nzCoverage.evaluate(nzHelper.point(1001, 0)), nullValue());
    assertThat(nzCoverage.evaluate(nzHelper.point(10000, 10000)), nullValue());

    // outside envelope
    assertThat(nzCoverage.evaluate(nzHelper.point(300000, 300000)), nullValue());
  }

  @Test
  public void findsExpectedDataReprojecting() {
    assertThat(nzCoverage.evaluate((Point) latLongHelper.reproject(p1)), is(nzP1));
    assertThat(nzCoverage.evaluate((Point) latLongHelper.reproject(p2)), is(nzP2));

    assertThat(nzCoverage.evaluate((Point) latLongHelper.reproject(nzHelper.point(5, 1000))), is(nzP1));
    assertThat(nzCoverage.evaluate((Point) latLongHelper.reproject(nzHelper.point(1000, 5))), is(nzP1));

    // too far from p1
    assertThat(nzCoverage.evaluate((Point) latLongHelper.reproject(nzHelper.point(0, 1010))), nullValue());
    assertThat(nzCoverage.evaluate((Point) latLongHelper.reproject(nzHelper.point(1010, 0))), nullValue());
    assertThat(nzCoverage.evaluate((Point) latLongHelper.reproject(nzHelper.point(10000, 10000))), nullValue());

    // outside envelope
    assertThat(nzCoverage.evaluate((Point) latLongHelper.reproject(nzHelper.point(300000, 300000))), nullValue());
  }

  @Test
  public void findsExpectedDataFromLatLongCoverage() {
    assertThat(latLongCoverage.evaluate(p1), is(latLongP1));
    assertThat(latLongCoverage.evaluate(p2), is(latLongP2));

    assertThat(latLongCoverage.evaluate(nzHelper.point(310, 1000)), is(latLongP1));
    assertThat(latLongCoverage.evaluate(nzHelper.point(1000, 5)), is(latLongP1));

    // too far from p1
    // moving across from the point isn't as accurate, the cutoff is about 750m here in NZ, this is
    // because we are converting a metric distance to degrees (for the cutoff) which can't be accurate in
    // all directions at all locations.
    assertThat(latLongCoverage.evaluate(nzHelper.point(240, 1000)), nullValue());
    assertThat(latLongCoverage.evaluate(nzHelper.point(1000, -5)), nullValue());
    assertThat(latLongCoverage.evaluate(nzHelper.point(10000, 10000)), nullValue());

    // outside envelope
    assertThat(latLongCoverage.evaluate(nzHelper.point(300000, 300000)), nullValue());
  }

  @Test
  public void hasExpectedSpatialData() {
    assertThat(nzCoverage.getCoordinateReferenceSystem(), is(nzHelper.getCrs()));

    Point expectedLowerLeft = nzHelper.point(0, 0);
    Point expectedUpperRight = nzHelper.point(201000, 201000);
    ReferencedEnvelope expected = new ReferencedEnvelope(expectedLowerLeft.getX(), expectedUpperRight.getX(),
        expectedLowerLeft.getY(), expectedUpperRight.getY(), nzHelper.getCrs());
    assertThat(nzCoverage.getEnvelope().get(), is(expected));
  }

  private NearestNeighbourCoverage fromRelation(
      Relation relation,
      double maxDistanceMeters
  ) {
      StructMember geomMember = TupleUtils.findGeometryMember(relation.getType(), FindOption.OPTIONAL);
      Referenced referencedGeom = geomMember.getType().findAllowNull(Referenced.class).get();

      NearestNeighbourIndex nni =
          NearestNeighbourIndex.metricMaxDistance(geomMember, sridSet, referencedGeom.getCrs(), maxDistanceMeters);

      relation.iterator().forEachRemaining(t -> nni.insert(t));

      return new NearestNeighbourCoverage(() -> nni, relation.getType(),
          referencedGeom.getCrs(), sridSet);
  }
}
