package org.opensha2.data;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import static org.opensha2.data.IntervalData.checkDataState;
import static org.opensha2.data.IntervalData.indexOf;
import static org.opensha2.data.IntervalData.keys;

import org.opensha2.data.IntervalData.AbstractVolume;
import org.opensha2.data.IntervalData.DefaultVolume;

import java.util.Arrays;
import java.util.List;

/**
 * A 3-dimensional volume of immutable, double-valued data that is arranged
 * according to increasing and uniformly spaced double-valued keys. Data volumes
 * are used to represent binned data, and so while row, column, and level keys
 * are bin centers, indexing is managed internally using bin edges. This
 * simplifies issues related to rounding/precision errors that occur when
 * indexing according to explicit double values.
 *
 * <p>To create an instance of an {@code IntervalVolume}, use a {@link Builder}.
 *
 * <p>Internally, an {@code IntervalVolume} is backed by a {@code double[][][]}
 * where 'row' maps to the 1st dimension, 'column' the 2nd dimension, and
 * 'level' the 3rd.
 *
 * <p>Note that interval volumes are not intended for use with very high
 * precision data and keys are currently limited to a precision of 4 decimal
 * places. This may change in the future.
 *
 * @author Peter Powers
 * @see IntervalData
 * @see IntervalArray
 * @see IntervalTable
 */
public interface IntervalVolume {

  /**
   * Return the value of the bin that maps to the supplied row, column, and
   * level values. Do not confuse this method with {@link #get(int, int, int)}
   * by row index.
   *
   * @param rowValue of bin to retrieve
   * @param columnValue of bin to retrieve
   * @param levelValue of bin to retrieve
   * @throws IndexOutOfBoundsException if any value is out of range
   */
  double get(double rowValue, double columnValue, double levelValue);

  /**
   * Return the value of the bin that maps to the supplied row, column, and
   * level indices. Do not confuse this method with
   * {@link #get(double, double, double)} by row value.
   *
   * @param rowIndex of bin to retrieve
   * @param columnIndex of bin to retrieve
   * @param levelIndex of bin to retrieve
   * @throws IndexOutOfBoundsException if any index is out of range
   */
  double get(int rowIndex, int columnIndex, int levelIndex);

  /**
   * Return an immutable view of the values that map to the supplied row and
   * column values. Do not confuse with {@link #column(int, int)} retrieval by
   * index.
   *
   * @param rowValue of bin to retrieve
   * @param columnValue of bin to retrieve
   */
  XySequence column(double rowValue, double columnValue);

  /**
   * Return an immutable view of the values that map to the supplied row and
   * column values. Do not confuse with {@link #column(double, double)}
   * retrieval by index.
   *
   * @param rowIndex of bin to retrieve
   * @param columnIndex of bin to retrieve
   */
  XySequence column(int rowIndex, int columnIndex);

  /**
   * Return the lower edge of the lowermost row bin.
   */
  double rowMin();

  /**
   * Return the upper edge of the uppermost row bin.
   */
  double rowMax();

  /**
   * Return the row bin discretization.
   */
  double rowΔ();

  /**
   * Return an immutable list <i>view</i> of the row keys (bin centers).
   */
  List<Double> rows();

  /**
   * Return the lower edge of the lowermost column bin.
   */
  double columnMin();

  /**
   * Return the upper edge of the uppermost column bin.
   */
  double columnMax();

  /**
   * Return the column bin discretization.
   */
  double columnΔ();

  /**
   * Return an immutable list <i>view</i> of the column keys (bin centers).
   */
  List<Double> columns();

  /**
   * Return the lower edge of the lowermost level bin.
   */
  double levelMin();

  /**
   * Return the upper edge of the uppermost level bin.
   */
  double levelMax();

  /**
   * Return the level bin discretization.
   */
  double levelΔ();

  /**
   * Return an immutable list <i>view</i> of the level keys (bin centers).
   */
  List<Double> levels();

  /**
   * Return a new {@code IntervalTable} created by summing the levels of this
   * volume.
   */
  IntervalTable collapse();

  /**
   * Return the indices of the bin with smallest value in the form
   * {@code [rowIndex, columnIndex, levelIndex]}.
   */
  int[] minIndex();

  /**
   * Return the indices of the bin with largest value in the form
   * {@code [rowIndex, columnIndex, levelIndex]}.
   */
  int[] maxIndex();

