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

import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_DOLLAR;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_EMPTY;
import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.S_SPACE;
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_SCALA;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

import org.adoptopenjdk.jitwatch.core.JITWatchConfig;
import org.adoptopenjdk.jitwatch.core.JITWatchConfig.BackgroundCompilation;
import org.adoptopenjdk.jitwatch.core.JITWatchConfig.CompressedOops;
import org.adoptopenjdk.jitwatch.core.JITWatchConfig.OnStackReplacement;
import org.adoptopenjdk.jitwatch.core.JITWatchConfig.TieredCompilation;
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.model.IReadOnlyJITDataModel;
import org.adoptopenjdk.jitwatch.model.MetaClass;
import org.adoptopenjdk.jitwatch.parser.ILogParser;
import org.adoptopenjdk.jitwatch.process.IExternalProcess;
import org.adoptopenjdk.jitwatch.process.compiler.ICompiler;
import org.adoptopenjdk.jitwatch.process.runtime.IRuntime;
import org.adoptopenjdk.jitwatch.ui.sandbox.ISandboxStage;
import org.adoptopenjdk.jitwatch.util.DisassemblyUtil;
import org.adoptopenjdk.jitwatch.util.FileUtil;
import org.adoptopenjdk.jitwatch.util.StringUtil;
import com.chrisnewland.freelogj.Logger;
import com.chrisnewland.freelogj.LoggerFactory;

public class Sandbox
{
	private static final Logger logger = LoggerFactory.getLogger(Sandbox.class);

	private ILogListener logListener;
	private ISandboxStage sandboxStage;

	public static final Path SANDBOX_DIR;
	public static final Path SANDBOX_SOURCE_DIR;
	public static final Path SANDBOX_CLASS_DIR;

	public static final Path PATH_STD_ERR;
	public static final Path PATH_STD_OUT;

	private static final String SANDBOX_LOGFILE = "sandbox.log";

	private File sandboxLogFile = new File(SANDBOX_DIR.toFile(), SANDBOX_LOGFILE);

	private ILogParser logParser;

	private LanguageManager languageManager;

	private IExternalProcess lastProcess;

	static
	{
		String userDir = System.getProperty("user.dir");

		SANDBOX_DIR = Paths.get(userDir, "sandbox");
		SANDBOX_SOURCE_DIR = Paths.get(SANDBOX_DIR.toString(), "sources");
		SANDBOX_CLASS_DIR = Paths.get(SANDBOX_DIR.toString(), "classes");

		PATH_STD_ERR = new File(Sandbox.SANDBOX_DIR.toFile(), "sandbox.err").toPath();
		PATH_STD_OUT = new File(Sandbox.SANDBOX_DIR.toFile(), "sandbox.out").toPath();

		initialise();
	}

	private static void initialise()
	{
		File sandboxSources = SANDBOX_SOURCE_DIR.toFile();

		if (!sandboxSources.exists())
		{
			logger.info("Creating Sandbox source directory {}", sandboxSources);

			sandboxSources.mkdirs();

			if (sandboxSources.exists())
			{
				copyExamples();
			}
		}

		File sandboxClasses = SANDBOX_CLASS_DIR.toFile();

		if (!sandboxClasses.exists())
		{
			sandboxClasses.mkdirs();
		}
	}

	public void reset()
	{
		logger.info("Resetting Sandbox to default settings");
		FileUtil.emptyDir(SANDBOX_DIR.toFile());
		initialise();
	}

	private static void copyExamples()
	{
		boolean runningFromJar = FileUtil.isRunningFromJar();

		logger.info("isRunningFromJar:{}", runningFromJar);

		File dstDir = SANDBOX_SOURCE_DIR.toFile();

		logger.info("Copying Sandbox examples to {}", dstDir);

		if (runningFromJar)
		{
			FileUtil.copyFilesFromJarToDir("examples", dstDir);
		}
		else
		{
			File srcDir = new File("core/src/main/resources/examples");

			FileUtil.copyFilesToDir(srcDir, dstDir);
		}
	}

	public Sandbox(ILogParser parser, ILogListener logger, ISandboxStage sandboxStage)
	{
		this.logParser = parser;
		this.logListener = logger;
		this.sandboxStage = sandboxStage;

		languageManager = new LanguageManager(logParser.getConfig(), logListener);
	}

