/*******************************************************************************
 * 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;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.aspectj.org.eclipse.jdt.internal.core.nd.Nd;
import org.aspectj.org.eclipse.jdt.internal.core.nd.NdNode;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.Database;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.IString;
import org.aspectj.org.eclipse.jdt.internal.core.nd.db.IndexException;
import org.aspectj.org.eclipse.jdt.internal.core.nd.field.FieldList;
import org.aspectj.org.eclipse.jdt.internal.core.nd.field.FieldLong;
import org.aspectj.org.eclipse.jdt.internal.core.nd.field.FieldOneToMany;
import org.aspectj.org.eclipse.jdt.internal.core.nd.field.FieldOneToMany.Visitor;
import org.aspectj.org.eclipse.jdt.internal.core.nd.field.FieldSearchIndex.IResultRank;
import org.aspectj.org.eclipse.jdt.internal.core.nd.field.FieldSearchIndex.SearchCriteria;
import org.aspectj.org.eclipse.jdt.internal.core.nd.field.FieldSearchKey;
import org.aspectj.org.eclipse.jdt.internal.core.nd.field.FieldShort;
import org.aspectj.org.eclipse.jdt.internal.core.nd.field.FieldString;
import org.aspectj.org.eclipse.jdt.internal.core.nd.field.StructDef;

/**
 * Represents a source of java classes (such as a .jar or .class file).
 */
public class NdResourceFile extends NdNode {
	public static final FieldSearchKey<JavaIndex> FILENAME;
	public static final FieldOneToMany<NdType> TYPES;
	public static final FieldLong TIME_LAST_USED;
	public static final FieldLong TIME_LAST_SCANNED;
	public static final FieldLong SIZE_LAST_SCANNED;
	public static final FieldLong HASHCODE_LAST_SCANNED;
	public static final FieldOneToMany<NdWorkspaceLocation> WORKSPACE_MAPPINGS;
	public static final FieldString JAVA_ROOT;
	public static final FieldLong JDK_LEVEL;
	public static final FieldList<NdZipEntry> ZIP_ENTRIES;
	public static final FieldString MANIFEST_CONTENT;
	public static final FieldShort FILE_FLAGS;

	/**
	 * Flag indicating that this is a corrupted zip file.
	 */
	public static final int FLG_CORRUPT_ZIP_FILE = 0x0001;

	@SuppressWarnings("hiding")
	public static final StructDef<NdResourceFile> type;

	static {
		type = StructDef.create(NdResourceFile.class, NdNode.type);
		FILENAME = FieldSearchKey.create(type, JavaIndex.FILES);
		TYPES = FieldOneToMany.create(type, NdType.FILE, 16);
		TIME_LAST_USED = type.addLong();
		TIME_LAST_SCANNED = type.addLong();
		SIZE_LAST_SCANNED = type.addLong();
		HASHCODE_LAST_SCANNED = type.addLong();
		WORKSPACE_MAPPINGS = FieldOneToMany.create(type, NdWorkspaceLocation.RESOURCE);
		JAVA_ROOT = type.addString();
		JDK_LEVEL = type.addLong();
		ZIP_ENTRIES = FieldList.create(type, NdZipEntry.type, 1);
		MANIFEST_CONTENT = type.addString();
		FILE_FLAGS = type.addShort();

		type.done();
	}

	private long jdkLevel;

	public NdResourceFile(Nd dom, long address) {
		super(dom, address);
	}

	public NdResourceFile(Nd nd) {
		super(nd);
	}

	public boolean isCorruptedZipFile() {
		return hasAllFlags(FLG_CORRUPT_ZIP_FILE);
	}

	public int getFlags() {
		return FILE_FLAGS.get(getNd(), this.address);
	}

	public boolean hasAllFlags(int flags) {
		int ourFlags = getFlags();

		return (ourFlags & flags) == flags;
	}

