/*
 * Copyright (c) 2013-2022 Chris Newland.
 * Licensed under https://github.com/AdoptOpenJDK/jitwatch/blob/master/LICENSE-BSD
 * Instructions: https://github.com/AdoptOpenJDK/jitwatch/wiki
 */
package org.adoptopenjdk.jitwatch.ui.sandbox;

import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_ASTERISK;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_EMPTY;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_NEWLINE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.VM_LANGUAGE_CLOJURE;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.VM_LANGUAGE_GROOVY;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.VM_LANGUAGE_JAVA;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.VM_LANGUAGE_JAVASCRIPT;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.VM_LANGUAGE_JRUBY;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.VM_LANGUAGE_KOTLIN;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.VM_LANGUAGE_SCALA;
import static org.adoptopenjdk.jitwatch.util.UserInterfaceUtil.FONT_MONOSPACE_FAMILY;
import static org.adoptopenjdk.jitwatch.util.UserInterfaceUtil.FONT_MONOSPACE_SIZE;

import java.io.File;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.adoptopenjdk.jitwatch.core.JITWatchConfig;
import org.adoptopenjdk.jitwatch.core.JITWatchConstants;
import org.adoptopenjdk.jitwatch.jvmlang.LanguageManager;
import org.adoptopenjdk.jitwatch.logger.ILogListener;
import org.adoptopenjdk.jitwatch.model.IMetaMember;
import org.adoptopenjdk.jitwatch.parser.ILogParseErrorListener;
import org.adoptopenjdk.jitwatch.parser.ILogParser;
import org.adoptopenjdk.jitwatch.process.IExternalProcess;
import org.adoptopenjdk.jitwatch.sandbox.Sandbox;
import org.adoptopenjdk.jitwatch.ui.Dialogs;
import org.adoptopenjdk.jitwatch.ui.Dialogs.Response;
import org.adoptopenjdk.jitwatch.ui.main.IStageAccessProxy;
import org.adoptopenjdk.jitwatch.ui.main.JITWatchUI;
import org.adoptopenjdk.jitwatch.ui.stage.IStageClosedListener;
import org.adoptopenjdk.jitwatch.ui.stage.StageManager;
import org.adoptopenjdk.jitwatch.util.DisassemblyUtil;
import org.adoptopenjdk.jitwatch.util.NetUtil;
import org.adoptopenjdk.jitwatch.util.UserInterfaceUtil;
import com.chrisnewland.freelogj.Logger;
import com.chrisnewland.freelogj.LoggerFactory;

import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.SplitPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;

public class SandboxStage extends Stage implements ISandboxStage, IStageClosedListener, ILogListener, ILogParseErrorListener
{
	private static final Logger logger = LoggerFactory.getLogger(SandboxStage.class);

	private TextArea taLog;

	private IStageAccessProxy accessProxy;

	private Sandbox sandbox;

	private TabPane tabPane;

	private Button btnSandboxConfig;

	private Button btnRun;

	private SandboxConfigStage sandboxConfigStage;

	private ObservableList<String> languageList = FXCollections.observableArrayList();

	private ComboBox<String> comboBoxVMLanguage = new ComboBox<>(languageList);

	private JITWatchConfig config;

	public SandboxStage(final IStageClosedListener closeListener, IStageAccessProxy proxy, final ILogParser parser)
	{
		this.accessProxy = proxy;

		config = parser.getConfig();

		config.switchToSandbox();

		setupVMLanguages();

		sandbox = new Sandbox(parser, this, this);

		setTitle("Sandbox - Code, Compile, Execute, and Analyse JIT logs");

		tabPane = new TabPane();

		tabPane.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>()
		{
			@Override public void changed(ObservableValue<? extends Tab> ov, Tab t, Tab t1)
			{
				Tab selectedTab = tabPane.getSelectionModel().getSelectedItem();

				if (selectedTab != null)
				{
					EditorPane pane = (EditorPane) selectedTab.getContent();

					setVMLanguage(pane);
				}
			}
		});

		SplitPane splitVertical = new SplitPane();
		splitVertical.setOrientation(Orientation.VERTICAL);

		taLog = new TextArea();

