/*******************************************************************************
 * Copyright (c) 2016, 2019 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.aspectj.org.eclipse.jdt.internal.core.builder;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.aspectj.org.eclipse.jdt.core.JavaCore;
import org.aspectj.org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.aspectj.org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.AccessRuleSet;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IBinaryType;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IModule;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer;
import org.aspectj.org.eclipse.jdt.internal.compiler.util.JRTUtil;
import org.aspectj.org.eclipse.jdt.internal.compiler.util.SimpleSet;
import org.aspectj.org.eclipse.jdt.internal.compiler.util.SuffixConstants;
import org.aspectj.org.eclipse.jdt.internal.core.util.Util;

public class ClasspathJrtWithReleaseOption extends ClasspathJrt {

	static String MODULE_INFO = "module-info.sig"; //$NON-NLS-1$

	final String release;
	String releaseInHex;
	private String[] subReleases;
	private java.nio.file.FileSystem fs;
	protected Path releasePath;
	protected Path modulePath;
	private String modPathString;
	private boolean isJRE12Plus;

	public ClasspathJrtWithReleaseOption(String zipFilename, AccessRuleSet accessRuleSet, IPath externalAnnotationPath,
			String release) throws CoreException {
		super();
		if (release == null || release.equals("")) { //$NON-NLS-1$
			throw new IllegalArgumentException("--release argument can not be null"); //$NON-NLS-1$
		}
		this.zipFilename = zipFilename;
		this.accessRuleSet = accessRuleSet;
		if (externalAnnotationPath != null)
			this.externalAnnotationPath = externalAnnotationPath.toString();
		this.release = getReleaseOptionFromCompliance(release);
		initialize();
		loadModules(this);
	}
	/*
	 * JDK 11 doesn't contain release 5. Hence
	 * if the compliance is below 6, we simply return the lowest supported
	 * release, which is 6.
	 */
	private String getReleaseOptionFromCompliance(String comp) {
		if (JavaCore.compareJavaVersions(comp, JavaCore.VERSION_1_5) <= 0) {
			return "6"; //$NON-NLS-1$
		}
		int index = comp.indexOf("1."); //$NON-NLS-1$
		if (index != -1) {
			return comp.substring(index + 2, comp.length());
		} else {
			return comp;
		}
	}
	private boolean isJRE12Plus(Path path) {
		try (DirectoryStream<java.nio.file.Path> stream = Files.newDirectoryStream(path)) {
			for (final java.nio.file.Path subdir : stream) {
				String rel = JRTUtil.sanitizedFileName(subdir);
				if (Files.exists(this.fs.getPath(rel, "system-modules"))) { //$NON-NLS-1$
					int parseInt = Integer.parseInt(rel, 16);
					return (parseInt > 11);
				}
			}
		} catch (IOException e) {
			this.fs = null;
		}
		return false;
	}
	/*
	 * Set up the paths where modules and regular classes need to be read. We need to deal with two different kind of
	 * formats of cy.sym: Post JDK 12: ct.sym -> 9 -> java/ -> lang/* 9-modules -> java.base -> module-info.sig
	 *
	 * From JDK 12 onward: ct.sym -> 9 -> java.base -> module-info.sig java/ -> lang/* Notably, 1) in JDK 12 modules
	 * classes and ordinary classes are located in the same location 2) in JDK 12, ordinary classes are found inside
	 * their respective modules
	 *
	 */
	protected void initialize() throws CoreException {
		this.releaseInHex = Integer.toHexString(Integer.parseInt(this.release)).toUpperCase();
		Path lib = Paths.get(this.zipFilename).getParent();
		Path filePath = Paths.get(lib.toString(), "ct.sym"); //$NON-NLS-1$
		URI t = filePath.toUri();
		if (!Files.exists(filePath)) {
			return;
		}
		URI uri = URI.create("jar:file:" + t.getRawPath()); //$NON-NLS-1$
		try {
			this.fs = FileSystems.getFileSystem(uri);
		} catch (FileSystemNotFoundException fne) {
			// Ignore and move on
		}
		if (this.fs == null) {
			HashMap<String, ?> env = new HashMap<>();
			try {
				this.fs = FileSystems.newFileSystem(uri, env);
			} catch (IOException e) {
				return;
			}
		}
		this.releasePath = this.fs.getPath("/"); //$NON-NLS-1$
		this.isJRE12Plus = isJRE12Plus(this.releasePath);
		Path modPath = this.fs.getPath(this.releaseInHex + (this.isJRE12Plus ? "" : "-modules")); //$NON-NLS-1$ //$NON-NLS-2$
		if (Files.exists(modPath)) {
			this.modulePath = modPath;
			this.modPathString = this.zipFilename + "|"+ modPath.toString(); //$NON-NLS-1$
		}

		if (!Files.exists(this.releasePath.resolve(this.releaseInHex))) {
			Exception e = new IllegalArgumentException("release " + this.release + " is not found in the system"); //$NON-NLS-1$//$NON-NLS-2$
			throw new CoreException(new Status(IStatus.ERROR, JavaCore.PLUGIN_ID, e.getMessage(), e));
		}
		if (Files.exists(this.fs.getPath(this.releaseInHex, "system-modules"))) { //$NON-NLS-1$
			this.fs = null;  // Fallback to default version
			return;
		}
		if (this.release != null) {
			List<String> sub = new ArrayList<>();
			try (DirectoryStream<java.nio.file.Path> stream = Files.newDirectoryStream(this.releasePath)) {
				for (final java.nio.file.Path subdir : stream) {
					String rel = JRTUtil.sanitizedFileName(subdir);
					if (rel.contains(this.releaseInHex)) {
						sub.add(rel);
					} else {
						continue;
					}
				}
			} catch (IOException e) {
				this.fs = null; // Fallback to default version
			}
			this.subReleases = sub.toArray(new String[sub.size()]);
		}
	}

	static HashMap<String, SimpleSet> findPackagesInModules(final ClasspathJrtWithReleaseOption jrt) {
		// In JDK 11 and before, classes are not listed under their respective modules
		// Hence, we simply go to the default module system for package-module mapping
		if (jrt.fs == null || !jrt.isJRE12Plus) {
			return ClasspathJrt.findPackagesInModules(jrt);
		}
		String zipFileName = jrt.zipFilename;
		HashMap<String, SimpleSet> cache = PackageCache.get(jrt.modPathString);
		if (cache != null) {
			return cache;
		}
		final HashMap<String, SimpleSet> packagesInModule = new HashMap<>();
		PackageCache.put(jrt.modPathString, packagesInModule);
		try {
			final File imageFile = new File(zipFileName);
			org.aspectj.org.eclipse.jdt.internal.compiler.util.JRTUtil.walkModuleImage(imageFile, jrt.release,
					new org.aspectj.org.eclipse.jdt.internal.compiler.util.JRTUtil.JrtFileVisitor<Path>() {
						SimpleSet packageSet = null;

						@Override
						public FileVisitResult visitPackage(Path dir, Path mod, BasicFileAttributes attrs)
								throws IOException {
							ClasspathJar.addToPackageSet(this.packageSet, dir.toString(), true);
							return FileVisitResult.CONTINUE;
						}

						@Override
						public FileVisitResult visitFile(Path file, Path mod, BasicFileAttributes attrs)
								throws IOException {
							return FileVisitResult.CONTINUE;
						}

						@Override
						public FileVisitResult visitModule(Path path, String name) throws IOException {
							this.packageSet = new SimpleSet(41);
							this.packageSet.add(""); //$NON-NLS-1$
							if (name.endsWith("/")) { //$NON-NLS-1$
								name = name.substring(0, name.length() - 1);
							}
							packagesInModule.put(name, this.packageSet);
							return FileVisitResult.CONTINUE;
						}
					}, JRTUtil.NOTIFY_PACKAGES | JRTUtil.NOTIFY_MODULES);
		} catch (IOException e) {
			// return empty handed
		}
		return packagesInModule;
	}

	public static void loadModules(final ClasspathJrtWithReleaseOption jrt) {
		if (jrt.fs == null || !jrt.isJRE12Plus) {
			ClasspathJrt.loadModules(jrt);
			return;
		}
		if (jrt.modPathString == null)
			return;
		Set<IModule> cache = ModulesCache.get(jrt.modPathString);
		if (cache == null) {
			try (DirectoryStream<java.nio.file.Path> stream = Files.newDirectoryStream(jrt.releasePath)) {
				for (final java.nio.file.Path subdir : stream) {
					if (!subdir.getFileName().toString().contains(jrt.releaseInHex)) {
						continue;
					}
					Files.walkFileTree(subdir, Collections.EMPTY_SET, 2, new FileVisitor<java.nio.file.Path>() {
						@Override
						public FileVisitResult preVisitDirectory(java.nio.file.Path dir, BasicFileAttributes attrs)
								throws IOException {
							return FileVisitResult.CONTINUE;
						}

						@Override
						public FileVisitResult visitFile(java.nio.file.Path f, BasicFileAttributes attrs)
								throws IOException {
							if (attrs.isDirectory() || f.getNameCount() < 3) {
								return FileVisitResult.CONTINUE;
							}
							if (f.getFileName().toString().equals(MODULE_INFO)) {
								byte[] content = JRTUtil.safeReadBytes(f);
								if (content == null)
									return FileVisitResult.CONTINUE;
								jrt.acceptModule(content);
							}
							return FileVisitResult.SKIP_SIBLINGS;
						}

						@Override
						public FileVisitResult visitFileFailed(java.nio.file.Path f, IOException exc)
								throws IOException {
							return FileVisitResult.CONTINUE;
						}

						@Override
						public FileVisitResult postVisitDirectory(java.nio.file.Path dir, IOException exc)
								throws IOException {
							return FileVisitResult.CONTINUE;
						}
					});
				}
			} catch (IOException e) {
				// Nothing much to do
			}
		}
	}


	@Override
	public NameEnvironmentAnswer findClass(String binaryFileName, String qualifiedPackageName, String moduleName,
			String qualifiedBinaryFileName, boolean asBinaryOnly, Predicate<String> moduleNameFilter) {

		if (this.fs == null) {
			return super.findClass(binaryFileName, qualifiedPackageName, moduleName, qualifiedBinaryFileName,
					asBinaryOnly, moduleNameFilter);
		}
		if (!isPackage(qualifiedPackageName, moduleName))
			return null; // most common case

		try {
			IBinaryType reader = null;
			byte[] content = null;
			String fileNameWithoutExtension = qualifiedBinaryFileName.substring(0,
												qualifiedBinaryFileName.length() - SuffixConstants.SUFFIX_CLASS.length);
			if (this.subReleases != null && this.subReleases.length > 0) {
				qualifiedBinaryFileName = qualifiedBinaryFileName.replace(".class", ".sig"); //$NON-NLS-1$ //$NON-NLS-2$
				outer: for (String rel : this.subReleases) {
					Path p = null;
					inner: if (this.isJRE12Plus) {
						if (moduleName != null) {
							p = this.fs.getPath(rel, moduleName, qualifiedBinaryFileName);
						}
						else {
							try (DirectoryStream<java.nio.file.Path> stream = Files
									.newDirectoryStream(this.fs.getPath(rel))) {
								for (final java.nio.file.Path subdir : stream) {
									p = subdir.resolve(qualifiedBinaryFileName);
									if (Files.exists(p)) {
										if (subdir.getNameCount() == 2 ) {
											moduleName = subdir.getName(1).toString();
										}
										break inner;
									}
								}
							}
						}
					} else {
						p = this.fs.getPath(rel, qualifiedBinaryFileName);
					}
					if (Files.exists(p)) {
						content = JRTUtil.safeReadBytes(p);
						if (content != null) {
							reader = new ClassFileReader(content, qualifiedBinaryFileName.toCharArray());
							if (moduleName != null)
								((ClassFileReader) reader).moduleName = moduleName.toCharArray();
							break outer;
						}
					}
				}
			} else {
				reader = ClassFileReader.readFromModule(new File(this.zipFilename), moduleName, qualifiedBinaryFileName,
						moduleNameFilter);
			}
			return createAnswer(fileNameWithoutExtension, reader);
		} catch (ClassFormatException | IOException e) {
			// treat as if class file is missing
		}
		return null;
	}

	@Override
	public Collection<String> getModuleNames(Collection<String> limitModules) {
		HashMap<String, SimpleSet> cache = findPackagesInModules(this);
		if (cache != null)
			return selectModules(cache.keySet(), limitModules);
		return Collections.emptyList();
	}

	@Override
	public void cleanup() {
		try {
			super.reset();
		} finally {
			// The same file system is also used in JRTUtil, so don't close it here.
			this.fs = null;
		}
	}

	@Override
	public boolean hasModule() {
		return this.fs == null ? super.hasModule() : this.modPathString != null;
	}

	@Override
	protected String getKey() {
		return this.fs == null ? super.getKey() : this.modPathString;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (!(o instanceof ClasspathJrtWithReleaseOption))
			return false;
		ClasspathJrtWithReleaseOption jar = (ClasspathJrtWithReleaseOption) o;
		if (!Util.equalOrNull(this.release, jar.release)) {
			return false;
		}
		return super.equals(o);
	}

	@Override
	public int hashCode() {
		int hash = this.zipFilename == null ? super.hashCode() : this.zipFilename.hashCode();
		return Util.combineHashCodes(hash, this.release.hashCode());
	}

	@Override
	public String toString() {
		String start = "Classpath jrt file " + this.zipFilename + " with --release option " + this.release; //$NON-NLS-1$ //$NON-NLS-2$
		return start;
	}

}
