package net.filebot.cli;

import static net.filebot.Logging.*;
import static net.filebot.util.ui.SwingUI.*;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dialog.ModalExclusionType;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import javax.script.Bindings;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JSplitPane;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.text.JTextComponent;

import org.fife.ui.rsyntaxtextarea.FileLocation;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.fife.ui.rsyntaxtextarea.TextEditorPane;
import org.fife.ui.rtextarea.RTextScrollPane;

import net.filebot.ApplicationFolder;
import net.filebot.ResourceManager;
import net.filebot.Settings;
import net.filebot.util.TeePrintStream;

public class GroovyPad extends JFrame {

	public static final String DEFAULT_SCRIPT = "runScript 'sysinfo'";

	public GroovyPad() throws IOException {
		super("Groovy Pad");

		JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, createEditor(), createOutputLog());
		splitPane.setResizeWeight(0.7);

		JComponent c = (JComponent) getContentPane();
		c.setLayout(new BorderLayout(0, 0));
		c.add(splitPane, BorderLayout.CENTER);

		JToolBar tools = new JToolBar("Run", JToolBar.HORIZONTAL);
		tools.setFloatable(true);
		tools.add(run);
		tools.add(cancel);
		tools.addSeparator();
		tools.add(newAction(DEFAULT_SCRIPT, ResourceManager.getIcon("status.info"), evt -> runScript(DEFAULT_SCRIPT)));
		c.add(tools, BorderLayout.NORTH);

		run.setEnabled(true);
		cancel.setEnabled(false);

