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

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

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.junit.Test;

import nz.org.riskscape.engine.problem.GeneralProblems;
import nz.org.riskscape.engine.types.TypeProblems;
import nz.org.riskscape.hdf5.cursor.H5DatasetCursor;
import nz.org.riskscape.hdf5.cursor.H5FixedSizeCursor;
import nz.org.riskscape.hdf5.types.H5ArrayType;
import nz.org.riskscape.hdf5.types.H5CompoundType;
import nz.org.riskscape.hdf5.types.H5IntegerType;
import nz.org.riskscape.hdf5.types.H5StringType;
import nz.org.riskscape.hdf5.types.H5Type;
import nz.org.riskscape.hdf5.types.H5VlenType;

public class HDF5SmokeTests extends Hdf5BaseTest {

  @Test
  public void canListAFilesContents() throws Exception {
    example = openFile("h5ex_g_iterate.h5");
    List<String> collectedNames = new ArrayList<>();
    List<H5Object> collectedObjects = new ArrayList<>();

    example.acceptRootGroup(root -> root.visit((parent, childName, supplier) -> {
      collectedNames.add(childName);
      collectedObjects.add(supplier.get());
    }));

    assertThat(collectedNames, contains("DS1", "G1", "L1"));

    H5Dataset ds1 = (H5Dataset) collectedObjects.remove(0);
    H5Group g1 = (H5Group) collectedObjects.remove(0);
    H5Dataset l1 = (H5Dataset) collectedObjects.remove(0);

    assertThat(ds1.getDatasetName(), equalTo("DS1"));
    assertThat(g1.getGroupName(), equalTo("G1"));
    assertThat(l1.getDatasetName(), equalTo("L1"));

    ds1.close();
    l1.close();

    g1.visit((parent, childName, supplier) -> {
      assertThat(childName, equalTo("DS2"));
      H5Dataset ds2 = (H5Dataset) supplier.get();
      assertThat(ds2.getDatasetName(), equalTo("DS2"));
      ds2.close();
    });

    g1.close();
  }

  @Test
  public void canReadIntegers() {
    dataset = openDataset("h5ex_d_soint.h5", "DS1");
    assertEquals(2048, dataset.getDataSpace().numElements());
    H5DatasetCursor cursor = dataset.openCursor();

    assertTrue(dataset.getDataType() instanceof H5IntegerType);
    H5IntegerType type = (H5IntegerType) dataset.getDataType();
    assertEquals(4, type.getDataSize());
    assertFalse(type.isUnsigned());

    assertReadExampleDataSet(cursor, dataset.getDataSpace().getExtent());
  }

  private void assertReadExampleDataSet(H5DatasetCursor cursor, long[] dimensions) {
    assertEquals("this test only supports 2D dataset", 2, dimensions.length);
    int expectedCount = (int) (dimensions[0] * dimensions[1]);
    // example dataset just has multiples of index position within the 2d array
    int multiplier = -1;
    int rowCounter = 0;
    int totalCount = 0;
    while (cursor.hasNext()) {
      Long integer = (Long) cursor.next();
      assertEquals(multiplier * rowCounter, integer.intValue());

      rowCounter++;
      if (rowCounter >= dimensions[1]) {
        multiplier++;
        rowCounter = 0;
      }
      totalCount++;
    }
    assertEquals(totalCount, expectedCount);
  }

  @Test
  public void canReadStrings() {
    dataset = openDataset("h5ex_t_string.h5", "DS1");
    assertEquals(4, dataset.getDataSpace().numElements());
    H5DatasetCursor cursor = dataset.openCursor();

    assertTrue(dataset.getDataType() instanceof H5StringType);
    H5StringType type = (H5StringType) dataset.getDataType();
    assertEquals(7, type.getDataSize());
    assertFalse(type.isVariableLength());

    List<String> contents = new ArrayList<>();
    while (cursor.hasNext()) {
      contents.add((String) cursor.next());
    }

    // note that reading trims any trailing whitespace from the string
    // (the HDF5 API example code used trim(), but maybe that's unnecessary?)
    List<String> expected = Arrays.asList("Parting", "is such", "sweet", "sorrow.");
    assertEquals(expected, contents);
  }

