// Copyright (c) 2003-present, Jodd Team (http://jodd.org)
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package jodd.jtx;

import jodd.log.Logger;
import jodd.log.LoggerFactory;

import static jodd.jtx.JtxIsolationLevel.*;
import static jodd.jtx.JtxStatus.STATUS_ACTIVE;

import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;

/**
 * {@link JtxTransaction} manager is responsible for handling transaction
 * propagation and resource managers. Holds various JTX configuration data.
 * <p>
 * Note that transactions are hold inside a thread-local transaction stack.
 * Therefore, if one transaction is created after the other during the
 * same thread, the second transaction will be aware that it is 'after'
 * the first one.
 */
public class JtxTransactionManager {

	private static final Logger log = LoggerFactory.getLogger(JtxTransactionManager.class);

	protected int maxResourcesPerTransaction;
	protected boolean oneResourceManager;
	protected boolean validateExistingTransaction;
	protected boolean ignoreScope;
	protected Map<Class, JtxResourceManager> resourceManagers;

	protected final ThreadLocal<ArrayList<JtxTransaction>> txStack = new ThreadLocal<>();

	/**
	 * Creates new transaction manager.
	 */
	public JtxTransactionManager() {
		this.maxResourcesPerTransaction = -1;
		this.resourceManagers = new HashMap<>();
	}

	// ---------------------------------------------------------------- config

	/**
	 * Returns max number of resources per transaction.
	 */
	public int getMaxResourcesPerTransaction() {
		return maxResourcesPerTransaction;
	}

	/**
	 * Sets max number of resources per transaction.
	 */
	public void setMaxResourcesPerTransaction(int maxResourcesPerTransaction) {
		this.maxResourcesPerTransaction = maxResourcesPerTransaction;
	}

	/**
	 * Returns whether existing transactions should be validated before participating in them.
	 */
	public boolean isValidateExistingTransaction() {
		return validateExistingTransaction;
	}

	/**
	 * Sets whether existing transactions should be validated before participating
	 * in them.
	 * <p>When participating in an existing transaction (e.g. with
	 * PROPAGATION_REQUIRES or PROPAGATION_SUPPORTS encountering an existing
	 * transaction), this outer transaction's characteristics will apply even
	 * to the inner transaction scope. Validation will detect incompatible
	 * isolation level and read-only settings on the inner transaction definition
	 * and reject participation accordingly through throwing a corresponding exception.
	 */
	public void setValidateExistingTransaction(boolean validateExistingTransaction) {
		this.validateExistingTransaction = validateExistingTransaction;
	}

	/**
	 * Returns <code>true</code> if this transaction manager works with just one resource.
	 */
	public boolean isSingleResourceManager() {
		return oneResourceManager;
	}
	/**
	 * Specifies if transaction manager works with just one resource.
	 */
	public void setSingleResourceManager(boolean oneResourceManager) {
		this.oneResourceManager = oneResourceManager;
	}

	/**
	 * Returns if transaction scope should be ignored.
	 */
	public boolean isIgnoreScope() {
		return ignoreScope;
	}

	/**
	 * Sets if transaction scope should be ignored. If ignored,
	 * there may be more then one transaction in one scope.
	 * Scopes may be ignored if set to <code>null</code>
	 */
	public void setIgnoreScope(boolean ignoreScope) {
		this.ignoreScope = ignoreScope;
	}

	// ---------------------------------------------------------------- count

	/**
	 * Returns total number of transactions associated with current thread.
	 */
	public int totalThreadTransactions() {
		ArrayList<JtxTransaction> txList = txStack.get();
		if (txList == null) {
			return 0;
		}
		return txList.size();
	}

	/**
	 * Returns total number of transactions of the specified status associated with current thread.
	 */
	public int totalThreadTransactionsWithStatus(JtxStatus status) {
		ArrayList<JtxTransaction> txlist = txStack.get();
		if (txlist == null) {
			return 0;
		}
		int count = 0;
		for (JtxTransaction tx : txlist) {
			if (tx.getStatus() == status) {
				count++;
			}
		}
		return count;
	}

	/**
	 * Returns total number of active transactions associated with current thread.
	 */
	public int totalActiveThreadTransactions() {
		return totalThreadTransactionsWithStatus(STATUS_ACTIVE);
	}

	/**
	 * Returns <code>true</code> if provided transaction
	 * is associated with current thread.
	 */
	public boolean isAssociatedWithThread(JtxTransaction tx) {
		ArrayList<JtxTransaction> txList = txStack.get();
		if (txList == null) {
			return false;
		}
		return txList.contains(tx);
	}

	// ---------------------------------------------------------------- thread work