		String style = "-fx-font-family:" + FONT_MONOSPACE_FAMILY + "; -fx-font-size:" + FONT_MONOSPACE_SIZE
				+ "px; -fx-background-color:white;";

		taLog.setStyle(style);

		Button btnNewEditor = UserInterfaceUtil.createButton("SANDBOX_NEW_EDITOR");
		btnNewEditor.setOnAction(new EventHandler<ActionEvent>()
		{
			@Override public void handle(ActionEvent e)
			{
				addEditor(null);
			}
		});

		Button btnOpen = UserInterfaceUtil.createButton("SANDBOX_OPEN_FILE");
		btnOpen.setOnAction(new EventHandler<ActionEvent>()
		{
			@Override public void handle(ActionEvent e)
			{
				if (tabPane.getSelectionModel().getSelectedItem() == null)
				{
					addEditor(null);
				}

				Tab selectedTab = tabPane.getSelectionModel().getSelectedItem();

				EditorPane pane = (EditorPane) selectedTab.getContent();

				if (pane.isModified())
				{
					pane.promptSave();
				}

				FileChooser fc = new FileChooser();

				fc.setTitle("Choose source file");

				fc.setInitialDirectory(Sandbox.SANDBOX_SOURCE_DIR.toFile());

				File result = fc.showOpenDialog(getStageForChooser());

				if (result != null)
				{
					pane.loadSource(result);
					selectedTab.setText(pane.getName());

					setVMLanguage(pane);

					saveEditorPaneConfig();
				}
			}
		});

		Button btnSave = UserInterfaceUtil.createButton("SANDBOX_SAVE_FILE");
		btnSave.setOnAction(new EventHandler<ActionEvent>()
		{
			@Override public void handle(ActionEvent e)
			{
				Tab selectedTab = tabPane.getSelectionModel().getSelectedItem();

				if (selectedTab != null)
				{
					EditorPane pane = (EditorPane) selectedTab.getContent();

					pane.saveFile();

					selectedTab.setText(pane.getName());

					setVMLanguage(pane);
				}
			}
		});

		btnSandboxConfig = UserInterfaceUtil.createButton("SANDBOX_CONFIGURE");
		btnSandboxConfig.setOnAction(new EventHandler<ActionEvent>()
		{
			@Override public void handle(ActionEvent e)
			{
				sandboxConfigStage = new SandboxConfigStage(SandboxStage.this, parser.getConfig());

				StageManager.addAndShow(SandboxStage.this, sandboxConfigStage, SandboxStage.this);

				btnSandboxConfig.setDisable(true);
			}
		});

		Button btnResetSandbox = UserInterfaceUtil.createButton("SANDBOX_RESET");
		btnResetSandbox.setOnAction(new EventHandler<ActionEvent>()
		{
			@Override public void handle(ActionEvent e)
			{
				Response resp = Dialogs.showYesNoDialog(SandboxStage.this, "Reset Sandbox?",
						"Delete all modified Sandbox sources and classes?");

				if (resp == Response.YES)
				{
					initialiseLog();
					sandbox.reset();
					loadDefaultEditors();
				}
			}
		});

		comboBoxVMLanguage.valueProperty().addListener(new ChangeListener<String>()
		{
			@Override public void changed(ObservableValue<? extends String> ov, String oldVal, String newVal)
			{
				if (newVal != null)
				{
					log("Changed language to " + newVal);
				}
			}
		});

		btnRun = UserInterfaceUtil.createButton("SANDBOX_RUN");
		btnRun.setOnAction(new EventHandler<ActionEvent>()
		{
			@Override public void handle(ActionEvent e)
			{
				btnRun.setDisable(true);

				Tab selectedTab = tabPane.getSelectionModel().getSelectedItem();

				if (selectedTab != null)
				{
					EditorPane pane = (EditorPane) selectedTab.getContent();

					if (pane.isModified())
					{
						pane.promptSave();
					}

					setVMLanguage(pane);

					runFile(pane);
				}
			}
		});

		Button btnOutput = UserInterfaceUtil.createButton("SANDBOX_VIEW_OUTPUT");
		btnOutput.setOnAction(new EventHandler<ActionEvent>()
		{
			@Override public void handle(ActionEvent e)
			{
				IExternalProcess lastProcess = sandbox.getLastProcess();

				String outputString;

				if (lastProcess != null)
				{
					outputString = lastProcess.getOutputStream();
				}
				else
				{
					outputString = "No output";
				}

				showOutput(outputString);
			}
		});

