/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 ****************************************************************/

package org.apache.cayenne.access;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.apache.cayenne.CayenneException;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.log4j.Level;

/**
 * A Cayenne transaction. Currently supports managing JDBC connections.
 * 
 * @author Andrus Adamchik
 * @since 1.1
 */
public abstract class Transaction {

  /**
   * A ThreadLocal that stores current thread transaction.
   * 
   * @since 1.2
   */
  static final ThreadLocal currentTransaction = new ThreadLocal();

  private static final Transaction NO_TRANSACTION = new Transaction() {

    @Override
    public void begin() {

    }

    @Override
    public void commit() {

    }

    @Override
    public void rollback() {

    }
  };

  public static final int STATUS_ACTIVE = 1;

  public static final int STATUS_COMMITTING = 2;

  public static final int STATUS_COMMITTED = 3;

  public static final int STATUS_ROLLEDBACK = 4;

  public static final int STATUS_ROLLING_BACK = 5;

  public static final int STATUS_NO_TRANSACTION = 6;

  public static final int STATUS_MARKED_ROLLEDBACK = 7;

  protected Map connections;

  protected int status;

  protected TransactionDelegate delegate;

  /**
   * @deprecated since 1.2
   */
  @Deprecated
  protected Level logLevel;

  static String decodeStatus(int status) {
    switch (status) {
      case STATUS_ACTIVE:
        return "STATUS_ACTIVE";
      case STATUS_COMMITTING:
        return "STATUS_COMMITTING";
      case STATUS_COMMITTED:
        return "STATUS_COMMITTED";
      case STATUS_ROLLEDBACK:
        return "STATUS_ROLLEDBACK";
      case STATUS_ROLLING_BACK:
        return "STATUS_ROLLING_BACK";
      case STATUS_NO_TRANSACTION:
        return "STATUS_NO_TRANSACTION";
      case STATUS_MARKED_ROLLEDBACK:
        return "STATUS_MARKED_ROLLEDBACK";
      default:
        return "Unknown Status - " + status;
    }
  }

  /**
   * Binds a Transaction to the current thread.
   * 
   * @since 1.2
   */
  public static void bindThreadTransaction(Transaction transaction) {
    currentTransaction.set(transaction);
  }

  /**
   * Returns a Transaction associated with the current thread, or null if there
   * is no such Transaction.
   * 
   * @since 1.2
   */
  public static Transaction getThreadTransaction() {
    return (Transaction) currentTransaction.get();
  }

  /**
   * Factory method returning a new transaction instance that would propagate
   * commit/rollback to participating connections. Connections will be closed
   * when commit or rollback is called.
   */
  public static Transaction internalTransaction(TransactionDelegate delegate) {
    return new InternalTransaction(delegate);
  }

  /**
   * Factory method returning a new transaction instance that would NOT
   * propagate commit/rollback to participating connections. Connections will
   * still be closed when commit or rollback is called.
   */
  public static Transaction externalTransaction(TransactionDelegate delegate) {
    return new ExternalTransaction(delegate);
  }

  /**
   * Factory method returning a transaction instance that does not alter the
   * state of participating connections in any way. Commit and rollback methods
   * do not do anything.
   */
  public static Transaction noTransaction() {
    return NO_TRANSACTION;
  }

  /**
   * Creates new inactive transaction.
   */
  protected Transaction() {
    status = STATUS_NO_TRANSACTION;
  }

  /**
   * Helper method that wraps a number of queries in this transaction, runs
   * them, and commits or rolls back depending on the outcome. This method
   * allows users to define their own custom Transactions and wrap Cayenne
   * queries in them.
   * 
   * @deprecated since 1.2 this method is not used in Cayenne and is deprecated.
   *             Thread-bound transactions should be used instead.
   */
  @Deprecated
  public void performQueries(QueryEngine engine, Collection queries,
      OperationObserver observer) throws CayenneRuntimeException {

    Transaction old = Transaction.getThreadTransaction();
    Transaction.bindThreadTransaction(this);

    try {
      // implicit begin..
      engine.performQueries(queries, observer);

      // don't commit iterated queries - leave it up to the caller
      // at the same time rollbacks of iterated queries must be processed here,
      // since caller will no longer be processing stuff on exception
      if (!observer.isIteratedResult()
        && (getStatus() == Transaction.STATUS_ACTIVE)) {
        commit();
      }
    } catch (Exception ex) {
      setRollbackOnly();

      // must rethrow
      if (ex instanceof CayenneRuntimeException) {
        throw (CayenneRuntimeException) ex;
      } else {
        throw new CayenneRuntimeException(ex);
      }
    } finally {
      Transaction.bindThreadTransaction(old);
      if (getStatus() == Transaction.STATUS_MARKED_ROLLEDBACK) {
        try {
          rollback();
        } catch (Exception rollbackEx) {
        }
      }
    }
  }

  /**
   * @deprecated since 1.2 unused
   */
  @Deprecated
  public Level getLogLevel() {
    return logLevel != null ? logLevel : Level.INFO;
  }

  /**
   * @deprecated since 1.2 unused
   */
  @Deprecated
  public void setLogLevel(Level logLevel) {
    this.logLevel = logLevel;
  }

  public TransactionDelegate getDelegate() {
    return delegate;
  }

  public void setDelegate(TransactionDelegate delegate) {
    this.delegate = delegate;
  }

  public int getStatus() {
    return status;
  }

  public synchronized void setRollbackOnly() {
    setStatus(STATUS_MARKED_ROLLEDBACK);
  }

  public synchronized void setStatus(int status) {
    if (delegate != null
      && status == STATUS_MARKED_ROLLEDBACK
      && !delegate.willMarkAsRollbackOnly(this)) {
      return;
    }

    this.status = status;
  }

  /**
   * Starts a Transaction. If Transaction is not started explicitly, it will be
   * started when the first connection is added.
   */
  public abstract void begin();

  /**
   * @deprecated since 1.2 use {@link #addConnection(String, Connection)}.
   */
  @Deprecated
  public void addConnection(Connection connection)
      throws IllegalStateException, SQLException, CayenneException {
    addConnection("x" + System.currentTimeMillis(), connection);
  }

  public abstract void commit() throws IllegalStateException, SQLException,
      CayenneException;

  public abstract void rollback() throws IllegalStateException, SQLException,
      CayenneException;

  /**
   * @since 1.2
   */
  public Connection getConnection(String name) {
    return (connections != null) ? (Connection) connections.get(name) : null;
  }

  /**
   * @since 1.2
   */
  public boolean addConnection(String name, Connection connection)
      throws SQLException {
    if (delegate != null && !delegate.willAddConnection(this, connection)) {
      return false;
    }

    if (connections == null) {
      connections = new HashMap();
    }

    return connections.put(name, connection) != connection;
  }
}
