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

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

import java.io.File;
import java.io.IOException;
import java.util.Map;

import org.geotools.api.geometry.Position;
import org.geotools.api.referencing.FactoryException;
import org.geotools.api.referencing.NoSuchAuthorityCodeException;
import org.geotools.api.referencing.operation.MathTransform;
import org.geotools.api.referencing.operation.TransformException;
import org.geotools.geometry.Position2D;
import org.geotools.referencing.CRS;
import org.hamcrest.Matcher;
import org.hamcrest.core.IsNull;
import org.junit.Before;
import org.junit.Test;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;

import com.codahale.metrics.Meter;

import nz.org.riskscape.engine.CrsHelper;
import nz.org.riskscape.engine.ProjectTest;
import nz.org.riskscape.engine.bind.ParameterSet;
import nz.org.riskscape.engine.data.Bookmark;
import nz.org.riskscape.engine.data.coverage.CoverageFileBookmarkResolver;
import nz.org.riskscape.engine.data.coverage.GridTypedCoverage;

public class SparseTiffCoverageTest extends ProjectTest {

  // for paths
  SparseTIFFImageReaderTest readerTest;

  CrsHelper crsHelper = new CrsHelper() {
  };

  GeometryFactory gf;


  public SparseTiffCoverageTest() throws IOException {
    readerTest = new SparseTIFFImageReaderTest();
  }

  @Before
  public void setup() throws NoSuchAuthorityCodeException, FactoryException {
    engine.getBookmarkResolvers().add(new CoverageFileBookmarkResolver(engine));
    gf = project.getSridSet().getGeometryFactory(CRS.decode("EPSG:32702"));
  }

  @Test
  public void sparseTiffCoverageCanBeQueried() throws Exception {

    /*
     * Bit of a fruit salad test where we compare sampling operations on the same points against the dataset but
     * in different configurations
     */

    // this is the tiff as is included in the getting started guide - we sanity check these values first
    GridTypedCoverage original = loadTiff(readerTest.originalTiff, Map.of("sparse-tiff", "false"));

    // then we load our sparse tiff but with the optimization turned off
    GridTypedCoverage unoptimized = loadTiff(readerTest.sparseTiff, Map.of("sparse-tiff", "false"));

    // our sparse tiff - it has the empty tiles
    GridTypedCoverage optimized  = loadTiff(readerTest.sparseTiff);

    // this one is tiled (not striped) but has compressed runs of no data instead - it's still optimized
    GridTypedCoverage tiled = loadTiff(readerTest.tiledTiff);

    // check one is optimized, but not the other
    assertThat(original.getCoverage(), not(instanceOf(SparseTiffCoverage.class)));
    assertThat(unoptimized.getCoverage(), not(instanceOf(SparseTiffCoverage.class)));
    assertThat(optimized.getCoverage(), instanceOf(SparseTiffCoverage.class));
    assertThat(tiled.getCoverage(), instanceOf(SparseTiffCoverage.class));

    SparseTiffCoverage tiffCoverage = (SparseTiffCoverage) optimized.getCoverage();
    MathTransform transform = tiffCoverage.getGridGeometry().getGridToCRS();

    // this should be an empty tile - top left
    Position2D position = (Position2D) transform.transform(new Position2D(1, 1), new Position2D());

    double[] value = tiffCoverage.evaluate((Position) position, (double[]) null);
    assertThat(value, is(tiffCoverage.getNoData()));

    // check a few different spots
    assertSample(new Coordinate(453895, 8449944), closeTo(1.174228, 0.00001), original, unoptimized, optimized, tiled);
    assertSample(new Coordinate(425557, 8448943), closeTo(5.783038, 0.00001), original, unoptimized, optimized, tiled);
    assertSample(new Coordinate(425501, 8446974), closeTo(2.643755, 0.00001), original, unoptimized, optimized, tiled);

    // a no data in an empty tile
    assertSample(new Coordinate(444995, 8453856), nullValue(), original, unoptimized, optimized, tiled);
    // no data right next to data
    assertSample(new Coordinate(452966, 8448256), nullValue(), original, unoptimized, optimized, tiled);
  }

