/*******************************************************************************
 * Copyright (c) 2000, 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;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.aspectj.org.eclipse.jdt.core.IClasspathAttribute;
import org.aspectj.org.eclipse.jdt.core.IClasspathEntry;
import org.aspectj.org.eclipse.jdt.core.IJavaElement;
import org.aspectj.org.eclipse.jdt.core.IModuleDescription;
import org.aspectj.org.eclipse.jdt.core.IPackageFragmentRoot;
import org.aspectj.org.eclipse.jdt.core.JavaCore;
import org.aspectj.org.eclipse.jdt.core.JavaModelException;
import org.aspectj.org.eclipse.jdt.core.compiler.CharOperation;
import org.aspectj.org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.aspectj.org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.aspectj.org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.aspectj.org.eclipse.jdt.internal.core.JavaModelManager.PerProjectInfo;
import org.aspectj.org.eclipse.jdt.internal.core.nd.IReader;
import org.aspectj.org.eclipse.jdt.internal.core.nd.java.JavaIndex;
import org.aspectj.org.eclipse.jdt.internal.core.nd.java.NdResourceFile;
import org.aspectj.org.eclipse.jdt.internal.core.nd.java.NdType;
import org.aspectj.org.eclipse.jdt.internal.core.nd.java.NdZipEntry;
import org.aspectj.org.eclipse.jdt.internal.core.util.HashtableOfArrayToObject;
import org.aspectj.org.eclipse.jdt.internal.core.util.Util;

/**
 * A package fragment root that corresponds to a .jar or .zip.
 *
 * <p>NOTE: The only visible entries from a .jar or .zip package fragment root
 * are .class files.
 * <p>NOTE: A jar package fragment root may or may not have an associated resource.
 *
 * @see org.aspectj.org.eclipse.jdt.core.IPackageFragmentRoot
 * @see org.aspectj.org.eclipse.jdt.internal.core.JarPackageFragmentRootInfo
 */
@SuppressWarnings({"rawtypes", "unchecked"})
public class JarPackageFragmentRoot extends PackageFragmentRoot {

	protected final static ArrayList EMPTY_LIST = new ArrayList();

	/**
	 * The path to the jar file
	 * (a workspace relative path if the jar is internal,
	 * or an OS path if the jar is external)
	 */
	protected final IPath jarPath;

	boolean knownToBeModuleLess;

	private boolean multiVersion;

	/**
	 * Reflects the extra attributes of the classpath entry declaring this root.
	 * Caution, this field is used in hashCode() & equals() to avoid overzealous sharing.
	 * Can be null, if lookup via the corresponding classpath entry failed.
	 */
	final protected IClasspathAttribute[] extraAttributes;

	/**
	 * Constructs a package fragment root which is the root of the Java package directory hierarchy
	 * based on a JAR file.
	 */
	public JarPackageFragmentRoot(IResource resource, IPath externalJarPath, JavaProject project, IClasspathAttribute[] attributes) {
		super(resource, project);
		this.jarPath = externalJarPath;
		if (attributes == null) {
			// attributes could either be
			// (1) provided by the caller (particularly when creating from memento),
			// (2) retrieved from the corresponding classpath entry (if a resolved classpath is available).
			// These two cases should cover all normal scenarios, else extraAttributes will be null.
			try {
				PerProjectInfo perProjectInfo = project.getPerProjectInfo();
				synchronized (perProjectInfo) {
					if (perProjectInfo.resolvedClasspath != null && perProjectInfo.unresolvedEntryStatus == JavaModelStatus.VERIFIED_OK) {
						IClasspathEntry classpathEntry = project.getClasspathEntryFor(externalJarPath);
						if (classpathEntry != null)
							attributes = classpathEntry.getExtraAttributes();
					}
				}
			} catch (JavaModelException e) {
				// ignore
			}
		}
		this.extraAttributes = attributes;
	}

