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

import java.nio.ByteBuffer;
import java.util.function.Function;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.lookup.LookupTable;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.hdf5.H5CompoundMember;
import nz.org.riskscape.hdf5.types.H5CompoundType;
import nz.org.riskscape.hdf5.H5Dataset;
import nz.org.riskscape.hdf5.cursor.H5FixedSizeCursor;

/**
 * Lookup the event metadata associated with an OpenQuake quake (primarily for probabilistic modelling).
 *
 * There are two datasets in the HDF5 file of interest here:
 * - /events : we first need to lookup the event metadata, specifically the ID of the rupture associated with it.
 * - /ruptures : this holds the details of the quake rupture that corresponds to the event. Specifically, we're
 *  interested in the number of occurrences ('n_occ') and the rate of occurrence ('occurrence_rate'). We can
 * then use these details to eventually calculate the probability of the quake     .
 *
 * Each quake event has both a seismic source and a rupture. The source is the geographic location of the
 * quake, whereas the rupture represents a quake of a given magnitude for a seismic source. The same source
 * can correspond to many different ruptures (i.e. different magnitudes), and the same rupture can correspond
 * to many different events (as we're modelling the uncertainty of a quake of any given magnitude).
 * The number of occurrences of a given rupture is stored as 'n_occ'. E.g. a rupture with n_occ=5 will have
 * 5 different events associated with it.
 *
 * There are two different 'flavours' of sources. These are defined by the 'uncertaintyModel' in the
 * OpenQuake job's input files (the 'uncertaintyModel' is defined in the 'source_model_logic_tree_file'
 * XML file).
 * - characteristicFaultSource: these have a fixed magnitude, and a fixed 'occurRates' (which maps directly
 * to the 'occurrence_rate' in the resulting HDF5 file).
 * - pointSource: these have a range of magnitudes. With a large number of SES, each magnitude in the range
 * ends up getting sampled by an event. In these cases, the 'occurrence_rate' gets calculated by OpenQuake.
 *
 * Note that these probability calculations are designed for a 'patched' version of OpenQuake where the
 * 'n_occ' values can be overridden manually when generating the HDF5 file. This changes the model to be
 * based on weighted events, rather than a straight SES event-based model.
 */
@RequiredArgsConstructor
public class EventMetadataLookup implements LookupTable {

  // currently we return all the probability details so we 'show our workings'
  private static final Struct VALUE_STRUCT = Struct.of("source_id", Types.TEXT,
      "rup_id", Types.INTEGER, "n_occ", Types.INTEGER, "occurrence_rate",
      Types.FLOATING, "event_rate", Types.FLOATING);

  @Getter
  private final Type keyType = Types.INTEGER;

  @Getter
  private final Type valueType = VALUE_STRUCT;

  private final H5FixedSizeCursor eventCursor;
  private final H5CompoundMember ruptureMember;

  private final H5FixedSizeCursor ruptureCursor;
  private final H5CompoundMember sourceIdMember;
  private final H5CompoundMember numberOccurencesMember;
  private final H5CompoundMember occurenceRateMember;
  private final Function<ByteBuffer, String> readSourceId;

  public EventMetadataLookup(H5Dataset eventDataset, H5Dataset ruptureDataset) {
    this.eventCursor = (H5FixedSizeCursor) eventDataset.openCursor();
    H5CompoundType eventType = (H5CompoundType) eventDataset.getDataType();
    this.ruptureMember = eventType.findMember("rup_id");

    this.ruptureCursor = (H5FixedSizeCursor) ruptureDataset.openCursor();
    H5CompoundType ruptureType = (H5CompoundType) ruptureDataset.getDataType();

    // the rupture table stores the source_id as a string, which is helpful for mapping back
    // the fault that caused the quake. However, this is only available on more recent OQ
    // releases, like 3.10 and later
    if (ruptureType.findIndex("source_id") > 0) {
      this.sourceIdMember = ruptureType.findMember("source_id");
      this.readSourceId = bb -> (String) ruptureCursor.peek(sourceIdMember);
    } else {
      // older versions like OQ 3.7 store an integer index into the /source_info dataset,
      // which you can use to find the source_id (in string form)
      this.sourceIdMember = ruptureType.findMember("srcidx");
      this.readSourceId = bb -> "srcidx=" + ruptureCursor.peek(sourceIdMember).toString();
    }
    this.numberOccurencesMember = ruptureType.findMember("n_occ");
    this.occurenceRateMember = ruptureType.findMember("occurrence_rate");
  }

  /**
   * @return the rupture ID associated with a given event
   */
  private long getRuptureId(long eventId) {
    // lookup the events data and return the corresponding 'rup_id'
    eventCursor.setCurrentIndex(eventId);
    ByteBuffer bb = eventCursor.getByteBuffer();
    return ((Number) eventCursor.peek(ruptureMember)).longValue();
  }

  @Override
  public Object lookup(Object key) {
    long eventId = (Long) key;

    // we need to lock the cursor while we position and read from it, as the cursor is shared
    // multiple threads (this isn't great for performance, but the HDF5 code itself is
    // surrounded by one giant lock, and in practice we're not reading tooo many events here)
    synchronized (this) {
      long ruptureId = getRuptureId(eventId);

      // lookup the associated rupture info to get the 'n_occ' and 'occurrence_rate' for the quake
      ruptureCursor.setCurrentIndex(ruptureId);
      ByteBuffer bb = ruptureCursor.getByteBuffer();
      String sourceId = readSourceId.apply(bb);
      long numberOccurrences = ((Number) ruptureCursor.peek(numberOccurencesMember)).longValue();
      double occurrenceRate = ((Number) ruptureCursor.peek(occurenceRateMember)).doubleValue();

      // The rate for each event will be: rupture.occurrence_rate / rupture.n_occ
      // E.g. if the occurrence_rate = 0.1 (per year) for a rupture (e.g. Wellington fault) and we have
      // 10 n_occ, then each of our events has (0.1 / 10) individual probability = 0.01.
      double eventRate = occurrenceRate / numberOccurrences;

      return Tuple.ofValues(VALUE_STRUCT, sourceId, ruptureId, numberOccurrences, occurrenceRate, eventRate);
    }
  }

}