	public void setFlags(int flags) {
		FILE_FLAGS.put(getNd(), this.address, (short) (getFlags() | flags));
	}

	/**
	 * Returns the set of all leaf zip entries that are not .class files. Does not include non-empty directories
	 * or .class files, but will contain all other zip entries from the original jar file. Returns the empty list
	 * for non-jar files.
	 */
	public List<NdZipEntry> getZipEntries() {
		return ZIP_ENTRIES.asList(getNd(), getAddress());
	}

	/**
	 * Returns the content of the JAR's MANIFEST.MF file, or null if either this isn't a .JAR file or it didn't contain
	 * a MANIFEST.MF file.
	 */
	public IString getManifestContent() {
		return MANIFEST_CONTENT.get(getNd(), getAddress());
	}

	/**
	 * Stores the content of the JAR's MANIFEST.MF file. This should only be invoked on resources that correspond to JAR
	 * files.
	 */
	public void setManifestContent(char[] newContent) {
		MANIFEST_CONTENT.put(getNd(), getAddress(), newContent);
	}

	public long getJdkLevel() {
		if (this.jdkLevel == 0) {
			this.jdkLevel = JDK_LEVEL.get(getNd(), this.address);
		}
		return this.jdkLevel;
	}

	public void setJdkLevel(long jdkLevel) {
		if (getJdkLevel() != jdkLevel) {
			JDK_LEVEL.put(getNd(), this.address, jdkLevel);
		}
	}

	/**
	 * Determines whether this file is still in the index. If a {@link NdResourceFile} instance is retained while the
	 * database lock is released and reobtained, this method should be invoked to ensure that the {@link NdResourceFile}
	 * has not been deleted in the meantime.
	 */
	public boolean isInIndex() {
		try {
			// In the common case where the resource file was deleted and the memory hasn't yet been reused,
			// this will fail.
			if (!this.nd.isValidAddress(this.address)
					|| NODE_TYPE.get(this.nd, this.address) != this.nd.getNodeType(getClass())) {
				return false;
			}

			char[] filename = FILENAME.get(getNd(), this.address).getChars();

			NdResourceFile result = JavaIndex.FILES.findBest(this.nd, Database.DATA_AREA_OFFSET,
					SearchCriteria.create(filename), new IResultRank() {
						@Override
						public long getRank(Nd testNd, long testAddress) {
							if (testAddress == NdResourceFile.this.address) {
								return 1;
							}
							return -1;
						}
					});

			return (this.equals(result));
		} catch (IndexException e) {
			// Read errors are expected here. It's possible that the resource file has been deleted and something
			// new was written to this address, in which case we may be reading random gibberish from the database.
			// This is likely to cause an exception.
			return false;
		}
	}

	public List<IPath> getAllWorkspaceLocations() {
		final List<IPath> result = new ArrayList<>();

		WORKSPACE_MAPPINGS.accept(getNd(), this.address, new Visitor<NdWorkspaceLocation>() {
			@Override
			public void visit(int index, NdWorkspaceLocation toVisit) {
				result.add(new Path(toVisit.getPath().getString()));
			}
		});

		return result;
	}

	public IPath getFirstWorkspaceLocation() {
		if (WORKSPACE_MAPPINGS.isEmpty(getNd(), this.address)) {
			return Path.EMPTY;
		}

		return new Path(WORKSPACE_MAPPINGS.get(getNd(), this.address, 0).getPath().toString());
	}

	public IPath getAnyOpenWorkspaceLocation(IWorkspaceRoot root) {
		int numMappings = WORKSPACE_MAPPINGS.size(getNd(), this.address);

		for (int mapping = 0; mapping < numMappings; mapping++) {
			NdWorkspaceLocation nextMapping = WORKSPACE_MAPPINGS.get(getNd(), this.address, mapping);

			IPath nextPath = new Path(nextMapping.getPath().getString());
			if (nextPath.isEmpty()) {
				continue;
			}

			IProject project = root.getProject(nextPath.segment(0));
			if (project.isOpen()) {
				return nextPath;
			}
		}

		return Path.EMPTY;
	}