  @Test
  public void canReadFloatsFromArrayType() throws Exception {
    dataset = openDataset("h5ex_t_array.h5", "DS1");
    H5FixedSizeCursor cursor = (H5FixedSizeCursor) dataset.openCursor();
    assertTrue(dataset.getDataType() instanceof H5ArrayType);
    H5ArrayType arrayType = (H5ArrayType) dataset.getDataType();
    // yes, in know these are integers, but there's no example dataset of floats in an array - it shouldn't matter for
    // this test
    H5IntegerType superType = (H5IntegerType) arrayType.getSuperType();
    assertFalse(superType.isUnsigned());
    int[] expectedInts = new int[] {
        0, 0, 0, 0, 0,
        0, -1, -2, -3, -4,
        0, -2, -4, -6, -8
    };

    float[] floats = arrayType.readFloats(cursor.getByteBuffer(), 0);

    // make sure we receive all the items we expecting
    assertEquals(expectedInts.length, floats.length);
    for (int i = 0; i < floats.length; i++) {
      // and they have the expected value
      assertEquals(expectedInts[i], floats[i], 0.00001);
    }
  }

  @Test
  public void canReadVLenData() throws Exception {
    dataset = openDataset("h5ex_t_vlenfloat.h5", "DS1");
    H5Type h5Type = dataset.getDataType();
    assertThat(h5Type, isA(H5VlenType.class));

    H5VlenType vlenType = (H5VlenType) h5Type;
    H5Type memberType = vlenType.getMemberType();
    assertThat(memberType, isA(H5IntegerType.class));

    int datasetSize = dataset.getDataSpace().numElementsAsInt();
    assertThat(datasetSize, equalTo(18133 * 2));

    // we will copy two elements at a time
    H5DataSpace memspace = H5DataSpace.createMemSpace(2);

    // use a separate file space to make it clear we can do this and that it provides a selection separate from the
    // dataset's 'main' one (which is probably a design flaw and reflects an early lack of understanding about what a
    // dataspace is)
    H5DataSpace filespace = dataset.newDataSpace();
    // read from two different elements. Note they have different number of entries (because they're vlen)
    filespace.selectElementsAt(new long[][] {
      {0, 0},
      {20, 1}
    });

    byte[][] buffer = dataset.readVlenElements(memspace, filespace, 2);
    Object[] asRead = memberType.readVlen(buffer[0]);

    assertThat(
      Arrays.asList(asRead),
      contains(
        18093L, 227997L, 349536L, 1332240L, 1825237L, 2715939L, 3031646L, 3194142L, 3473201L, 3772510L, 4107468L,
        4373319L, 4661695L, 4935686L, 6350952L, 6826816L, 6984224L, 7113500L, 7262729L, 7375475L, 7476167L, 7606002L,
        7729724L, 7855714L
      )
    );

    assertThat(
      Arrays.asList(memberType.readVlen(buffer[1])),
      contains(
          3L, 45825L, 140948L, 228072L, 349668L, 462201L, 565391L, 684641L, 827551L, 948275L, 1053407L, 1180594L,
          1452049L, 1574764L, 1680523L, 2127027L, 2435261L, 2715983L, 3359669L, 3652067L, 3942574L, 4267869L, 4512953L,
          4774754L, 4935818L, 5200619L, 5315745L, 5592060L, 5721834L, 6028619L, 6181551L, 7855778L
      )
    );
  }