	/**
	 * Removes transaction association with current thread.
	 * Transaction should be properly handled (committed or rolledback)
	 * before removing from current thread.
	 * Also removes thread list from this thread.
	 */
	protected boolean removeTransaction(JtxTransaction tx) {
		ArrayList<JtxTransaction> txList = txStack.get();
		if (txList == null) {
			return false;
		}

		boolean removed = txList.remove(tx);
		if (removed) {
			totalTransactions--;
		}

		if (txList.isEmpty()) {
			txStack.remove();
		}

		return removed;
	}


	/**
	 * Returns last transaction associated with current thread or
	 * <code>null</code> when thread has no associated transactions created
	 * by this transaction manager.
	 */
	public JtxTransaction getTransaction() {
		ArrayList<JtxTransaction> txlist = txStack.get();
		if (txlist == null) {
			return null;
		}
		if (txlist.isEmpty()) {
			return null;
		}
		return txlist.get(txlist.size() - 1);	// get last
	}

	/**
	 * Associate transaction to current thread.
	 */
	protected void associateTransaction(JtxTransaction tx) {
		totalTransactions++;
		ArrayList<JtxTransaction> txList = txStack.get();
		if (txList == null) {
			txList = new ArrayList<>();
			txStack.set(txList);
		}
		txList.add(tx);	// add last
	}

	protected int totalTransactions;

	/**
	 * Returns total number of transactions issued by this transaction manager.
	 */
	public int totalTransactions() {
		return totalTransactions;
	}

	// ---------------------------------------------------------------- create

	/**
	 * Creates new {@link jodd.jtx.JtxTransaction} instance.
	 * Custom implementations of manager may override this method for
	 * creating custom transaction instances.
	 */
	protected JtxTransaction createNewTransaction(JtxTransactionMode tm, Object scope, boolean active) {
		return new JtxTransaction(this, tm, scope, active);
	}


	// ---------------------------------------------------------------- propagation

	public JtxTransaction requestTransaction(JtxTransactionMode mode) {
		return requestTransaction(mode, null);
	}

	/**
	 * Requests transaction with specified {@link JtxTransactionMode mode}.
	 * Depending on propagation behavior, it will return either <b>existing</b> or <b>new</b> transaction.
	 * Only one transaction can be opened over one scope.
	 * The exception may be thrown indicating propagation mismatch.
	 */
	public JtxTransaction requestTransaction(JtxTransactionMode mode, Object scope) {
		if (log.isDebugEnabled()) {
			log.debug("Requesting TX " + mode.toString());
		}
		JtxTransaction currentTx = getTransaction();
		if (!isNewTxScope(currentTx, scope)) {
			return currentTx;
		}
		switch (mode.getPropagationBehavior()) {
			case PROPAGATION_REQUIRED: return propRequired(currentTx, mode, scope);
			case PROPAGATION_SUPPORTS: return propSupports(currentTx, mode, scope);
			case PROPAGATION_MANDATORY: return propMandatory(currentTx, mode, scope);
			case PROPAGATION_REQUIRES_NEW: return propRequiresNew(currentTx, mode, scope);
			case PROPAGATION_NOT_SUPPORTED: return propNotSupported(currentTx, mode, scope);
			case PROPAGATION_NEVER: return propNever(currentTx, mode, scope);
		}
		throw new JtxException("Invalid TX propagation value: " + mode.getPropagationBehavior().value());
	}

	/**
	 * Returns <code>true</code> if scope is specified and it is different then of existing transaction.
	 */
	protected boolean isNewTxScope(JtxTransaction currentTx, Object destScope) {
		if (ignoreScope) {
			return true;
		}
		if (currentTx == null) {
			return true;
		}
		if (destScope == null) {
			return true;
		}
		if (currentTx.getScope() == null) {
			return true;
		}
		return !destScope.equals(currentTx.getScope());
	}

	/**
	 * Check if propagation of a transaction is possible, due to source and destination transaction modes.
	 * @see #setValidateExistingTransaction(boolean) 
	 */
	protected void continueTx(JtxTransaction sourceTx, JtxTransactionMode destMode) {
		if (!validateExistingTransaction) {
			return;
		}
		JtxTransactionMode sourceMode = sourceTx.getTransactionMode();
		JtxIsolationLevel destIsolationLevel = destMode.getIsolationLevel();
		if (destIsolationLevel != ISOLATION_DEFAULT) {
			JtxIsolationLevel currentIsolationLevel = sourceMode.getIsolationLevel();
			if (currentIsolationLevel != destIsolationLevel) {
				throw new JtxException("Participating TX specifies isolation level: " + destIsolationLevel +
						" which is incompatible with existing TX: " + currentIsolationLevel);
			}
		}
		if ((!destMode.isReadOnly()) && (sourceMode.isReadOnly())) {
			throw new JtxException("Participating TX is not marked as read-only, but existing TX is");
		}
	}


