/*******************************************************************************
 * Copyright (c) 2006, 2013 Wind River Systems, 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:
 *     Markus Schorn - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.callhierarchy;

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

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.dom.ast.DOMException;
import org.eclipse.cdt.core.dom.ast.EScopeKind;
import org.eclipse.cdt.core.dom.ast.IASTFunctionCallExpression;
import org.eclipse.cdt.core.dom.ast.IASTImplicitName;
import org.eclipse.cdt.core.dom.ast.IASTName;
import org.eclipse.cdt.core.dom.ast.IASTNode;
import org.eclipse.cdt.core.dom.ast.IBinding;
import org.eclipse.cdt.core.dom.ast.IEnumerator;
import org.eclipse.cdt.core.dom.ast.IFunction;
import org.eclipse.cdt.core.dom.ast.IScope;
import org.eclipse.cdt.core.dom.ast.IVariable;
import org.eclipse.cdt.core.dom.ast.c.ICExternalBinding;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPASTFunctionCallExpression;
import org.eclipse.cdt.core.dom.ast.cpp.ICPPSpecialization;
import org.eclipse.cdt.core.index.IIndex;
import org.eclipse.cdt.core.index.IIndexManager;
import org.eclipse.cdt.core.index.IIndexName;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.internal.core.model.ext.ICElementHandle;
import org.eclipse.cdt.internal.corext.util.CModelUtil;
import org.eclipse.cdt.internal.ui.actions.OpenActionUtil;
import org.eclipse.cdt.internal.ui.util.ExceptionHandler;
import org.eclipse.cdt.internal.ui.util.StatusLineHandler;
import org.eclipse.cdt.internal.ui.viewsupport.CElementLabels;
import org.eclipse.cdt.internal.ui.viewsupport.IndexUI;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IViewReference;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.texteditor.ITextEditor;

public class CallHierarchyUI {
	static final int INDEX_SEARCH_OPTION = IIndexManager.ADD_DEPENDENCIES | IIndexManager.ADD_DEPENDENT
			| IIndexManager.ADD_EXTENSION_FRAGMENTS_CALL_HIERARCHY;
	private static final ICElement[] NO_ELEMENTS = {};
	private static boolean sIsJUnitTest;

	/**
	 * List of the Call Hierarchy views in LRU order, where the most recently used view is at index 0.
	 */
	private static List<CHViewPart> fLRUCallHierarchyViews = new ArrayList<>();
	private static int fViewCount;

	private static final int MAX_HISTORY_SIZE = 10;
	private static List<ICElement> fHistoryEntries = new ArrayList<>(MAX_HISTORY_SIZE);

	public static void setIsJUnitTest(boolean val) {
		sIsJUnitTest = val;
	}

	public static void open(final IWorkbenchWindow window, final ICElement input) {
		if (input != null) {
			final Display display = Display.getCurrent();

			Job job = new Job(CHMessages.CallHierarchyUI_label) {
				@Override
				protected IStatus run(IProgressMonitor monitor) {
					final ICElement[] elems = findDefinitions(input);
					if (elems != null && elems.length > 0) {
						display.asyncExec(() -> internalOpen(window, elems));
					}
					return Status.OK_STATUS;
				}
			};
			job.setUser(true);
			job.schedule();
		}
	}

	private static CHViewPart internalOpen(IWorkbenchWindow window, ICElement input) {
		IWorkbenchPage page = window.getActivePage();
		try {
			CHViewPart viewPart = findLRUCallHierarchyViewPart(page); //find the first view which is not pinned
			String secondaryId = null;
			if (viewPart == null) {
				if (page.findViewReference(CUIPlugin.ID_CALL_HIERARCHY) != null) { //all the current views are pinned, open a new instance
					secondaryId = String.valueOf(++fViewCount);
				}
			} else {
				secondaryId = viewPart.getViewSite().getSecondaryId();
			}

			viewPart = (CHViewPart) page.showView(CUIPlugin.ID_CALL_HIERARCHY, secondaryId,
					IWorkbenchPage.VIEW_ACTIVATE);
			viewPart.setInput(input);
			return viewPart;
		} catch (CoreException e) {
			ExceptionHandler.handle(e, window.getShell(), CHMessages.OpenCallHierarchyAction_label, null);
		}
		return null;
	}

	private static CHViewPart internalOpen(IWorkbenchWindow window, ICElement[] input) {
		ICElement elem = null;
		switch (input.length) {
		case 0:
			break;
		case 1:
			elem = input[0];
			break;
		default:
			if (sIsJUnitTest) {
				throw new RuntimeException("ambiguous input"); //$NON-NLS-1$
			}
			elem = OpenActionUtil.selectCElement(input, window.getShell(), CHMessages.CallHierarchyUI_label,
					CHMessages.CallHierarchyUI_selectMessage,
					CElementLabels.ALL_DEFAULT | CElementLabels.MF_POST_FILE_QUALIFIED, 0);
			break;
		}
		if (elem != null) {
			return internalOpen(window, elem);
		}
		return null;
	}

	public static void open(final ITextEditor editor, final ITextSelection sel) {
		if (editor != null) {
			ICElement inputCElement = CUIPlugin.getDefault().getWorkingCopyManager()
					.getWorkingCopy(editor.getEditorInput());
			if (inputCElement != null) {
				final ICProject project = inputCElement.getCProject();
				final IEditorInput editorInput = editor.getEditorInput();
				final Display display = Display.getCurrent();

				Job job = new Job(CHMessages.CallHierarchyUI_label) {
					@Override
					protected IStatus run(IProgressMonitor monitor) {
						try {
							StatusLineHandler.clearStatusLine(editor.getSite());
							final ICElement[] elems = findDefinitions(project, editorInput, sel);
							if (elems.length > 0) {
								display.asyncExec(() -> internalOpen(editor.getSite().getWorkbenchWindow(), elems));
							} else {
								StatusLineHandler.showStatusLineMessage(editor.getSite(),
										CHMessages.CallHierarchyUI_openFailureMessage);
							}
							return Status.OK_STATUS;
						} catch (CoreException e) {
							return e.getStatus();
						}
					}

					@Override
					public boolean belongsTo(Object family) {
						return family == CallHierarchyUI.class;
					}
				};
				job.setUser(true);
				job.schedule();
			}
		}
	}

	private static ICElement[] findDefinitions(ICProject project, IEditorInput editorInput, ITextSelection sel)
			throws CoreException {
		try {
			IIndex index = CCorePlugin.getIndexManager().getIndex(project, INDEX_SEARCH_OPTION);
			index.acquireReadLock();
			try {
				IASTName name = IndexUI.getSelectedName(editorInput, sel);
				if (name != null) {
					IBinding binding = name.resolveBinding();
					if (!CallHierarchyUI.isRelevantForCallHierarchy(binding)) {
						for (IASTNode parent = name; parent != null; parent = parent.getParent()) {
							if (parent.getPropertyInParent() == IASTFunctionCallExpression.FUNCTION_NAME) {
								ICPPASTFunctionCallExpression fcall = (ICPPASTFunctionCallExpression) parent
										.getParent();
								if (fcall != null) {
									IASTImplicitName[] implicit = fcall.getImplicitNames();
									if (implicit.length > 0)
										binding = implicit[0].resolveBinding();
								}
								break;
							}
						}
					}
					if (CallHierarchyUI.isRelevantForCallHierarchy(binding)) {
						if (name.isDefinition()) {
							ICElement elem = IndexUI.getCElementForName(project, index, name);
							if (elem != null) {
								return new ICElement[] { elem };
							}
							return NO_ELEMENTS;
						}

						ICElement[] elems = IndexUI.findAllDefinitions(index, binding);
						if (elems.length != 0)
							return elems;

						if (name.isDeclaration()) {
							ICElementHandle elem = IndexUI.getCElementForName(project, index, name);
							if (elem != null) {
								return new ICElement[] { elem };
							}
							return NO_ELEMENTS;
						}

						ICElementHandle elem = IndexUI.findAnyDeclaration(index, project, binding);
						if (elem != null) {
							return new ICElement[] { elem };
						}

						if (binding instanceof ICPPSpecialization) {
							return findSpecializationDeclaration(binding, project, index);
						}
						return NO_ELEMENTS;
					}
				}
			} finally {
				index.releaseReadLock();
			}
		} catch (CoreException e) {
			CUIPlugin.log(e);
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}
		return NO_ELEMENTS;
	}

	private static ICElement[] findSpecializationDeclaration(IBinding binding, ICProject project, IIndex index)
			throws CoreException {
		while (binding instanceof ICPPSpecialization) {
			IBinding original = ((ICPPSpecialization) binding).getSpecializedBinding();
			ICElementHandle[] elems = IndexUI.findAllDefinitions(index, original);
			if (elems.length == 0) {
				ICElementHandle elem = IndexUI.findAnyDeclaration(index, project, original);
				if (elem != null) {
					elems = new ICElementHandle[] { elem };
				}
			}
			if (elems.length > 0) {
				return elems;
			}
			binding = original;
		}
		return NO_ELEMENTS;
	}

	public static ICElement[] findDefinitions(ICElement input) {
		try {
			final ITranslationUnit tu = CModelUtil.getTranslationUnit(input);
			if (tu != null) {
				final ICProject project = tu.getCProject();
				final IIndex index = CCorePlugin.getIndexManager().getIndex(project, INDEX_SEARCH_OPTION);

				index.acquireReadLock();
				try {
					if (needToFindDefinition(input)) {
						IBinding binding = IndexUI.elementToBinding(index, input);
						if (binding != null) {
							ICElement[] result = IndexUI.findAllDefinitions(index, binding);
							if (result.length > 0) {
								return result;
							}
						}
					}
					IIndexName name = IndexUI.elementToName(index, input);
					if (name != null) {
						ICElementHandle handle = IndexUI.getCElementForName(tu, index, name);
						return new ICElement[] { handle };
					}
				} finally {
					index.releaseReadLock();
				}
			}
		} catch (CoreException e) {
			CUIPlugin.log(e);
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}
		return new ICElement[] { input };
	}

	private static boolean needToFindDefinition(ICElement elem) {
		switch (elem.getElementType()) {
		case ICElement.C_FUNCTION_DECLARATION:
		case ICElement.C_METHOD_DECLARATION:
		case ICElement.C_TEMPLATE_FUNCTION_DECLARATION:
		case ICElement.C_TEMPLATE_METHOD_DECLARATION:
			return true;
		}
		return false;
	}

	public static boolean isRelevantForCallHierarchy(IBinding binding) {
		if (binding instanceof ICExternalBinding || binding instanceof IEnumerator || binding instanceof IFunction)
			return true;

		if (binding instanceof IVariable) {
			try {
				final IScope scope = binding.getScope();
				if (scope != null && scope.getKind() == EScopeKind.eLocal)
					return false;
			} catch (DOMException e) {
			}
			return true;
		}
		return false;
	}

	public static boolean isRelevantForCallHierarchy(ICElement elem) {
		if (elem == null) {
			return false;
		}
		switch (elem.getElementType()) {
		case ICElement.C_ENUMERATOR:
		case ICElement.C_FIELD:
		case ICElement.C_FUNCTION:
		case ICElement.C_FUNCTION_DECLARATION:
		case ICElement.C_METHOD:
		case ICElement.C_METHOD_DECLARATION:
		case ICElement.C_TEMPLATE_FUNCTION:
		case ICElement.C_TEMPLATE_FUNCTION_DECLARATION:
		case ICElement.C_TEMPLATE_METHOD:
		case ICElement.C_TEMPLATE_METHOD_DECLARATION:
		case ICElement.C_TEMPLATE_VARIABLE:
		case ICElement.C_VARIABLE:
		case ICElement.C_VARIABLE_DECLARATION:
			return true;
		}
		return false;
	}

	/**
	 * Adds the activated view part to the head of the list.
	 *
	 * @param view the Call Hierarchy view part
	 */
	static void callHierarchyViewActivated(CHViewPart view) {
		fLRUCallHierarchyViews.remove(view);
		fLRUCallHierarchyViews.add(0, view);
	}

	/**
	 * Removes the closed view part from the list.
	 *
	 * @param view the closed view part
	 */
	static void callHierarchyViewClosed(CHViewPart view) {
		fLRUCallHierarchyViews.remove(view);
	}

	/**
	 * Clears the history and updates all the open views.
	 */
	static void clearHistory() {
		setHistoryEntries(ICElement.EMPTY_ARRAY);
		for (CHViewPart part : fLRUCallHierarchyViews) {
			part.setInput(null);
		}
	}

	/**
	 * Finds the first Call Hierarchy view part instance that is not pinned.
	 *
	 * @param page the active page
	 * @return the Call Hierarchy view part to open or <code>null</code> if none found
	 */
	private static CHViewPart findLRUCallHierarchyViewPart(IWorkbenchPage page) {
		boolean viewFoundInPage = false;
		for (CHViewPart view : fLRUCallHierarchyViews) {
			if (page.equals(view.getSite().getPage())) {
				if (!view.isPinned()) {
					return view;
				}
				viewFoundInPage = true;
			}
		}
		if (!viewFoundInPage) {
			// Find unresolved views
			IViewReference[] viewReferences = page.getViewReferences();
			for (IViewReference curr : viewReferences) {
				if (CUIPlugin.ID_CALL_HIERARCHY.equals(curr.getId()) && page.equals(curr.getPage())) {
					CHViewPart view = (CHViewPart) curr.getView(true);
					if (view != null && !view.isPinned()) {
						return view;
					}
				}
			}
		}
		return null;
	}

	static public ICElement[] getHistoryEntries() {
		return fHistoryEntries.toArray(new ICElement[fHistoryEntries.size()]);
	}

	static public void setHistoryEntries(ICElement[] remaining) {
		fHistoryEntries.clear();
		fHistoryEntries.addAll(Arrays.asList(remaining));
	}

	static public void updateHistory(ICElement input) {
		if (input != null) {
			fHistoryEntries.remove(input);
			fHistoryEntries.add(0, input);
			if (fHistoryEntries.size() > MAX_HISTORY_SIZE) {
				fHistoryEntries.remove(MAX_HISTORY_SIZE - 1);
			}
		}
	}
}
