/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.support.compress;

import static java.util.concurrent.TimeUnit.MINUTES;

import freenet.support.LogThresholdCallback;
import freenet.support.TimeUtil;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.List;
import java.util.ArrayDeque;
import java.util.Queue;

import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.io.Closer;

/** Creates and manages decompressor threads. This class is 
 * given all decompressors which should be applied to an
 * InputStream via addDecompressor. The decompressors will be
 * strung together and executed when the execute method is called.
 * This class also stores any errors which may arise.
 * @author sajack
*/
public class DecompressorThreadManager {

	final Queue<DecompressorThread> threads;
	PipedInputStream input;
	PipedOutputStream output = new PipedOutputStream();
	final long maxLen;
	private boolean finished = false;
	private Throwable error = null;

        private static volatile boolean logMINOR;
	static {
		Logger.registerLogThresholdCallback(new LogThresholdCallback(){
			@Override
			public void shouldUpdate(){
				logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
			}
		});
	}

	/** Creates a new DecompressorThreadManager
	 * @param inputStream The stream that will be decompressed, if compressed
	 * @param maxLen The maximum number of bytes to extract
	 */
	public DecompressorThreadManager(PipedInputStream inputStream, List<? extends Compressor> decompressors, long maxLen) throws IOException {
		threads = new ArrayDeque<DecompressorThread>(decompressors.size());
		this.maxLen = maxLen;
		if(inputStream == null) {
			IOException e = new IOException("Input stream may not be null");
			onFailure(e);
			throw e;
		}
		input = inputStream;
		while(!decompressors.isEmpty()) {
			Compressor compressor = decompressors.remove(decompressors.size()-1);
			if(logMINOR) Logger.minor(this, "Decompressing with "+compressor);
			DecompressorThread thread = new DecompressorThread(compressor, this, input, output, maxLen);
			threads.add(thread);
			input = new PipedInputStream(output);
			output = new PipedOutputStream();
		}
	}

	/** Creates and executes a new thread for each decompressor,
	 * chaining the output of the previous to the next.
	 * @return An InputStream from which uncompressed data may be read from
	 */
	public synchronized PipedInputStream execute() throws Throwable {
		if(error != null) throw error;
		if(threads.isEmpty()) {
			onFinish();
			return input;
		}
		try {
			int count = 0;
			while(!threads.isEmpty()){
				if(getError() != null) throw getError();
				DecompressorThread threadRunnable = threads.remove();
				if(threads.isEmpty()) threadRunnable.setLast();
				Thread t = new Thread(threadRunnable, "DecompressorThread"+count);
				t.start();
				if(logMINOR) Logger.minor(this, "Started decompressor thread "+t);
				count++;
			}
			output.close();
		} catch(Throwable t) {
			onFailure(t);
			throw t;
		} finally {
			Closer.close(output);
		}
		return input;
		
	}

	/** Informs the manager that a nonrecoverable exception has occured in the
	 * decompression threads
	 * @param e The thrown exception
	 */
	public synchronized void onFailure(Throwable t) {
		error = t;
		onFinish();
	}

	/** Marks that the decompression of the stream has finished and wakes
	 * threads blocking on completion */
	public synchronized void onFinish() {
		finished = true;
		notifyAll();
	}

	/** Blocks until all threads have finished executing and cleaning up.*/
	public synchronized void waitFinished() throws Throwable {
		long start = System.currentTimeMillis();
		while(!finished) {
			try {
				// FIXME remove the timeout here.
				// Something wierd is happening...
				//wait(0)
				wait(MINUTES.toMillis(20));
				long time = System.currentTimeMillis()-start;
				if(time > MINUTES.toMillis(20))
					Logger.error(this, "Still waiting for decompressor chain after "+TimeUtil.formatTime(time));
			} catch(InterruptedException e) {
				//Do nothing
			}
		}
		if(error != null) throw error;
	}

	/** Returns an exception which was thrown during decompression
	 * @return Returns an exception which was caught during the decompression
	 */
	public synchronized Throwable getError() {
		return error;
	}

	/**Represents a thread which invokes a decompressor upon an
	 * input stream. These threads should be instantiated by a
	 * <code>DecompressorThreadManager</code>
	 * @author sajack
	 */
	class DecompressorThread implements Runnable {

		/**The compressor whose decompress method will be invoked*/
		final Compressor compressor;
		/**The stream compressed data will be read from*/
		private InputStream input;
		/**The stream decompressed data will be written*/
		private OutputStream output;
		/**A upper limit to how much data may be decompressed. This is passed to the decompressor*/
		final long maxLen;
		/**The manager which created the thread*/
		final DecompressorThreadManager manager;
		/**Whether or not this thread should signal the manager that decompression has finished*/
		boolean isLast = false;

		public DecompressorThread(Compressor compressor, DecompressorThreadManager manager, InputStream input, PipedOutputStream output, long maxLen) {
			this.compressor = compressor;
			this.input = new BufferedInputStream(input);
			this.output = new BufferedOutputStream(output);
			this.maxLen = maxLen;
			this.manager = manager;
		}

		/**Begins the decompression */
		@Override
		public void run() {
			if(logMINOR) Logger.minor(this, "Decompressing...");
			try {
				if(manager.getError() == null) {
					compressor.decompress(input, output, maxLen, maxLen * 4);
					input.close();
					output.close();
					// Avoid relatively expensive repeated close on normal completion
					input = null;
					output = null;
					if(isLast) manager.onFinish();
				}
				if(logMINOR) Logger.minor(this, "Finished decompressing...");
			} catch (Exception e) {
				manager.onFailure(e);
			} finally {
				Closer.close(input);
				Closer.close(output);
			}
		}

		/** Should be called before executing the thread when there 
		 * are no further decompressors pending*/
		public void setLast() {
			isLast = true;
		}
	}
}