	/**
	 * Propagation: REQUIRED
	 * <pre>{@code
	 * None -> T2
	 * T1   -> T1 (cont.)
	 * }</pre>
	 */
	protected JtxTransaction propRequired(JtxTransaction currentTx, JtxTransactionMode mode, Object scope) {
		if ((currentTx == null) || (currentTx.isNoTransaction())) {
			currentTx = createNewTransaction(mode, scope, true);
		} else {
			continueTx(currentTx, mode);
		}
		return currentTx;
	}

	/**
	 * Propagation: REQUIRES_NEW
	 * <pre>{@code
	 * None -> T2
	 * T1   -> T2
	 * }</pre>
	 */
	@SuppressWarnings({"UnusedDeclaration"})
	protected JtxTransaction propRequiresNew(JtxTransaction currentTx, JtxTransactionMode mode, Object scope) {
		return createNewTransaction(mode, scope, true);
	}

	/**
	 * Propagation: SUPPORTS
	 * <pre>{@code
	 * None -> None
	 * T1   -> T1 (cont.)
	 * }</pre>
	 */
	protected JtxTransaction propSupports(JtxTransaction currentTx, JtxTransactionMode mode, Object scope) {
		if ((currentTx != null) && (!currentTx.isNoTransaction())) {
			continueTx(currentTx, mode);
		}
		if (currentTx == null) {
			currentTx = createNewTransaction(mode, scope, false);
		}
		return currentTx;
	}

	/**
	 * Propagation: MANDATORY
	 * <pre>{@code
	 * None -> Error
	 * T1   -> T1 (cont.)
	 * }</pre>
	 */
	@SuppressWarnings({"UnusedDeclaration"})
	protected JtxTransaction propMandatory(JtxTransaction currentTx, JtxTransactionMode mode, Object scope) {
		if ((currentTx == null) || (currentTx.isNoTransaction())) {
			throw new JtxException("No existing TX found for TX marked with propagation 'mandatory'");
		}
		continueTx(currentTx, mode);
		return currentTx;
	}

	/**
	 * Propagation: NOT_SUPPORTED
	 * <pre>{@code
	 * None -> None
	 * T1   -> None
	 * }</pre>
	 */
	protected JtxTransaction propNotSupported(JtxTransaction currentTx, JtxTransactionMode mode, Object scope) {
		if (currentTx == null) {
			return createNewTransaction(mode, scope, false);
		}
		if (currentTx.isNoTransaction()) {
			return currentTx;
		}
		return createNewTransaction(mode, scope, false);
	}

	/**
	 * Propagation: NEVER
	 * <pre>{@code
	 * None -> None
	 * T1   -> Error
	 * }</pre>
	 */
	protected JtxTransaction propNever(JtxTransaction currentTx, JtxTransactionMode mode, Object scope) {
		if ((currentTx != null) && (!currentTx.isNoTransaction())) {
			throw new JtxException("Existing TX found for TX marked with propagation 'never'");
		}
		if (currentTx == null) {
			currentTx = createNewTransaction(mode, scope, false);
		}
		return currentTx;
	}

	// ---------------------------------------------------------------- resources

	/**
	 * Registers new {@link JtxResourceManager resource manager}.
	 */
	public void registerResourceManager(JtxResourceManager resourceManager) {
		if ((oneResourceManager) && (!resourceManagers.isEmpty())) {
			throw new JtxException("TX manager allows only one resource manager");
		}
		this.resourceManagers.put(resourceManager.getResourceType(), resourceManager);
	}

	/**
	 * Lookups resource manager for provided type. Throws an exception if provider doesn't exists.
	 */
	protected <E> JtxResourceManager<E> lookupResourceManager(Class<E> resourceType) {
		//noinspection unchecked
		JtxResourceManager<E> resourceManager = this.resourceManagers.get(resourceType);
		if (resourceManager == null) {
			throw new JtxException("No registered resource manager for resource type: " + resourceType.getSimpleName());
		}
		return resourceManager;
	}


	// ---------------------------------------------------------------- close

	/**
	 * Closes transaction manager. All registered {@link JtxResourceManager}
	 * will be closed.
	 */
	public void close() {
		for (JtxResourceManager resourceManager : this.resourceManagers.values()) {
			try {
				resourceManager.close();
			} catch (Exception ex) {
				// ignore
			}
		}
		resourceManagers.clear();
	}

}

