/*******************************************************************************
 * Copyright (c) 2015, 2016 Google, Inc 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:
 *   Stefan Xenos (Google) - Initial implementation
 *******************************************************************************/
package org.aspectj.org.eclipse.jdt.internal.core.nd.java.model;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.aspectj.org.eclipse.jdt.core.IJavaModelStatusConstants;
import org.aspectj.org.eclipse.jdt.core.IOrdinaryClassFile;
import org.aspectj.org.eclipse.jdt.core.IType;
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.ClassFileReader;
import org.aspectj.org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IBinaryType;
import org.aspectj.org.eclipse.jdt.internal.compiler.env.IDependent;
import org.aspectj.org.eclipse.jdt.internal.compiler.util.SuffixConstants;
import org.aspectj.org.eclipse.jdt.internal.core.ClassFile;
import org.aspectj.org.eclipse.jdt.internal.core.JarPackageFragmentRoot;
import org.aspectj.org.eclipse.jdt.internal.core.JavaModelManager;
import org.aspectj.org.eclipse.jdt.internal.core.PackageFragment;
import org.aspectj.org.eclipse.jdt.internal.core.PackageFragmentRoot;
import org.aspectj.org.eclipse.jdt.internal.core.nd.IReader;
import org.aspectj.org.eclipse.jdt.internal.core.nd.Nd;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.IndexException;
import org.aspectj.org.eclipse.jdt.internal.core.nd.indexer.Indexer;
import org.aspectj.org.eclipse.jdt.internal.core.nd.java.JavaIndex;
import org.aspectj.org.eclipse.jdt.internal.core.nd.java.JavaNames;
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.TypeRef;
import org.aspectj.org.eclipse.jdt.internal.core.nd.util.CharArrayUtils;
import org.aspectj.org.eclipse.jdt.internal.core.util.Util;

public class BinaryTypeFactory {
	public static final class NotInIndexException extends Exception {
		private static final long serialVersionUID = 2859848007651528256L;

		public NotInIndexException() {
		}
	}

	/**
	 * Returns a descriptor for the given class within the given package fragment, or null if the fragment doesn't have
	 * a location on the filesystem.
	 */
	private static BinaryTypeDescriptor createDescriptor(PackageFragment pkg, ClassFile classFile) {
		PackageFragmentRoot root = (PackageFragmentRoot) pkg.getParent();
		IPath location = JavaIndex.getLocationForElement(root);
		if (location == null) {
			return null;
		}
		String entryName = Util.concatWith(pkg.names, classFile.getElementName(), '/');
		String name = Util.concatWith(pkg.names, classFile.getName(), '/');
		String overridePath = root.getClassFilePath(entryName);
		if (overridePath != entryName) {
			entryName = overridePath;
			String versionPath = overridePath.substring(0, overridePath.indexOf(entryName));
			name = versionPath + name;
		}
		char[] fieldDescriptor = CharArrayUtils.concat(new char[] { 'L' },
				name.toCharArray(), new char[] { ';' });
		IPath workspacePath = root.getPath();
		String indexPath;

		if (root instanceof JarPackageFragmentRoot) {
			// The old version returned this, but it doesn't conform to the spec on IBinaryType.getFileName():
			indexPath = root.getHandleIdentifier() + IDependent.JAR_FILE_ENTRY_SEPARATOR + entryName;
			// Version that conforms to the JavaDoc spec on IBinaryType.getFileName() -- note that this breaks
			// InlineMethodTests in the JDT UI project. Need to investigate why before using it.
			//indexPath = workspacePath.toString() + IDependent.JAR_FILE_ENTRY_SEPARATOR + entryName;
		} else {
			location = location.append(entryName);
			indexPath = workspacePath.append(entryName).toString();
			workspacePath = classFile.resource().getFullPath();
		}

		return new BinaryTypeDescriptor(location.toString().toCharArray(), fieldDescriptor,
				workspacePath.toString().toCharArray(), indexPath.toCharArray());
	}

	public static BinaryTypeDescriptor createDescriptor(IOrdinaryClassFile classFile) {
		ClassFile concreteClass = (ClassFile)classFile;
		PackageFragment parent = (PackageFragment) classFile.getParent();

		return createDescriptor(parent, concreteClass);
	}

	public static BinaryTypeDescriptor createDescriptor(IType type) {
		return createDescriptor(type.getClassFile());
	}

	public static IBinaryType create(IOrdinaryClassFile classFile, IProgressMonitor monitor) throws JavaModelException, ClassFormatException {
		BinaryTypeDescriptor descriptor = createDescriptor(classFile);
		return readType(descriptor, monitor);
	}

	/**
	 * Reads the given binary type. If the type can be found in the index with a fingerprint that exactly matches
	 * the file on disk, the type is read from the index. Otherwise the type is read from disk. Returns null if
	 * no such type exists.
	 * @throws ClassFormatException 
	 */
	public static IBinaryType readType(BinaryTypeDescriptor descriptor, IProgressMonitor monitor) throws JavaModelException, ClassFormatException {

		if (JavaIndex.isEnabled()) {
			try {
				return readFromIndex(JavaIndex.getIndex(), descriptor, monitor);
			} catch (NotInIndexException e) {
				// fall back to reading the zip file, below
			}
		}

		return rawReadType(descriptor, true);
	}

