package scribtest;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.commons.io.IOUtils;


public class ExecUtil {
	/**
	 * The summary of a process.
	 * @author Tiago Cogumbreiro (cogumbreiro@users.sf.net)
	 *
	 */
	public static class ProcessSummary {
		/**
		 * The standard output.
		 */
		public final String stdout;
		/**
		 * The standard error output.
		 */
		public final String stderr;
		/**
		 * The exit value of the process.
		 */
		public final int exitValue;
		public ProcessSummary(String stdout, String stderr, int exitValue) {
			this.stdout = stdout;
			this.stderr = stderr;
			this.exitValue = exitValue;
		}
	}
	public static String joinPath(String ...parts) {
		return join(File.separator, parts);
	}
	public static String joinPath(Iterable<String> parts) {
		return join(File.separator, parts);
	}
	public static String join(String separator, String ...parts) {
		int count = 0;
		StringBuilder builder = new StringBuilder();
		for (String part : parts) {
			if (count > 0) {
				builder.append(separator);
			}
			builder.append(part);
			count++;
		}
		return builder.toString();
	}
	public static String join(String separator, Iterable<String> parts) {
		int count = 0;
		StringBuilder builder = new StringBuilder();
		for (String part : parts) {
			if (count > 0) {
				builder.append(separator);
			}
			builder.append(part);
			count++;
		}
		return builder.toString();
	}
	
	/**
	 * Runs a process, up to a certain timeout,
	 * 
	 * @throws IOException
	 * @throws TimeoutException
	 * @throws ExecutionException
	 * @throws InterruptedException
	 */
	public static ProcessSummary execUntil(long timeout, String... command)
			throws IOException, InterruptedException, ExecutionException {
		return execUntil(new TreeMap<String, String>(), timeout, command);
	}
	
	/**
	 * Runs a process, up to a certain timeout,
	 * 
	 * @throws IOException
	 * @throws TimeoutException
	 * @throws ExecutionException
	 * @throws InterruptedException
	 */
	public static ProcessSummary execUntil(Map<String, String> env, long timeout, String... command)
			throws IOException, InterruptedException, ExecutionException {
		ProcessBuilder builder = new ProcessBuilder(command);
		builder.environment().putAll(env);
		Process process = builder.start();
		ExecutorService executorService = Executors.newFixedThreadPool(3);
		CyclicBarrier onStart = new CyclicBarrier(3);
		Future<String> stderr = executorService.submit(toString(process.getErrorStream(),
				onStart));
		Future<String> stdout = executorService.submit(toString(process.getInputStream(),
				onStart));
		Future<Integer> waitFor = executorService.submit(waitFor(process,
				onStart));
		ProcessSummary result = null;
		try {
			waitFor.get(timeout, TimeUnit.MILLISECONDS);
		} catch (TimeoutException e) {
			// timeouts are ok
		} finally {
			process.destroy();
			waitFor.get();
			result = new ProcessSummary(stdout.get(), stderr.get(), process.exitValue());
			executorService.shutdown();
		}
		return result;
	}

	/**
	 * Waits for a process to terminate.
	 * 
	 * @param process The process it should wait for.
	 * @param onStart Notifies others upon starting.
	 * @return The return code.
	 */
	public static Callable<Integer> waitFor(final Process process,
			final CyclicBarrier onStart) {
		return new Callable<Integer>() {
			public Integer call() throws InterruptedException, BrokenBarrierException {
				onStart.await();
				return process.waitFor();
			}
		};
	}

	/**
	 * Converts an input stream to a string as much as possible. It stops
	 * at IOExceptions, but returns what it read thus far.
	 * @param in The input stream.
	 * @param onStart Notify others that it started. 
	 */
	private static Callable<String> toString(final InputStream input, final CyclicBarrier onStart) {
		return new Callable<String>() {
			public String call() throws InterruptedException, BrokenBarrierException, IOException {
				onStart.await();
				try {
					return IOUtils.toString(input);
				} finally {
					IOUtils.closeQuietly(input);
				}
			}
		};
	}

}
