/*******************************************************************************
 * Copyright (c) 2000, 2022 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.eclipse.jdt.internal.core.search.indexing;

import static org.eclipse.jdt.internal.core.JavaModelManager.trace;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.core.search.SearchDocument;
import org.eclipse.jdt.internal.compiler.CompilationResult;
import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies;
import org.eclipse.jdt.internal.compiler.ISourceElementRequestor;
import org.eclipse.jdt.internal.compiler.SourceElementParser;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.ast.FunctionalExpression;
import org.eclipse.jdt.internal.compiler.ast.LambdaExpression;
import org.eclipse.jdt.internal.compiler.ast.ReferenceExpression;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.env.AccessRestriction;
import org.eclipse.jdt.internal.compiler.env.IBinaryType;
import org.eclipse.jdt.internal.compiler.env.ICompilationUnit;
import org.eclipse.jdt.internal.compiler.env.INameEnvironment;
import org.eclipse.jdt.internal.compiler.env.ISourceType;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.impl.ITypeRequestor;
import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.PackageBinding;
import org.eclipse.jdt.internal.compiler.parser.Parser;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;
import org.eclipse.jdt.internal.compiler.problem.ProblemReporter;
import org.eclipse.jdt.internal.compiler.util.SuffixConstants;
import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner;
import org.eclipse.jdt.internal.core.JavaModel;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.core.JavaProject;
import org.eclipse.jdt.internal.core.SourceTypeElementInfo;
import org.eclipse.jdt.internal.core.jdom.CompilationUnit;
import org.eclipse.jdt.internal.core.search.matching.JavaSearchNameEnvironment;
import org.eclipse.jdt.internal.core.search.matching.MethodPattern;
import org.eclipse.jdt.internal.core.search.processing.JobManager;

/**
 * A SourceIndexer indexes java files using a java parser. The following items are indexed:
 * Declarations of:<br>
 * - Classes<br>
 * - Interfaces;<br>
 * - Methods;<br>
 * - Fields;<br>
 * - Lambda expressions;<br>
 * References to:<br>
 * - Methods (with number of arguments); <br>
 * - Fields;<br>
 * - Types;<br>
 * - Constructors.
 */
public class SourceIndexer extends AbstractIndexer implements ITypeRequestor, SuffixConstants {

	private LookupEnvironment lookupEnvironment;
	private CompilerOptions options;
	public ISourceElementRequestor requestor;
	private Parser basicParser;
	private CompilationUnit compilationUnit;
	private CompilationUnitDeclaration cud;
	private static final boolean DEBUG = false;

	public SourceIndexer(SearchDocument document) {
		super(document);
		this.requestor = new SourceIndexerRequestor(this);
	}
	@Override
	public void indexDocument() {
		// Create a new Parser
		String documentPath = this.document.getPath();
		SourceElementParser parser = this.document.getParser();
		if (parser == null) {
			IPath path = new Path(documentPath);
			IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.segment(0));
			parser = JavaModelManager.getJavaModelManager().indexManager.getSourceElementParser(JavaCore.create(project), this.requestor);
		} else {
			parser.setRequestor(this.requestor);
		}

