/*
 * Copyright 1999-2004 The Apache Software Foundation.
 * 
 * Licensed 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.sandesha2.workers;

import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;

import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.util.threadpool.ThreadFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.sandesha2.SandeshaException;
import org.apache.sandesha2.i18n.SandeshaMessageHelper;
import org.apache.sandesha2.i18n.SandeshaMessageKeys;
import org.apache.sandesha2.storage.StorageManager;
import org.apache.sandesha2.util.SandeshaUtil;

/**
 * Aggregates pause and stop logic between sender and invoker threads.
 */
public abstract class SandeshaThread extends Thread{

	private static final Log log = LogFactory.getLog(SandeshaThread.class);

	private boolean runThread = false;
	private boolean hasStoppedRunning = false;
	private boolean hasPausedRunning = false;
	private boolean pauseRequired = false;
	
	private int sleepTime;
  private WorkerLock lock = null;

	private ArrayList workingSequences = new ArrayList();
	
	protected transient ThreadFactory threadPool;
	protected ConfigurationContext context = null;
	protected StorageManager storageManager = null;
	private boolean reRunThread;

	public SandeshaThread(int sleepTime) {
		this.sleepTime = sleepTime;
  	lock = new WorkerLock ();
	}
	
	public final WorkerLock getWorkerLock() {
		return lock;
	}
	
	public synchronized void stopThreadForSequence(String sequenceID, boolean rmSource){
		if (log.isDebugEnabled())
			log.debug("Enter: SandeshaThread::stopThreadForSequence, " + sequenceID);
		
		// We do not actually stop the thread here, as the workers are smart enough
		// to sleep when there is no work to do. If we were to exit the thread then
		// we wouldn't be able to start back up when the thread gets some more work
		// to do.
		workingSequences.remove(new SequenceEntry(sequenceID, rmSource));
		
		if (log.isDebugEnabled())
			log.debug("Exit: SandeshaThread::stopThreadForSequence");		
	}
	
	/**
	 * Waits for the invoking thread to pause
	 */
	public synchronized void blockForPause(){
		while(pauseRequired){
			//someone else is requesting a pause - wait for them to finish
			try{
				wait(sleepTime);
			}catch(InterruptedException e){
				//ignore
			}
		}
		
	  //we can now request a pause - the next pause will be ours
	  pauseRequired = true;
				
		if(hasStoppedRunning() || !isThreadStarted()){
			throw new IllegalStateException(SandeshaMessageHelper.getMessage(SandeshaMessageKeys.cannotPauseThread));
		}
		while(!hasPausedRunning){
			//wait for our pause to come around
			try{
				wait(sleepTime);
			}catch(InterruptedException e){
				//ignore
			}
			
		}
		//the sandesha thread is now paused
	}
	
	public synchronized void finishPause(){
		//indicate that the current pause is no longer required.
		pauseRequired = false;
		notifyAll();
	}
	
	public synchronized void stopRunning() {
		if (log.isDebugEnabled())
			log.debug("Enter: SandeshaThread::stopRunning, " + this);

		//NOTE: we do not take acount of pausing when stopping.
		//The call to stop will wait until the invoker has exited the loop
		if (isThreadStarted()) {
			// the invoker is started so stop it
			runThread = false;
			// wait for it to finish
			while (!hasStoppedRunning()) {
				try {
					wait(sleepTime);
				} catch (InterruptedException e1) {
					//ignore
				}
			}
		}
		
		if (log.isDebugEnabled())
			log.debug("Exit: SandeshaThread::stopRunning, " + this);
	}
	
	public synchronized boolean isThreadStarted() {

		if (!runThread && log.isDebugEnabled())
			log.debug("SandeshaThread not started");	

		return runThread;
	}
	

