/*******************************************************************************
 * Copyright (c) 2000, 2012 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 75800: [call hierarchy] should allow searches for fields
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.callhierarchy;

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

import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;

import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IViewReference;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;

import org.eclipse.ui.texteditor.ITextEditor;

import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.internal.corext.callhierarchy.CallHierarchy;
import org.eclipse.jdt.internal.corext.callhierarchy.CallLocation;
import org.eclipse.jdt.internal.corext.callhierarchy.MethodWrapper;
import org.eclipse.jdt.internal.corext.util.Messages;

import org.eclipse.jdt.ui.JavaUI;

import org.eclipse.jdt.internal.ui.IJavaStatusConstants;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.actions.SelectionConverter;
import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility;
import org.eclipse.jdt.internal.ui.util.ExceptionHandler;

public class CallHierarchyUI {
    private static final int DEFAULT_MAX_CALL_DEPTH= 10;
    private static final String PREF_MAX_CALL_DEPTH = "PREF_MAX_CALL_DEPTH"; //$NON-NLS-1$

    private static CallHierarchyUI fgInstance;
    private int fViewCount= 0;
    private final List<IMember[]> fMethodHistory= new ArrayList<>();

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

    private CallHierarchyUI() {
        // Do nothing
    }

    public static CallHierarchyUI getDefault() {
        if (fgInstance == null) {
            fgInstance = new CallHierarchyUI();
        }

        return fgInstance;
    }

    /**
     * Returns the maximum tree level allowed
     * @return int
     */
    public int getMaxCallDepth() {
        int maxCallDepth;

        IPreferenceStore settings = JavaPlugin.getDefault().getPreferenceStore();
        maxCallDepth = settings.getInt(PREF_MAX_CALL_DEPTH);
        if (maxCallDepth < 1 || maxCallDepth > 99) {
            maxCallDepth= DEFAULT_MAX_CALL_DEPTH;
        }

        return maxCallDepth;
    }

    public void setMaxCallDepth(int maxCallDepth) {
        IPreferenceStore settings = JavaPlugin.getDefault().getPreferenceStore();
        settings.setValue(PREF_MAX_CALL_DEPTH, maxCallDepth);
    }

    public static void jumpToMember(IJavaElement element) {
        if (element != null) {
            try {
                JavaUI.openInEditor(element, true, true);
            } catch (JavaModelException | PartInitException e) {
                JavaPlugin.log(e);
            }
        }
    }

    public static void jumpToLocation(CallLocation callLocation) {
        try {
            IEditorPart methodEditor = JavaUI.openInEditor(callLocation.getMember(), false, false);
            if (methodEditor instanceof ITextEditor) {
                ITextEditor editor = (ITextEditor) methodEditor;
                editor.selectAndReveal(callLocation.getStart(),
                    (callLocation.getEnd() - callLocation.getStart()));
            }
        } catch (JavaModelException | PartInitException e) {
            JavaPlugin.log(e);
        }
    }

	/**
	 * Opens the element in the editor or shows an error dialog if that fails.
	 *
	 * @param element the element to open
	 * @param shell parent shell for error dialog
	 * @param activateOnOpen <code>true</code> if the editor should be activated
	 * @return <code>true</code> iff no error occurred while trying to open the editor,
	 *         <code>false</code> iff an error dialog was raised.
	 */
	public static boolean openInEditor(Object element, Shell shell, boolean activateOnOpen) {
        CallLocation callLocation= CallHierarchy.getCallLocation(element);

        try {
	        IMember enclosingMember;
	        int selectionStart;
			int selectionLength;

	        if (callLocation != null) {
				enclosingMember= callLocation.getMember();
				selectionStart= callLocation.getStart();
				selectionLength= callLocation.getEnd() - selectionStart;
	        } else if (element instanceof MethodWrapper) {
	        	enclosingMember= ((MethodWrapper) element).getMember();
	        	ISourceRange selectionRange= enclosingMember.getNameRange();
	        	if (selectionRange == null)
	        		selectionRange= enclosingMember.getSourceRange();
	        	if (selectionRange == null)
	        		return true;
	        	selectionStart= selectionRange.getOffset();
	        	selectionLength= selectionRange.getLength();
	        } else {
	            return true;
	        }

			IEditorPart methodEditor = JavaUI.openInEditor(enclosingMember, activateOnOpen, false);
            if (methodEditor instanceof ITextEditor) {
                ITextEditor editor = (ITextEditor) methodEditor;
				editor.selectAndReveal(selectionStart, selectionLength);
            }
            return true;
        } catch (JavaModelException e) {
            JavaPlugin.log(new Status(IStatus.ERROR, JavaPlugin.getPluginId(),
                    IJavaStatusConstants.INTERNAL_ERROR,
                    CallHierarchyMessages.CallHierarchyUI_open_in_editor_error_message, e));

            ErrorDialog.openError(shell, CallHierarchyMessages.OpenLocationAction_error_title,
                CallHierarchyMessages.CallHierarchyUI_open_in_editor_error_message,
                e.getStatus());
            return false;
        } catch (PartInitException x) {
            String name;
        	if (callLocation != null)
        		name= callLocation.getCalledMember().getElementName();
        	else if (element instanceof MethodWrapper)
        		name= ((MethodWrapper) element).getName();
        	else
        		name= "";  //$NON-NLS-1$
            MessageDialog.openError(shell, CallHierarchyMessages.OpenLocationAction_error_title,
                Messages.format(
                    CallHierarchyMessages.CallHierarchyUI_open_in_editor_error_messageArgs,
                    new String[] { name, x.getMessage() }));
            return false;
        }
    }

    public static IEditorPart isOpenInEditor(Object elem) {
        IJavaElement javaElement= null;
        if (elem instanceof MethodWrapper) {
            javaElement= ((MethodWrapper) elem).getMember();
        } else if (elem instanceof CallLocation) {
            javaElement= ((CallLocation) elem).getMember();
        }
        if (javaElement != null) {
            return EditorUtility.isOpenInEditor(javaElement);
        }
        return null;
    }

    public static CallHierarchyViewPart openSelectionDialog(IMember[] candidates, IWorkbenchWindow window) {
        Assert.isTrue(candidates != null);

        IMember input= null;
        if (candidates.length > 1) {
            String title= CallHierarchyMessages.CallHierarchyUI_selectionDialog_title;
            String message= CallHierarchyMessages.CallHierarchyUI_selectionDialog_message;
            input= (IMember) SelectionConverter.selectJavaElement(candidates, window.getShell(), title, message);
        } else if (candidates.length == 1) {
            input= candidates[0];
        }
        if (input == null)
            return openView(new IMember[] {}, window);

        return openView(new IMember[] { input }, window);
    }

    public static CallHierarchyViewPart openView(IMember[] input, IWorkbenchWindow window) {
    	if (input.length == 0) {
			MessageDialog.openInformation(window.getShell(), CallHierarchyMessages.CallHierarchyUI_selectionDialog_title,
					CallHierarchyMessages.CallHierarchyUI_open_operation_unavialable);
			return null;
		}
        IWorkbenchPage page= window.getActivePage();
		try {
			CallHierarchyViewPart viewPart= getDefault().findLRUCallHierarchyViewPart(page); //find the first view which is not pinned
			String secondaryId= null;
			if (viewPart == null) {
				if (page.findViewReference(CallHierarchyViewPart.ID_CALL_HIERARCHY) != null) //all the current views are pinned, open a new instance
					secondaryId= String.valueOf(++getDefault().fViewCount);
			} else
				secondaryId= viewPart.getViewSite().getSecondaryId();
			viewPart= (CallHierarchyViewPart)page.showView(CallHierarchyViewPart.ID_CALL_HIERARCHY, secondaryId, IWorkbenchPage.VIEW_ACTIVATE);
			viewPart.setInputElements(input);
			return viewPart;
        } catch (CoreException e) {
            ExceptionHandler.handle(e, window.getShell(),
                CallHierarchyMessages.CallHierarchyUI_error_open_view, e.getMessage());
        }
        return 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
	 * @since 3.7
	 */
	private CallHierarchyViewPart findLRUCallHierarchyViewPart(IWorkbenchPage page) {
		boolean viewFoundInPage= false;
		for (CallHierarchyViewPart view : fLRUCallHierarchyViews) {
			if (page.equals(view.getSite().getPage())) {
				if (!view.isPinned()) {
					return view;
				}
				viewFoundInPage= true;
			}
		}
		if (!viewFoundInPage) {
			// find unresolved views
			for (IViewReference curr : page.getViewReferences()) {
				if (CallHierarchyViewPart.ID_CALL_HIERARCHY.equals(curr.getId()) && page.equals(curr.getPage())) {
					CallHierarchyViewPart view= (CallHierarchyViewPart)curr.getView(true);
					if (view != null && !view.isPinned()) {
						return view;
					}
				}
			}
		}
		return null;
	}

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

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

    /**
     * Converts an ISelection (containing MethodWrapper instances) to an ISelection
     * with the MethodWrapper's replaced by their corresponding IMembers. If the selection
     * contains elements which are not MethodWrapper instances or not already IMember instances
     * they are discarded.
     * @param selection The selection to convert.
     * @return An ISelection containing IMember's in place of MethodWrapper instances.
     */
    static ISelection convertSelection(ISelection selection) {
        if (selection.isEmpty()) {
            return selection;
        }

        if (selection instanceof IStructuredSelection) {
            IStructuredSelection structuredSelection= (IStructuredSelection) selection;
            List<IMember> javaElements= new ArrayList<>();
            for (Object element : structuredSelection) {
                if (element instanceof MethodWrapper) {
                    IMember member= ((MethodWrapper)element).getMember();
                    if (member != null) {
                        javaElements.add(member);
                    }
                } else if (element instanceof IMember) {
                    javaElements.add((IMember) element);
                } else if (element instanceof CallLocation) {
                    IMember member = ((CallLocation) element).getMember();
                    javaElements.add(member);
                }
            }
            return new StructuredSelection(javaElements);
        }
        return StructuredSelection.EMPTY;
    }

	/**
	 * Clears the history and updates all the open views.
	 *
	 * @since 3.7
	 */
	void clearHistory() {
		for (CallHierarchyViewPart part : fLRUCallHierarchyViews) {
			part.setHistoryEntries(new IMember[0][]);
			part.setInputElements(null);
		}
	}

	/**
	 * Returns the method history.
	 *
	 * @return the method history
	 * @since 3.7
	 */
	List<IMember[]> getMethodHistory() {
		return fMethodHistory;
	}
}