	/**
	 * Compute the package fragment children of this package fragment root.
	 * These are all of the directory zip entries, and any directories implied
	 * by the path of class files contained in the jar of this package fragment root.
	 */
	@Override
	protected boolean computeChildren(OpenableElementInfo info, IResource underlyingResource) throws JavaModelException {
		final HashtableOfArrayToObject rawPackageInfo = new HashtableOfArrayToObject();
		final Map<String, String> overridden = new HashMap<>();
		IJavaElement[] children = NO_ELEMENTS;
		try {
			// always create the default package
			rawPackageInfo.put(CharOperation.NO_STRINGS, new ArrayList[] { EMPTY_LIST, EMPTY_LIST });

			boolean usedIndex = false;
			if (JavaIndex.isEnabled()) {
				JavaIndex index = JavaIndex.getIndex();
				try (IReader reader = index.getNd().acquireReadLock()) {
					IPath resourcePath = JavaIndex.getLocationForElement(this);
					if (!resourcePath.isEmpty()) {
						NdResourceFile resourceFile = index.getResourceFile(resourcePath.toString().toCharArray());
						if (index.isUpToDate(resourceFile)) {
							usedIndex = true;
							long level = resourceFile.getJdkLevel();
							String compliance = CompilerOptions.versionFromJdkLevel(level);
							// Locate all the non-classfile entries
							for (NdZipEntry next : resourceFile.getZipEntries()) {
								String filename = next.getFileName().getString();
								initRawPackageInfo(rawPackageInfo, filename, filename.endsWith("/"), compliance); //$NON-NLS-1$
							}

							// Locate all the classfile entries
							for (NdType type : resourceFile.getTypes()) {
								String path = new String(type.getTypeId().getBinaryName()) + ".class"; //$NON-NLS-1$
								initRawPackageInfo(rawPackageInfo, path, false, compliance);
							}
						}
					}
				}
			}

			// If we weren't able to compute the set of children from the index (either the index was disabled or didn't
			// contain an up-to-date entry for this .jar) then fetch it directly from the .jar
			if (!usedIndex) {
				Object file = JavaModel.getTarget(getPath(), true);
				long classLevel = Util.getJdkLevel(file);
				String projectCompliance = this.getJavaProject().getOption(JavaCore.COMPILER_COMPLIANCE, true);
				long projectLevel = CompilerOptions.versionToJdkLevel(projectCompliance);
				ZipFile jar = null;
				try {
					jar = getJar();
					String version = "META-INF/versions/";  //$NON-NLS-1$
					List<String> versions = new ArrayList<>();
					if (projectLevel >= ClassFileConstants.JDK9 && jar.getEntry(version) != null) {
						int earliestJavaVersion = ClassFileConstants.MAJOR_VERSION_9;
						long latestJDK = CompilerOptions.releaseToJDKLevel(projectCompliance);
						int latestJavaVer = (int) (latestJDK >> 16);

						for(int i = latestJavaVer; i >= earliestJavaVersion; i--) {
							String s = "" + + (i - 44); //$NON-NLS-1$
							String versionPath = version + s;
							if (jar.getEntry(versionPath) != null) {
								versions.add(s);
							}
						}
					}

					String[] supportedVersions = versions.toArray(new String[versions.size()]);
					if (supportedVersions.length > 0) {
						this.multiVersion = true;
					}
					int length = version.length();
					for (Enumeration<? extends ZipEntry> e= jar.entries(); e.hasMoreElements();) {
						ZipEntry member= e.nextElement();
						String name = member.getName();
						if (this.multiVersion && name.length() > (length + 2) && name.startsWith(version)) {
							int end = name.indexOf('/', length);
							if (end >= name.length()) continue;
							String versionPath = name.substring(0, end);
							String ver = name.substring(length, end);
							if(versions.contains(ver) && org.aspectj.org.eclipse.jdt.internal.compiler.util.Util.isClassFileName(name)) {
								name = name.substring(end + 1);
								overridden.put(name, versionPath);
							}
						}
						initRawPackageInfo(rawPackageInfo, name, member.isDirectory(), CompilerOptions.versionFromJdkLevel(classLevel));
					}
				}  finally {
					JavaModelManager.getJavaModelManager().closeZipFile(jar);
				}
			}
			// loop through all of referenced packages, creating package fragments if necessary
			// and cache the entry names in the rawPackageInfo table
			children = new IJavaElement[rawPackageInfo.size()];
			int index = 0;
			for (int i = 0, length = rawPackageInfo.keyTable.length; i < length; i++) {
				String[] pkgName = (String[]) rawPackageInfo.keyTable[i];
				if (pkgName == null) continue;
				children[index++] = getPackageFragment(pkgName);
			}
		} catch (CoreException e) {
			if (e.getCause() instanceof ZipException) {
				// not a ZIP archive, leave the children empty
				Util.log(IStatus.ERROR, "Invalid ZIP archive: " + toStringWithAncestors()); //$NON-NLS-1$
				children = NO_ELEMENTS;
			} else if (e instanceof JavaModelException) {
				throw (JavaModelException)e;
			} else {
				throw new JavaModelException(e);
			}
		}
		info.setChildren(children);
		((JarPackageFragmentRootInfo) info).rawPackageInfo = rawPackageInfo;
		((JarPackageFragmentRootInfo) info).overriddenClasses = overridden;
		return true;
	}
	protected IJavaElement[] createChildren(final HashtableOfArrayToObject rawPackageInfo) {
		IJavaElement[] children;
		// loop through all of referenced packages, creating package fragments if necessary
		// and cache the entry names in the rawPackageInfo table
		children = new IJavaElement[rawPackageInfo.size()];
		int index = 0;
		for (int i = 0, length = rawPackageInfo.keyTable.length; i < length; i++) {
			String[] pkgName = (String[]) rawPackageInfo.keyTable[i];
			if (pkgName == null) continue;
			children[index++] = getPackageFragment(pkgName);
		}
		return children;
	}
	/**
	 * Returns a new element info for this element.
	 */
	@Override
	protected Object createElementInfo() {
		return new JarPackageFragmentRootInfo();
	}
	/**
	 * A Jar is always K_BINARY.
	 */
	@Override
	protected int determineKind(IResource underlyingResource) {
		return IPackageFragmentRoot.K_BINARY;
	}
	/**
	 * Returns true if this handle represents the same jar
	 * as the given handle. Two jars are equal if they share
	 * the same zip file.
	 *
	 * @see Object#equals
	 */
	@Override
	public boolean equals(Object o) {
		if (this == o)
			return true;
		if (o instanceof JarPackageFragmentRoot) {
			JarPackageFragmentRoot other= (JarPackageFragmentRoot) o;
			return this.jarPath.equals(other.jarPath)
					&& Arrays.equals(this.extraAttributes, other.extraAttributes);
		}
		return false;
	}
	@Override
	public String getElementName() {
		return this.jarPath.lastSegment();
	}
	/**
	 * Returns the underlying ZipFile for this Jar package fragment root.
	 *
	 * @exception CoreException if an error occurs accessing the jar
	 */
	public ZipFile getJar() throws CoreException {
		return JavaModelManager.getJavaModelManager().getZipFile(getPath());
	}
	/**
	 * @see IPackageFragmentRoot
	 */
	@Override
	public int getKind() {
		return IPackageFragmentRoot.K_BINARY;
	}
	@Override
	int internalKind() throws JavaModelException {
		return IPackageFragmentRoot.K_BINARY;
	}
	/**
	 * Returns an array of non-java resources contained in the receiver.
	 */
	@Override
	public Object[] getNonJavaResources() throws JavaModelException {
		// We want to show non java resources of the default package at the root (see PR #1G58NB8)
		Object[] defaultPkgResources =  ((JarPackageFragment) getPackageFragment(CharOperation.NO_STRINGS)).storedNonJavaResources();
		int length = defaultPkgResources.length;
		if (length == 0)
			return defaultPkgResources;
		Object[] nonJavaResources = new Object[length];
		for (int i = 0; i < length; i++) {
			JarEntryResource nonJavaResource = (JarEntryResource) defaultPkgResources[i];
			nonJavaResources[i] = nonJavaResource.clone(this);
		}
		return nonJavaResources;
	}
	@Override
	public PackageFragment getPackageFragment(String[] pkgName) {
		return new JarPackageFragment(this, pkgName);
	}
	@Override
	public PackageFragment getPackageFragment(String[] pkgName, String mod) {
		return new JarPackageFragment(this, pkgName); // Overridden in JImageModuleFragmentBridge
	}

