package com.refactorerl.ui.presentation;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.console.IConsoleConstants;
import org.eclipse.ui.console.IConsoleManager;
import org.eclipse.ui.console.IConsoleView;
import org.eclipse.ui.console.IOConsole;
import org.eclipse.ui.progress.UIJob;

import com.refactorerl.ui.communication.process.IOHandler;

public class DebugConsoles {
	private final static String DEBUG_CONSOLE_NAME = "RefactorErl Debug Console";

	private static IConsole findConsole(String name) {
		ConsolePlugin plugin = ConsolePlugin.getDefault();
		IConsoleManager conMan = plugin.getConsoleManager();
		IConsole[] existing = conMan.getConsoles();
		for (int i = 0; i < existing.length; i++)
			if (name.equals(existing[i].getName()))
				return existing[i];

		return null;
	}

	private static IOConsole findMessageConsole(String name) {
		IConsole console = findConsole(name);
		if (console != null) {
			if (console instanceof IOConsole) {
				return (IOConsole) console;
			} else
				throw new IllegalArgumentException(
						"Name is already used by a non-IOConsole console");
		}
		ConsolePlugin plugin = ConsolePlugin.getDefault();
		IConsoleManager conMan = plugin.getConsoleManager();
		IOConsole mconsole = new IOConsole(name, null);
		conMan.addConsoles(new IConsole[] { mconsole });
		return mconsole;
	}

	private static Map<IOConsole, OutputStream> outputStreams = new HashMap<>();

	// eclipse bug(?): repeating IOConsole.newOutputStream() causes
	// IOConsoleInputStream.readLine() to crash at some point. hence singleton.
	// the created OutputStream never gets closed.
	private static OutputStream getConsoleOutputStream(IOConsole console) {
		OutputStream os = outputStreams.get(console);
		if (os == null) {
			os = console.newOutputStream();
			outputStreams.put(console, os);
		}

		return os;
	}

	// todo(?): all instance could have its own console with a counter
	// closed consoles could be marked by changing their names
	// or something
	public static class DebugConsole implements IOHandler {

		private final IOConsole console;
		private PrintWriter toConsole;
		private BufferedReader fromConsole;
		private PrintWriter toProcess;
		private BufferedReader fromProcess;
		private BufferedReader errorFromProcess;

		private boolean opened = false;
		private Thread process2Console;
		private Thread console2Process;
		private Thread errProcess2Console;

		public DebugConsole() {
			console = findMessageConsole(DEBUG_CONSOLE_NAME);
		}

		public void open(InputStream fromProcessStream,
				InputStream errorFromProcessStream, OutputStream toProcessStream) {
			if (opened)
				throw new IllegalStateException("Console already opened");
			console.clearConsole();
			toConsole = new PrintWriter(getConsoleOutputStream(console));
			fromConsole = new BufferedReader(new InputStreamReader(
					console.getInputStream()));

			toProcess = new PrintWriter(toProcessStream);
			fromProcess = new BufferedReader(new InputStreamReader(
					fromProcessStream));
			errorFromProcess = new BufferedReader(new InputStreamReader(
					errorFromProcessStream));

			startProcessToConsoleThread();
			startConsoleToProcessThread();
			// startProcessErrorToConsoleThread(); // not needed

			opened = true;
		}

		public void close() throws IOException {
			if (!opened)
				throw new IllegalStateException(
						"Console not opened or already closed.");
			opened = false;
			
			console2Process.interrupt();
			process2Console.interrupt();
			// errProcess2Console.interrupt();
			// toConsole.close();
		}

		private void startConsoleToProcessThread() {
			console2Process = new Thread(new Runnable() {

				@Override
				public void run() {
					String line = "";
					do {
						try {
							// eclipse bug?
							line = fromConsole.readLine();
							toProcess.println(line);
							toProcess.flush();
						} catch (IOException e) {
							if (opened)
								e.printStackTrace();
							break;
						}
					} while (opened);
				}
			});
			console2Process.start();
		}

		private void startProcessToConsoleThread() {
			process2Console = new Thread(new Runnable() {

				@Override
				public void run() {
					do {
						try {
							int c = fromProcess.read();
							if (c == -1)
								break;

							toConsole.write(c);
							toConsole.flush();

							job.cancel();
							job.schedule(500);

							// expensive but safer alternative:
							// job.schedule();
							// job.join();

						} catch (IOException e) {
							if (opened)
								e.printStackTrace();
							break;
						}
					} while (opened);
				}
			});
			process2Console.start();
		}

		private void startProcessErrorToConsoleThread() {
			errProcess2Console = new Thread(new Runnable() {

				@Override
				public void run() {
					String line = "";
					do {
						try {
							line = errorFromProcess.readLine();
							toConsole.println(line);
							toConsole.flush();
						} catch (IOException e) {
							if (opened)
								e.printStackTrace();
							break;
						}
					} while (opened);
				}
			});
			errProcess2Console.start();
		}

		private UIJob job = new UIJob("writeToDebugConsole") {

			@Override
			public IStatus runInUIThread(IProgressMonitor monitor) {
				try {
					IWorkbench wb = PlatformUI.getWorkbench();
					IWorkbenchPage page = wb.getActiveWorkbenchWindow()
							.getActivePage();
					String id = IConsoleConstants.ID_CONSOLE_VIEW;

					console.activate();

					IConsoleView view = (IConsoleView) page.showView(id);
					view.warnOfContentChange(console);
					view.display(console);

				} catch (PartInitException e) {
					e.printStackTrace();
					try {
						close();
					} catch (IOException e2) {
						e2.printStackTrace();
					}

				}
				return Status.OK_STATUS;
			}
		};
	};

}
