/*******************************************************************************
 * Copyright (c) 2019 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.aspectj.org.eclipse.jdt.internal.compiler.batch;

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.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import org.aspectj.org.eclipse.jdt.internal.compiler.batch.FileSystem.Classpath;
import org.aspectj.org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
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.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.Util;

public class ClasspathJep247Jdk12 extends ClasspathJep247 {

	Map<String, IModule> modules;
	static String MODULE_INFO = "module-info.sig"; //$NON-NLS-1$

	public ClasspathJep247Jdk12(File jdkHome, String release, AccessRuleSet accessRuleSet) {
		super(jdkHome, release, accessRuleSet);
	}
	@Override
	public List<Classpath> fetchLinkedJars(FileSystem.ClasspathSectionProblemReporter problemReporter) {
		 return null;
	}
	@Override
	public NameEnvironmentAnswer findClass(char[] typeName, String qualifiedPackageName, String moduleName, String qualifiedBinaryFileName) {
		return findClass(typeName, qualifiedPackageName, moduleName, qualifiedBinaryFileName, false);
	}
	@Override
	public NameEnvironmentAnswer findClass(char[] typeName, String qualifiedPackageName, String moduleName, String qualifiedBinaryFileName, boolean asBinaryOnly) {
		if (!isPackage(qualifiedPackageName, moduleName))
			return null; // most common case

		try {
			ClassFileReader reader = null;
			byte[] content = null;
			qualifiedBinaryFileName = qualifiedBinaryFileName.replace(".class", ".sig"); //$NON-NLS-1$ //$NON-NLS-2$
			if (this.subReleases != null && this.subReleases.length > 0) {
				done: for (String rel : this.subReleases) {
					if (moduleName == null) {
						Path p = this.fs.getPath(rel);
						try (DirectoryStream<java.nio.file.Path> stream = Files.newDirectoryStream(p)) {
							for (final java.nio.file.Path subdir: stream) {
								Path f = this.fs.getPath(rel, JRTUtil.sanitizedFileName(subdir), qualifiedBinaryFileName);
								if (Files.exists(f)) {
									content = JRTUtil.safeReadBytes(f);
									if (content != null)
										break done;
								}
							}
						}
					} else {
						Path p = this.fs.getPath(rel, moduleName, qualifiedBinaryFileName);
						if (Files.exists(p)) {
							content = JRTUtil.safeReadBytes(p);
							if (content != null)
								break;
						}
					}
				}
			} else {
				content = JRTUtil.safeReadBytes(this.fs.getPath(this.releaseInHex, qualifiedBinaryFileName));
			}
			if (content != null) {
				reader = new ClassFileReader(content, qualifiedBinaryFileName.toCharArray());
				char[] modName = moduleName != null ? moduleName.toCharArray() : null;
				return new NameEnvironmentAnswer(reader, fetchAccessRestriction(qualifiedBinaryFileName), modName);
			}
		} catch (ClassFormatException | IOException e) {
			// continue
		}
		return null;
	}

	@Override
	public void initialize() throws IOException {
		if (this.compliance == null) {
			return;
		}
		if (this.fs != null) {
			super.initialize();
			return;
		}
		this.releaseInHex = Integer.toHexString(Integer.parseInt(this.compliance)).toUpperCase();
		Path filePath = this.jdkHome.toPath().resolve("lib").resolve("ct.sym"); //$NON-NLS-1$ //$NON-NLS-2$
		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<>();
			this.fs = FileSystems.newFileSystem(uri, env);
		}
		this.releasePath = this.fs.getPath("/"); //$NON-NLS-1$
		if (!Files.exists(this.fs.getPath(this.releaseInHex))) {
			throw new IllegalArgumentException("release " + this.compliance + " is not found in the system");  //$NON-NLS-1$//$NON-NLS-2$
		}
		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);
			}
			this.subReleases = sub.toArray(new String[sub.size()]);
		} catch (IOException e) {
			//e.printStackTrace();
		}
		super.initialize();
	}
	@Override
	public void loadModules() {
		// Modules below level 8 are not dealt with here. Leave it to ClasspathJrt
		if (this.jdklevel <= ClassFileConstants.JDK1_8) {
			super.loadModules();
			return;
		}
		final Path modPath = this.fs.getPath(this.releaseInHex);
		this.modulePath = this.file.getPath() + "|" + modPath.toString(); //$NON-NLS-1$
		this.modules = ModulesCache.get(this.modulePath);
		if (this.modules == null) {
			try (DirectoryStream<java.nio.file.Path> stream = Files.newDirectoryStream(this.releasePath)) {
				HashMap<String,IModule> newCache = new HashMap<>();
				for (final java.nio.file.Path subdir: stream) {
					String rel = JRTUtil.sanitizedFileName(subdir);
					if (!rel.contains(this.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) && Files.exists(f)) {
								byte[] content = JRTUtil.safeReadBytes(f);
								if (content == null)
									return FileVisitResult.CONTINUE;
								Path m = f.subpath(1, f.getNameCount() - 1);
								String name = JRTUtil.sanitizedFileName(m);
								ClasspathJep247Jdk12.this.acceptModule(name, content, newCache);
								ClasspathJep247Jdk12.this.moduleNamesCache.add(name);
							}
							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;
						}
					});
				}
				synchronized(ModulesCache) {
					if (ModulesCache.get(this.modulePath) == null) {
						this.modules = Collections.unmodifiableMap(newCache);
						ModulesCache.put(this.modulePath, this.modules);
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		} else {
			this.moduleNamesCache.addAll(this.modules.keySet());
		}
	}
	@Override
	public Collection<String> getModuleNames(Collection<String> limitModule, Function<String, IModule> getModule) {
		return selectModules(this.moduleNamesCache, limitModule, getModule);
	}
	@Override
	public IModule getModule(char[] moduleName) {
		if (this.modules != null) {
			return this.modules.get(String.valueOf(moduleName));
		}
		return null;
	}
	void acceptModule(String name, byte[] content, Map<String, IModule> cache) {
		if (content == null)
			return;

		if (cache.containsKey(name))
			return;

		ClassFileReader reader = null;
		try {
			reader = new ClassFileReader(content, IModule.MODULE_INFO_CLASS.toCharArray());
		} catch (ClassFormatException e) {
			e.printStackTrace();
		}
		if (reader != null) {
			acceptModule(reader, cache);
		}
	}
	@Override
	void acceptModule(ClassFileReader reader, Map<String, IModule> cache) {
		// Modules below level 8 are not dealt with here. Leave it to ClasspathJrt
		if (this.jdklevel <= ClassFileConstants.JDK1_8) {
			super.acceptModule(reader, cache);
			return;
		}
		if (reader != null) {
			IModule moduleDecl = reader.getModuleDeclaration();
			if (moduleDecl != null) {
				cache.put(String.valueOf(moduleDecl.name()), moduleDecl);
			}
		}
	}
	@Override
	public synchronized char[][] getModulesDeclaringPackage(String qualifiedPackageName, String moduleName) {
		// Ignore moduleName as this has nothing to do with modules (as of now)
		if (this.packageCache != null)
			return singletonModuleNameIf(this.packageCache.contains(qualifiedPackageName));

		this.packageCache = new HashSet<>(41);
		this.packageCache.add(Util.EMPTY_STRING);
		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)) {
					continue;
				}
				try (DirectoryStream<java.nio.file.Path> stream2 = Files.newDirectoryStream(subdir)) {
					for (final java.nio.file.Path subdir2: stream2) {
						Files.walkFileTree(subdir2, new FileVisitor<java.nio.file.Path>() {
							@Override
							public FileVisitResult preVisitDirectory(java.nio.file.Path dir, BasicFileAttributes attrs) throws IOException {
								if (dir.getNameCount() <= 2)
									return FileVisitResult.CONTINUE;
								Path relative = dir.subpath(2, dir.getNameCount());
								addToPackageCache(relative.toString(), false);
								return FileVisitResult.CONTINUE;
							}

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

							@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) {
			e.printStackTrace();
			// Rethrow
		}
		return singletonModuleNameIf(this.packageCache.contains(qualifiedPackageName));
	}
}
