/* ###
 * IP: GHIDRA
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ghidra.app.plugin.core.script;

import static org.junit.Assert.*;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.junit.*;
import org.osgi.framework.Bundle;

import generic.jar.ResourceFile;
import ghidra.app.plugin.core.osgi.*;
import ghidra.test.AbstractGhidraHeadlessIntegrationTest;

public class BundleHostTest extends AbstractGhidraHeadlessIntegrationTest {
	BundleHost bundleHost;
	CapturingBundleHostListener capturingBundleHostListener;

	Set<Path> tempDirs = new HashSet<>();
	LinkedList<GhidraBundle> bundleStack = new LinkedList<>();
	GhidraBundle currentBundle;

	protected static void wipe(Path path) throws IOException {
		if (Files.exists(path)) {
			try (Stream<Path> walk = Files.walk(path)) {
				for (Path p : (Iterable<Path>) walk.sorted(Comparator.reverseOrder())::iterator) {
					Files.deleteIfExists(p);
				}
			}
		}
	}

	protected GhidraBundle pushNewBundle() throws IOException {
		String dir = String.format("sourcebundle%03d", tempDirs.size());
		Path tmpDir = new File(getTestDirectoryPath(), dir).toPath();
		Files.createDirectories(tmpDir);
		tempDirs.add(tmpDir);

		ResourceFile sourceDirectory = new ResourceFile(tmpDir.toFile());
		currentBundle = bundleHost.add(sourceDirectory, true, false);
		bundleStack.push(currentBundle);
		return currentBundle;
	}

	static class CapturingBundleHostListener implements BundleHostListener {
		String lastBuildSummary;

		@Override
		public void bundleBuilt(GhidraBundle gbundle, String summary) {
			if (summary != null) {
				this.lastBuildSummary = summary;
			}
		}
	}

	@Before
	public void setup() throws OSGiException, IOException {
		wipe(GhidraSourceBundle.getCompiledBundlesDir());

		bundleHost = new BundleHost();
		bundleHost.startFramework();
		capturingBundleHostListener = new CapturingBundleHostListener();
		bundleHost.addListener(capturingBundleHostListener);

		pushNewBundle();
	}

	@After
	public void tearDown() throws IOException {
		bundleHost.dispose();
		capturingBundleHostListener = null;
		bundleHost = null;

		for (Path tmpdir : tempDirs) {
			wipe(tmpdir);
		}
	}

	protected void buildWithExpectations(String expectedCompilerOutput, String expectedSummary)
			throws Exception {
		StringWriter stringWriter = new StringWriter();

		currentBundle.build(new PrintWriter(stringWriter));
		stringWriter.flush();

		assertEquals("unexpected output during build", expectedCompilerOutput,
			stringWriter.getBuffer().toString());

		assertEquals("wrong summary", expectedSummary,
			capturingBundleHostListener.lastBuildSummary);
	}

	protected void activate() throws Exception {
		Bundle bundle = bundleHost.install(currentBundle);
		assertNotNull("failed to install bundle", bundle);
		bundle.start();
	}

	protected void buildAndActivate() throws Exception {
		buildWithExpectations("", "");
		activate();
	}

	protected Class<?> loadClass(String classname) throws ClassNotFoundException {
		Class<?> clazz = currentBundle.getOSGiBundle().loadClass(classname);
		assertNotNull("failed to load class", clazz);
		return clazz;
	}

	protected void addClass(String fullclassname, String body) throws IOException {
		addClass("", fullclassname, body);
	}

	protected void addClass(String imports, String fullclassname, String body) throws IOException {
		addClass("", imports, fullclassname, body);
	}

	protected void addClass(String meta, String imports, String fullclassname, String body)
			throws IOException {
		String simplename;
		Path tmpsource = currentBundle.getFile().getFile(false).toPath();

		if (fullclassname.contains(".")) {
			String packagename;

			Pattern pattern = Pattern.compile("^(.*)\\.([^.]*)$");
			Matcher matcher = pattern.matcher(fullclassname);
			if (!matcher.matches()) {
				throw new IllegalArgumentException(
					"fullclassname must be of the form \"xxxx.xxxx.Xxxx\"");
			}
			packagename = matcher.group(1);
			simplename = matcher.group(2);

			for (String n : packagename.split("\\.")) {
				tmpsource = tmpsource.resolve(n);
			}
			Files.createDirectories(tmpsource);
			tmpsource = tmpsource.resolve(simplename + ".java");

			Files.writeString(tmpsource,
				String.format("%s\npackage %s;\n%s\npublic class %s {\n%s\n}\n", meta, packagename,
					imports, simplename, body));
		}
		else {
			simplename = fullclassname;
			tmpsource = tmpsource.resolve(simplename + ".java");
			Files.writeString(tmpsource, String.format("%s\n%s\npublic class %s {\n%s\n}\n", meta,
				imports, simplename, body));

		}

	}

	protected Object getInstance(String classname) throws Exception {
		Class<?> clazz = loadClass(classname);
		Object object = clazz.getDeclaredConstructor().newInstance();
		assertNotNull("failed to create instance", object);
		return object;
	}

	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

	@Test
	public void testSimpleBuildAndLoadclass() throws Exception {
		// @formatter:off
		addClass(
			"apackage.AClass", 
			"@Override\n" + 
			"public String toString() {\n" + 
			"	return \"yupyup\";\n" + 
			"}\n" 
		);
		// @formatter:on

		buildAndActivate();

		assertEquals("wrong response from instantiated class", "yupyup",
			getInstance("apackage.AClass").toString());
	}

	@Test
	public void testCompileWhatYouCan() throws Exception {
		// @formatter:off
		addClass(
			"apackage.AClass", 
			"@Override\n" + 
			"public String toString() {\n" + 
			"	return \"yupyup\";\n" + 
			"}\n" 
		);

		addClass(
			"apackage.BClass", 
			"@Override\n" + 
			"public String toString() {\n" +
			"   failing java goes here\n" +
			"	return \"yupyup\";\n" + 
			"}\n" 
		);

		buildWithExpectations(
			"BClass.java:7: error: ';' expected\n" + 
			"   failing java goes here\n" + 
			"               ^\n" + 
			"BClass.java:7: error: ';' expected\n" + 
			"   failing java goes here\n" + 
			"                         ^\n" + 
			"skipping "+currentBundle.getFile().toString()+File.separator+"apackage"+File.separator+"BClass.java\n"
			,
			"1 source file with errors"
		); 
		// @formatter:on

		activate();

		assertEquals("wrong response from instantiated class", "yupyup",
			getInstance("apackage.AClass").toString());
	}

	@Test
	public void testLibraryInBundle() throws Exception {
		// @formatter:off
		addClass(
			"lib.Library", 
			"public static String sup() {\n" + 
			"	return \"yupyup\";\n" + 
			"}\n" 
		);
		addClass(
			"import lib.Library;\n",
			"apackage.AClass", 
			"@Override\n" + 
			"public String toString() {\n" + 
			"	return \"lib says \" + Library.sup();\n" + 
			"}\n"
		);
		// @formatter:on
		buildAndActivate();
		assertEquals("wrong response from instantiated class", "lib says yupyup",
			getInstance("apackage.AClass").toString());
	}

	@Test
	public void testCantLoadLibraryFromOtherBundle() throws Exception {
		// @formatter:off
		addClass(
			"lib.Library", 
			"public static String sup() {\n" + 
			"	return \"yupyup\";\n" + 
			"}\n" 
		);
		buildAndActivate();
		Class<?> libclass = loadClass("lib.Library");
		assertEquals("wrong response from loaded class", "yupyup",
			libclass.getMethod("sup").invoke(null).toString());

		
		pushNewBundle();
		addClass(
			"import lib.Library;\n",
			"apackage.AClass", 
			"@Override\n" + 
			"public String toString() {\n" + 
			"	return \"lib says \" + Library.sup();\n" + 
			"}\n"
		);

		buildWithExpectations(
			"AClass.java:3: error: package lib does not exist\n" + 
			"import lib.Library;\n" + 
			"          ^\n" + 
			"AClass.java:8: error: cannot find symbol\n" + 
			"	return \"lib says \" + Library.sup();\n" + 
			"	                     ^\n" + 
			"  symbol:   variable Library\n" + 
			"  location: class apackage.AClass\n" +
			"skipping "+currentBundle.getFile().toString()+File.separator+"apackage"+File.separator+"AClass.java\n"
			,
			"1 source file with errors"
		);
		// @formatter:on
	}

	@Test
	public void testLoadLibraryFromOtherBundleWithImportsTag() throws Exception {
		// @formatter:off
		addClass(
			"lib.Library", 
			"public static String sup() {\n" + 
			"	return \"yupyup\";\n" + 
			"}\n" 
		);
		buildAndActivate();
		Class<?> libclass = loadClass("lib.Library");
		assertEquals("wrong response from loaded class", "yupyup",
			libclass.getMethod("sup").invoke(null).toString());

		
		pushNewBundle();
		// @importpackage tag is only parsed from classes in default package
		addClass(
			"//@importpackage lib\n"
			,
			"import lib.Library;\n"
			,
			"AClass", 
			"@Override\n" + 
			"public String toString() {\n" + 
			"	return \"lib says \" + Library.sup();\n" + 
			"}\n"
		);

		buildAndActivate();
		assertEquals("wrong response from instantiated class", "lib says yupyup",
			getInstance("AClass").toString());
		// @formatter:on
	}

	@Test
	public void testLoadLibraryFromOtherBundleWithManifest() throws Exception {
		// @formatter:off
		addClass(
			"lib.Library", 
			"public static String sup() {\n" + 
			"	return \"yupyup\";\n" + 
			"}\n" 
		);
		buildAndActivate();
		
		pushNewBundle();
		addClass(
			"import lib.Library;\n",
			"apackage.AClass", 
			"@Override\n" + 
			"public String toString() {\n" + 
			"	return \"lib says \" + Library.sup();\n" + 
			"}\n"
		);
		
		Path path = currentBundle.getFile().getFile(false).toPath();
		path=path.resolve("META-INF");
		Files.createDirectories(path);
		Path manifest=path.resolve("MANIFEST.MF");
		
		Files.writeString(manifest,
			"Manifest-Version: 1.0\n" + 
			"Bundle-ManifestVersion: 2\n" + 
			"Import-Package: lib\n" + 
			"Require-Capability: osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=11))\"\n" + 
			"Bundle-SymbolicName: ghidratesting.bundleX\n" + 
			"Bundle-Version: 1.0\n" + 
			"Bundle-Name: bundlex\n" 
		);

		buildAndActivate();
		// @formatter:on
		assertEquals("wrong response from instantiated class", "lib says yupyup",
			getInstance("apackage.AClass").toString());

	}

}