	@Override
	public String getClassFilePath(String classname) {
		if (this.multiVersion) {
			JarPackageFragmentRootInfo elementInfo;
			try {
				elementInfo = (JarPackageFragmentRootInfo) getElementInfo();
				String versionPath = elementInfo.overriddenClasses.get(classname);
				return versionPath == null ? classname : versionPath + '/' + classname;
			} catch (JavaModelException e) {
				// move on
			}
		}
		return classname;
	}
	@Override
	public IModuleDescription getModuleDescription() {
		if (this.knownToBeModuleLess)
			return null;
		IModuleDescription module = super.getModuleDescription();
		if (module == null)
			this.knownToBeModuleLess = true;
		return module;
	}

	@Override
	public IPath internalPath() {
		if (isExternal()) {
			return this.jarPath;
		} else {
			return super.internalPath();
		}
	}
	@Override
	public IResource resource(PackageFragmentRoot root) {
		if (this.resource == null) {
			// external jar
			return null;
		}
		return super.resource(root);
	}


	/**
	 * @see IJavaElement
	 */
	@Override
	public IResource getUnderlyingResource() throws JavaModelException {
		if (isExternal()) {
			if (!exists()) throw newNotPresentException();
			return null;
		} else {
			return super.getUnderlyingResource();
		}
	}
	@Override
	public int hashCode() {
		return this.jarPath.hashCode() + Arrays.hashCode(this.extraAttributes);
	}
	protected void initRawPackageInfo(HashtableOfArrayToObject rawPackageInfo, String entryName, boolean isDirectory, String compliance) {
		int lastSeparator;
		if (isDirectory) {
			if (entryName.charAt(entryName.length() - 1) == '/') {
				lastSeparator = entryName.length() - 1;
			} else {
				lastSeparator = entryName.length();
			}
		} else {
			lastSeparator = entryName.lastIndexOf('/');
		}
		String[] pkgName = Util.splitOn('/', entryName, 0, lastSeparator);
		String[] existing = null;
		int length = pkgName.length;
		int existingLength = length;
		while (existingLength >= 0) {
			existing = (String[]) rawPackageInfo.getKey(pkgName, existingLength);
			if (existing != null) break;
			existingLength--;
		}
		JavaModelManager manager = JavaModelManager.getJavaModelManager();
		for (int i = existingLength; i < length; i++) {
			// sourceLevel must be null because we know nothing about it based on a jar file
			if (Util.isValidFolderNameForPackage(pkgName[i], null, compliance)) {
				System.arraycopy(existing, 0, existing = new String[i+1], 0, i);
				existing[i] = manager.intern(pkgName[i]);
				rawPackageInfo.put(existing, new ArrayList[] { EMPTY_LIST, EMPTY_LIST });
			} else {
				// non-Java resource folder
				if (!isDirectory) {
					ArrayList[] children = (ArrayList[]) rawPackageInfo.get(existing);
					if (children[1/*NON_JAVA*/] == EMPTY_LIST) children[1/*NON_JAVA*/] = new ArrayList();
					children[1/*NON_JAVA*/].add(entryName);
				}
				return;
			}
		}
		if (isDirectory)
			return;

		// add classfile info amongst children
		ArrayList[] children = (ArrayList[]) rawPackageInfo.get(pkgName);
		if (org.aspectj.org.eclipse.jdt.internal.compiler.util.Util.isClassFileName(entryName)) {
			if (children[0/*JAVA*/] == EMPTY_LIST) children[0/*JAVA*/] = new ArrayList();
			String nameWithoutExtension = entryName.substring(lastSeparator + 1, entryName.length() - 6);
			children[0/*JAVA*/].add(nameWithoutExtension);
		} else {
			if (children[1/*NON_JAVA*/] == EMPTY_LIST) children[1/*NON_JAVA*/] = new ArrayList();
			children[1/*NON_JAVA*/].add(entryName);
		}

	}
	/**
	 * @see IPackageFragmentRoot
	 */
	@Override
	public boolean isArchive() {
		return true;
	}
	/**
	 * @see IPackageFragmentRoot
	 */
	@Override
	public boolean isExternal() {
		return resource() == null;
	}
	/**
	 * Jars and jar entries are all read only
	 */
	@Override
	public boolean isReadOnly() {
		return true;
	}
	/**
	 * Returns whether the corresponding resource or associated file exists
	 */
	@Override
	protected boolean resourceExists(IResource underlyingResource) {
		if (underlyingResource == null) {
			return
				JavaModel.getExternalTarget(
					getPath()/*don't make the path relative as this is an external archive*/,
					true/*check existence*/) != null;
		} else {
			return super.resourceExists(underlyingResource);
		}
	}