	/**
	 * Returns a workspace path to this resource if possible and the absolute filesystem location if not.
	 */
	public IPath getPath() {
		IPath workspacePath = getFirstWorkspaceLocation();

		if (workspacePath.isEmpty()) {
			return new Path(getLocation().getString());
		}

		return workspacePath;
	}

	public List<NdWorkspaceLocation> getWorkspaceMappings() {
		return WORKSPACE_MAPPINGS.asList(getNd(), this.address);
	}

	public IString getLocation() {
		return FILENAME.get(getNd(), this.address);
	}

	public void setLocation(String filename) {
		FILENAME.put(getNd(), this.address, filename);
	}

	public FileFingerprint getFingerprint() {
		return new FileFingerprint(
				getTimeLastScanned(),
				getSizeLastScanned(),
				getHashcodeLastScanned());
	}

	private long getHashcodeLastScanned() {
		return HASHCODE_LAST_SCANNED.get(getNd(), this.address);
	}

	/**
	 * Returns true iff the indexer has finished writing the contents of this file to the index. Returns false if
	 * indexing may still be going on. If this returns false, readers should ignore all contents of this file.
	 *
	 * @return true iff the contents of this file are usable
	 */
	public boolean isDoneIndexing() {
		return getTimeLastScanned() != 0;
	}

	public long getTimeLastScanned() {
		return TIME_LAST_SCANNED.get(getNd(), this.address);
	}

	public long getSizeLastScanned() {
		return SIZE_LAST_SCANNED.get(getNd(), this.address);
	}

	public long getTimeLastUsed() {
		return TIME_LAST_USED.get(getNd(), this.address);
	}

	public void setTimeLastUsed(long timeLastUsed) {
		TIME_LAST_USED.put(getNd(), this.address, timeLastUsed);
	}

	public void setFingerprint(FileFingerprint newFingerprint) {
		TIME_LAST_SCANNED.put(getNd(), this.address, newFingerprint.getTime());
		HASHCODE_LAST_SCANNED.put(getNd(), this.address, newFingerprint.getHash());
		SIZE_LAST_SCANNED.put(getNd(), this.address, newFingerprint.getSize());
	}

	public void setPackageFragmentRoot(char[] javaRoot) {
		JAVA_ROOT.put(getNd(), this.address, javaRoot);
	}

	/**
	 * Returns the absolute path to the java root for this .jar or .class file. If this is a .jar file, it returns its
	 * own filename.
	 */
	public IString getPackageFragmentRoot() {
		IString javaRoot = JAVA_ROOT.get(getNd(), this.address);
		if (javaRoot.length() == 0) {
			return getLocation();
		}
		return javaRoot;
	}

	public void markAsInvalid() {
		TIME_LAST_SCANNED.put(getNd(), this.address, 0);
	}

	public int getTypeCount() {
		return TYPES.size(getNd(), this.address);
	}

	public List<NdType> getTypes() {
		return TYPES.asList(getNd(), this.address);
	}

	public NdType getType(int index) {
		return TYPES.get(getNd(), this.address, index);
	}

	@Override
	public String toString() {
		try {
			return FILENAME.get(getNd(), this.address).toString();
		} catch (RuntimeException e) {
			// This is called most often from the debugger, so we want to return something meaningful even
			// if the code is buggy, the database is corrupt, or we don't have a read lock.
			return super.toString();
		}
	}

	public void allocateZipEntries(int expectedNumberOfZipEntries) {
		ZIP_ENTRIES.allocate(this.nd, this.address, expectedNumberOfZipEntries);
	}

	public NdZipEntry addZipEntry(String fileName) {
		NdZipEntry result = ZIP_ENTRIES.append(getNd(), getAddress());
		result.setFilename(fileName);
		return result;
	}
}
