/* ###
 * 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.osgi;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.*;
import java.util.*;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.felix.framework.util.manifestparser.ManifestParser;
import org.osgi.framework.*;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.BundleRequirement;

import ghidra.util.Msg;

public class OSGiUtils {
	/*
	 * Match group 1 contains the file name from a resource string, e.g.  from
	 *     file:/path/to/some.jar!/Some.class
	 * we get "/path/to/some.jar", everything between ':' and '!'
	 * 
	 */
	private static Pattern JAR_FILENAME_EXTRACTOR = Pattern.compile("^.*:(.*)!.*$");

	/*
	 * Match group 1 contains the name of the Java package from an OSGi resolution
	 * error message.  If present, match group 2 will be the version constraint.
	 * 
	 * e.g. for the requirement
	 *   (&(osgi.wiring.package=x.y.z)(version>=1.2.3))
	 * 
	 * PACKAGE_NAME_EXTRACTOR will match with group 1 "x.y.z" and group 2 "(version>=1.2.3)". 
	 * 
	 */
	private static Pattern PACKAGE_NAME_EXTRACTOR =
		Pattern.compile("\\(osgi\\.wiring\\.package=([^)]*)\\)(\\(version[^)]*\\))?");

	/**
	 * The syntax of the error generated when OSGi requirements cannot be resolved is
	 * difficult to parse, so we try to extract package name and versions. 
	 * 
	 * @param osgiExceptionMessage the exception message
	 * @return a list of package names, possibly including versions
	 */
	static List<String> extractPackageNamesFromFailedResolution(String osgiExceptionMessage) {
		try (Scanner s = new Scanner(osgiExceptionMessage)) {
			return s.findAll(PACKAGE_NAME_EXTRACTOR).map(m -> {
				if (m.group(2) != null) {
					return m.group(1) + " " + m.group(2);
				}
				return m.group(1);
			}).collect(Collectors.toList());
		}
	}

	static String getEventTypeString(BundleEvent e) {
		switch (e.getType()) {
			case BundleEvent.INSTALLED:
				return "INSTALLED";
			case BundleEvent.RESOLVED:
				return "RESOLVED";
			case BundleEvent.LAZY_ACTIVATION:
				return "LAZY_ACTIVATION";
			case BundleEvent.STARTING:
				return "STARTING";
			case BundleEvent.STARTED:
				return "STARTED";
			case BundleEvent.STOPPING:
				return "STOPPING";
			case BundleEvent.STOPPED:
				return "STOPPED";
			case BundleEvent.UPDATED:
				return "UPDATED";
			case BundleEvent.UNRESOLVED:
				return "UNRESOLVED";
			case BundleEvent.UNINSTALLED:
				return "UNINSTALLED";
			default:
				return "???";
		}
	}

	static String getStateString(Bundle bundle) {
		switch (bundle.getState()) {
			case Bundle.UNINSTALLED:
				return "UNINSTALLED";
			case Bundle.INSTALLED:
				return "INSTALLED";
			case Bundle.RESOLVED:
				return "RESOLVED";
			case Bundle.STARTING:
				return "STARTING";
			case Bundle.STOPPING:
				return "STOPPING";
			case Bundle.ACTIVE:
				return "ACTIVE";
			default:
				return "unknown state";
		}
	}

	/**
	 * parse Import-Package string from a bundle manifest
	 * 
	 * @param importPackageString Import-Package value
	 * @return deduced requirements or null if there was an error
	 * @throws BundleException on parse failure
	 */
	static List<BundleRequirement> parseImportPackage(String importPackageString)
			throws BundleException {
		Map<String, Object> headerMap = new HashMap<>();
		headerMap.put(Constants.IMPORT_PACKAGE, importPackageString);
		ManifestParser manifestParser = new ManifestParser(null, null, null, headerMap);
		return manifestParser.getRequirements();
	}

	/**
	 * parse Export-Package string from a bundle manifest
	 * 
	 * @param exportPackageString Import-Package value
	 * @return deduced capabilities or null if there was an error
	 * @throws BundleException on parse failure
	 */
	static List<BundleCapability> parseExportPackage(String exportPackageString)
			throws BundleException {
		Map<String, Object> headerMap = new HashMap<>();
		headerMap.put(Constants.EXPORT_PACKAGE, exportPackageString);
		ManifestParser manifestParser = new ManifestParser(null, null, null, headerMap);
		return manifestParser.getCapabilities();
	}

	// from https://dzone.com/articles/locate-jar-classpath-given
	static String findJarForClass(Class<?> c) {
		URL location;
		String classLocation = c.getName().replace('.', '/') + ".class";
		ClassLoader loader = c.getClassLoader();
		if (loader == null) {
			location = ClassLoader.getSystemResource(classLocation);
		}
		else {
			location = loader.getResource(classLocation);
		}
		if (location != null) {
			Matcher matcher = JAR_FILENAME_EXTRACTOR.matcher(location.toString());
			if (matcher.find()) {
				return matcher.group(1);
			}
			return null; // not loaded from jar?
		}
		return null;
	}

	static void getPackagesFromClasspath(Set<String> s) {
		getClasspathElements().forEach(p -> {
			if (Files.isDirectory(p)) {
				collectPackagesFromDirectory(p, s);
			}
			else if (p.toString().endsWith(".jar")) {
				collectPackagesFromJar(p, s);
			}
		});
	}

	static Stream<Path> getClasspathElements() {
		String classpathStr = System.getProperty("java.class.path");
		return Collections.list(new StringTokenizer(classpathStr, File.pathSeparator))
				.stream()
				.map(String.class::cast)
				.map(Paths::get)
				.map(Path::normalize);
	}

	static void collectPackagesFromDirectory(Path dirPath, Set<String> packages) {
		try (Stream<Path> walk = Files.walk(dirPath)) {
			walk.filter(p -> p.toString().endsWith(".class")).forEach(path -> {
				String relativePath = dirPath.relativize(path).toString();
				int lastSlash = relativePath.lastIndexOf(File.separatorChar);
				packages.add(lastSlash > 0
						? relativePath.substring(0, lastSlash).replace(File.separatorChar, '.')
						: "");
			});
		}
		catch (IOException e) {
			Msg.error(OSGiUtils.class, "Error while collecting packages from directory", e);
		}
	}

	static void collectPackagesFromJar(Path jarPath, Set<String> packages) {
		try {
			try (JarFile j = new JarFile(jarPath.toFile())) {
				j.stream().filter(entry -> entry.getName().endsWith(".class")).forEach(jarEntry -> {
					String entryName = jarEntry.getName();
					int lastSlash = entryName.lastIndexOf('/');
					packages.add(
						lastSlash > 0 ? entryName.substring(0, lastSlash).replace('/', '.') : "");
				});
			}
		}
		catch (IOException e) {
			Msg.error(OSGiUtils.class, "Error while collecting packages from jar", e);
		}
	}

}