  @Test
  public void confirmSparseTilesAreNotLoaded() throws Exception {

    // our sparse tiff - it has the empty tiles
    GridTypedCoverage optimized  = loadTiff(readerTest.sparseTiff);

    // this one is tiled (not striped) but has compressed runs of no data instead - it's still optimized
    GridTypedCoverage tiled = loadTiff(readerTest.tiledTiff);

    // load the sparse one again, but this time with the sparse tiff parameter set to false so we can confirm it does
    // change when our optimization isn't applying
    GridTypedCoverage unoptimized = loadTiff(readerTest.sparseTiff, Map.of("sparse-tiff", "false"));

    Meter misses = engine.getDiagnostics().getMetricRegistry().getMeters().get("tile-cache.misses");
    Meter hits = engine.getDiagnostics().getMetricRegistry().getMeters().get("tile-cache.hits");
    long missCount = misses.getCount();
    long hitCount = hits.getCount();

    // check our no-data tile
    assertSample(new Coordinate(444995, 8453856), nullValue(), optimized);

    // no change to tile cache metrics
    assertThat(misses.getCount(), equalTo(missCount));
    assertThat(hits.getCount(), equalTo(hitCount));

    assertSample(new Coordinate(444995, 8453856), nullValue(), tiled);

    // same again - even though it is not sparse, we can still scan the empty tiles to avoid empty tile lookups
    assertThat(misses.getCount(), equalTo(missCount));
    assertThat(hits.getCount(), equalTo(hitCount));

    // now the base case
    assertSample(new Coordinate(444995, 8453856), nullValue(), unoptimized);
    assertThat(misses.getCount(), equalTo(missCount + 1)); // misses has increased by one - we know it did a tile lookup
    // NB the hit count will have increased because assertSample will be doing an evaluate all
  }

  @Test
  public void coveragesWithoutNoDataRevertToNormalCoverage() throws Exception {
    GridTypedCoverage coverage = loadTiff(readerTest.rmNoDataTiff);

    assertThat(coverage.getCoverage(), not(instanceOf(SparseTiffCoverage.class)));

    // sanity check that our two no data points from before are not null
    assertSample(new Coordinate(444995, 8453856), equalTo(-99999D), coverage);
    assertSample(new Coordinate(452966, 8448256), equalTo(-99999D), coverage);
  }

  // check that the given coordinate yields a value matching sampleMatcher across all the given coverages
  private void assertSample(
      Coordinate coordinate,
      Matcher<? super Double> sampleMatcher,
      GridTypedCoverage... coverages
  ) throws TransformException {
    for (GridTypedCoverage coverage : coverages) {
      try {
        assertThat((Double) coverage.evaluate(gf.createPoint(coordinate)), sampleMatcher);
      } catch (AssertionError e) {
        throw new AssertionError("Assertion failed for coverage " + coverage + ":" + e.getMessage(), e);
      }
    }

    for (GridTypedCoverage coverage : coverages) {
      try {
        Matcher listMatcher;

        if (sampleMatcher instanceof IsNull) {
          listMatcher = empty();
        } else {
          listMatcher = contains(hasProperty("right", sampleMatcher));
        }
        assertThat(
            coverage.getEvaluateIntersectionOp().get().apply(gf.createPoint(coordinate)),
            listMatcher
        );
      } catch (AssertionError e) {
        throw new AssertionError("Assertion failed for coverage " + coverage + ":" + e.getMessage(), e);
      }
    }
  }

  GridTypedCoverage loadTiff(File file) {
    return loadTiff(file, Map.of("sparse-tiff", "true"));
  }

  GridTypedCoverage loadTiff(File file, Map<String, Object> params) {

    Bookmark bookmark = Bookmark.fromURI(file.toPath().toUri());

    bookmark = bookmark.addUnparsed(ParameterSet.normaliseParameterMap(params));

    return project.getEngine().getBookmarkResolvers()
        .resolveAndValidate(bookmark, executionContext, GridTypedCoverage.class).get().getData(GridTypedCoverage.class)
        .get();
  }
}