		BorderPane borderPane = new BorderPane();

		Scene scene = UserInterfaceUtil.getScene(borderPane, JITWatchUI.WINDOW_WIDTH, JITWatchUI.WINDOW_HEIGHT);

		HBox hBoxTools = new HBox();

		hBoxTools.setSpacing(10);
		hBoxTools.setPadding(new Insets(10));

		hBoxTools.getChildren().add(btnNewEditor);
		hBoxTools.getChildren().add(btnOpen);
		hBoxTools.getChildren().add(btnSave);
		hBoxTools.getChildren().add(btnSandboxConfig);
		hBoxTools.getChildren().add(btnResetSandbox);
		hBoxTools.getChildren().add(comboBoxVMLanguage);
		hBoxTools.getChildren().add(btnRun);
		hBoxTools.getChildren().add(btnOutput);

		Region spacer = new Region();
		HBox.setHgrow(spacer, Priority.ALWAYS);

		Button buttonSnapShot = UserInterfaceUtil.getSnapshotButton(scene, "Sandbox");

		hBoxTools.getChildren().add(spacer);
		hBoxTools.getChildren().add(buttonSnapShot);

		splitVertical.getItems().add(tabPane);
		splitVertical.getItems().add(taLog);

		splitVertical.setDividerPositions(0.75, 0.25);

		VBox vBoxMain = new VBox();
		vBoxMain.getChildren().add(hBoxTools);
		vBoxMain.getChildren().add(splitVertical);

		borderPane.setTop(hBoxTools);
		borderPane.setCenter(splitVertical);

		initialiseLog();

		setScene(scene);

		setOnCloseRequest(new EventHandler<WindowEvent>()
		{
			@Override public void handle(WindowEvent arg0)
			{
				saveEditorPaneConfig();

				closeListener.handleStageClosed(SandboxStage.this);
			}
		});

