/*******************************************************************************
 * Copyright (c) 2000, 2019 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:
 *   Jesper Kamstrup Linnet (eclipse@kamstrup-linnet.dk) - initial API and implementation
 *          (report 36180: Callers/Callees view)
 *   Stephan Herrmann (stephan@cs.tu-berlin.de):
 *          - bug 206949: [call hierarchy] filter field accesses (only write or only read)
 *   Red Hat Inc - refactored to jdt.core.manipulatio
 *******************************************************************************/
package org.eclipse.jdt.internal.corext.callhierarchy;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;

import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IInitializer;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
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.core.search.SearchPattern;

import org.eclipse.jdt.internal.core.manipulation.JavaManipulationPlugin;
import org.eclipse.jdt.internal.corext.util.JdtFlags;
import org.eclipse.jdt.internal.corext.util.SearchUtils;

public class CallerMethodWrapper extends MethodWrapper {
	/**
	 * Value of the expand with constructors mode.
	 *
	 * @since 3.5
	 */
	private boolean fExpandWithConstructors;

	/**
	 * Tells whether the expand with constructors mode has been set.
	 *
	 * @see #setExpandWithConstructors(boolean)
	 * @since 3.5
	 */
	private boolean fIsExpandWithConstructorsSet;

	public CallerMethodWrapper(MethodWrapper parent, MethodCall methodCall) {
		super(parent, methodCall);
	}

    protected IJavaSearchScope getSearchScope() {
        return CallHierarchyCore.getDefault().getSearchScope();
    }

    @Override
	protected String getTaskName() {
        return CallHierarchyMessages.CallerMethodWrapper_taskname;
    }

	@Override
	public MethodWrapper createMethodWrapper(MethodCall methodCall) {
        return new CallerMethodWrapper(this, methodCall);
    }

	/*
	 * @see org.eclipse.jdt.internal.corext.callhierarchy.MethodWrapper#canHaveChildren()
	 */
	@Override
	public boolean canHaveChildren() {
		IMember member= getMember();
		if (member instanceof IField) {
			if (getLevel() == 1)
				return true;
			int mode= getFieldSearchMode();
			return mode == IJavaSearchConstants.REFERENCES || mode == IJavaSearchConstants.READ_ACCESSES;
		}
		return member instanceof IMethod || member instanceof IType;
	}

	/**
	 * @return The result of the search for children
	 * @see org.eclipse.jdt.internal.ui.callhierarchy.MethodWrapper#findChildren(org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	protected Map<String, MethodCall> findChildren(IProgressMonitor progressMonitor) {
		try {

			SubMonitor monitor = SubMonitor.convert(progressMonitor,"" , 95); //$NON-NLS-1$

			checkCanceled(progressMonitor);

			IMember member= getMember();
			SearchPattern pattern= null;
			IType type= null;
			if (member instanceof IType) {
				type= (IType) member;
			} else if (member instanceof IInitializer && ! Flags.isStatic(member.getFlags())) {
				type= (IType) member.getParent();
			}
			if (type != null) {
				if (type.isAnonymous()) {
					// search engine does not find reference to anonymous, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=207774
					CallSearchResultCollector resultCollector= new CallSearchResultCollector();
					IJavaElement parent= type.getParent();
					if (parent instanceof IMember) {
						IMember parentMember= (IMember) parent;
						ISourceRange nameRange= type.getNameRange();
						int start= nameRange != null ? nameRange.getOffset() : -1;
						int len= nameRange != null ? nameRange.getLength() : 0;
						resultCollector.addMember(type, parentMember, start, start + len);
						return resultCollector.getCallers();
					}
				} else if (type.getParent() instanceof IMethod) {
					// good enough for local types (does not find super(..) references in subtype constructors):
					pattern= SearchPattern.createPattern(type,
							IJavaSearchConstants.CLASS_INSTANCE_CREATION_TYPE_REFERENCE,
							SearchUtils.GENERICS_AGNOSTIC_MATCH_RULE);
				} else {
					pattern= SearchPattern.createPattern(type.getFullyQualifiedName('.'),
							IJavaSearchConstants.CONSTRUCTOR,
							IJavaSearchConstants.REFERENCES,
							SearchUtils.GENERICS_AGNOSTIC_MATCH_RULE);
				}
			}
			if (pattern == null) {
				int limitTo= IJavaSearchConstants.REFERENCES;
				if (member.getElementType() == IJavaElement.FIELD)
					limitTo= getFieldSearchMode();
				pattern= SearchPattern.createPattern(member, limitTo, SearchUtils.GENERICS_AGNOSTIC_MATCH_RULE);
			}
			if (pattern == null) { // e.g. for initializers
				return new HashMap<>(0);
			}

			SearchEngine searchEngine= new SearchEngine();
			MethodReferencesSearchRequestor searchRequestor= new MethodReferencesSearchRequestor();
			IJavaSearchScope defaultSearchScope= getSearchScope();
			boolean isWorkspaceScope= SearchEngine.createWorkspaceScope().equals(defaultSearchScope);
			IJavaSearchScope searchScope= isWorkspaceScope ? getAccurateSearchScope(defaultSearchScope, member) : defaultSearchScope;
			searchEngine.search(pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }, searchScope, searchRequestor,
					monitor);
			return searchRequestor.getCallers();

		} catch (CoreException e) {
			JavaManipulationPlugin.log(e);
			return new HashMap<>(0);
		}
	}

	private IJavaSearchScope getAccurateSearchScope(IJavaSearchScope defaultSearchScope, IMember member) throws JavaModelException {
		if (!JdtFlags.isPrivate(member) || isRecordComponent(member))
			return defaultSearchScope;

		if (member.getCompilationUnit() != null) {
			return SearchEngine.createJavaSearchScope(new IJavaElement[] { member.getCompilationUnit() });
		} else if (member.getClassFile() != null) {
			// member could be called from an inner class-> search
			// package fragment (see also bug 109053):
			return SearchEngine.createJavaSearchScope(new IJavaElement[] { member.getAncestor(IJavaElement.PACKAGE_FRAGMENT) });
		} else {
			return defaultSearchScope;
		}
	}

	/**
	 * Returns the value of expand with constructors mode.
	 *
	 * @return <code>true</code> if in expand with constructors mode, <code>false</code> otherwise or if not yet set
	 * @see #isExpandWithConstructorsSet()
	 *
	 * @since 3.5
	 */
	public boolean getExpandWithConstructors() {
		return fIsExpandWithConstructorsSet && fExpandWithConstructors;
	}

	/**
	 * Sets the expand with constructors mode.
	 *
	 * @param value <code>true</code> if in expand with constructors mode, <code>false</code>
	 *            otherwise
	 * @since 3.5
	 */
	public void setExpandWithConstructors(boolean value) {
		fExpandWithConstructors= value;
		fIsExpandWithConstructorsSet= true;

	}

	/**
	 * Tells whether the expand with constructors mode has been set.
	 *
	 * @return <code>true</code> if expand with constructors mode has been set explicitly, <code>false</code> otherwise
	 * @see #setExpandWithConstructors(boolean)
	 * @since 3.5
	 */
	public boolean isExpandWithConstructorsSet() {
		return fIsExpandWithConstructorsSet;
	}

	private static boolean isRecordComponent(IMember member) throws JavaModelException {
		return member.getElementType() == IJavaElement.FIELD && member instanceof IField f && f.isRecordComponent();
	}
}
