/*******************************************************************************
 * Copyright (c) 2016 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.core.search.indexing;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.FileVisitResult;
import java.nio.file.attribute.BasicFileAttributes;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.eclipse.jdt.internal.compiler.util.JRTUtil;
import org.eclipse.jdt.internal.compiler.util.SimpleLookupTable;
import org.eclipse.jdt.internal.compiler.util.Util;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.core.index.Index;
import org.eclipse.jdt.internal.core.index.IndexLocation;
import org.eclipse.jdt.internal.core.search.JavaSearchDocument;
import org.eclipse.jdt.internal.core.search.processing.JobManager;

public class AddJrtToIndex extends BinaryContainer {

	IFile resource;
	private IndexLocation indexFileURL;
	private final boolean forceIndexUpdate;
	static final char JAR_SEPARATOR = IJavaSearchScope.JAR_FILE_ENTRY_SEPARATOR.charAt(0);
	
	enum FILE_INDEX_STATE {
		EXISTS,
		DELETED
	}

	public AddJrtToIndex(IFile resource, IndexLocation indexFile, IndexManager manager, final boolean updateIndex) {
		super(resource.getFullPath(), manager);
		this.resource = resource;
		this.indexFileURL = indexFile;
		this.forceIndexUpdate = updateIndex;
	}
	public AddJrtToIndex(IPath jrtPath, IndexLocation indexFile, IndexManager manager, final boolean updateIndex) {
		// external JAR scenario - no resource
		super(jrtPath, manager);
		this.indexFileURL = indexFile;
		this.forceIndexUpdate = updateIndex;
	}
	public boolean equals(Object o) {
		if (o instanceof AddJrtToIndex) {
			if (this.resource != null)
				return this.resource.equals(((AddJrtToIndex) o).resource);
			if (this.containerPath != null)
				return this.containerPath.equals(((AddJrtToIndex) o).containerPath);
		}
		return false;
	}
	public int hashCode() {
		if (this.resource != null)
			return this.resource.hashCode();
		if (this.containerPath != null)
			return this.containerPath.hashCode();
		return -1;
	}
	
	private class JrtTraverser implements org.eclipse.jdt.internal.compiler.util.JRTUtil.JrtFileVisitor<java.nio.file.Path> {
		
		SimpleLookupTable indexedFileNames;
		public JrtTraverser() {
		}
		public JrtTraverser(SimpleLookupTable indexedFileNames) {
			this.indexedFileNames = indexedFileNames;
		}

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

		@Override
		public FileVisitResult visitFile(java.nio.file.Path path, java.nio.file.Path mod, BasicFileAttributes attrs)
				throws IOException {
			String name = path.getFileName().toString();
			if (Util.isClassFileName(name) && 
					isValidPackageNameForClassOrisModule(name)) {
				this.indexedFileNames.put(name, FILE_INDEX_STATE.EXISTS);
			}
			return FileVisitResult.CONTINUE;
		}
		@Override
		public FileVisitResult visitModule(java.nio.file.Path mod) throws IOException {
			return FileVisitResult.CONTINUE;
		}
	}
	
	private class JrtIndexer extends JrtTraverser {
		final SearchParticipant participant;
		final IPath indexPath;
		final IndexManager indexManager;
		final IPath container;
		final Index index;
		final File jrt;

		public JrtIndexer(File jrt, SearchParticipant participant, Index index, IPath container, IndexManager indexManager) {
			this.jrt = jrt;
			this.participant = (participant != null) ? participant : SearchEngine.getDefaultSearchParticipant();
			this.index = index;
			IndexLocation indexLocation = index.getIndexLocation();
			this.indexPath = indexLocation != null ? new Path(indexLocation.getCanonicalFilePath()) : null;
			this.container = container;
			this.indexManager = indexManager;
		}

		public FileVisitResult visitFile(java.nio.file.Path path, java.nio.file.Path mod, BasicFileAttributes attrs)
				throws IOException {
			String name = path.getFileName().toString();
			if (Util.isClassFileName(name) && 
					isValidPackageNameForClassOrisModule(name)) {
				try {
					String fullPath = path.toString();
					byte[] classFileBytes;
					classFileBytes = JRTUtil.getClassfileContent(this.jrt, fullPath, mod.toString());
					String docFullPath =  this.container.toString() + JAR_SEPARATOR + mod.toString() + JAR_SEPARATOR + fullPath;
					JavaSearchDocument entryDocument = new JavaSearchDocument(docFullPath, classFileBytes, this.participant);
					this.indexManager.indexDocument(entryDocument, this.participant, this.index, this.indexPath);
				} catch (IOException | ClassFormatException e) {
					e.printStackTrace();
				}
			}
			return FileVisitResult.CONTINUE;
		}
	}

	public boolean execute(IProgressMonitor progressMonitor) {

		if (this.isCancelled || progressMonitor != null && progressMonitor.isCanceled()) return true;

		if (hasPreBuiltIndex()) {
			boolean added = this.manager.addIndex(this.containerPath, this.indexFileURL);
			if (added) return true;	
			this.indexFileURL = null;
		}

		try {
			// if index is already cached, then do not perform any check
			// MUST reset the IndexManager if a jar file is changed
			if (this.manager.getIndexForUpdate(this.containerPath, false, /*do not reuse index file*/ false /*do not create if none*/) != null) {
				if (JobManager.VERBOSE)
					org.eclipse.jdt.internal.core.util.Util.verbose("-> no indexing required (index already exists) for " + this.containerPath); //$NON-NLS-1$
				return true;
			}

			final Index index = this.manager.getIndexForUpdate(this.containerPath, true, /*reuse index file*/ true /*create if none*/);
			if (index == null) {
				if (JobManager.VERBOSE)
					org.eclipse.jdt.internal.core.util.Util.verbose("-> index could not be created for " + this.containerPath); //$NON-NLS-1$
				return true;
			}
			index.separator = JAR_SEPARATOR;
			ReadWriteMonitor monitor = index.monitor;
			if (monitor == null) {
				if (JobManager.VERBOSE)
					org.eclipse.jdt.internal.core.util.Util.verbose("-> index for " + this.containerPath + " just got deleted"); //$NON-NLS-1$//$NON-NLS-2$
				return true; // index got deleted since acquired
			}
			try {
				final String fileName;
				final IPath container;
				monitor.enterWrite(); // ask permission to write

				if (this.resource != null) {
					URI location = this.resource.getLocationURI();
					if (location == null) return false;
					if (JavaModelManager.JRT_ACCESS_VERBOSE)
						System.out.println("(" + Thread.currentThread() + ") [AddJrtFileToIndex.execute()] Creating ZipFile on " + location.getPath()); //$NON-NLS-1$	//$NON-NLS-2$
					File file = null;
					try {
						file = org.eclipse.jdt.internal.core.util.Util.toLocalFile(location, progressMonitor);
					} catch (CoreException e) {
						if (JobManager.VERBOSE) {
							org.eclipse.jdt.internal.core.util.Util.verbose("-> failed to index " + location.getPath() + " because of the following exception:"); //$NON-NLS-1$ //$NON-NLS-2$
							e.printStackTrace();
						}
					}
					if (file == null) {
						if (JobManager.VERBOSE)
							org.eclipse.jdt.internal.core.util.Util.verbose("-> failed to index " + location.getPath() + " because the file could not be fetched"); //$NON-NLS-1$ //$NON-NLS-2$
						return false;
					}
					fileName = file.getAbsolutePath();
					container =  this.resource.getFullPath().makeRelative();
					// absolute path relative to the workspace
				} else {
					
					fileName = this.containerPath.toOSString();
					container = this.containerPath;
				}


				if (JobManager.VERBOSE)
					org.eclipse.jdt.internal.core.util.Util.verbose("-> indexing " + fileName); //$NON-NLS-1$
				long initialTime = System.currentTimeMillis();
				String[] paths = index.queryDocumentNames(""); // all file names //$NON-NLS-1$
				if (paths != null) {
					int max = paths.length;
					/* check integrity of the existing index file
					 * if the length is equal to 0, we want to index the whole jrt again
					 * If not, then we want to check that there is no missing entry, if
					 * one entry is missing then we recreate the index
					 */
					
					final SimpleLookupTable indexedFileNames = new SimpleLookupTable(max == 0 ? 33 : max + 11);
					for (int i = 0; i < max; i++)
						indexedFileNames.put(paths[i], FILE_INDEX_STATE.DELETED);
					
					org.eclipse.jdt.internal.compiler.util.JRTUtil.walkModuleImage(new File(fileName), 
							new JrtTraverser(indexedFileNames), JRTUtil.NOTIFY_FILES);

					boolean needToReindex = indexedFileNames.elementSize != max; // a new file was added
					if (!needToReindex) {
						Object[] valueTable = indexedFileNames.valueTable;
						for (int i = 0, l = valueTable.length; i < l; i++) {
							if (valueTable[i] == FILE_INDEX_STATE.DELETED) {
								needToReindex = true; // a file was deleted so re-index
								break;
							}
						}
						if (!needToReindex) {
							if (JobManager.VERBOSE)
								org.eclipse.jdt.internal.core.util.Util.verbose("-> no indexing required (index is consistent with library) for " //$NON-NLS-1$
								+ fileName + " (" //$NON-NLS-1$
								+ (System.currentTimeMillis() - initialTime) + "ms)"); //$NON-NLS-1$
							this.manager.saveIndex(index); // to ensure its placed into the saved state
							return true;
						}
					}
				}

				// Index the jrt for the first time or reindex the jrt in case the previous index file has been corrupted
				// index already existed: recreate it so that we forget about previous entries
				if (!this.manager.resetIndex(this.containerPath)) {
					// failed to recreate index, see 73330
					this.manager.removeIndex(this.containerPath);
					return false;
				}
				
				File jrt = new File(fileName);
				org.eclipse.jdt.internal.compiler.util.JRTUtil.walkModuleImage(jrt, 
						new JrtIndexer(jrt, SearchEngine.getDefaultSearchParticipant(), index, container, this.manager), JRTUtil.NOTIFY_FILES);

				if(this.forceIndexUpdate) {
					this.manager.savePreBuiltIndex(index);
				}
				else {
					this.manager.saveIndex(index);
				}
				if (JobManager.VERBOSE)
					org.eclipse.jdt.internal.core.util.Util.verbose("-> done indexing of " //$NON-NLS-1$
						+ fileName + " (" //$NON-NLS-1$
						+ (System.currentTimeMillis() - initialTime) + "ms)"); //$NON-NLS-1$
			} finally {
				monitor.exitWrite();
			}
		} catch (IOException e ) {
			if (JobManager.VERBOSE) {
				org.eclipse.jdt.internal.core.util.Util.verbose("-> failed to index " + this.containerPath + " because of the following exception:"); //$NON-NLS-1$ //$NON-NLS-2$
				e.printStackTrace();
			}
			this.manager.removeIndex(this.containerPath);
			return false;
		}
		return true;
	}
	public String getJobFamily() {
		if (this.resource != null)
			return super.getJobFamily();
		return this.containerPath.toOSString(); // external jar
	}	
	protected Integer updatedIndexState() {

		Integer updateState = null;
		if(hasPreBuiltIndex()) {
			updateState = IndexManager.REUSE_STATE;
		}
		else {
			updateState = IndexManager.REBUILDING_STATE;
		}
		return updateState;
	}
	public String toString() {
		return "indexing " + this.containerPath.toString(); //$NON-NLS-1$
	}

	protected boolean hasPreBuiltIndex() {
		return !this.forceIndexUpdate && (this.indexFileURL != null && this.indexFileURL.exists());
	}
}