package com.avaje.ebeaninternal.api;

import com.avaje.ebean.TxScope;
import com.avaje.ebean.config.PersistBatch;

import java.util.ArrayList;

/**
 * Used internally to handle the scoping of transactions for methods.
 */
public class ScopeTrans implements Thread.UncaughtExceptionHandler {

  private static final int OPCODE_ATHROW = 191;

  private final SpiTransactionScopeManager scopeMgr;

  /**
   * The suspended transaction (can be null).
   */
  private final SpiTransaction suspendedTransaction;

  /**
   * The transaction in scope (can be null).
   */
  private final SpiTransaction transaction;

  /**
   * If true by default rollback on Checked exceptions.
   */
  private final boolean rollbackOnChecked;

  /**
   * True if the transaction was created and hence should be committed
   * on finally if it hasn't already been rolled back.
   */
  private final boolean created;

  /**
   * Explicit set of Exceptions that DO NOT cause a rollback to occur.
   */
  private final ArrayList<Class<? extends Throwable>> noRollbackFor;

  /**
   * Explicit set of Exceptions that DO cause a rollback to occur.
   */
  private final ArrayList<Class<? extends Throwable>> rollbackFor;

  private PersistBatch restoreBatch;

  private PersistBatch restoreBatchOnCascade;

  private int restoreBatchSize;

  private Boolean restoreBatchGeneratedKeys;

  /**
   * Flag set when a rollback has occurred.
   */
  private boolean rolledBack;


  public ScopeTrans(boolean rollbackOnChecked, boolean created, SpiTransaction transaction, TxScope txScope,
                    SpiTransaction suspendedTransaction, SpiTransactionScopeManager scopeMgr) {

    this.rollbackOnChecked = rollbackOnChecked;
    this.created = created;
    this.transaction = transaction;
    this.suspendedTransaction = suspendedTransaction;
    this.scopeMgr = scopeMgr;

    this.noRollbackFor = txScope.getNoRollbackFor();
    this.rollbackFor = txScope.getRollbackFor();

    if (transaction != null) {
      if (!created && txScope.isBatchSet() || txScope.isBatchOnCascadeSet() || txScope.isBatchSizeSet()) {
        restoreBatch = transaction.getBatch();
        restoreBatchOnCascade = transaction.getBatchOnCascade();
        restoreBatchSize = transaction.getBatchSize();
        restoreBatchGeneratedKeys = transaction.getBatchGetGeneratedKeys();
      }
      if (txScope.isBatchSet()) {
        transaction.setBatch(txScope.getBatch());
      }
      if (txScope.isBatchOnCascadeSet()) {
        transaction.setBatchOnCascade(txScope.getBatchOnCascade());
      }
      if (txScope.isBatchSizeSet()) {
        transaction.setBatchSize(txScope.getBatchSize());
      }
      if (txScope.isSkipGeneratedKeys()) {
        transaction.setBatchGetGeneratedKeys(false);
      }
    }

  }

  /**
   * Return the current/active transaction.
   */
  protected SpiTransaction getTransaction() {
    return transaction;
  }

  /**
   * Called when the Thread catches any uncaught exception.
   * For example, an unexpected NullPointerException or Error.
   */
  public void uncaughtException(Thread thread, Throwable e) {

    // rollback transaction if required
    caughtThrowable(e);

    // reinstate suspended transaction
    onFinally();
  }

  /**
   * Returned via RETURN or expected Exception from the method.
   *
   * @param returnOrThrowable the return value or Throwable
   * @param opCode            indicates
   */
  public void onExit(Object returnOrThrowable, int opCode) {

    if (opCode == OPCODE_ATHROW) {
      // exited with a Throwable
      caughtThrowable((Throwable) returnOrThrowable);
    }
    onFinally();
  }


  /**
   * Commit if the transaction exists and has not already been rolled back.
   * Also reinstate the suspended transaction if there was one.
   */
  public void onFinally() {

    try {
      if (!rolledBack) {
        commitTransaction();
      }
    } finally {
      restoreSuspended();
    }
  }

  protected void restoreSuspended() {
    if (suspendedTransaction != null) {
      // put the previously suspended transaction
      // back onto the ThreadLocal or equivalent
      scopeMgr.replace(suspendedTransaction);
    }
  }

  protected void commitTransaction() {
    if (created) {
      transaction.commit();
    } else {
      if (restoreBatch != null) {
        transaction.setBatch(restoreBatch);
      }
      if (restoreBatchOnCascade != null) {
        transaction.setBatchOnCascade(restoreBatchOnCascade);
      }
      if (restoreBatchSize > 0) {
        transaction.setBatchSize(restoreBatchSize);
      }
      if (restoreBatchGeneratedKeys != null) {
        transaction.setBatchGetGeneratedKeys(restoreBatchGeneratedKeys);
      }
    }
  }

  /**
   * An Error was caught and this ALWAYS causes a rollback to occur.
   * Returns the error and this should be thrown by the calling code.
   */
  public Error caughtError(Error e) {
    rollback(e);
    return e;
  }

  /**
   * Mark the underlying transaction as rollback only.
   */
  public void setRollbackOnly() {
    if (transaction != null) {
      transaction.setRollbackOnly();
    }
  }

  /**
   * An Exception was caught and may or may not cause a rollback to occur.
   * Returns the exception and this should be thrown by the calling code.
   */
  public <T extends Throwable> T caughtThrowable(T e) {

    if (isRollbackThrowable(e)) {
      rollback(e);
    }
    return e;
  }

  protected void rollback(Throwable e) {
    if (transaction != null && transaction.isActive()) {
      // transaction is null for NOT_SUPPORTED and sometimes SUPPORTS
      // and Inactive (already rolled back) if nested REQUIRED
      transaction.rollback(e);
    }
    rolledBack = true;
  }

  /**
   * Return true if this throwable should cause a rollback to occur.
   */
  private boolean isRollbackThrowable(Throwable e) {

    if (e instanceof Error) {
      return true;
    }

    if (noRollbackFor != null) {
      for (int i = 0; i < noRollbackFor.size(); i++) {
        if (noRollbackFor.get(i).equals(e.getClass())) {

          // explicit no rollback for this one
          return false;
        }
      }
    }

    if (rollbackFor != null) {
      for (int i = 0; i < rollbackFor.size(); i++) {
        if (rollbackFor.get(i).equals(e.getClass())) {
          // explicit rollback for this one
          return true;
        }
      }
    }


    if (e instanceof RuntimeException) {
      return true;

    } else {
      // checked exceptions...
      // EJB defaults this to false which is not intuitive IMO
      // Ebean makes this configurable (default to true)
      return rollbackOnChecked;
    }
  }


}