	/**
	 * Ensure that the worker thread is aware of the given sequence. As source sequences
	 * do not have a proper sequence id at the time they are bootstrapped, the caller
	 * must pass in the internal sequence id when rmSource is true.
	 */
	public synchronized void runThreadForSequence(ConfigurationContext context, String sequenceID, boolean rmSource){
		if(log.isDebugEnabled()) 
			log.debug("Entry: SandeshaThread::runThreadForSequence, " + this + ", " + sequenceID + ", " + rmSource);

		SequenceEntry entry = new SequenceEntry(sequenceID, rmSource);
		if (!workingSequences.contains(entry)) workingSequences.add(entry);
		
		if (!isThreadStarted()) {
			if(log.isDebugEnabled()) log.debug("Starting thread");

			this.context = context;
			
			// Get the axis2 thread pool
			threadPool = context.getThreadPool();
			
			runThread = true; // so that isStarted()=true.
			
			super.start();
			
			// Set the SandeshaThread to have the same context classloader as the application
			try{
				AccessController.doPrivileged(new PrivilegedExceptionAction() {
					public Object run() throws Exception {
						SandeshaThread.this.setContextClassLoader(Thread.currentThread().getContextClassLoader());
						return null;
					}
				});				
			}
			catch(Exception e){
				log.error(e);
				throw new RuntimeException(e);
			}
		} else {
			if(log.isDebugEnabled()) log.debug("Waking thread");
			wakeThread();
		}

		if(log.isDebugEnabled()) log.debug("Exit: SandeshaThread::runThreadForSequence");
	}
	
	/**
	 * 
	 * @return a List of SequenceEntry instances
	 */
	public synchronized ArrayList getSequences() {
		// Need to copy the list for thread safety
		ArrayList result = new ArrayList();
		result.addAll(workingSequences);
		return result;
	}

	protected synchronized boolean hasStoppedRunning() {
		return hasStoppedRunning;
	}
	
	protected synchronized void doPauseIfNeeded(){
		//see if we need to pause
			
			while(pauseRequired){
				if(!hasPausedRunning){
					//let the requester of this pause know we are now pausing
				  hasPausedRunning = true;
				  notifyAll();						
				}
				//now we pause
			  try{
			  	wait(sleepTime);
			  }catch(InterruptedException e){
			  	//ignore
			  }
			}//end while
			//the request to pause has finished so we are no longer pausing
			hasPausedRunning = false;
	}

	/**
	 * Wake the current thread as there is work to be done.
	 * Also flag that if we miss a notify, then there is 
	 * work to be done.  Implementing threads should check this value
	 * before waiting
	 *
	 */
	public synchronized void wakeThread() {
		reRunThread = true;
		
		if (!hasPausedRunning)
			notifyAll();
	}
	
	/**
	 * Indicate that the main loop has been run	 
	 */
	public synchronized void setRanMainLoop() {
		reRunThread = false;
	}
	
	/**
	 * Test to check if a notify has been called when not waiting
	 * 
	 * @return
	 */
	protected synchronized boolean runMainLoop () {
		return reRunThread;
	}
	
	/**
	 * The main work loop, to be implemented by any child class. If the child wants
	 * to sleep before the next loop then they should return true.
	 */
	protected abstract boolean internalRun();
	
	public void run() {
		try {
			boolean sleep = false;

			while (isThreadStarted()) {
				try {
					synchronized (this) {		
						if(sleep && !runMainLoop()) wait(sleepTime);
						// Indicate that we are running the main loop
						setRanMainLoop();
					}
				} catch (InterruptedException e1) {
					log.debug("SandeshaThread was interupted...");
					log.debug(e1.getMessage());
					log.debug("End printing Interrupt...");
				}

				//pause if we have to
				doPauseIfNeeded();

				// Ensure we have context and a storage manager
				if (context == null) {
					String message = SandeshaMessageHelper
							.getMessage(SandeshaMessageKeys.configContextNotSet);
					message = SandeshaMessageHelper.getMessage(
							SandeshaMessageKeys.cannotCointinueSender, message);
					log.debug(message);
					throw new RuntimeException(message);
				}

				if(storageManager == null) {
					try {
						storageManager = SandeshaUtil.getSandeshaStorageManager(context, context.getAxisConfiguration());
					} catch (SandeshaException e2) {
						String message = SandeshaMessageHelper.getMessage(SandeshaMessageKeys.cannotCointinueSender, e2.toString());
						log.debug(message);
						throw new RuntimeException(message);
					}
				}

				// Call into the real function
				sleep = internalRun();
			}
		} finally {
			// flag that we have exited the run loop and notify any waiting
			// threads
			synchronized (this) {
				if(log.isDebugEnabled()) log.debug("SandeshaThread really stopping " + this);
				hasStoppedRunning = true;
				notifyAll();
			}
		}
	}
}
