/*
 * LparseWrapper.java
 *
 * Copyright (C) 2006 - 2007 Martin Slota
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

/*
 * History:
 * v0.1 (2007-01-06): initial version
 * v0.2 (2007-01-15):
 * - tests added
 * v0.2.1 (2007-01-27):
 * - tests rewritten
 * - documentation added
 * v0.2.2 (2007-01-28):
 * - implementation changed so that setInput and setOptions handle null
 *   correctly (i.e. set the corresponding parameter to an empty string)
 * - documentation updated
 * v0.2.3 (2007-02-12):
 * - deleted the throws declaration for exec()
 * v0.2.4 (2007-02-14):
 * - merged with LparseOutputFilter
 * v0.2.5 (2007-02-17):
 * - setLparsePath added
 * v0.2.6 (2007-03-06):
 * - a constructor removed
 * - documentation fixed
 * - test of setLparsePath
 * v1.0.0 (2007-05-05):
 * - changed to make parallel pipelining possible
 */

package lp.wrap;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * A class responsible for executing lparse with the given command line
 * arguments. It also parses the resulting error stream and creates
 * corresponding {@link LparseMessage} objects.
 *
 * The command line options can be set by calling the
 * {@link #setOptions(String)} method. Initially they are set to am empty
 * string.
 *
 * Lparse can be executed by calling the {@link #exec()} method. This method can
 * be called multiple times and each time the current command line options
 * are passed to the lparse binary.
 *
 * @author Martin Slota
 * @version 1.0.0
 */
public class LparseWrapper {
	/**
	 * The singleton {@link WrapperUtils} instance used in the implementation.
	 */
	private static final WrapperUtils WU = WrapperUtils.getInstance();
	
	/**
	 * The path to the lparse binary.
	 */
	private String lparsePath;
	
	/**
	 * The command line options used when executing lparse in the
	 * {@link #exec()} method.
	 */
	private String options;
	
	/**
	 * The lparse process.
	 */
	private Process process;
	
	/**
	 * A {@link Runnable} that keeps reading and parsing the standard error
	 * output of the lparse process while it is being executed. This prevents
	 * reaching the upper limit of the native operating system's buffer.
	 */
	private final ErrorStreamProcessor errorProcessor;
	
	/**
	 * The thread on which {@link #errorProcessor} is running.
	 */
	private Thread errorThread;
	
	/**
	 * Contains the lparse error message if one is found on its standard error
	 * output.
	 */
	private LparseMessage error;
	
	/**
	 * A list of warnings that lparse issued on the previous input.
	 */
	private final List<LparseMessage> warnings;
	
	/**
	 * Creates a new instance of {@code LparseWrapper}.
	 *
	 * Initially the path to the lparse binary path is set to "lparse", i.e. it
	 * is assumed that lparse can be executed by issuing the "lparse" command on
	 * the command line of the native operating system. This setting can be
	 * changed by calling the {@link #setLparsePath(String)} method.
	 *
	 * Command line options for lparse are set to an empty string and can be set
	 * to a different value using the {@link #setOptions(String)} method.
	 */
	public LparseWrapper() {
		lparsePath = "lparse";
		options = "";
		process = null;
		errorProcessor = new ErrorStreamProcessor();
		errorThread = null;
		error = null;
		warnings = new ArrayList<LparseMessage>();
	}
	
	/**
	 * Sets the path to lparse binary that is used to invoke lparse.
	 *
	 * @param lparsePath path to the lparse binary
	 * @throws IllegalArgumentException if {@code lparsePath} is {@code null} or
	 * an empty string
	 */
	public void setLparsePath(String lparsePath) {
		if (lparsePath == null || "".equals(lparsePath)) {
			throw new IllegalArgumentException(
					"The path to lparse must not be null or empty!");
		}
		this.lparsePath = lparsePath;
	}
	
	/**
	 * Sets the command line options that are passed to lparse by the
	 * {@link #exec()} method.
	 *
	 * If {@code null} is passed in as a parameter, options string is set to an
	 * empty string.
	 *
	 * @param options command line options for lparse
	 * @return a reference to this {@code LparseWrapper}
	 */
	public LparseWrapper setOptions(String options) {
		this.options = options == null ? "" : options;
		return this;
	}
	
	/**
	 * Executes lparse and passes the current command line options to it (see
	 * {@link #setOptions(String method)}).
	 *
	 * @throws WrapperException if an {@link IOException} occurs while creating
	 * the process
	 */
	public void exec() {
		process = WU.exec(lparsePath + " " + options/*, input*/);
		errorProcessor.setErrorStream(process.getErrorStream());
		errorThread = new Thread(errorProcessor);
		errorThread.start();
	}
	
	/**
	 * Returns the standard input stream of the currently executed lparse
	 * process.
	 *
	 * @return as specified above
	 */
	public OutputStream getStdin() {
		return process.getOutputStream();
	}
	
	/**
	 * Closes the standard output stream of the currently executed lparse
	 * process.
	 *
	 * @throws WrapperException if an I/O error occurs while closing the
	 * standard input stream of the lparse process
	 */
	public void closeStdin() {
		try {
			process.getOutputStream().close();
		} catch (IOException e) {
			throw new WrapperException("Error while closing the standard " +
					"input stream of the lparse process", e);
		}
	}
	
	/**
	 * Returns the standard output stream of the currently executed lparse
	 * process.
	 *
	 * @return as specified above
	 */
	public InputStream getStdout() {
		return process.getInputStream();
	}
	
	/**
	 * Waits for the currently executed lparse process to finish computation and
	 * returns its exit value (see {@link Process#waitFor()}.
	 *
	 * @return the return value of the lparse process
	 * @throws WrapperException if an {@link InterruptedException} occurs while
	 * waiting for the error stream thread or lparse process to finish their
	 * tasks
	 */
	public int waitFor() {
		try {
			errorThread.join();
		} catch (InterruptedException e) {
			throw new WrapperException(	"A thread reading standard error " +
					"output of the process was interrupted!", e);
		}
		
		try {
			return process.waitFor();
		} catch (InterruptedException e) {
			throw new WrapperException("The process was interrupted!", e);
		}
	}
	
	/**
	 * Returns an lparse error message that was issued by lparse on the last
	 * input. Should only be called after {@link #waitFor()} was called.
	 *
	 * @return as specified above
	 */
	public LparseMessage getError() {
		return error;
	}
	
	/**
	 * Returns lparse warning messages that were issued by lparse on the last
	 * input. Should only be called after {@link #waitFor()} was called.
	 *
	 * @return as specified above
	 */
	public List<LparseMessage> getWarnings() {
		return warnings;
	}
	
	/**
	 * A helper thread that reads and parses contents of an lparse standard
	 * error stream and stores the resulting {@link LparseMessage} objects in
	 * {@link LparseWrapper#error} and {@link LparseWrapper#warnings}.
	 */
	private class ErrorStreamProcessor implements Runnable {
		/**
		 * The {@code InputStream} to read stream.
		 */
		private InputStream stream;
		
		/**
		 * Constructs a new instance of {@code ErrorStreamProcessor}.
		 */
		public ErrorStreamProcessor() {
			stream = null;
		}
		
		/**
		 * Sets the error stream to read from.
		 */
		public void setErrorStream(InputStream input) {
			stream = input;
			error = null;
			warnings.clear();
		}
		
		/**
		 * Runs the thread&#8212;here the stream set by
		 * {@link #setErrorStream(InputStream)} is read and parsed. Resulting
		 * {@link LparseMessage} objects are stored in
		 * {@link LparseWrapper#error} and {@link LparseWrapper#warnings}.
		 *
		 * @throws IOException (wrapped in an {@link WrapperException}) if an
		 * I/O error occurs while parsing the stream
		 */
		public void run() {
			try {
				parseMessages();
			} catch (IOException e) {
				throw new WrapperException("An I/O error occured while " +
						"reading the standard error input!", e);
			}
		}
		
		/**
		 * Parses all errors and warnings issued by lparse from its error output
		 * stream.
		 *
		 * @throws IOException if an I/O error occurs while parsing the stream
		 */
		private void parseMessages() throws IOException {
			BufferedReader stderrReader = new BufferedReader(
					new InputStreamReader(stream));
			LparseMessage current = null;
			String line = stderrReader.readLine();
			int lineNumber;
			
			boolean isEmptyStderr = true;
			StringBuilder message = new StringBuilder();
			while (line != null) {
				boolean isWarning = true;
				if (!"".equals(line.trim()))
					isEmptyStderr = false;
				int firstColon = line.indexOf(':');
				if (firstColon == -1) {
					line = stderrReader.readLine();
				} else {
					lineNumber = Integer.valueOf(
							line.substring(0, firstColon));
					String rest = line.substring(firstColon + 1);
					int warningPos = rest.indexOf("Warning:");
					if (warningPos == -1) {
						isWarning = false;
						int errorPos = rest.indexOf("Error:");
						if (errorPos != -1)
							rest = rest.substring(errorPos + "Error:".length());
					} else {
						rest = rest.substring(warningPos + "Warning:".length());
					}
					rest = rest.trim();
					message.setLength(0);
					message.append(Character.toUpperCase(rest.charAt(0)));
					message.append(rest.substring(1));
					line = stderrReader.readLine();
					while (line != null && line.length() > 0 &&
							Character.isWhitespace(line.charAt(0))) {
						message.append(' ');
						line  = line.trim();
						if (message.charAt(message.length() - 2) == '.') {
							message.append(Character.toUpperCase(line.charAt(0)));
							message.append(line.substring(1));
						} else {
							message.append(line);
						}
						line = stderrReader.readLine();
					}
					current = new LparseMessage(message.toString(),
							lineNumber, isWarning);
					if (isWarning) {
						warnings.add(current);
					} else {
						error = current;
					}
				}
			}
			if (!isEmptyStderr && error == null && warnings.isEmpty()) {
				error = new LparseMessage(null, -1);
			}
		}
	}
}