		installAction(c, KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), run);
		installAction(c, KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK), run);

		addWindowListener(new WindowAdapter() {

			@Override
			public void windowClosed(WindowEvent evt) {
				cancel.actionPerformed(null);
				console.unhook();

				try {
					editor.save();
				} catch (IOException e) {
					// ignore
				}
			}
		});

		console = new MessageConsole(output);
		console.hook();

		shell = createScriptShell();
		editor.requestFocusInWindow();

		setModalExclusionType(ModalExclusionType.TOOLKIT_EXCLUDE);
		setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
		setLocationByPlatform(true);
		setSize(800, 600);
	}

	protected MessageConsole console;
	protected TextEditorPane editor;
	protected TextEditorPane output;

	protected JComponent createEditor() {
		editor = new TextEditorPane(TextEditorPane.INSERT_MODE, false);
		editor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_GROOVY);
		editor.setAutoscrolls(false);
		editor.setAnimateBracketMatching(false);
		editor.setAntiAliasingEnabled(true);
		editor.setAutoIndentEnabled(true);
		editor.setBracketMatchingEnabled(true);
		editor.setCloseCurlyBraces(true);
		editor.setClearWhitespaceLinesEnabled(true);
		editor.setCodeFoldingEnabled(true);
		editor.setHighlightSecondaryLanguages(false);
		editor.setRoundedSelectionEdges(false);
		editor.setTabsEmulated(false);

		try {
			// use this default value so people can easily submit bug reports with fn:sysinfo logs
			File pad = ApplicationFolder.AppData.resolve("pad.groovy");

			if (!pad.exists()) {
				ScriptShellMethods.saveAs(DEFAULT_SCRIPT, pad);
			}

			// restore on open
			editor.load(FileLocation.create(pad), "UTF-8");
		} catch (Exception e) {
			debug.log(Level.WARNING, e, e::toString);
		}

		return new RTextScrollPane(editor, true);
	}

	protected JComponent createOutputLog() throws IOException {
		output = new TextEditorPane(TextEditorPane.INSERT_MODE, false);
		output.setEditable(false);
		output.setReadOnly(true);
		output.setAutoscrolls(true);
		output.setBackground(new Color(255, 255, 218));

		output.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_NONE);
		output.setAnimateBracketMatching(false);
		output.setAntiAliasingEnabled(true);
		output.setAutoIndentEnabled(false);
		output.setBracketMatchingEnabled(false);
		output.setCloseCurlyBraces(false);
		output.setClearWhitespaceLinesEnabled(false);
		output.setCodeFoldingEnabled(false);
		output.setHighlightCurrentLine(false);
		output.setHighlightSecondaryLanguages(false);
		output.setRoundedSelectionEdges(false);
		output.setTabsEmulated(false);

		return new RTextScrollPane(output, true);
	}

	protected final ScriptShell shell;

	protected ScriptShell createScriptShell() {
		try {
			return new ScriptShell(s -> ScriptSource.GITHUB_STABLE.getScriptProvider(s).getScript(s), new CmdlineOperations(), new HashMap<String, Object>());
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	protected final Action run = newAction("Run", ResourceManager.getIcon("script.go"), this::runScript);
	protected final Action cancel = newAction("Cancel", ResourceManager.getIcon("script.cancel"), this::cancelScript);

	private Runner currentRunner = null;

	public void runScript(ActionEvent evt) {
		try {
			editor.save();
		} catch (IOException e) {
			debug.log(Level.WARNING, e, e::toString);
		}

		runScript(editor.getText());
	}

	public void runScript(String script) {
		if (currentRunner == null || currentRunner.isDone()) {
			currentRunner = new Runner(script) {

				@Override
				protected void done() {
					run.setEnabled(true);
					cancel.setEnabled(false);
				}
			};

			run.setEnabled(false);
			cancel.setEnabled(true);
			output.setText(null);

			currentRunner.execute();
		}
	}

	@SuppressWarnings("deprecation")
	protected void cancelScript(ActionEvent evt) {
		if (currentRunner != null && !currentRunner.isDone()) {
			currentRunner.cancel(true);
			currentRunner.getExecutionThread().stop();

			try {
				currentRunner.get(2, TimeUnit.SECONDS);
			} catch (Exception e) {
				debug.log(Level.WARNING, e, e::getMessage);
			}
		}
	}

	protected class Runner extends SwingWorker<Object, Object> {

		private Thread executionThread;
		private Object result;

		public Runner(final String script) {
			executionThread = new Thread("GroovyPadRunner") {

				@Override
				public void run() {
					try {
						Bindings bindings = new SimpleBindings();
						bindings.put(ScriptShell.SHELL_ARGS_BINDING_NAME, Settings.getApplicationArguments());
						bindings.put(ScriptShell.ARGV_BINDING_NAME, Settings.getApplicationArguments().getFiles(false));

						result = shell.evaluate(script, bindings);

						// print result and make sure to flush Groovy output
						SimpleBindings resultBindings = new SimpleBindings();
						resultBindings.put("result", result);
						if (result != null) {
							shell.evaluate("print('Result: '); println(result);", resultBindings);
						} else {
							shell.evaluate("println();", resultBindings);
						}
					} catch (ScriptException e) {
						while (e.getCause() instanceof ScriptException) {
							e = (ScriptException) e.getCause();
						}
						e.printStackTrace();
					} catch (Throwable e) {
						e.printStackTrace();
					}
				};
			};

			executionThread.setDaemon(false);
			executionThread.setPriority(Thread.MIN_PRIORITY);
		}

		@Override
		protected Object doInBackground() throws Exception {
			executionThread.start();
			executionThread.join();
			return result;
		}

		public Thread getExecutionThread() {
			return executionThread;
		}
	};

	public static class MessageConsole {

		private final PrintStream system_out = System.out;
		private final PrintStream system_err = System.err;

		private JTextComponent textComponent;

		public MessageConsole(JTextComponent textComponent) {
			this.textComponent = textComponent;
		}

		public void hook() {
			try {
				System.setOut(new TeePrintStream(new ConsoleOutputStream(), true, "UTF-8", system_out));
				System.setErr(new TeePrintStream(new ConsoleOutputStream(), true, "UTF-8", system_err));
			} catch (UnsupportedEncodingException e) {
				debug.log(Level.WARNING, e, e::getMessage);
			}
		}

		public void unhook() {
			System.setOut(system_out);
			System.setErr(system_err);
		}

		private class ConsoleOutputStream extends ByteArrayOutputStream {

			@Override
			public void flush() {
				try {
					String message = this.toString("UTF-8");
					reset();
					commit(message);
				} catch (Exception e) {
					// can't happen
				}
			}

			private void commit(final String line) {
				SwingUtilities.invokeLater(() -> {
					try {
						int offset = textComponent.getDocument().getLength();
						textComponent.getDocument().insertString(offset, line, null);
						textComponent.setCaretPosition(textComponent.getDocument().getLength());
					} catch (Exception e) {
						// ignore
					}
				});
			}
		}
	}

}
