/*
 * JBoss, Home of Professional Open Source
 * Copyright 2009 Red Hat Inc. and/or its affiliates and other
 * contributors as indicated by the @author tags. All rights reserved.
 * See the copyright.txt in the distribution for a full listing of
 * individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.infinispan.transaction.tm;


import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author bela
 * @since 4.0
 */
public class DummyTransaction implements Transaction {
   private static final Log log = LogFactory.getLog(DummyTransaction.class);
   private static boolean trace = log.isTraceEnabled();

   private volatile int status = Status.STATUS_UNKNOWN;
   protected final DummyBaseTransactionManager tm_;
   protected final Xid xid;

   protected Set<Synchronization> syncs;
   private final List<XAResource> enlistedResources = new ArrayList<XAResource>(2);
   private int prepareStatus;

   public DummyTransaction(DummyBaseTransactionManager tm) {
      tm_ = tm;
      status = Status.STATUS_ACTIVE;
      if (tm.isUseXaXid()) {
         xid = new DummyXid(tm.transactionManagerId);
      } else {
         xid = new DummyNoXaXid();
      }
   }

   /**
    * Attempt to commit this transaction.
    *
    * @throws RollbackException          If the transaction was marked for rollback only, the transaction is rolled back
    *                                    and this exception is thrown.
    * @throws SystemException            If the transaction service fails in an unexpected way.
    * @throws HeuristicMixedException    If a heuristic decision was made and some some parts of the transaction have
    *                                    been committed while other parts have been rolled back.
    * @throws HeuristicRollbackException If a heuristic decision to roll back the transaction was made.
    * @throws SecurityException          If the caller is not allowed to commit this transaction.
    */
   @Override
   public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, SystemException {

      try {
         runPrepare();
         if (status == Status.STATUS_MARKED_ROLLBACK || status == Status.STATUS_ROLLING_BACK) {
            runRollback();
            throw new RollbackException("Exception rolled back, status is: " + status);
         }
         runCommitTx();
      } finally {
         tm_.setTransaction(null);
      }
   }

   /**
    * Rolls back this transaction.
    *
    * @throws IllegalStateException If the transaction is in a state where it cannot be rolled back. This could be
    *                               because the transaction is no longer active, or because it is in the {@link
    *                               Status#STATUS_PREPARED prepared state}.
    * @throws SystemException       If the transaction service fails in an unexpected way.
    */
   @Override
   public void rollback() throws IllegalStateException, SystemException {
      try {
         status = Status.STATUS_ROLLING_BACK;
         runRollback();
         status = Status.STATUS_ROLLEDBACK;
         notifyAfterCompletion(Status.STATUS_ROLLEDBACK);
      } catch (Throwable t) {
         log.errorRollingBack(t);
         throw new IllegalStateException(t);
      }
      // Disassociate tx from thread.
      tm_.setTransaction(null);
   }

   /**
    * Mark the transaction so that the only possible outcome is a rollback.
    *
    * @throws IllegalStateException If the transaction is not in an active state.
    * @throws SystemException       If the transaction service fails in an unexpected way.
    */
   @Override
   public void setRollbackOnly() throws IllegalStateException, SystemException {
      status = Status.STATUS_MARKED_ROLLBACK;
   }

   /**
    * Get the status of the transaction.
    *
    * @return The status of the transaction. This is one of the {@link Status} constants.
    * @throws SystemException If the transaction service fails in an unexpected way.
    */
   @Override
   public int getStatus() throws SystemException {
      return status;
   }

   /**
    * Enlist an XA resource with this transaction.
    *
    * @return <code>true</code> if the resource could be enlisted with this transaction, otherwise <code>false</code>.
    * @throws RollbackException     If the transaction is marked for rollback only.
    * @throws IllegalStateException If the transaction is in a state where resources cannot be enlisted. This could be
    *                               because the transaction is no longer active, or because it is in the {@link
    *                               Status#STATUS_PREPARED prepared state}.
    * @throws SystemException       If the transaction service fails in an unexpected way.
    */
   @Override
   public boolean enlistResource(XAResource xaRes) throws RollbackException, IllegalStateException, SystemException {
      this.enlistedResources.add(xaRes);
      try {
         xaRes.start(xid, 0);
      } catch (XAException e) {
         log.errorEnlistingResource(e);
         throw new SystemException(e.getMessage());
      }
      return true;
   }

   /**
    * De-list an XA resource from this transaction.
    *
    * @return <code>true</code> if the resource could be de-listed from this transaction, otherwise <code>false</code>.
    * @throws IllegalStateException If the transaction is in a state where resources cannot be de-listed. This could be
    *                               because the transaction is no longer active.
    * @throws SystemException       If the transaction service fails in an unexpected way.
    */
   @Override
   public boolean delistResource(XAResource xaRes, int flag)
         throws IllegalStateException, SystemException {
      throw new SystemException("not supported");
   }