  /**
   * A supplier of values with which to fill a {@code IntervalVolume}.
   */
  interface Loader {

    /**
     * Compute the value corresponding to the supplied row, column, and level
     * keys.
     *
     * @param row value
     * @param column value
     * @param level value
     */
    public double compute(double row, double column, double level);
  }

  /**
   * A builder of immutable {@code IntervalVolume}s.
   *
   * <p>Rows, columns, and levels must be specified before any data can be
   * added. Note that any supplied {@code max} values may not correspond to the
   * final upper edge of the uppermost bins if {@code max - min} is not evenly
   * divisible by {@code Δ}.
   */
  public static final class Builder {

    private double[][][] data;

    private double rowMin;
    private double rowMax;
    private double rowΔ;
    private double[] rows;

    private double columnMin;
    private double columnMax;
    private double columnΔ;
    private double[] columns;

    private double levelMin;
    private double levelMax;
    private double levelΔ;
    private double[] levels;

    private boolean built = false;
    private boolean initialized = false;

    /**
     * Create a new builder.
     */
    public Builder() {}

    /**
     * Create a new builder with the structure and content identical to that of
     * the supplied volume.
     *
     * @param volume to copy
     */
    public static Builder copyOf(IntervalVolume volume) {
      /* Safe covariant cast. */
      DefaultVolume defaultVolume = (DefaultVolume) volume;
      Builder builder = copyStructure(defaultVolume);
      builder.data = Data.copyOf(defaultVolume.data);
      builder.init();
      return builder;
    }

    /**
     * Create a new builder with a structure identical to that of the supplied
     * model.
     *
     * @param model data volume
     */
    public static Builder fromModel(IntervalVolume model) {
      /* Safe covariant cast. */
      Builder builder = copyStructure((AbstractVolume) model);
      builder.init();
      return builder;
    }

    private static Builder copyStructure(AbstractVolume from) {
      Builder to = new Builder();
      to.rowMin = from.rowMin;
      to.rowMax = from.rowMax;
      to.rowΔ = from.rowΔ;
      to.rows = from.rows;
      to.columnMin = from.columnMin;
      to.columnMax = from.columnMax;
      to.columnΔ = from.columnΔ;
      to.columns = from.columns;
      to.levelMin = from.levelMin;
      to.levelMax = from.levelMax;
      to.levelΔ = from.levelΔ;
      to.levels = from.levels;
      return to;
    }

    /**
     * Define the data volume rows.
     *
     * @param min lower edge of lowermost row bin
     * @param max upper edge of uppermost row bin
     * @param Δ bin discretization
     */
    public Builder rows(double min, double max, double Δ) {
      rowMin = min;
      rowMax = max;
      rowΔ = Δ;
      rows = keys(min, max, Δ);
      init();
      return this;
    }

    /**
     * Define the data volume columns.
     *
     * @param min lower edge of lowermost column bin
     * @param max upper edge of uppermost column bin
     * @param Δ bin discretization
     */
    public Builder columns(double min, double max, double Δ) {
      columnMin = min;
      columnMax = max;
      columnΔ = Δ;
      columns = keys(min, max, Δ);
      init();
      return this;
    }

    /**
     * Define the data volume levels.
     *
     * @param min lower edge of lowermost column bin
     * @param max upper edge of uppermost column bin
     * @param Δ bin discretization
     */
    public Builder levels(double min, double max, double Δ) {
      levelMin = min;
      levelMax = max;
      levelΔ = Δ;
      levels = keys(min, max, Δ);
      init();
      return this;
    }

    private void init() {
      checkState(!initialized, "Builder has already been initialized");
      if (rows != null && columns != null && levels != null) {
        if (data == null) {
          data = new double[rows.length][columns.length][levels.length];
        }
        initialized = true;
      }
    }

    /**
     * Return the index of the row that would contain the supplied value.
     * @param row value
     */
    public int rowIndex(double row) {
      return indexOf(rowMin, rowΔ, row, rows.length);
    }

    /**
     * Return the index of the column that would contain the supplied value.
     * @param column value
     */
    public int columnIndex(double column) {
      return indexOf(columnMin, columnΔ, column, columns.length);
    }