	public void runSandbox(String language, List<File> compileList, File fileToRun) throws Exception
	{
		logListener.handleLogEntry("Running Sandbox");
		logListener.handleLogEntry("Language is " + language);

		String languagePath = logParser.getConfig().getVMLanguagePath(language);

		if (S_EMPTY.equals(languagePath) && (VM_LANGUAGE_JAVA.equals(language) || VM_LANGUAGE_JAVASCRIPT.equals(language)))
		{
			languagePath = System.getProperty("java.home");

			logListener.handleLogEntry("Using runtime JVM for " + language);
		}

		logListener.handleLogEntry(language + " home dir: " + languagePath);

		ICompiler compiler = languageManager.getCompiler(language, languagePath);

		if (compiler == null)
		{
			logListener.handleErrorEntry(language + " compiler path not set. Please click Configure Sandbox and set up the path.");
			return;
		}

		IRuntime runtime = languageManager.getRuntime(language, languagePath);

		if (runtime == null)
		{
			logListener.handleErrorEntry(language + " runtime path not set. Please click Configure Sandbox and set up the path.");
			return;
		}

		logListener.handleLogEntry("Compiling: " + StringUtil.listToString(compileList));

		lastProcess = compiler;

		String compilationSwitchesText = logParser.getConfig().getExtraVMCompilationSwitches().trim();

		List<String> compilationSwitches = new ArrayList<>();

		if (!compilationSwitchesText.isEmpty())
		{
			compilationSwitches.addAll(Arrays.asList(compilationSwitchesText.split("\\s+")));
		}

		boolean compiledOK = compiler.compile(compileList, buildUniqueClasspath(logParser.getConfig()), compilationSwitches, SANDBOX_CLASS_DIR.toFile(),
				Collections.<String, String>emptyMap(), logListener);

		logListener.handleLogEntry("Compilation success: " + compiledOK);

		if (compiledOK)
		{
			String fqClassNameToRun = runtime.getClassToExecute(fileToRun);

			lastProcess = runtime;

			long start = System.currentTimeMillis();

			boolean executionSuccess = executeClass(fqClassNameToRun, runtime, logParser.getConfig().isSandboxIntelMode());

			long stop = System.currentTimeMillis();

			logListener.handleLogEntry("Execution success: " + executionSuccess + " in " + (stop - start) + "ms");

			if (executionSuccess)
			{
				runJITWatch();

				if (!logParser.hasParseError())
				{
					String fqClassNameForTriView = runtime.getClassForTriView(fileToRun);

					showTriView(language, fqClassNameForTriView);
				}
			}
			else
			{
				sandboxStage.showError(runtime.getErrorStream());
			}
		}
		else
		{
			sandboxStage.showError(compiler.getErrorStream());
		}
	}

	public IExternalProcess getLastProcess()
	{
		return lastProcess;
	}

	private List<String> buildUniqueClasspath(JITWatchConfig config)
	{
		List<String> classpath = new ArrayList<>();

		classpath.add(SANDBOX_CLASS_DIR.toString());

		for (String path : config.getConfiguredClassLocations())
		{
			if (!classpath.contains(path))
			{
				classpath.add(path);
			}
		}

		return classpath;
	}

	private boolean executeClass(String fqClassName, IRuntime runtime, boolean intelMode) throws Exception
	{
		List<String> classpath = buildUniqueClasspath(logParser.getConfig());

		List<String> options = new ArrayList<>();
		options.add("-XX:+UnlockDiagnosticVMOptions");
		//options.add("-XX:+TraceClassLoading");
		options.add("-XX:+LogCompilation");
		options.add("-XX:LogFile=" + sandboxLogFile.getCanonicalPath());

		if (logParser.getConfig().isPrintAssembly())
		{
			options.add("-XX:+PrintAssembly");

			if (intelMode)
			{
				options.add("-XX:PrintAssemblyOptions=intel");
			}
		}

		boolean isDisableInlining = logParser.getConfig().isDisableInlining();

		if (isDisableInlining)
		{
			options.add("-XX:-Inline");
		}

		TieredCompilation tieredMode = logParser.getConfig().getTieredCompilationMode();

		if (tieredMode == TieredCompilation.FORCE_TIERED)
		{
			options.add("-XX:+TieredCompilation");
		}
		else if (tieredMode == TieredCompilation.FORCE_NO_TIERED)
		{
			options.add("-XX:-TieredCompilation");
		}

		CompressedOops oopsMode = logParser.getConfig().getCompressedOopsMode();

		if (oopsMode == CompressedOops.FORCE_COMPRESSED)
		{
			options.add("-XX:+UseCompressedOops");
		}
		else if (oopsMode == CompressedOops.FORCE_NO_COMPRESSED)
		{
			options.add("-XX:-UseCompressedOops");
		}

		BackgroundCompilation backgroundCompilationMode = logParser.getConfig().getBackgroundCompilationMode();

		if (backgroundCompilationMode == BackgroundCompilation.FORCE_BACKGROUND_COMPILATION)
		{
			options.add("-XX:+BackgroundCompilation");
		}
		else if (backgroundCompilationMode == BackgroundCompilation.FORCE_NO_BACKGROUND_COMPILATION)
		{
			options.add("-XX:-BackgroundCompilation");
		}

		OnStackReplacement onStackReplacementMode = logParser.getConfig().getOnStackReplacementMode();

		if (onStackReplacementMode == OnStackReplacement.FORCE_ON_STACK_REPLACEMENT)
		{
			options.add("-XX:+UseOnStackReplacement");
		}
		else if (onStackReplacementMode == OnStackReplacement.FORCE_NO_ON_STACK_REPLACEMENT)
		{
			options.add("-XX:-UseOnStackReplacement");
		}

		if (!isDisableInlining && logParser.getConfig().getFreqInlineSize() != JITWatchConstants.DEFAULT_FREQ_INLINE_SIZE)
		{
			options.add("-XX:FreqInlineSize=" + logParser.getConfig().getFreqInlineSize());
		}

		if (!isDisableInlining && logParser.getConfig().getMaxInlineSize() != JITWatchConstants.DEFAULT_MAX_INLINE_SIZE)
		{
			options.add("-XX:MaxInlineSize=" + logParser.getConfig().getMaxInlineSize());
		}

		if (logParser.getConfig().getCompileThreshold() != JITWatchConstants.DEFAULT_COMPILER_THRESHOLD)
		{
			options.add("-XX:CompileThreshold=" + logParser.getConfig().getCompileThreshold());
		}

		if (logParser.getConfig().getExtraVMRuntimeSwitches().length() > 0)
		{
			String extraSwitchString = logParser.getConfig().getExtraVMRuntimeSwitches();
			String[] switches = extraSwitchString.split(S_SPACE);

			for (String sw : switches)
			{
				options.add(sw);
			}
		}

		logListener.handleLogEntry("Executing: " + fqClassName);
		logListener.handleLogEntry("Classpath: " + StringUtil.listToString(classpath, File.pathSeparatorChar));
		logListener.handleLogEntry("VM options: " + StringUtil.listToString(options));

		Map<String, String> environment = new LinkedHashMap<>();

		if (DisassemblyUtil.downloadedDisassemblerPresent())
		{
			environment.put(DisassemblyUtil.getDynamicLibraryPath(),
					Paths.get(DisassemblyUtil.getDisassemblerFilename()).toAbsolutePath().getParent().toString());
		}

		Path workingDirPath = null;

		if (!logParser.getConfig().getSandboxWorkingDir().isEmpty())
		{
			workingDirPath = Paths.get(logParser.getConfig().getSandboxWorkingDir());
		}

		return runtime.execute(workingDirPath, fqClassName, classpath, options, environment, logListener);
	}