	public static ClassFileReader rawReadType(BinaryTypeDescriptor descriptor, boolean fullyInitialize) throws JavaModelException, ClassFormatException {
		try {
			return rawReadTypeTestForExists(descriptor, fullyInitialize, true);
		} catch (FileNotFoundException e) {
			throw new JavaModelException(e, IJavaModelStatusConstants.IO_EXCEPTION);
		}
	}

	/**
	 * Read the class file from disk, circumventing the index's cache. This should only be used by callers
	 * that need to read information from the class file which aren't present in the index (such as method bodies).
	 * 
	 * @return the newly-created ClassFileReader or null if the given class file does not exist.
	 * @throws ClassFormatException if the class file existed but was corrupt
	 * @throws JavaModelException if unable to read the class file due to a transient failure
	 * @throws FileNotFoundException if the file does not exist
	 */
	public static ClassFileReader rawReadTypeTestForExists(BinaryTypeDescriptor descriptor, boolean fullyInitialize,
			boolean useInvalidArchiveCache) throws JavaModelException, ClassFormatException, FileNotFoundException {
		if (descriptor == null) {
			return null;
		}
		if (descriptor.isInJarFile()) {
			if (CharOperation.indexOf("jrt-fs.jar".toCharArray(), descriptor.location, false) == -1) { //$NON-NLS-1$
				ZipFile zip = null;
				try {
					zip = JavaModelManager.getJavaModelManager().getZipFile(new Path(new String(descriptor.workspacePath)),
							useInvalidArchiveCache);
					char[] entryNameCharArray = CharArrayUtils.concat(
							JavaNames.fieldDescriptorToBinaryName(descriptor.fieldDescriptor), SuffixConstants.SUFFIX_class);
					String entryName = new String(entryNameCharArray);
					ZipEntry ze = zip.getEntry(entryName);
					if (ze != null) {
						byte contents[];
						try {
							contents = org.aspectj.org.eclipse.jdt.internal.compiler.util.Util.getZipEntryByteContent(ze, zip);
						} catch (IOException ioe) {
							throw new JavaModelException(ioe, IJavaModelStatusConstants.IO_EXCEPTION);
						}
						return new ClassFileReader(contents, descriptor.indexPath, fullyInitialize);
					}
				} catch (CoreException e) {
					throw new JavaModelException(e);
				} finally {
					JavaModelManager.getJavaModelManager().closeZipFile(zip);
				}
			}
		} else {
			IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(new String(descriptor.workspacePath)));
			byte[] contents;
			try (InputStream stream = file.getContents(true)) {
				contents = org.aspectj.org.eclipse.jdt.internal.compiler.util.Util.getInputStreamAsByteArray(stream, -1);
			} catch (CoreException e) {
				IStatus status = e.getStatus();
				if (status.getCode() == IResourceStatus.RESOURCE_NOT_FOUND) {
					throw new FileNotFoundException();
				}
				throw new JavaModelException(e);
			} catch (IOException e) {
				throw new JavaModelException(e, IJavaModelStatusConstants.IO_EXCEPTION);
			}
			return new ClassFileReader(contents, file.getFullPath().toString().toCharArray(), fullyInitialize);
		}
		return null;
	}

	/**
	 * Tries to read the given IBinaryType from the index. The return value is lightweight and may be cached
	 * with minimal memory cost. Returns an IBinaryType if the type was found in the index and the index
	 * was up-to-date. Throws a NotInIndexException if the index does not contain an up-to-date cache of the
	 * requested file. Returns null if the index contains an up-to-date cache of the requested file and it was
	 * able to determine that the requested class does not exist in that file.
	 */
	public static IBinaryType readFromIndex(JavaIndex index, BinaryTypeDescriptor descriptor, IProgressMonitor monitor) throws JavaModelException, NotInIndexException {
		// If the new index is enabled, check if we have this class file cached in the index already		
		char[] fieldDescriptor = descriptor.fieldDescriptor;

		Nd nd = index.getNd();

		if (descriptor.location != null) {
			// Acquire a read lock on the index
			try (IReader lock = nd.acquireReadLock()) {
				try {
					TypeRef typeRef = TypeRef.create(nd, descriptor.location, fieldDescriptor);
					NdType type = typeRef.get();

					if (type == null) {
						// If we couldn't find the type in the index, determine whether the cause is
						// that the type is known not to exist or whether the resource just hasn't
						// been indexed yet

						NdResourceFile resourceFile = index.getResourceFile(descriptor.location);
						if (index.isUpToDate(resourceFile)) {
							return null;
						}
						throw new NotInIndexException();
					}
					NdResourceFile resourceFile = type.getResourceFile();
					if (index.isUpToDate(resourceFile)) {
						IndexBinaryType result = new IndexBinaryType(typeRef, descriptor.indexPath);

						// We already have the database lock open and have located the element, so we may as
						// well prefetch the inexpensive attributes.
						result.initSimpleAttributes();

						return result;
					}
					throw new NotInIndexException();
				} catch (CoreException e) {
					throw new JavaModelException(e);
				}
			} catch (IndexException e) {
				Package.log("Index corruption detected. Rebuilding index.", e); //$NON-NLS-1$
				Indexer.getInstance().requestRebuildIndex();
			}
		}

		throw new NotInIndexException();
	}
}
