package org.infinispan.transaction.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;

import org.infinispan.transaction.totalorder.TotalOrderLatch;
import org.infinispan.transaction.xa.GlobalTransaction;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

/**
 * Represents a state for a Remote Transaction when the Total Order based protocol is used.
 *
 * @author Pedro Ruivo
 * @since 5.3
 */
public class TotalOrderRemoteTransactionState {

   private static final Log log = LogFactory.getLog(TotalOrderRemoteTransactionState.class);
   private static final boolean trace = log.isTraceEnabled();
   private final EnumSet<State> transactionState;
   private final GlobalTransaction globalTransaction;
   private List<Object> lockedKeys;
   private TotalOrderLatch block;
   private List<TotalOrderLatch> dependencies;

   public TotalOrderRemoteTransactionState(GlobalTransaction globalTransaction) {
      this.transactionState = EnumSet.noneOf(State.class);
      this.globalTransaction = globalTransaction;
   }

   /**
    * check if the transaction is marked for rollback (by the Rollback Command)
    *
    * @return true if it is marked for rollback, false otherwise
    */
   public synchronized boolean isRollbackReceived() {
      return transactionState.contains(State.ROLLBACK_ONLY);
   }

   /**
    * check if the transaction is marked for commit (by the Commit Command)
    *
    * @return true if it is marked for commit, false otherwise
    */
   public synchronized boolean isCommitReceived() {
      return transactionState.contains(State.COMMIT_ONLY);
   }

   /**
    * mark the transaction as prepared (the validation was finished) and notify a possible pending commit or rollback
    * command
    */
   public synchronized void prepared() {
      if (trace) {
         log.tracef("[%s] Current status is %s, setting status to: PREPARED", globalTransaction.globalId(),
                    transactionState);
      }
      transactionState.add(State.PREPARED);
      notifyAll();
   }

   /**
    * mark the transaction as preparing, blocking the commit and rollback commands until the {@link #prepared()} is
    * invoked
    */
   public synchronized void preparing() {
      if (trace) {
         log.tracef("[%s] Current status is %s, setting status to: PREPARING", globalTransaction.globalId(),
                    transactionState);
      }
      transactionState.add(State.PREPARING);
   }

   /**
    * Commit and rollback commands invokes this method and they are blocked here if the state is PREPARING
    *
    * @param commit true if it is a commit command, false otherwise
    * @return true if the command needs to be processed, false otherwise
    * @throws InterruptedException when it is interrupted while waiting
    */
   public final synchronized boolean waitUntilPrepared(boolean commit)
         throws InterruptedException {
      boolean result;

      State state = commit ? State.COMMIT_ONLY : State.ROLLBACK_ONLY;
      if (trace) {
         log.tracef("[%s] Current status is %s, setting status to: %s", globalTransaction.globalId(),
                    transactionState, state);
      }
      transactionState.add(state);

      if (transactionState.contains(State.PREPARED)) {
         result = true;
         if (trace) {
            log.tracef("[%s] Transaction is PREPARED", globalTransaction.globalId());
         }
      } else if (transactionState.contains(State.PREPARING)) {
         wait();
         result = true;
         if (trace) {
            log.tracef("[%s] Transaction was in PREPARING state but now it is prepared", globalTransaction.globalId());
         }
      } else {
         if (trace) {
            log.tracef("[%s] Transaction is not delivered yet", globalTransaction.globalId());
         }
         result = false;
      }
      return result;
   }

   /**
    * @return true if the transaction has received the prepare and the commit or rollback
    */
   public final synchronized boolean isFinished() {
      return transactionState.contains(State.PREPARED) &&
            (transactionState.contains(State.COMMIT_ONLY) || transactionState.contains(State.ROLLBACK_ONLY));
   }

   /**
    * @return the keys locked in {@code org.infinispan.transaction.totalorder.TotalOrderManager}
    */
   public final synchronized Collection<Object> getLockedKeys() {
      return lockedKeys;
   }

   /**
    * @return the {@code TotalOrderLatch} associated to this transaction
    */
   public final synchronized TotalOrderLatch getTransactionSynchronizedBlock() {
      return block;
   }

   /**
    * Sets the {@code TotalOrderLatch} to be associated to this transaction
    */
   public final synchronized void setTransactionSynchronizedBlock(TotalOrderLatch block) {
      this.block = block;
   }

   /**
    * @return the global transaction
    */
   public final synchronized GlobalTransaction getGlobalTransaction() {
      return globalTransaction;
   }

   public final synchronized void awaitUntilReset() throws InterruptedException {
      while (block != null && lockedKeys != null) {
         wait();
      }
   }

   public final synchronized void reset() {
      this.block = null;
      this.lockedKeys = null;
      notifyAll();
   }

   @Override
   public String toString() {
      return "TotalOrderRemoteTransactionState{" +
            "transactionState=" + transactionState +
            ", globalTransaction='" + globalTransaction.globalId() + '\'' +
            '}';
   }

   @Override
   public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      TotalOrderRemoteTransactionState state = (TotalOrderRemoteTransactionState) o;

      return !(globalTransaction != null ? !globalTransaction.equals(state.globalTransaction) :
                     state.globalTransaction != null);

   }

   @Override
   public int hashCode() {
      return globalTransaction != null ? globalTransaction.hashCode() : 0;
   }

   public final synchronized void addSynchronizedBlock(TotalOrderLatch block) {
      if (dependencies == null) {
         dependencies = new ArrayList<TotalOrderLatch>(8);
      }
      dependencies.add(block);
   }

   public final synchronized void addAllSynchronizedBlocks(Collection<TotalOrderLatch> blocks) {
      if (dependencies == null) {
         dependencies = new ArrayList<TotalOrderLatch>(blocks);
      } else {
         dependencies.addAll(blocks);
      }
   }

   public final synchronized void addKeysLockedForClear() {
      lockedKeys = null;
   }

   public final synchronized void addLockedKey(Object key) {
      if (lockedKeys == null) {
         lockedKeys = new ArrayList<Object>(8);
      }
      lockedKeys.add(key);
   }

   public synchronized Collection<TotalOrderLatch> getConflictingTransactionBlocks() {
      return dependencies == null ? Collections.<TotalOrderLatch>emptyList() : dependencies;
   }

   private static enum State {
      /**
       * the prepare command was received and started the validation
       */
      PREPARING,
      /**
       * the prepare command was received and finished the validation
       */
      PREPARED,
      /**
       * the rollback command was received before the prepare command and the transaction must be aborted
       */
      ROLLBACK_ONLY,
      /**
       * the commit command was received before the prepare command and the transaction must be committed
       */
      COMMIT_ONLY
   }
}
