package edu.stanford.nlp.loglinear.model;

import java.util.Iterator;

/**
 * Created on 8/12/15.
 * @author keenon
 * <p>
 * Holds and provides access to an N-dimensional array.
 * <p>
 * Yes, generics will lead to unfortunate boxing and unboxing in the TableFactor case, we'll handle that if it becomes a
 * problem.
 */
public class NDArray<T> implements Iterable<int[]> {
  // public data
  private int[] dimensions;

  // implementation details
  private T[] values;

  /**
   * Constructor takes a list of neighbor variables to use for this factor. This must not change after construction,
   * and the number of states of those variables must also not change.
   *
   * @param dimensions list of neighbor variables assignment range sizes
   */
  @SuppressWarnings("unchecked")
  public NDArray(int[] dimensions) {
    for (int size : dimensions) {
      assert (size > 0);
    }
    this.dimensions = dimensions;
    values = (T[]) new Object[combinatorialNeighborStatesCount()];
  }

  /**
   * Set a single value in the factor table.
   *
   * @param assignment a list of variable settings, in the same order as the neighbors array of the factor
   * @param value      the value to put into the factor table
   */
  public void setAssignmentValue(int[] assignment, T value) {
    values[getTableAccessOffset(assignment)] = value;
  }

  /**
   * Retrieve a single value for an assignment.
   *
   * @param assignment a list of variable settings, in the same order as the neighbors array of the factor
   * @return the value for the given assignment. Can be null if not been set yet.
   */
  public T getAssignmentValue(int[] assignment) {
    return values[getTableAccessOffset(assignment)];
  }

  /**
   * @return the size array of the neighbors of the feature factor, passed by value to ensure immutability.
   */
  public int[] getDimensions() {
    return dimensions.clone();
  }

  /**
   * WARNING: This is pass by reference to avoid massive GC overload during heavy iterations, and because the standard
   * use case is to use the assignments array as an accessor. Please, clone if you save a copy, otherwise the array
   * will mutate underneath you.
   *
   * @return an iterator over all possible assignments to this factor
   */
  @Override
  public Iterator<int[]> iterator() {
    return new Iterator<int[]>() {
      Iterator<int[]> unsafe = fastPassByReferenceIterator();

      @Override
      public boolean hasNext() {
        return unsafe.hasNext();
      }

      @Override
      public int[] next() {
        return unsafe.next().clone();
      }
    };
  }

  /**
   * This is its own function because people will inevitably attempt this optimization of not cloning the array we
   * hand to the iterator, to save on GC, and it should not be default behavior. If you know what you're doing, then
   * this may be the iterator for you.
   *
   * @return an iterator that will mutate the value it returns to you, so you must clone if you want to keep a copy
   */
  public Iterator<int[]> fastPassByReferenceIterator() {
    final int[] assignments = new int[dimensions.length];
    if (dimensions.length > 0) assignments[0] = -1;

    return new Iterator<int[]>() {
      @Override
      public boolean hasNext() {
        for (int i = 0; i < assignments.length; i++) {
          if (assignments[i] < dimensions[i] - 1) return true;
        }
        return false;
      }

      @Override
      public int[] next() {
        // Add one to the first position
        assignments[0]++;
        // Carry any resulting overflow all the way to the end.
        for (int i = 0; i < assignments.length; i++) {
          if (assignments[i] >= dimensions[i]) {
            assignments[i] = 0;
            if (i < assignments.length - 1) {
              assignments[i + 1]++;
            }
          } else {
            break;
          }
        }
        return assignments;
      }
    };
  }

  /**
   * @return the total number of states this factor must represent to include all neighbors.
   */
  public int combinatorialNeighborStatesCount() {
    int c = 1;
    for (int n : dimensions) {
      c *= n;
    }
    return c;
  }

  /**
   * Clones the table, but keeps the values by reference.
   *
   * @return a new NDArray, a perfect replica of this one
   */
  public NDArray<T> cloneArray() {
    NDArray<T> copy = new NDArray<>(dimensions.clone());
    copy.values = values.clone();
    return copy;
  }

  ////////////////////////////////////////////////////////////////////////////
  // PRIVATE IMPLEMENTATION
  ////////////////////////////////////////////////////////////////////////////

  /**
   * Compute the distance into the one dimensional factorTable array that corresponds to a setting of all the
   * neighbors of the factor.
   *
   * @param assignment assignment indices, in same order as neighbors array
   * @return the offset index
   */
  private int getTableAccessOffset(int[] assignment) {
    assert (assignment.length == dimensions.length);
    int offset = 0;
    for (int i = 0; i < assignment.length; i++) {
      assert (assignment[i] < dimensions[i]);
      offset = (offset * dimensions[i]) + assignment[i];
    }
    return offset;
  }
}