   /**
    * Register a {@link Synchronization} callback with this transaction.
    *
    * @throws RollbackException     If the transaction is marked for rollback only.
    * @throws IllegalStateException If the transaction is in a state where {@link Synchronization} callbacks cannot be
    *                               registered. This could be because the transaction is no longer active, or because it
    *                               is in the {@link Status#STATUS_PREPARED prepared state}.
    * @throws SystemException       If the transaction service fails in an unexpected way.
    */
   @Override
   public void registerSynchronization(Synchronization sync) throws RollbackException, IllegalStateException, SystemException {
      if (sync == null)
         throw new IllegalArgumentException("null synchronization " + this);
      if (syncs == null) syncs = new HashSet<Synchronization>(8);

      switch (status) {
         case Status.STATUS_ACTIVE:
         case Status.STATUS_PREPARING:
            break;
         case Status.STATUS_PREPARED:
            throw new IllegalStateException("already prepared. " + this);
         case Status.STATUS_COMMITTING:
            throw new IllegalStateException("already started committing. " + this);
         case Status.STATUS_COMMITTED:
            throw new IllegalStateException("already committed. " + this);
         case Status.STATUS_MARKED_ROLLBACK:
            throw new RollbackException("already marked for rollback " + this);
         case Status.STATUS_ROLLING_BACK:
            throw new RollbackException("already started rolling back. " + this);
         case Status.STATUS_ROLLEDBACK:
            throw new RollbackException("already rolled back. " + this);
         case Status.STATUS_NO_TRANSACTION:
            throw new IllegalStateException("no transaction. " + this);
         case Status.STATUS_UNKNOWN:
            throw new IllegalStateException("unknown state " + this);
         default:
            throw new IllegalStateException("illegal status: " + status + " tx=" + this);
      }

      if (trace) {
         log.tracef("registering synchronization handler %s", sync);
      }
      syncs.add(sync);

   }

   public boolean notifyBeforeCompletion() {
      boolean retval = true;
      if (syncs == null) return true;
      for (Synchronization s : syncs) {
         if (trace) log.tracef("processing beforeCompletion for %s", s);
         try {
            s.beforeCompletion();
         } catch (Throwable t) {
            retval = false;
            status = Status.STATUS_MARKED_ROLLBACK;
            log.beforeCompletionFailed(s, t);
         }
      }
      return retval;
   }

   public boolean runPrepare() throws SystemException {

      boolean successfulInit = notifyBeforeCompletion();

      if (successfulInit) {
         status = Status.STATUS_PREPARING;
      } else {
         status = Status.STATUS_ROLLING_BACK;
      }

      DummyTransaction transaction = tm_.getTransaction();
      Collection<XAResource> resources = transaction.getEnlistedResources();
      for (XAResource res : resources) {
         try {
            int prepareStatus = res.prepare(xid);
            transaction.setPrepareStatus(prepareStatus);
         } catch (XAException e) {
            log.trace("The resource wants to rollback!", e);
            status = Status.STATUS_ROLLING_BACK;
            return false;
         } catch (Throwable th) {
            log.unexpectedErrorFromResourceManager(th);
            throw new SystemException(th.getMessage());
         }
      }

      if (status == Status.STATUS_MARKED_ROLLBACK || status == Status.STATUS_ROLLING_BACK) {
         return false;
      }

      status = Status.STATUS_PREPARED;
      return true;
   }

   private void setPrepareStatus(int prepareStatus) {
      this.prepareStatus = prepareStatus;
   }

   public void notifyAfterCompletion(int status) {
      if (syncs == null) return;
      for (Synchronization s : syncs) {
         if (trace) {
            log.tracef("processing afterCompletion for %s", s);
         }
         try {
            s.afterCompletion(status);
         } catch (Throwable t) {
            log.afterCompletionFailed(s, t);
         }
      }
      syncs.clear();
   }

   public Collection<XAResource> getEnlistedResources() {
      return enlistedResources;
   }

   public void runRollback() {
      DummyTransaction transaction = tm_.getTransaction();
      Collection<XAResource> resources = transaction.getEnlistedResources();
      for (XAResource res : resources) {
         try {
            res.rollback(xid);
         } catch (XAException e) {
            log.errorRollingBack(e);
         }
      }
   }

   public void runCommitTx() throws HeuristicMixedException {
      try {
         status = Status.STATUS_COMMITTING;

         DummyTransaction transaction = tm_.getTransaction();
         if (transaction.getPrepareStatus() == XAResource.XA_RDONLY) {
            log.debug("This is a read-only tx");
         } else {
            Collection<XAResource> resources = transaction.getEnlistedResources();
            for (XAResource res : resources) {
               try {
                  //we only do 2-phase commits
                  res.commit(xid, false);
               } catch (XAException e) {
                  log.errorCommittingTx(e);
                  throw new HeuristicMixedException(e.getMessage());
               }
            }
         }
         status = Status.STATUS_COMMITTED;
      } catch (HeuristicMixedException e) {
         status = Status.STATUS_UNKNOWN;
         throw e;
      } finally {
         //notify synchronizations
         notifyAfterCompletion(status);
         tm_.setTransaction(null);
      }
   }

   public void setStatus(int stat) {
      this.status = stat;
   }

   @Override
   public String toString() {
      return "DummyTransaction{" +
            "xid=" + xid +
            ", status=" + status +
            '}';
   }

   public int getPrepareStatus() {
      return prepareStatus;
   }

   public XAResource firstEnlistedResource() {
      return getEnlistedResources().iterator().next();
   }

   public Xid getXid() {
      return xid;
   }

   public Collection<Synchronization> getEnlistedSynchronization() {
      return syncs;
   }

   /**
    * Must be defined for increased performance
    */
   @Override
   public final int hashCode() {
      return xid.hashCode();
   }

   @Override
   public final boolean equals(Object obj) {
      return this == obj;
   }

}
