package org.checkerframework.dataflow.analysis;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.dataflow.cfg.node.Node;
import org.plumelib.util.StringsPlume;
import org.plumelib.util.UniqueId;

/**
 * {@code TransferInput} is used as the input type of the individual transfer functions of a {@link
 * ForwardTransferFunction} or a {@link BackwardTransferFunction}. It also contains a reference to
 * the node to which the transfer function will be applied.
 *
 * <p>A {@code TransferInput} contains one or two stores. If two stores are present, one belongs to
 * "then", and the other to "else".
 *
 * @param <V> type of the abstract value that is tracked
 * @param <S> the store type used in the analysis
 */
public class TransferInput<V extends AbstractValue<V>, S extends Store<S>> implements UniqueId {

  /** The corresponding node. */
  // TODO: explain when the node is changed.
  protected @Nullable Node node;

  /**
   * The regular result store (or {@code null} if none is present, because {@link #thenStore} and
   * {@link #elseStore} are set). The following invariant is maintained:
   *
   * <pre><code>
   * store == null &hArr; thenStore != null &amp;&amp; elseStore != null
   * </code></pre>
   */
  protected final @Nullable S store;

  /**
   * The "then" result store (or {@code null} if none is present). See invariant at {@link #store}.
   */
  protected final @Nullable S thenStore;

  /**
   * The "else" result store (or {@code null} if none is present). See invariant at {@link #store}.
   */
  protected final @Nullable S elseStore;

  /** The corresponding analysis class to get intermediate flow results. */
  protected final Analysis<V, S, ?> analysis;

  /** The unique ID for the next-created object. */
  private static final AtomicLong nextUid = new AtomicLong(0);

  /** The unique ID of this object. */
  private final transient long uid = nextUid.getAndIncrement();

  @Override
  public long getUid(@UnknownInitialization TransferInput<V, S> this) {
    return uid;
  }

  /**
   * Private helper constructor; all TransferInput construction bottoms out here.
   *
   * @param node the corresponding node
   * @param store the regular result store, or {@code null} if none is present
   * @param thenStore the "then" result store, or {@code null} if none is present
   * @param elseStore the "else" result store, or {@code null} if none is present
   * @param analysis analysis the corresponding analysis class to get intermediate flow results
   */
  private TransferInput(
      @Nullable Node node,
      @Nullable S store,
      @Nullable S thenStore,
      @Nullable S elseStore,
      Analysis<V, S, ?> analysis) {
    if (store == null) {
      assert thenStore != null && elseStore != null;
    } else {
      assert thenStore == null && elseStore == null;
    }
    this.node = node;
    this.store = store;
    this.thenStore = thenStore;
    this.elseStore = elseStore;
    this.analysis = analysis;
  }

  /**
   * Create a {@link TransferInput}, given a {@link TransferResult} and a node-value mapping.
   *
   * <p><em>Aliasing</em>: The stores returned by any methods of {@code to} will be stored
   * internally and are not allowed to be used elsewhere. Full control of them is transferred to
   * this object.
   *
   * <p>The node-value mapping {@code nodeValues} is provided by the analysis and is only read from
   * within this {@link TransferInput}.
   *
   * @param n {@link #node}
   * @param analysis {@link #analysis}
   * @param to a transfer result
   */
  public TransferInput(Node n, Analysis<V, S, ?> analysis, TransferResult<V, S> to) {
    this(
        n,
        to.containsTwoStores() ? null : to.getRegularStore(),
        to.containsTwoStores() ? to.getThenStore() : null,
        to.containsTwoStores() ? to.getElseStore() : null,
        analysis);
  }

  /**
   * Create a {@link TransferInput}, given a store and a node-value mapping.
   *
   * <p><em>Aliasing</em>: The store {@code s} will be stored internally and is not allowed to be
   * used elsewhere. Full control over {@code s} is transferred to this object.
   *
   * <p>The node-value mapping {@code nodeValues} is provided by the analysis and is only read from
   * within this {@link TransferInput}.
   *
   * @param n {@link #node}
   * @param analysis {@link #analysis}
   * @param s {@link #store}
   */
  public TransferInput(@Nullable Node n, Analysis<V, S, ?> analysis, S s) {
    this(n, s, null, null, analysis);
  }

  /**
   * Create a {@link TransferInput}, given two stores and a node-value mapping.
   *
   * <p><em>Aliasing</em>: The two stores {@code s1} and {@code s2} will be stored internally and
   * are not allowed to be used elsewhere. Full control of them is transferred to this object.
   *
   * @param n a node
   * @param analysis {@link #analysis}
   * @param s1 {@link #thenStore}
   * @param s2 {@link #elseStore}
   */
  public TransferInput(@Nullable Node n, Analysis<V, S, ?> analysis, S s1, S s2) {
    this(n, null, s1, s2, analysis);
  }

  /**
   * Copy constructor.
   *
   * @param from a {@link TransferInput} to copy
   */
  @SuppressWarnings("nullness:dereference.of.nullable") // object invariant: store vs thenStore
  protected TransferInput(TransferInput<V, S> from) {
    this(
        from.node,
        from.store == null ? null : from.store.copy(),
        from.store == null ? from.thenStore.copy() : null,
        from.store == null ? from.elseStore.copy() : null,
        from.analysis);
  }