	private void runJITWatch() throws IOException
	{
		JITWatchConfig config = logParser.getConfig();

		List<String> sourceLocations = new ArrayList<>(config.getSourceLocations());
		List<String> classLocations = new ArrayList<>(config.getConfiguredClassLocations());

		String sandboxSourceDirString = SANDBOX_SOURCE_DIR.toString();
		String sandboxClassDirString = SANDBOX_CLASS_DIR.toString();

		boolean configChanged = false;

		if (!sourceLocations.contains(sandboxSourceDirString))
		{
			configChanged = true;
			sourceLocations.add(sandboxSourceDirString);
		}

		if (!classLocations.contains(sandboxClassDirString))
		{
			configChanged = true;
			classLocations.add(sandboxClassDirString);
		}

		File jdkSrcZip = FileUtil.getJDKSourceZip();

		if (jdkSrcZip != null)
		{
			String jdkSourceZipString = jdkSrcZip.toPath().toString();

			if (!sourceLocations.contains(jdkSourceZipString))
			{
				configChanged = true;
				sourceLocations.add(jdkSourceZipString);
			}
		}

		config.setSourceLocations(sourceLocations);
		config.setClassLocations(classLocations);

		if (configChanged)
		{
			config.saveConfig();
		}

		logListener.handleLogEntry("Parsing JIT log: " + sandboxLogFile.toString());

		logParser.processLogFile(sandboxLogFile, sandboxStage);

		logListener.handleLogEntry("Parsing complete");
	}

	private void showTriView(String language, String openClassInTriView)
	{
		IReadOnlyJITDataModel model = logParser.getModel();

		IMetaMember triViewMember = getMemberForClass(openClassInTriView, model);

		if (triViewMember == null && VM_LANGUAGE_SCALA.equals(language) && openClassInTriView.endsWith(S_DOLLAR))
		{
			// Scala and nothing found for Foo$ so try Foo
			triViewMember = getMemberForClass(openClassInTriView.substring(0, openClassInTriView.length() - 1), model);
		}

		sandboxStage.openTriView(triViewMember);
	}

	private IMetaMember getMemberForClass(String openClassInTriView, IReadOnlyJITDataModel model)
	{
		IMetaMember triViewMember = null;

		logListener.handleLogEntry("Looking up class: " + openClassInTriView);

		MetaClass metaClass = model.getPackageManager().getMetaClass(openClassInTriView);

		if (metaClass != null)
		{
			logListener.handleLogEntry("looking for compiled members of " + metaClass.getFullyQualifiedName());

			// select first compiled member if any
			List<IMetaMember> memberList = metaClass.getMetaMembers();

			for (IMetaMember mm : memberList)
			{
				logListener.handleLogEntry("Checking JIT compilation status of " + mm.toString());

				if (triViewMember == null)
				{
					// take the first member encountered
					triViewMember = mm;
				}

				if (mm.isCompiled())
				{
					// override with the first JIT-compiled member
					triViewMember = mm;
					break;
				}
			}
		}

		return triViewMember;
	}
}