    /**
     * Return the index of the level that would contain the supplied value.
     * @param level value
     */
    public int levelIndex(double level) {
      return indexOf(levelMin, levelΔ, level, levels.length);
    }

    /**
     * Set the value at the specified row, column, and level. Be careful not to
     * confuse this with {@link #set(int, int, int, double)}.
     *
     * @param row key
     * @param column key
     * @param level key
     * @param value to set
     */
    public Builder set(double row, double column, double level, double value) {
      return set(rowIndex(row), columnIndex(column), levelIndex(level), value);
    }

    /**
     * Set the value at the specified row, column, and level indices. Be careful
     * not to confuse this with {@link #set(double, double, double, double)}.
     *
     * @param row index
     * @param column index
     * @param level index
     * @param value to set
     */
    public Builder set(int row, int column, int level, double value) {
      data[row][column][level] = value;
      return this;
    }

    /**
     * Add to the existing value at the specified row, column, and level. Be
     * careful not to confuse this with {@link #add(int, int, int, double)}.
     *
     * @param row key
     * @param column key
     * @param level key
     * @param value to add
     */
    public Builder add(double row, double column, double level, double value) {
      return add(rowIndex(row), columnIndex(column), levelIndex(level), value);
    }

    /**
     * Add to the existing value at the specified row, column, and level
     * indices. Be careful not to confuse this with
     * {@link #add(double, double, double, double)}.
     *
     * @param row index
     * @param column index
     * @param level index
     * @param value
     */
    public Builder add(int row, int column, int level, double value) {
      data[row][column][level] += value;
      return this;
    }

    /**
     * Add the values in the supplied volume to this builder. This operation is
     * very efficient if this builder and the supplied volume are sourced from
     * the same model.
     *
     * @param volume to add
     * @throws IllegalArgumentException if the rows, columns, and levels of the
     *         supplied volume do not match those of this volume
     * @see #fromModel(IntervalVolume)
     */
    public Builder add(IntervalVolume volume) {
      // safe covariant cast
      validateVolume((AbstractVolume) volume);
      // safe covariant cast until other concrete implementations exist
      Data.uncheckedAdd(data, ((DefaultVolume) volume).data);
      return this;
    }

    /**
     * Multiply ({@code scale}) all values in this builder.
     * @param scale factor
     */
    public Builder multiply(double scale) {
      Data.multiply(scale, data);
      return this;
    }

    /*
     * Check hash codes of row, column, and level arrays in case fromModel or
     * copyOf has been used, otherwise check array equality.
     */
    AbstractVolume validateVolume(AbstractVolume that) {
      checkArgument((this.rows.hashCode() == that.rows.hashCode() &&
          this.columns.hashCode() == that.columns.hashCode() &&
          this.levels.hashCode() == that.levels.hashCode()) ||
          (Arrays.equals(this.rows, that.rows) &&
              Arrays.equals(this.columns, that.columns) &&
              Arrays.equals(this.levels, that.levels)));
      return that;
    }

    /*
     * Data is not copied on build() so we dereference data arrays to prevent
     * lingering builders from further modifying data.
     */
    private void dereference() {
      data = null;
      rows = null;
      columns = null;
      levels = null;
    }

    /**
     * Return a newly-created, immutable, 3-dimensional interval data container
     * populated with values computed by the supplied loader. Calling this
     * method will overwrite any values already supplied via {@code set*} or
     * {@code add*} methods.
     *
     * @param loader that will compute values
     */
    public IntervalVolume build(Loader loader) {
      checkNotNull(loader);
      for (int i = 0; i < rows.length; i++) {
        double row = rows[i];
        for (int j = 0; j < columns.length; j++) {
          double column = columns[j];
          for (int k = 0; k < levels.length; k++) {
            data[i][j][k] = loader.compute(row, column, levels[k]);
          }
        }
      }
      return build();
    }

    /**
     * Return a newly-created, immutable 3-dimensional interval data container
     * populated with the contents of this {@code Builder}.
     */
    public IntervalVolume build() {
      checkState(built != true, "This builder has already been used");
      checkDataState(rows, columns, levels);
      IntervalVolume volume = new DefaultVolume(
          rowMin, rowMax, rowΔ, rows,
          columnMin, columnMax, columnΔ, columns,
          levelMin, levelMax, levelΔ, levels,
          data);
      dereference();
      return volume;
    }
  }

}
