package com.hammurapi.common.concurrent.transactions;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;

import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

import com.hammurapi.common.concurrent.Logger;

/**
 * Collects undo tasks and executes them if transaction rolls back.
 * @author Pavel Vlasov
 *
 */
class Undoer implements XAResource {
	
	private ProgressMonitor progressMonitor;

	private Logger logger;

	Undoer(ProgressMonitor progressMonitor, Logger logger) {
		this.progressMonitor = progressMonitor;
		this.logger = logger;
	}
	
	static final Level DEBUG_LEVEL = Level.FINE;
	
	private Map<Xid, Collection<Runnable>> undoTasks = new ConcurrentHashMap<Xid, Collection<Runnable>>();
	private int transactionTimeout;

	@Override
	public void commit(Xid xid, boolean onePhase) throws XAException {
		logger.log(DEBUG_LEVEL, "[Undoer] commit("+xid+", "+onePhase+")");
		// Commit - no need to execute undo-s - just clean up.
		Collection<Runnable> xidTasks = undoTasks.remove(xid);
		if (xidTasks==null) {
			throw new XAException(XAException.XAER_NOTA);
		}
	}
	
	private Xid activeXid;

	@Override
	public void end(Xid xid, int flag) throws XAException {
		logger.log(DEBUG_LEVEL,  Thread.currentThread()+" [Undoer] end("+xid+", "+flag+")");
		if (activeXid == null || !activeXid.equals(xid)) {
			throw new XAException(XAException.XAER_INVAL);
		}
		activeXid = null;
		// Do not allow any more changes.
		if ((flag & TMSUSPEND)==0) {
			Collection<Runnable> xidTasks = undoTasks.get(xid);
			if (xidTasks==null) {
				throw new XAException(XAException.XAER_NOTA);
			}
			undoTasks.put(xid, Collections.unmodifiableCollection(xidTasks));
		}			
	}

	@Override
	public void forget(Xid xid) throws XAException {
		logger.log(DEBUG_LEVEL, "[Undoer] forget("+xid+")");
		commit(xid, false);			
	}

	@Override
	public int getTransactionTimeout() throws XAException {
		return transactionTimeout;
	}

	@Override
	public boolean isSameRM(XAResource other) throws XAException {
		return equals(other);
	}

	@Override
	public int prepare(Xid xid) throws XAException {
		return XA_OK;
	}

	@Override
	public Xid[] recover(int flags) throws XAException {
		logger.log(DEBUG_LEVEL, "[Undoer] recover("+flags+")");
		return undoTasks.keySet().toArray(new Xid[undoTasks.size()]);
	}

	@Override
	public void rollback(Xid xid) throws XAException {
		logger.log(DEBUG_LEVEL, "[Undoer] rollback("+xid+")");
		Collection<Runnable> xidTasks = undoTasks.get(xid);
		if (xidTasks==null) {
			throw new XAException(XAException.XAER_NOTA);
		}
		boolean hadExceptions = false;
		int counter = 0;
		for (Runnable task: xidTasks) {
			try {
				if (progressMonitor!=null) {
					progressMonitor.onProgress(counter, 1, xidTasks.size()-counter, "Executing undo task: "+task);
				}
				logger.finer("Executing undo task "+task);
				task.run();
				++counter;
			} catch (Exception e) {
				logger.log(Level.SEVERE, "Error executing undo task: "+e, e);
				hadExceptions = true;
			}
		}
		if (hadExceptions) {
			throw new XAException("There were exceptions during rollback");
		}
	}

	@Override
	public boolean setTransactionTimeout(int transactionTimeout) throws XAException {
		this.transactionTimeout = transactionTimeout;
		return true;
	}

	@Override
	public void start(Xid xid, int flag) throws XAException {
		if (activeXid != null) {
			throw new XAException(XAException.XAER_INVAL);
		}
		activeXid = xid;
		logger.log(DEBUG_LEVEL, "[Undoer] start("+xid+", "+flag+")");
		Collection<Runnable> xidTasks = undoTasks.get(xid);
		if (xidTasks==null) {
			xidTasks = new ConcurrentLinkedQueue<Runnable>();
			undoTasks.put(xid, xidTasks);
		}
		activeXid = xid;
	}
	
	void addUndoTask(Runnable undoTask) throws XAException {
		logger.log(DEBUG_LEVEL, Thread.currentThread()+" [Undoer] addUndoTask()");
		if (activeXid == null) {
			throw new XAException(XAException.XAER_INVAL);
		}
		Collection<Runnable> xidTasks = undoTasks.get(activeXid);
		if (xidTasks==null) {
			throw new XAException(XAException.XAER_NOTA);
		}
		xidTasks.add(undoTask);			
	}
	
}