		loadLastEditorPanes();
	}

	@Override public void setModified(EditorPane pane, boolean isModified)
	{
		for (Tab tab : tabPane.getTabs())
		{
			EditorPane currentPane = (EditorPane) tab.getContent();

			if (currentPane == pane)
			{
				String tabText = tab.getText();

				if (isModified)
				{
					if (!tabText.endsWith(S_ASTERISK))
					{
						tab.setText(tabText + S_ASTERISK);
					}
				}
				else
				{
					tab.setText(pane.getName());
				}
			}
		}
	}

	private void setVMLanguage(EditorPane pane)
	{
		if (pane != null)
		{
			setVMLanguageFromFile(pane.getSourceFile());
		}
	}

	@Override public void runFile(final EditorPane pane)
	{
		saveUnsavedEditors();

		new Thread(new Runnable()
		{
			@Override public void run()
			{
				runSandbox(pane.getSourceFile());
				btnRun.setDisable(false);
			}
		}).start();
	}

	private void initialiseLog()
	{
		taLog.setText(S_EMPTY);
		log("Sandbox ready. OS is '" + System.getProperty("os.name") + "' Architecture is '" + System.getProperty("os.arch") + "'");
	}

	public void checkHsdis()
	{
		String disassemblerFilename = DisassemblyUtil.getDisassemblerFilename();

		log("Looking for disassembler: " + disassemblerFilename);

		Path downloadPath = Paths.get(disassemblerFilename);

		Path disassemblerPath = DisassemblyUtil.getDisassemblerFilePath();

		boolean noPromptHsdis = config.isNoPromptHsdis();

		if (!noPromptHsdis && !DisassemblyUtil.isDisassemblerAvailable())
		{
			log("Disassembler not found");

			String hsdisDownloadUrl = "https://chriswhocodes.com/hsdis/";

			String message = "The disassembly plugin " + disassemblerFilename + " could not be found.\n Would you like to download it from "+hsdisDownloadUrl+" ?";

			Response response = Dialogs.showYesNoDialogNever(SandboxStage.this, "Download disassembly plugin", message);

			switch (response)
			{
			case YES:
			{
				String url = hsdisDownloadUrl + disassemblerFilename;

				log("Downloading hsdis from " + url);

				NetUtil.fetchBinary(url, downloadPath);

				if (downloadPath.toFile().exists())
				{
					log("Downloaded OK");
				}
				else
				{
					log("Download failed");
				}
			}
			break;
			case NEVER:
			{
				config.setNoPromptHsdis(true);
				config.saveConfig();
			}
			break;
			}
		}

		if (DisassemblyUtil.downloadedDisassemblerPresent())
		{
			log("Disassembler available: " + downloadPath.toAbsolutePath());
		}
		else if (disassemblerPath != null)
		{
			log("Disassembler available: " + disassemblerPath);
		}

		//pb.environment.set(LD_LIBRARY_PATH);
	}

	private void loadLastEditorPanes()
	{
		List<String> panes = config.getLastEditorPaneList();

		if (panes.isEmpty())
		{
			loadDefaultEditors();
		}
		else
		{
			tabPane.getTabs().clear();

			for (String panePath : panes)
			{
				addEditor(new File(panePath));
			}
		}
	}

	private void loadDefaultEditors()
	{
		tabPane.getTabs().clear();

		addEditor(new File(Sandbox.SANDBOX_SOURCE_DIR.toFile(), "SimpleInliningTest.java"));

		saveEditorPaneConfig();
	}

	private void addEditor(File filename)
	{
		final EditorPane pane = new EditorPane(this);

		if (filename != null)
		{
			pane.loadSource(filename);
		}

		final Tab tab = new Tab();
		tab.setContent(pane);
		tab.setText(pane.getName());

		EventHandler<Event> closeHandler = new EventHandler<Event>()
		{
			@Override public void handle(Event e)
			{
				if (pane.isModified())
				{
					pane.promptSave();
				}

				tabPane.getTabs().remove(tab);
			}
		};

		// JavaFX 2.2 (from Java 7) has no onCloseRequestProperty
		if (JITWatchUI.IS_JAVA_FX2)
		{
			tab.setOnClosed(closeHandler);
		}
		else
		{
			// Use reflection to call setOnCloseRequestProperty for Java 8
			try
			{
				MethodType mt = MethodType.methodType(void.class, EventHandler.class);

				MethodHandle mh = MethodHandles.lookup().findVirtual(Tab.class, "setOnCloseRequest", mt);

				// fails with invokeExact due to generic type erasure?
				mh.invoke(tab, closeHandler);

			}
			catch (Throwable t)
			{
				logger.error("Exception: {}", t.getMessage(), t);
			}
		}

		tabPane.getTabs().add(tab);

		pane.requestFocus();

		setVMLanguage(pane);
		saveEditorPaneConfig();
	}

	private void saveEditorPaneConfig()
	{
		List<String> editorPanePaths = new ArrayList<>();

		for (Tab tab : tabPane.getTabs())
		{
			EditorPane pane = (EditorPane) tab.getContent();

			if (pane != null && pane.getSourceFile() != null)
			{
				String editorPanePath = pane.getSourceFile().getAbsolutePath();
				editorPanePaths.add(editorPanePath);
			}
		}

		config.setLastEditorPaneList(editorPanePaths);
		config.saveConfig();
	}

	private void saveUnsavedEditors()
	{
		for (Tab tab : tabPane.getTabs())
		{
			EditorPane pane = (EditorPane) tab.getContent();

			pane.promptSave();
		}
	}

	private void runSandbox(File fileToRun)
	{
		try
		{
			Platform.runLater(new Runnable()
			{
				@Override public void run()
				{
					taLog.setText(S_EMPTY);
				}
			});

			String language = comboBoxVMLanguage.getValue();

			if (language != null)
			{
				List<File> compileList = new ArrayList<>();

				for (Tab tab : tabPane.getTabs())
				{
					EditorPane pane = (EditorPane) tab.getContent();

					File sourceFile = pane.getSourceFile();

					if (sourceFile != null)
					{
						if (LanguageManager.isCompilable(language, sourceFile))
						{
							compileList.add(sourceFile);
						}
					}
				}

				if (compileList.size() > 0)
				{
					sandbox.runSandbox(language, compileList, fileToRun);
				}
				else
				{
					log("Nothing to compile?");
				}
			}
		}
		catch (Exception e)
		{
			logger.error("Sandbox failure", e);
		}
	}

	@Override public void addSourceFolder(File sourceFolder)
	{
		config.addSourceFolder(sourceFolder);
	}

	private void setVMLanguageFromFile(File sourceFile)
	{
		String language = LanguageManager.getLanguageFromFile(sourceFile);

		if (language != null)
		{
			comboBoxVMLanguage.getSelectionModel().select(language);
		}
	}

	@Override public void handleLogEntry(String text)
	{
		log(text);
	}

	@Override public void handleErrorEntry(String text)
	{
		log(text);
	}

	@Override public void log(final String text)
	{
		Platform.runLater(new Runnable()
		{
			@Override public void run()
			{
				taLog.appendText(text + S_NEWLINE);
			}
		});
	}

	@Override public void openTriView(final IMetaMember member)
	{
		log("Launching TriView for " + member);

		Platform.runLater(new Runnable()
		{
			@Override public void run()
			{
				accessProxy.openTriView(member, 0);
			}
		});
	}

	@Override public void showOutput(final String output)
	{
		Platform.runLater(new Runnable()
		{
			@Override public void run()
			{
				// TODO perhaps filter out classloading statements?
				accessProxy.openTextViewer("Sandbox Output", output, false, false);
			}
		});
	}

	@Override public void showError(final String error)
	{
		Platform.runLater(new Runnable()
		{
			@Override public void run()
			{
				accessProxy.openTextViewer("Error", error, false, false);
			}
		});
	}

	@Override public void handleStageClosed(Stage stage)
	{
		StageManager.closeStage(stage);

		if (stage instanceof SandboxConfigStage)
		{
			btnSandboxConfig.setDisable(false);
		}
	}

	private void setupVMLanguages()
	{
		List<String> vmLanguageList = config.getVMLanguageList();

		addVMLanguages(config);

		vmLanguageList = config.getVMLanguageList();

		Collections.sort(vmLanguageList);

		languageList.addAll(vmLanguageList);

		comboBoxVMLanguage.getSelectionModel().select(VM_LANGUAGE_JAVA);
	}

	private void addVMLanguages(JITWatchConfig config)
	{
		List<String> vmLanguageList = config.getVMLanguageList();

		if (!vmLanguageList.contains(JITWatchConstants.VM_LANGUAGE_JAVA))
		{
			config.addOrUpdateVMLanguage(VM_LANGUAGE_JAVA, S_EMPTY);
		}
		if (!vmLanguageList.contains(JITWatchConstants.VM_LANGUAGE_SCALA))
		{
			config.addOrUpdateVMLanguage(VM_LANGUAGE_SCALA, S_EMPTY);
		}
		if (!vmLanguageList.contains(JITWatchConstants.VM_LANGUAGE_JRUBY))
		{
			config.addOrUpdateVMLanguage(VM_LANGUAGE_JRUBY, S_EMPTY);
		}
		if (!vmLanguageList.contains(JITWatchConstants.VM_LANGUAGE_GROOVY))
		{
			config.addOrUpdateVMLanguage(VM_LANGUAGE_GROOVY, S_EMPTY);
		}
		if (!vmLanguageList.contains(JITWatchConstants.VM_LANGUAGE_KOTLIN))
		{
			config.addOrUpdateVMLanguage(VM_LANGUAGE_KOTLIN, S_EMPTY);
		}
		if (!vmLanguageList.contains(JITWatchConstants.VM_LANGUAGE_JAVASCRIPT))
		{
			config.addOrUpdateVMLanguage(VM_LANGUAGE_JAVASCRIPT, S_EMPTY);
		}
		if (!vmLanguageList.contains(JITWatchConstants.VM_LANGUAGE_CLOJURE))
		{
			config.addOrUpdateVMLanguage(VM_LANGUAGE_CLOJURE, S_EMPTY);
		}

		config.saveConfig();
	}

	@Override public void handleError(final String title, final String body)
	{
		logger.error(title);

		Platform.runLater(new Runnable()
		{
			@Override public void run()
			{
				Dialogs.showOKDialog(SandboxStage.this, title, body);
			}
		});
	}

	@Override public Stage getStageForChooser()
	{
		return this;
	}
}