	@Override
	protected void toStringAncestors(StringBuffer buffer) {
		if (isExternal())
			// don't show project as it is irrelevant for external jar files.
			// also see https://bugs.eclipse.org/bugs/show_bug.cgi?id=146615
			return;
		super.toStringAncestors(buffer);
	}

	public URL getIndexPath() {
		try {
			IClasspathEntry entry = ((JavaProject) getParent()).getClasspathEntryFor(getPath());
			if (entry != null) return ((ClasspathEntry)entry).getLibraryIndexLocation();
		} catch (JavaModelException e) {
			// ignore exception
		}
		return null;
	}

	@Override
	public Manifest getManifest() {
		ZipFile jar = null;
		try {
			jar = getJar();
			ZipEntry mfEntry = jar.getEntry(TypeConstants.META_INF_MANIFEST_MF);
			if (mfEntry != null) {
				try (InputStream is = jar.getInputStream(mfEntry)) {
					return new Manifest(is);
				}
			}
		} catch (CoreException | IOException e) {
			// must do without manifest
		} finally {
			JavaModelManager.getJavaModelManager().closeZipFile(jar);
		}
		return null;
	}

//	@Override
//	public boolean isModule() {
//	 	try {
//	 		return ((PackageFragmentRootInfo) getElementInfo()).isModule(resource(), this);
//	 	} catch (JavaModelException e) {
//	 		return false;
//	 	}
//	}
}
