/*******************************************************************************
 * Copyright (c) 2015, 2016 Google, Inc and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Stefan Xenos (Google) - Initial implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.core.nd.java;

import java.io.File;
import java.util.List;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.core.nd.Nd;
import org.eclipse.jdt.internal.core.nd.NdNode;
import org.eclipse.jdt.internal.core.nd.NdNodeTypeRegistry;
import org.eclipse.jdt.internal.core.nd.db.ChunkCache;
import org.eclipse.jdt.internal.core.nd.db.Database;
import org.eclipse.jdt.internal.core.nd.field.FieldSearchIndex;
import org.eclipse.jdt.internal.core.nd.field.FieldSearchIndex.IResultRank;
import org.eclipse.jdt.internal.core.nd.field.FieldSearchIndex.SearchCriteria;
import org.eclipse.jdt.internal.core.nd.field.StructDef;
import org.eclipse.jdt.internal.core.nd.indexer.FileStateCache;
import org.eclipse.jdt.internal.core.nd.util.CharArrayUtils;

public class JavaIndex {
	// Version constants
	static final int CURRENT_VERSION = Nd.version(1, 49);
	static final int MAX_SUPPORTED_VERSION = Nd.version(1, 49);
	static final int MIN_SUPPORTED_VERSION = Nd.version(1, 49);

	public static final String ENABLE_NEW_JAVA_INDEX = "enableNewJavaIndex"; //$NON-NLS-1$
	public static final boolean ENABLE_NEW_JAVA_INDEX_DEFAULT = false;

	// Fields for the search header
	public static final FieldSearchIndex<NdResourceFile> FILES;
	public static final FieldSearchIndex<NdTypeId> SIMPLE_INDEX;
	public static final FieldSearchIndex<NdTypeId> TYPES;
	public static final StructDef<JavaIndex> type;

	static {
		type = StructDef.create(JavaIndex.class);
		FILES = FieldSearchIndex.create(type, NdResourceFile.FILENAME);
		SIMPLE_INDEX = FieldSearchIndex.create(type, NdTypeId.SIMPLE_NAME);
		TYPES = FieldSearchIndex.create(type, NdTypeId.FIELD_DESCRIPTOR);
		type.done();

		// This struct needs to fit within the first database chunk.
		assert type.getFactory().getRecordSize() <= Database.CHUNK_SIZE;
	}

	private final static class BestResourceFile implements FieldSearchIndex.IResultRank {
		public BestResourceFile() {
		}

		@Override
		public long getRank(Nd resourceFileNd, long resourceFileAddress) {
			return NdResourceFile.TIME_LAST_SCANNED.get(resourceFileNd, resourceFileAddress);
		}
	}

	private static final BestResourceFile bestResourceFile = new BestResourceFile();
	private final long address;
	private Nd nd;
	private IResultRank anyResult = new IResultRank() {
		@Override
		public long getRank(Nd dom, long address1) {
			return 1;
		}
	};
	private static Nd globalNd;
	private static final String INDEX_FILENAME = "index.db"; //$NON-NLS-1$
	private final static Object ndMutex = new Object();

	public JavaIndex(Nd dom, long address) {
		this.address = address;
		this.nd = dom;
	}

	/**
	 * Returns the most-recently-scanned resource file with the given name or null if none
	 */
	public NdResourceFile getResourceFile(char[] location) {
		return FILES.findBest(this.nd, this.address, FieldSearchIndex.SearchCriteria.create(location),
				bestResourceFile);
	}

	/**
	 * Returns true iff the given resource file is up-to-date with the filesystem. Returns false
	 * if the argument is null or there is a possibility it being out-of-date with the file system.
	 * 
	 * @param file the index file to look up or null
	 * @throws CoreException 
	 */
	public boolean isUpToDate(NdResourceFile file) throws CoreException {
		if (file != null && file.isDoneIndexing()) {
			String location = file.getLocation().getString();

			FileStateCache cache = FileStateCache.getCache(getNd());
			Boolean cachedResult = cache.isUpToDate(location);
			if (cachedResult != null) {
				return cachedResult;
			}

			Path locationPath = new Path(location);
			boolean result = file.getFingerprint().test(locationPath, null).matches();
			cache.put(location, result);
			return result;
		}
		return false;
	}

	public void dirty(String location) {
		FileStateCache.getCache(getNd()).clear();
	}

	public List<NdResourceFile> findResourcesWithPath(String thePath) {
		return FILES.findAll(this.nd, this.address, FieldSearchIndex.SearchCriteria.create(thePath.toCharArray()));
	}

	public List<NdResourceFile> getAllResourceFiles() {
		return FILES.asList(this.nd, this.address);
	}

	public NdTypeId findType(char[] fieldDescriptor) {
		SearchCriteria searchCriteria = SearchCriteria.create(fieldDescriptor);
		return TYPES.findBest(this.nd, this.address, searchCriteria, this.anyResult);
	}

	public List<NdTypeId> findTypesBySimpleName(char[] query) {
		SearchCriteria searchCriteria = SearchCriteria.create(query).prefix(true);
		return SIMPLE_INDEX.findAll(this.nd, this.address, searchCriteria);
	}

	public List<NdTypeId> findTypesBySimpleName(char[] query, int count) {
		SearchCriteria searchCriteria = SearchCriteria.create(query).prefix(true);
		return SIMPLE_INDEX.findAll(this.nd, this.address, searchCriteria, count);
	}

	public boolean visitFieldDescriptorsStartingWith(char[] fieldDescriptorPrefix, FieldSearchIndex.Visitor<NdTypeId> visitor) {
		SearchCriteria searchCriteria = SearchCriteria.create(fieldDescriptorPrefix).prefix(true);
		return TYPES.visitAll(this.nd, this.address, searchCriteria, visitor);
	}

	/**
	 * Returns a type ID or creates a new one if it does not exist. The caller must
	 * attach a reference to it after calling this method or it may leak.
	 */
	public NdTypeId createTypeId(char[] fieldDescriptor) {
		NdTypeId existingType = findType(fieldDescriptor);

		if (existingType != null) {
			return existingType;
		}

		if (fieldDescriptor.length > 1) {
			if (fieldDescriptor[0] == 'L') {
				if (fieldDescriptor[fieldDescriptor.length - 1] != ';') {
					throw new IllegalStateException(new String(fieldDescriptor) + " is not a valid field descriptor"); //$NON-NLS-1$
				}
			}
		}

		NdTypeId result = new NdTypeId(this.nd, fieldDescriptor);
		if (!CharArrayUtils.equals(result.getFieldDescriptor().getChars(), fieldDescriptor)) {
			throw new IllegalStateException("Field descriptor didn't match"); //$NON-NLS-1$
		}
		return result;
	}

	public Nd getNd() {
		return this.nd;
	}

	/**
	 * Converts a JDT-style path (which may be a resource-relative path or absolute filesystem location) into a location
	 * (which is unconditionally a filesystem location) or null if none.
	 * <p>
	 * The logic used in {@link #getLocationForPath(IPath)}, {@link #getLocationForElement(IJavaElement)}, and
	 * {@link JavaModelManager#getLocalFile(IPath)} should be equivalent.
	 */
	public static IPath getLocationForPath(IPath path) {
		IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();

		IResource resource = root.findMember(path);

		if (resource != null) {
			return resource.getLocation();
		}

		return path;
	}

	/**
	 * Returns the absolute filesystem location of the given element or the empty path if none
	 * <p>
	 * The logic used in {@link #getLocationForPath(IPath)}, {@link #getLocationForElement(IJavaElement)}, and
	 * {@link JavaModelManager#getLocalFile(IPath)} should be equivalent.
	 */
	public static IPath getLocationForElement(IJavaElement next) {
		IResource resource = next.getResource();

		if (resource != null) {
			return resource.getLocation() == null ? Path.EMPTY : resource.getLocation();
		}

		return next.getPath();
	}

	public static boolean isEnabled() {
		IPreferencesService preferenceService = Platform.getPreferencesService();
		if (preferenceService == null) {
			return true;
		}
		return preferenceService.getBoolean(JavaCore.PLUGIN_ID, ENABLE_NEW_JAVA_INDEX, ENABLE_NEW_JAVA_INDEX_DEFAULT,
				null);
	}

	public static Nd createNd(File databaseFile, ChunkCache chunkCache) {
		return new Nd(databaseFile, chunkCache, createTypeRegistry(),
				MIN_SUPPORTED_VERSION, MAX_SUPPORTED_VERSION, CURRENT_VERSION);
	}

	public static Nd getGlobalNd() {
		Nd localNd;
		synchronized (ndMutex) {
			localNd = globalNd;
		}

		if (localNd != null) {
			return localNd;
		}

		localNd = createNd(getDBFile(), ChunkCache.getSharedInstance());

		synchronized (ndMutex) {
			if (globalNd == null) {
				globalNd = localNd;
			}
			return globalNd;
		}
	}

	public static JavaIndex getIndex(Nd nd) {
		return new JavaIndex(nd, Database.DATA_AREA_OFFSET);
	}

	public static JavaIndex getIndex() {
		return getIndex(getGlobalNd());
	}

	public static int getCurrentVersion() {
		return CURRENT_VERSION;
	}

	static File getDBFile() {
		IPath stateLocation = JavaCore.getPlugin().getStateLocation();
		return stateLocation.append(INDEX_FILENAME).toFile();
	}

	static NdNodeTypeRegistry<NdNode> createTypeRegistry() {
		NdNodeTypeRegistry<NdNode> registry = new NdNodeTypeRegistry<>();

		registry.register(0x0028, NdBinding.type.getFactory());
		registry.register(0x0030, NdComplexTypeSignature.type.getFactory());
		registry.register(0x0038, NdConstant.type.getFactory());
		registry.register(0x0040, NdConstantAnnotation.type.getFactory());
		registry.register(0x0050, NdConstantArray.type.getFactory());
		registry.register(0x0060, NdConstantBoolean.type.getFactory());
		registry.register(0x0070, NdConstantByte.type.getFactory());
		registry.register(0x0080, NdConstantChar.type.getFactory());
		registry.register(0x0090, NdConstantClass.type.getFactory());
		registry.register(0x00A0, NdConstantDouble.type.getFactory());
		registry.register(0x00B0, NdConstantEnum.type.getFactory());
		registry.register(0x00C0, NdConstantFloat.type.getFactory());
		registry.register(0x00D0, NdConstantInt.type.getFactory());
		registry.register(0x00E0, NdConstantLong.type.getFactory());
		registry.register(0x00F0, NdConstantShort.type.getFactory());
		registry.register(0x0100, NdConstantString.type.getFactory());
		registry.register(0x0110, NdMethod.type.getFactory());
		registry.register(0x0118, NdMethodAnnotationData.type.getFactory());
		registry.register(0x0150, NdResourceFile.type.getFactory());
		registry.register(0x0170, NdType.type.getFactory());
		registry.register(0x0190, NdTypeArgument.type.getFactory());
		registry.register(0x01A0, NdTypeInterface.type.getFactory());
		registry.register(0x01C0, NdTypeSignature.type.getFactory());
		registry.register(0x01D0, NdTypeId.type.getFactory());
		registry.register(0x01E0, NdTypeInterface.type.getFactory());
		registry.register(0x01F0, NdVariable.type.getFactory());
		registry.register(0x0200, NdWorkspaceLocation.type.getFactory());
		return registry;
	}
}