  @Test
  public void canReadDatasetElements() throws Exception {
    dataset = openDataset("h5ex_t_1d_dataset.h5", "DS1");
    assertThat(dataset.getDataType(), isA(H5CompoundType.class));
    H5CompoundType h5Type = (H5CompoundType) dataset.getDataType();

    assertThat(dataset.getDataSpace().numElementsAsInt(), equalTo(34));

    // read the first 5 and last 5 elements from the dataset, via 2 different dataspaces
    H5DataSpace dspace1 = dataset.newDataSpace();
    H5DataSpace dspace2 = dataset.newDataSpace();
    ByteBuffer bb1 = dspace1.readElements(0, 5);
    ByteBuffer bb2 = dspace2.readElements(29, 5);

    H5CompoundMember idMember = h5Type.findMember("id");
    H5CompoundMember rupIdMember = h5Type.findMember("rup_id");
    long[] expectedIds1 = new long[] {0L, 1L, 2L, 3L, 4L};
    long[] expectedRupIds1 = new long[] {0L, 0L, 0L, 1L, 2L};
    long[] expectedIds2 = new long[] {29L, 30L, 31L, 32L, 33L};
    long[] expectedRupIds2 = new long[] {26L, 27L, 28L, 29L, 30L};

    // check the data read matches up with what we expect
    for (int i = 0; i < expectedIds1.length; i++) {
      int byteOffset = (int) (h5Type.getDataSize() * i);

      assertThat(((Number) idMember.read(bb1, byteOffset)).longValue(), is(expectedIds1[i]));
      assertThat(((Number) rupIdMember.read(bb1, byteOffset)).longValue(), is(expectedRupIds1[i]));

      assertThat(((Number) idMember.read(bb2, byteOffset)).longValue(), is(expectedIds2[i]));
      assertThat(((Number) rupIdMember.read(bb2, byteOffset)).longValue(), is(expectedRupIds2[i]));
    }
  }

  @Test
  public void canReadCompressedDataset() {
    dataset = openDataset("h5ex_d_gzip.h5", "DS1");
    assertEquals(32 * 64, dataset.getDataSpace().numElements());
    H5DatasetCursor cursor = dataset.openCursor();

    // this is the same as the integer example dataset, but compressed
    assertTrue(dataset.getDataType() instanceof H5IntegerType);
    assertReadExampleDataSet(cursor, dataset.getDataSpace().getExtent());
  }

  @Test
  public void canReadAttributes() {
    dataset = openDataset("h5ex_t_att.h5", "DS1");
    H5Group group = (H5Group) example.getRootGroup().openChild("G1");

    // check we can read different attribute types from a dataset
    assertThat(H5Attribute.readValue(dataset, "INT", Long.class).get(), is(42L));
    assertThat(H5Attribute.readValue(dataset, "FLOAT", Double.class).get(), is(3.5D));

    // and from other H5Objects like groups
    assertThat(H5Attribute.readValue(group, "FLOAT", Double.class).get(), is(3.14D));
    assertThat(H5Attribute.readValue(group, "TEXT", String.class).get(), is("foo"));
  }

  @Test
  public void canGetUsefulErrorsWhenReadingAttributes() {
    dataset = openDataset("h5ex_t_att.h5", "DS1");

    // try to read an attribute that exists, but just not on this dataset
    assertThat(H5Attribute.readValue(dataset, "TEXT", String.class), failedResult(equalTo(
            GeneralProblems.get().noSuchObjectExists("TEXT", H5Attribute.class))
    ));

    // try to read an attribute as the wrong type
    assertThat(H5Attribute.readValue(dataset, "INT", String.class), failedResult(equalTo(
        TypeProblems.get().mismatch(new H5Attribute(dataset, "INT"), String.class, Long.class))
    ));
  }

  @Test
  public void canMatchAttributesAndTypesByEquality() {
    // this is basic stuff but at one point it didn't work :)
    dataset = openDataset("h5ex_t_att.h5", "DS1");
    H5Group group = (H5Group) example.getRootGroup().openChild("G1");
    H5Attribute datasetAttr1 = new H5Attribute(dataset, "FLOAT");
    H5Attribute groupAttr = new H5Attribute(group, "FLOAT");
    H5Attribute datasetAttr2 = new H5Attribute(dataset, "FLOAT");

    // two copies of the same attribute should match
    assertEquals(datasetAttr1, datasetAttr2);
    // attributes with the same name but different parents should not match
    assertNotEquals(datasetAttr1, groupAttr);

    // check the types of the attributes that differ - they're both floats, so are equal
    assertEquals(datasetAttr1.getDataType(), groupAttr.getDataType());
    // but attributes with different types should obviously not be equal
    H5Attribute textAttr = new H5Attribute(group, "TEXT");
    assertNotEquals(textAttr.getDataType(), groupAttr.getDataType());
  }
}