  /**
   * Returns the {@link Node} for this {@link TransferInput}.
   *
   * @return the {@link Node} for this {@link TransferInput}
   */
  public @Nullable Node getNode() {
    return node;
  }

  /**
   * Returns the abstract value of node {@code n}, which is required to be a "sub-node" (that is, a
   * direct or indirect child) of the node this transfer input is associated with. Furthermore,
   * {@code n} cannot be a l-value node. Returns {@code null} if no value is available.
   *
   * @param n a node
   * @return the abstract value of node {@code n}, or {@code null} if no value is available
   */
  public @Nullable V getValueOfSubNode(Node n) {
    return analysis.getValue(n);
  }

  /**
   * Returns the regular result store produced if no exception is thrown by the {@link Node}
   * corresponding to this transfer function result.
   *
   * @return the regular result store produced if no exception is thrown by the {@link Node}
   *     corresponding to this transfer function result
   */
  public S getRegularStore() {
    if (store == null) {
      assert thenStore != null && elseStore != null : "@AssumeAssertion(nullness): invariant";
      return thenStore.leastUpperBound(elseStore);
    } else {
      return store;
    }
  }

  /**
   * Returns the result store produced if the {@link Node} this result belongs to evaluates to
   * {@code true}.
   *
   * @return the result store produced if the {@link Node} this result belongs to evaluates to
   *     {@code true}
   */
  public S getThenStore() {
    if (store == null) {
      assert thenStore != null : "@AssumeAssertion(nullness): invariant";
      return thenStore;
    }
    return store;
  }

  /**
   * Returns the result store produced if the {@link Node} this result belongs to evaluates to
   * {@code false}.
   *
   * @return the result store produced if the {@link Node} this result belongs to evaluates to
   *     {@code false}
   */
  public S getElseStore() {
    if (store == null) {
      assert elseStore != null : "@AssumeAssertion(nullness): invariant";
      return elseStore;
    }
    // copy the store such that it is the same as the result of getThenStore
    // (that is, identical according to equals), but two different objects.
    return store.copy();
  }

  /**
   * Returns {@code true} if and only if this transfer input contains two stores that are
   * potentially not equal. Note that the result {@code true} does not imply that {@code
   * getRegularStore} cannot be called (or vice versa for {@code false}). Rather, it indicates that
   * {@code getThenStore} or {@code getElseStore} can be used to give more precise results.
   * Otherwise, if the result is {@code false}, then all three methods {@code getRegularStore},
   * {@code getThenStore}, and {@code getElseStore} return equivalent stores.
   *
   * @return {@code true} if and only if this transfer input contains two stores that are
   *     potentially not equal
   */
  public boolean containsTwoStores() {
    return store == null;
  }

  /**
   * Returns an exact copy of this store.
   *
   * @return an exact copy of this store
   */
  public TransferInput<V, S> copy() {
    return new TransferInput<>(this);
  }

  /**
   * Compute the least upper bound of two stores.
   *
   * <p><em>Important</em>: This method must fulfill the same contract as {@code leastUpperBound} of
   * {@link Store}.
   *
   * @param other a transfer input
   * @return the least upper bound of this and {@code other}
   */
  public TransferInput<V, S> leastUpperBound(TransferInput<V, S> other) {
    if (store == null) {
      S newThenStore = getThenStore().leastUpperBound(other.getThenStore());
      S newElseStore = getElseStore().leastUpperBound(other.getElseStore());
      return new TransferInput<>(node, analysis, newThenStore, newElseStore);
    } else {
      if (other.store == null) {
        // make sure we do not lose precision and keep two stores if at
        // least one of the two TransferInput's has two stores.
        return other.leastUpperBound(this);
      }
      return new TransferInput<>(node, analysis, store.leastUpperBound(other.getRegularStore()));
    }
  }

  @Override
  public boolean equals(@Nullable Object o) {
    if (o instanceof TransferInput) {
      @SuppressWarnings("unchecked")
      TransferInput<V, S> other = (TransferInput<V, S>) o;
      if (containsTwoStores()) {
        if (other.containsTwoStores()) {
          return getThenStore().equals(other.getThenStore())
              && getElseStore().equals(other.getElseStore());
        }
      } else {
        if (!other.containsTwoStores()) {
          return getRegularStore().equals(other.getRegularStore());
        }
      }
    }
    return false;
  }

  @Override
  public int hashCode() {
    return Objects.hash(this.analysis, this.node, this.store, this.thenStore, this.elseStore);
  }

  @Override
  public String toString() {
    if (store == null) {
      return "[then="
          + StringsPlume.indentLinesExceptFirst(2, thenStore)
          + ","
          + System.lineSeparator()
          + "  else="
          + StringsPlume.indentLinesExceptFirst(2, elseStore)
          + "]";
    } else {
      return "[" + store + "]";
    }
  }
}