		// Launch the parser
		char[] source = null;
		char[] name = null;
		try {
			source = this.document.getCharContents();
			name = documentPath.toCharArray();
		} catch(Exception e){
			// ignore
		}
		if (source == null || name == null) return; // could not retrieve document info (e.g. resource was discarded)
		this.compilationUnit = new CompilationUnit(source, name);
		try {
			if (parser.parseCompilationUnit(this.compilationUnit, true, null).hasFunctionalTypes())
				this.document.requireIndexingResolvedDocument();
		} catch (Exception e) {
			if (JobManager.VERBOSE) {
				trace("", e); //$NON-NLS-1$
			}
		}
	}

	@Override
	public void accept(IBinaryType binaryType, PackageBinding packageBinding, AccessRestriction accessRestriction) {
		this.lookupEnvironment.createBinaryTypeFrom(binaryType, packageBinding, accessRestriction);
	}

	@Override
	public void accept(ICompilationUnit unit, AccessRestriction accessRestriction) {
		CompilationResult unitResult = new CompilationResult(unit, 1, 1, this.options.maxProblemsPerUnit);
		CompilationUnitDeclaration parsedUnit = this.basicParser.dietParse(unit, unitResult);
		this.lookupEnvironment.buildTypeBindings(parsedUnit, accessRestriction);
		this.lookupEnvironment.completeTypeBindings(parsedUnit, true);
	}

	@Override
	public void accept(ISourceType[] sourceTypes, PackageBinding packageBinding, AccessRestriction accessRestriction) {
		ISourceType sourceType = sourceTypes[0];
		while (sourceType.getEnclosingType() != null)
			sourceType = sourceType.getEnclosingType();
		SourceTypeElementInfo elementInfo = (SourceTypeElementInfo) sourceType;
		IType type = elementInfo.getHandle();
		ICompilationUnit sourceUnit = (ICompilationUnit) type.getCompilationUnit();
		accept(sourceUnit, accessRestriction);
	}

	public void resolveDocument() {
		try {
			IPath path = new Path(this.document.getPath());
			IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.segment(0));
			JavaModel model = JavaModelManager.getJavaModelManager().getJavaModel();
			JavaProject javaProject = (JavaProject) model.getJavaProject(project);

			this.options = new CompilerOptions(javaProject.getOptions(true));
			ProblemReporter problemReporter =
					new ProblemReporter(
							DefaultErrorHandlingPolicies.proceedWithAllProblems(),
							this.options,
							new DefaultProblemFactory());

			// Re-parse using normal parser, IndexingParser swallows several nodes, see comment above class.
			this.basicParser = new Parser(problemReporter, false);
			this.basicParser.reportOnlyOneSyntaxError = true;
			this.basicParser.scanner.taskTags = null;
			this.cud = this.basicParser.parse(this.compilationUnit, new CompilationResult(this.compilationUnit, 0, 0, this.options.maxProblemsPerUnit));
			// Use a non model name environment to avoid locks, monitors and such.
			INameEnvironment nameEnvironment = new JavaSearchNameEnvironment(javaProject, JavaModelManager.getJavaModelManager().getWorkingCopies(DefaultWorkingCopyOwner.PRIMARY, true/*add primary WCs*/));
			this.lookupEnvironment = new LookupEnvironment(this, this.options, problemReporter, nameEnvironment);
			reduceParseTree(this.cud);
			this.lookupEnvironment.buildTypeBindings(this.cud, null);
			this.lookupEnvironment.completeTypeBindings();
			this.cud.scope.faultInTypes();
			this.cud.resolve();
		} catch (Exception e) {
			if (JobManager.VERBOSE) {
				trace("", e); //$NON-NLS-1$
			}
		}
	}

	/**
	 * Called prior to the unit being resolved. Reduce the parse tree where possible.
	 */
	private void reduceParseTree(CompilationUnitDeclaration unit) {
		// remove statements from methods that have no functional interface types.
		TypeDeclaration[] types = unit.types;
		for (int i = 0, l = types == null ? 0 : types.length; i < l; i++)
			purgeMethodStatements(types[i]);
	}

	private void purgeMethodStatements(TypeDeclaration type) {
		AbstractMethodDeclaration[] methods = type.methods;
		for (int j = 0, length = methods == null ? 0 : methods.length; j < length; j++) {
			AbstractMethodDeclaration method = methods[j];
			/*
			 * In case the method defines a local type, skip purging the method body.
			 * We don't know if the local type defines a method that uses a method reference or a lambda.
			 * See:
			 *   https://github.com/eclipse-jdt/eclipse.jdt.core/issues/432
			 *   https://bugs.eclipse.org/bugs/show_bug.cgi?id=566435
			 */
			if (method != null && (method.bits & (ASTNode.HasFunctionalInterfaceTypes | ASTNode.HasLocalType)) == 0) {
				method.statements = null;
				method.javadoc = null;
			}
		}

		TypeDeclaration[] memberTypes = type.memberTypes;
		if (memberTypes != null)
			for (int i = 0, l = memberTypes.length; i < l; i++)
				purgeMethodStatements(memberTypes[i]);
	}

	@Override
	public void indexResolvedDocument() {
		try {
			if (DEBUG) {
				trace(new String(this.cud.compilationResult.fileName) + ':');
			}
			for (int i = 0, length = this.cud.functionalExpressionsCount; i < length; i++) {
				FunctionalExpression expression = this.cud.functionalExpressions[i];
				if (expression instanceof LambdaExpression) {
					LambdaExpression lambdaExpression = (LambdaExpression) expression;
					if (lambdaExpression.binding != null && lambdaExpression.binding.isValidBinding()) {
						final char[] superinterface = lambdaExpression.resolvedType.sourceName();
						if (DEBUG) {
							trace('\t' + new String(superinterface) + '.' +
									new String(lambdaExpression.descriptor.selector) + "-> {}"); //$NON-NLS-1$
						}
						SourceIndexer.this.addIndexEntry(IIndexConstants.METHOD_DECL, MethodPattern.createIndexKey(lambdaExpression.descriptor.selector, lambdaExpression.descriptor.parameters.length));

						addClassDeclaration(0,  // most entries are blank, that is fine, since lambda type/method cannot be searched.
								CharOperation.NO_CHAR, // package name
								ONE_ZERO,
								ONE_ZERO_CHAR, // enclosing types.
								CharOperation.NO_CHAR, // super class
								new char[][] { superinterface },
								CharOperation.NO_CHAR_CHAR,
								true); // not primary.

					} else {
						if (DEBUG) {
							trace("\tnull/bad binding in lambda"); //$NON-NLS-1$
						}
					}
				} else {
					ReferenceExpression referenceExpression = (ReferenceExpression) expression;
					if (referenceExpression.isArrayConstructorReference())
						continue;
					MethodBinding binding = referenceExpression.getMethodBinding();
					if (binding != null && binding.isValidBinding()) {
						if (DEBUG) {
							trace('\t' + new String(referenceExpression.resolvedType.sourceName()) + "::"  //$NON-NLS-1$
									+ new String(referenceExpression.descriptor.selector) + " == " + new String(binding.declaringClass.sourceName()) + '.' + //$NON-NLS-1$
									new String(binding.selector));
						}
						if (referenceExpression.isMethodReference())
							SourceIndexer.this.addMethodReference(binding.selector, binding.parameters.length);
						else
							SourceIndexer.this.addConstructorReference(binding.declaringClass.sourceName(), binding.parameters.length);
					} else {
						if (DEBUG) {
							trace("\tnull/bad binding in reference expression"); //$NON-NLS-1$
						}
					}
				}
			}
		} catch (Exception e) {
			if (JobManager.VERBOSE) {
				trace("", e); //$NON-NLS-1$
			}
		}
	}
}
