/*******************************************************************************
 * Copyright (c) 2005, 2016 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.jeview.views;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.jdt.jeview.EditorUtility;
import org.eclipse.jdt.jeview.JEPluginImages;
import org.eclipse.jdt.jeview.JEViewPlugin;
import org.eclipse.jdt.jeview.properties.ClasspathEntryProperties;
import org.eclipse.jdt.jeview.properties.JarEntryResourceProperties;
import org.eclipse.jdt.jeview.properties.JavaElementProperties;
import org.eclipse.jdt.jeview.properties.MarkerProperties;
import org.eclipse.jdt.jeview.properties.MemberValuePairProperties;
import org.eclipse.jdt.jeview.properties.ResourceProperties;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;

import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.NotEnabledException;
import org.eclipse.core.commands.NotHandledException;
import org.eclipse.core.commands.common.NotDefinedException;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.Status;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.window.Window;

import org.eclipse.jface.text.ITextSelection;

import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IPageLayout;
import org.eclipse.ui.IStorageEditorInput;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.IWorkbenchCommandConstants;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.ContributionItemFactory;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.keys.IBindingService;
import org.eclipse.ui.part.DrillDownAdapter;
import org.eclipse.ui.part.IShowInSource;
import org.eclipse.ui.part.IShowInTarget;
import org.eclipse.ui.part.ShowInContext;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.views.properties.IPropertySheetPage;
import org.eclipse.ui.views.properties.PropertySheetPage;

import org.eclipse.ui.texteditor.ITextEditor;

import org.eclipse.jdt.core.ElementChangedEvent;
import org.eclipse.jdt.core.IAnnotatable;
import org.eclipse.jdt.core.IAnnotation;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.ICodeAssist;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IElementChangedListener;
import org.eclipse.jdt.core.IJarEntryResource;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaModel;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMemberValuePair;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.ui.JavaElementLabelProvider;
import org.eclipse.jdt.ui.JavaUI;


public class JavaElementView extends ViewPart implements IShowInSource, IShowInTarget {
	TreeViewer fViewer;
	private DrillDownAdapter fDrillDownAdapter;
	JERoot fInput;

	private Action fFocusAction;
	private Action fFindTypeAction;
	private Action fFindType2Action;
	private Action fResolveTypeAction;
	private Action fGetAnnotationAction;
	private Action fCreateFromBindingKeyAction;
	private Action fResetAction;
	private Action fCodeSelectAction;
	private Action fElementAtAction;
	private Action fCreateFromHandleAction;
	private Action fRefreshAction;
	TreeCopyAction fCopyAction;
	private Action fCompareAction;
	private Action fPropertiesAction;
	Action fDoubleClickAction;

	private Action fLogDeltasAction;

	private PropertySheetPage fPropertySheetPage;

	private static class InputDialog extends org.eclipse.jface.dialogs.InputDialog {
		public InputDialog(Shell parentShell, String dialogTitle, String dialogMessage, String initialValue, IInputValidator validator) {
			super(parentShell, dialogTitle, dialogMessage, initialValue, validator);
		}
		@Override
		protected boolean isResizable() {
			return true;
		}
	}
	private static class JEViewSelectionProvider implements ISelectionProvider {
		private final TreeViewer fViewer;

		ListenerList<ISelectionChangedListener> fSelectionChangedListeners= new ListenerList<>();
		private IStructuredSelection fLastSelection;

		public JEViewSelectionProvider(TreeViewer viewer) {
			fViewer= viewer;
			fViewer.addSelectionChangedListener(event -> fireSelectionChanged());
		}

		void fireSelectionChanged() {
			if (fSelectionChangedListeners != null) {
				IStructuredSelection selection = getSelection();

				if (fLastSelection != null) {
					List<?> newSelection = selection.toList();
					List<?> oldSelection = fLastSelection.toList();
					int size = newSelection.size();
					if (size == oldSelection.size()){
						for (int i= 0; i < size; i++) {
							Object newElement = newSelection.get(i);
							Object oldElement = oldSelection.get(i);
							if (newElement != oldElement && newElement.equals(oldElement) && newElement instanceof IJavaElement) {
								// send out a fake selection event to make the Properties view update getKey():
								SelectionChangedEvent event= new SelectionChangedEvent(this, StructuredSelection.EMPTY);
								for (ISelectionChangedListener listener : fSelectionChangedListeners) {
									listener.selectionChanged(event);
								}
								break;
							}
						}
					}
				}
				fLastSelection= selection;

				SelectionChangedEvent event= new SelectionChangedEvent(this, selection);

				for (ISelectionChangedListener listener : fSelectionChangedListeners) {
					listener.selectionChanged(event);
				}
			}
		}

		@Override
		public void addSelectionChangedListener(ISelectionChangedListener listener) {
			fSelectionChangedListeners.add(listener);
		}

		@Override
		public IStructuredSelection getSelection() {
			IStructuredSelection selection= (IStructuredSelection) fViewer.getSelection();
			ArrayList<Object> externalSelection= new ArrayList<>();
			for (Iterator<?> iter= selection.iterator(); iter.hasNext();) {
				Object element= iter.next();
				if (element instanceof JavaElement) {
					IJavaElement javaElement= ((JavaElement) element).getJavaElement();
					if (javaElement != null && ! (javaElement instanceof IJavaModel)) // various selection listeners assume getJavaProject() is non-null
						externalSelection.add(javaElement);
				} else if (element instanceof JEResource) {
					IResource resource= ((JEResource) element).getResource();
					if (resource != null &&! (resource instanceof IWorkspaceRoot)) // various selection listeners assume getProject() is non-null
						externalSelection.add(resource);
				} else if (element instanceof JEAttribute) {
					Object wrappedObject= ((JEAttribute) element).getWrappedObject();
					if (wrappedObject != null) {
						externalSelection.add(wrappedObject);
					}
				}
			}
			return new StructuredSelection(externalSelection);
		}

		@Override
		public void removeSelectionChangedListener(ISelectionChangedListener listener) {
			fSelectionChangedListeners.remove(listener);
		}

		@Override
		public void setSelection(ISelection selection) {
			//not supported
		}
	}

	public JavaElementView() {
		//
	}

	@Override
	public void createPartControl(Composite parent) {
		fViewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
		fDrillDownAdapter = new DrillDownAdapter(fViewer) {
			@SuppressWarnings("synthetic-access")
			@Override
			protected void updateNavigationButtons() {
				super.updateNavigationButtons();
				if (fViewer.getInput() instanceof JEAttribute && ! fViewer.getInput().equals(fInput)) {
					setContentDescription(((JEAttribute) fViewer.getInput()).getLabel());
				} else {
					setContentDescription("");
				}

			}
		};
		fViewer.setContentProvider(new JEViewContentProvider());
		fViewer.setLabelProvider(new JEViewLabelProvider());
		reset();
		makeActions();
		hookContextMenu();
		hookDoubleClickAction();
		getSite().setSelectionProvider(new JEViewSelectionProvider(fViewer));
		fViewer.addSelectionChangedListener(event -> fCopyAction.setEnabled(! event.getSelection().isEmpty()));
		contributeToActionBars();
	}

	void reset() {
		setSingleInput(getJavaModel());
	}

	private IJavaModel getJavaModel() {
		return JavaCore.create(JEViewPlugin.getWorkspace().getRoot());
	}

	void setSingleInput(Object javaElementOrResource) {
		setInput(Collections.singleton(javaElementOrResource));
	}

	void setInput(Collection<?> javaElementsOrResources) {
		fInput= new JERoot(javaElementsOrResources);
		fViewer.setInput(fInput);
		ITreeContentProvider tcp= (ITreeContentProvider) fViewer.getContentProvider();
		Object[] elements= tcp.getElements(fInput);
		if (elements.length > 0) {
			fViewer.setSelection(new StructuredSelection(elements[0]));
			if (elements.length == 1) {
				fViewer.setExpandedState(elements[0], true);
			}
		}
		fDrillDownAdapter.reset();
	}

	void setEmptyInput() {
		setInput(Collections.emptySet());
	}

	private void hookContextMenu() {
		MenuManager menuMgr = new MenuManager("#PopupMenu");
		menuMgr.setRemoveAllWhenShown(true);
		menuMgr.addMenuListener(this::fillContextMenu);
		Menu menu = menuMgr.createContextMenu(fViewer.getControl());
		fViewer.getControl().setMenu(menu);
		getSite().registerContextMenu(menuMgr, fViewer);
	}

	private void contributeToActionBars() {
		IActionBars bars = getViewSite().getActionBars();
		fillLocalPullDown(bars.getMenuManager());
		fillLocalToolBar(bars.getToolBarManager());
		bars.setGlobalActionHandler(ActionFactory.REFRESH.getId(), fRefreshAction);
		bars.setGlobalActionHandler(ActionFactory.COPY.getId(), fCopyAction);
		bars.setGlobalActionHandler(ActionFactory.PROPERTIES.getId(), fPropertiesAction);
	}

	private void fillLocalPullDown(IMenuManager manager) {
		manager.add(fCodeSelectAction);
		manager.add(fElementAtAction);
		manager.add(fCreateFromHandleAction);
		manager.add(fResetAction);
		manager.add(fLogDeltasAction);
		manager.add(new Separator());
		manager.add(fRefreshAction);
	}

	void fillContextMenu(IMenuManager manager) {
		addFocusActionOrNot(manager);
		manager.add(fResetAction);
		manager.add(fRefreshAction);
		manager.add(new Separator());

		if (! getSite().getSelectionProvider().getSelection().isEmpty()) {
			MenuManager showInSubMenu= new MenuManager(getShowInMenuLabel());
			IWorkbenchWindow workbenchWindow= getSite().getWorkbenchWindow();
			showInSubMenu.add(ContributionItemFactory.VIEWS_SHOW_IN.create(workbenchWindow));
			manager.add(showInSubMenu);
		}
		addElementActionsOrNot(manager);
		manager.add(new Separator());

		manager.add(fCopyAction);
		manager.add(new Separator());

		fDrillDownAdapter.addNavigationActions(manager);
		manager.add(new Separator());
		// Other plug-ins can contribute there actions here
		manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
		manager.add(new Separator());
		addCompareActionOrNot(manager);
		manager.add(fPropertiesAction);
	}

	private String getShowInMenuLabel() {
		String keyBinding= null;

		IBindingService bindingService= PlatformUI.getWorkbench().getAdapter(IBindingService.class);
		if (bindingService != null)
			keyBinding= bindingService.getBestActiveBindingFormattedFor(IWorkbenchCommandConstants.NAVIGATE_SHOW_IN_QUICK_MENU);

		if (keyBinding == null)
			keyBinding= ""; //$NON-NLS-1$

		return "Sho&w In" + '\t' + keyBinding;
	}

	private void addFocusActionOrNot(IMenuManager manager) {
		if (fViewer.getSelection() instanceof IStructuredSelection) {
			IStructuredSelection structuredSelection= (IStructuredSelection) fViewer.getSelection();
			if (structuredSelection.size() == 1) {
				Object first= structuredSelection.getFirstElement();
				if (first instanceof JavaElement) {
					IJavaElement javaElement = ((JavaElement) first).getJavaElement();
					if (javaElement != null) {
						String name= javaElement.getElementName();
						fFocusAction.setText("Fo&cus On '" + name + '\'');
						manager.add(fFocusAction);
					}
				} else if (first instanceof JEResource) {
					IResource resource= ((JEResource) first).getResource();
					if (resource != null) {
						String name= resource.getName();
						fFocusAction.setText("Fo&cus On '" + name + '\'');
						manager.add(fFocusAction);
					}
				}
			}
		}
	}

	private void addElementActionsOrNot(IMenuManager manager) {
		if (fViewer.getSelection() instanceof IStructuredSelection) {
			IStructuredSelection structuredSelection= (IStructuredSelection) fViewer.getSelection();
			if (structuredSelection.size() == 1) {
				Object first= structuredSelection.getFirstElement();
				if (first instanceof JavaElement) {
					IJavaElement javaElement= ((JavaElement) first).getJavaElement();
					if (javaElement instanceof IJavaProject) {
						manager.add(fFindTypeAction);
						manager.add(fFindType2Action);
						manager.add(fCreateFromBindingKeyAction);
					}
					if (javaElement instanceof IType) {
						manager.add(fResolveTypeAction);
					}
					if (javaElement instanceof IAnnotatable) {
						manager.add(fGetAnnotationAction);
					}
				}
			}
		}
	}

	private void addCompareActionOrNot(IMenuManager manager) {
		if (fViewer.getSelection() instanceof ITreeSelection) {
			ITreeSelection treeSelection = (ITreeSelection) fViewer.getSelection();
			if (treeSelection.getPaths().length == 2) {
				manager.add(fCompareAction);
			}
		}
	}

	private void fillLocalToolBar(IToolBarManager manager) {
		manager.add(fCodeSelectAction);
		manager.add(fElementAtAction);
		manager.add(fResetAction);
		manager.add(fRefreshAction);
		manager.add(new Separator());
		fDrillDownAdapter.addNavigationActions(manager);
	}

	private void makeActions() {
		fCodeSelectAction= new Action("Set Input from Editor (&codeSelect)", JEPluginImages.IMG_SET_FOCUS_CODE_SELECT) {
			@Override public void run() {
				IEditorPart editor= getSite().getPage().getActiveEditor();
				if (editor == null) {
					setEmptyInput();
					return;
				}
				IEditorInput input= editor.getEditorInput();
				ISelectionProvider selectionProvider= editor.getSite().getSelectionProvider();
				if (input == null || selectionProvider == null) {
					setEmptyInput();
					return;
				}
				ISelection selection= selectionProvider.getSelection();
				if (! (selection instanceof ITextSelection)) {
					setEmptyInput();
					return;
				}
				IJavaElement javaElement= input.getAdapter(IJavaElement.class);
				if (javaElement == null) {
					setEmptyInput();
					return;
				}

				IJavaElement[] resolved;
				try {
					resolved= codeResolve(javaElement, (ITextSelection) selection);
				} catch (JavaModelException e) {
					setEmptyInput();
					return;
				}
				if (resolved.length == 0) {
					setEmptyInput();
					return;
				}

				setInput(Arrays.asList(resolved));
			}
		};
		fCodeSelectAction.setToolTipText("Set input from current editor's selection (codeSelect)");

		fElementAtAction= new Action("Set Input from Editor location (&getElementAt)", JEPluginImages.IMG_SET_FOCUS) {
			@Override public void run() {
				IEditorPart editor= getSite().getPage().getActiveEditor();
				if (editor == null) {
					setEmptyInput();
					return;
				}
				IEditorInput input= editor.getEditorInput();
				ISelectionProvider selectionProvider= editor.getSite().getSelectionProvider();
				if (input == null || selectionProvider == null) {
					setEmptyInput();
					return;
				}
				ISelection selection= selectionProvider.getSelection();
				if (! (selection instanceof ITextSelection)) {
					setEmptyInput();
					return;
				}
				IJavaElement javaElement= input.getAdapter(IJavaElement.class);
				if (javaElement == null) {
					setEmptyInput();
					return;
				}

				IJavaElement resolved;
				try {
					resolved= getElementAtOffset(javaElement, (ITextSelection) selection);
				} catch (JavaModelException e) {
					setEmptyInput();
					return;
				}
				if (resolved == null) {
					setEmptyInput();
					return;
				}

				setSingleInput(resolved);
			}
		};
		fElementAtAction.setToolTipText("Set input from current editor's selection location (getElementAt)");

		fCreateFromHandleAction= new Action("Create From &Handle...") {
			@Override public void run() {
				InputDialog dialog= new InputDialog(getSite().getShell(), "Create Java Element From Handle Identifier", "&Handle identifier:", "", null);
				if (dialog.open() != Window.OK)
					return;
				String handleIdentifier= dialog.getValue();
				IJavaElement javaElement= JavaCore.create(handleIdentifier);
				setSingleInput(javaElement);
			}
		};

		fFocusAction= new Action() {
			@Override public void run() {
				Object selected= ((IStructuredSelection) fViewer.getSelection()).getFirstElement();
				if (selected instanceof JavaElement) {
					setSingleInput(((JavaElement) selected).getJavaElement());
				} else if (selected instanceof JEResource) {
					setSingleInput(((JEResource) selected).getResource());
				}
			}
		};
		fFocusAction.setToolTipText("Focus on Selection");

		fFindTypeAction= new Action() {
			@Override public void run() {
				Object selected= ((IStructuredSelection) fViewer.getSelection()).getFirstElement();
				final IJavaProject project= (IJavaProject) ((JavaElement) selected).getJavaElement();

				InputDialog dialog= new InputDialog(getSite().getShell(), "IJavaProject#findType(String fullyQualifiedName)", "fullyQualifiedName:", "", null);
				if (dialog.open() != Window.OK)
					return;

				final String fullyQualifiedName= dialog.getValue();
				try {
					IType type = project.findType(fullyQualifiedName);
					JavaElement element= new JavaElement(fInput, fullyQualifiedName, type);
					fViewer.add(fInput, element);
					fViewer.setSelection(new StructuredSelection(element));
				} catch (JavaModelException e) {
					JEViewPlugin.log(e);
				}
			}
		};
		fFindTypeAction.setText("findType(String)...");

		fFindType2Action= new Action() {
			@Override public void run() {
				Object selected= ((IStructuredSelection) fViewer.getSelection()).getFirstElement();
				final IJavaProject project= (IJavaProject) ((JavaElement) selected).getJavaElement();

				InputDialog dialog= new InputDialog(getSite().getShell(), "IJavaProject#findType(String fullyQualifiedName, IProgressMonitor pm)", "fullyQualifiedName:", "", null);
				if (dialog.open() != Window.OK)
					return;
				final String fullyQualifiedName= dialog.getValue();

				class Runner implements IRunnableWithProgress {
					IType type;
					@Override
					public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
						try {
							type= project.findType(fullyQualifiedName, monitor);
						} catch (JavaModelException e) {
							throw new InvocationTargetException(e);
						}
					}
				}
				Runner runner= new Runner();
				try {
					PlatformUI.getWorkbench().getProgressService().busyCursorWhile(runner);
				} catch (InvocationTargetException | InterruptedException e) {
					JEViewPlugin.log(e);
				}
				JavaElement element= new JavaElement(fInput, fullyQualifiedName, runner.type);
				fViewer.add(fInput, element);
				fViewer.setSelection(new StructuredSelection(element));
			}
		};
		fFindType2Action.setText("findType(String, IProgressMonitor)...");

		fResolveTypeAction= new Action() {
			@Override public void run() {
				Object selected= ((IStructuredSelection) fViewer.getSelection()).getFirstElement();
				final IType type= (IType) ((JavaElement) selected).getJavaElement();

				InputDialog dialog= new InputDialog(getSite().getShell(), "IType#resolveType(String typeName)", "typeName:", "", null);
				if (dialog.open() != Window.OK)
					return;
				final String typeName= dialog.getValue();

				JavaElementChildrenProperty element= new JavaElementChildrenProperty(fInput, "'" + type.getFullyQualifiedName() + "'.resolveType(\"" + typeName +"\")") {
					@Override protected JEAttribute[] computeChildren() throws Exception {
						String[][] resolvedTypes= type.resolveType(typeName);
						if (resolvedTypes == null) {
							return new JEAttribute[] { new Null(this, "result") };
						}
						JEAttribute[] resolvedJEAttributes= new JEAttribute[resolvedTypes.length];
						for (int i= 0; i < resolvedTypes.length; i++) {
							String[] resolvedType= resolvedTypes[i];
							resolvedJEAttributes[i]= new JavaElementProperty(this, null, resolvedType[0] + ", " + resolvedType[1]);
						}
						return resolvedJEAttributes;
					}
				};
				fViewer.add(fInput, element);
				fViewer.setSelection(new StructuredSelection(element));
				fViewer.setExpandedState(element, true);
			}
		};
		fResolveTypeAction.setText("resolveType(String)...");

		fGetAnnotationAction= new Action() {
			@Override public void run() {
				Object selected= ((IStructuredSelection) fViewer.getSelection()).getFirstElement();
				final IAnnotatable annotatable= (IAnnotatable) ((JavaElement) selected).getJavaElement();

				InputDialog dialog= new InputDialog(getSite().getShell(), "IAnnotatable#getAnnotation(String name)", "name:", "", null);
				if (dialog.open() != Window.OK)
					return;
				final String name= dialog.getValue();

				JavaElementChildrenProperty element= new JavaElementChildrenProperty(fInput, "'" + ((IJavaElement) annotatable).getElementName() + "'.getAnnotation(\"" + name +"\")") {
					@Override protected JEAttribute[] computeChildren() throws Exception {
						IAnnotation annotation= annotatable.getAnnotation(name);
						return new JEAttribute[] { new JavaElement(this, annotation) };
					}
				};
				fViewer.add(fInput, element);
				fViewer.setSelection(new StructuredSelection(element));
				fViewer.setExpandedState(element, true);
			}
		};
		fGetAnnotationAction.setText("getAnnotation(String)...");

		fCreateFromBindingKeyAction= new Action("Create From &Binding Key...") {
			@Override public void run() {
				Object selected= ((IStructuredSelection) fViewer.getSelection()).getFirstElement();
				final IJavaProject project= (IJavaProject) ((JavaElement) selected).getJavaElement();

				InputDialog dialog= new InputDialog(getSite().getShell(), "IJavaProject#findElement(String bindingKey, WorkingCopyOwner owner)", "&bindingKey:", "", null);
				if (dialog.open() != Window.OK)
					return;
				final String bindingKey= dialog.getValue();

				class Runner implements IRunnableWithProgress {
					IJavaElement element;
					@Override
					public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
						try {
							element= project.findElement(bindingKey, null);
						} catch (JavaModelException e) {
							throw new InvocationTargetException(e);
						}
					}
				}
				Runner runner= new Runner();
				try {
					PlatformUI.getWorkbench().getProgressService().busyCursorWhile(runner);
				} catch (InvocationTargetException | InterruptedException e) {
					JEViewPlugin.log(e);
				}
				JavaElement element= new JavaElement(fInput, bindingKey, runner.element);
				fViewer.add(fInput, element);
				fViewer.setSelection(new StructuredSelection(element));
			}
		};

		fResetAction= new Action("&Reset View", getJavaModelImageDescriptor()) {
			@Override public void run() {
				reset();
			}
		};
		fResetAction.setToolTipText("Reset View to JavaModel");

		fRefreshAction= new Action("Re&fresh", JEPluginImages.IMG_REFRESH) {
			@Override public void run() {
				BusyIndicator.showWhile(getSite().getShell().getDisplay(), () -> fViewer.refresh());
			}
		};
		fRefreshAction.setToolTipText("Refresh");
		fRefreshAction.setActionDefinitionId("org.eclipse.ui.file.refresh");

		fCopyAction= new TreeCopyAction(new Tree[] {fViewer.getTree()});

		fPropertiesAction= new Action("&Properties", JEPluginImages.IMG_PROPERTIES) {
			@Override
			public void run() {
				String viewId = IPageLayout.ID_PROP_SHEET;
				IWorkbenchPage page= getViewSite().getPage();
				IViewPart view;
				try {
					view= page.showView(viewId);
					page.activate(JavaElementView.this);
					page.bringToTop(view);
				} catch (PartInitException e) {
					JEViewPlugin.log("could not find Properties view", e);
				}
			}
		};
		fPropertiesAction.setActionDefinitionId(IWorkbenchCommandConstants.FILE_PROPERTIES);

		fDoubleClickAction = new Action() {
			private Object fPreviousDouble;

			@Override public void run() {
				ISelection selection = fViewer.getSelection();
				Object obj = ((IStructuredSelection)selection).getFirstElement();

				boolean isSecondDoubleClick= (obj == fPreviousDouble);
				fPreviousDouble= isSecondDoubleClick ? null : obj;

				if (obj instanceof JavaElement) {
					IJavaElement javaElement= ((JavaElement) obj).getJavaElement();
					if (javaElement != null) {
						switch (javaElement.getElementType()) {
							case IJavaElement.JAVA_MODEL :
								break;

							case IJavaElement.JAVA_PROJECT :
							case IJavaElement.PACKAGE_FRAGMENT_ROOT :
							case IJavaElement.PACKAGE_FRAGMENT :
								ICommandService service= getSite().getService(ICommandService.class);
								Command showInCommand= service.getCommand(IWorkbenchCommandConstants.NAVIGATE_SHOW_IN);
								Map<String, String> params= Collections.singletonMap("org.eclipse.ui.navigate.showIn.targetId", JavaUI.ID_PACKAGES);
								try {
									Object context= getSite().getService(IHandlerService.class).getCurrentState();
									showInCommand.executeWithChecks(new ExecutionEvent(null, params, null, context));
								} catch (ExecutionException | NotDefinedException | NotEnabledException | NotHandledException e1) {
									showAndLogError("Could not show element", e1);
								}
								break;

							default :
								try {
									IEditorPart editorPart= JavaUI.openInEditor(javaElement);
									if (editorPart != null) {
										if (isSecondDoubleClick && javaElement instanceof ISourceReference && editorPart instanceof ITextEditor) {
											ISourceRange sourceRange= ((ISourceReference) javaElement).getSourceRange();
											EditorUtility.selectInEditor((ITextEditor) editorPart, sourceRange.getOffset(), sourceRange.getLength());
										} else {
											JavaUI.revealInEditor(editorPart, javaElement);
										}
									}
								} catch (PartInitException | JavaModelException e) {
									showAndLogError("Could not open editor.", e); //$NON-NLS-1$
								}
						}
					}

				} else if (obj instanceof Error) {
					Error error= (Error) obj;
					JEViewPlugin.log(error.getException());

				} else if (obj instanceof JEMarker) {
					JEMarker marker= (JEMarker) obj;
					try {
						IDE.openEditor(getSite().getPage(), marker.getMarker());
					} catch (PartInitException e) {
						showAndLogError("Could not open editor.", e); //$NON-NLS-1$
					}
				}
			}
		};

		fCompareAction= new Action() {
			@Override public void run() {
				TreePath[] selection= ((ITreeSelection) fViewer.getSelection()).getPaths();
				Object first= ((JEAttribute) selection[0].getLastSegment()).getWrappedObject();
				Object second= ((JEAttribute) selection[1].getLastSegment()).getWrappedObject();
				boolean identical= first == second;
				boolean equals1= first != null && first.equals(second);
				boolean equals2= second != null && second.equals(first);
				boolean inconsistentEquals= equals1 != equals2;

				String msg= "==: " + identical + "\nequals(..): " + (inconsistentEquals ? "INCONSISTENT" : equals1);
				MessageDialog.openInformation(fViewer.getTree().getShell(), "Comparison", msg);
			}
		};
		fCompareAction.setText("C&ompare with Each Other...");


		fLogDeltasAction= new Action("Log Java element deltas", IAction.AS_CHECK_BOX) {

			private IElementChangedListener fListener= new IElementChangedListener() {
				@Override
				@SuppressWarnings("deprecation")
				public void elementChanged(ElementChangedEvent event) {
					StringBuilder type= new StringBuilder("\nEvent type: ");
					switch (event.getType()) {
					case ElementChangedEvent.POST_CHANGE:
						type.append("POST_CHANGE");
						break;
					case ElementChangedEvent.POST_RECONCILE:
						type.append("POST_RECONCILE");
						break;
					case ElementChangedEvent.PRE_AUTO_BUILD:
						type.append("PRE_AUTO_BUILD");
						break;
					default:
						type.append(event.getType());
						break;
					}
					JEViewPlugin.logMessage(event.getDelta().toString() + type.toString());
				}
			};

			@Override public void run() {
				String message;
				if (!isChecked()) {
					JavaCore.removeElementChangedListener(fListener);
					message= "Element listener removed";
				} else {
					JavaCore.addElementChangedListener(fListener);
					message= "Element listener added. Deltas are now written to log.";
				}
				MessageDialog.openInformation(fViewer.getTree().getShell(), "Log Java Element Deltas", message);
			}
		};

	}


	static IJavaElement[] codeResolve(IJavaElement input, ITextSelection selection) throws JavaModelException {
		if (input instanceof ICodeAssist) {
			if (input instanceof ICompilationUnit) {
				reconcile((ICompilationUnit) input);
			}
			IJavaElement[] elements= ((ICodeAssist)input).codeSelect(selection.getOffset(), selection.getLength());
			if (elements != null && elements.length > 0)
				return elements;
		}
		return new IJavaElement[0];
	}

	static IJavaElement getElementAtOffset(IJavaElement input, ITextSelection selection) throws JavaModelException {
		if (input instanceof ICompilationUnit) {
			ICompilationUnit cunit= (ICompilationUnit) input;
			reconcile(cunit);
			IJavaElement ref= cunit.getElementAt(selection.getOffset());
			if (ref == null)
				return input;
			else
				return ref;
		} else if (input instanceof IClassFile) {
			IJavaElement ref= ((IClassFile)input).getElementAt(selection.getOffset());
			if (ref == null)
				return input;
			else
				return ref;
		}
		return input;
	}

	/* see JavaModelUtil.reconcile((ICompilationUnit) input) */
	static void reconcile(ICompilationUnit unit) throws JavaModelException {
		synchronized(unit)  {
			unit.reconcile(
				ICompilationUnit.NO_AST,
				false /* don't force problem detection */,
				null /* use primary owner */,
				null /* no progress monitor */);
		}
	}

	private ImageDescriptor getJavaModelImageDescriptor() {
		JavaElementLabelProvider lp= new JavaElementLabelProvider(JavaElementLabelProvider.SHOW_SMALL_ICONS);
		Image modelImage= lp.getImage(getJavaModel());
		ImageDescriptor modelImageDescriptor= ImageDescriptor.createFromImage(modelImage);
		lp.dispose();
		return modelImageDescriptor;
	}

	private void hookDoubleClickAction() {
		fViewer.addDoubleClickListener(event -> fDoubleClickAction.run());
	}

	void showAndLogError(String message, CoreException e) {
		JEViewPlugin.log(message, e);
		ErrorDialog.openError(getSite().getShell(), "Java Element View", message, e.getStatus()); //$NON-NLS-1$
	}

	void showAndLogError(String message, Exception e) {
		IStatus status= new Status(IStatus.ERROR, JEViewPlugin.getPluginId(), 0, message, e);
		JEViewPlugin.log(status);
		ErrorDialog.openError(getSite().getShell(), "Java Element View", null, status); //$NON-NLS-1$
	}

	void showMessage(String message) {
		MessageDialog.openInformation(
			fViewer.getControl().getShell(),
			"Java Element View",
			message);
	}

	/**
	 * Passing the focus request to the viewer's control.
	 */
	@Override
	public void setFocus() {
		fViewer.getControl().setFocus();
	}

	@Override
	public ShowInContext getShowInContext() {
		return new ShowInContext(null, getSite().getSelectionProvider().getSelection());
	}

	@Override
	public boolean show(ShowInContext context) {
		ISelection selection= context.getSelection();
		if (selection instanceof IStructuredSelection) {
			IStructuredSelection structuredSelection= ((IStructuredSelection) selection);
			if (structuredSelection.size() >= 1) {
				List<Object> input= new ArrayList<>();
				for (Iterator<?> iter = structuredSelection.iterator(); iter.hasNext();) {
					Object item= iter.next();
					if (item instanceof IJavaElement || item instanceof IResource || item instanceof IJarEntryResource) {
						input.add(item);
					}
				}
				if (input.size() > 0) {
					setInput(input);
					return true;
				}
			}
		}

		Object input= context.getInput();
		if (input instanceof IEditorInput) {
			Object elementOfInput= getElementOfInput((IEditorInput)context.getInput());
			if (elementOfInput != null) {
				setSingleInput(elementOfInput);
				return true;
			}
		}

		return false;
	}

	Object getElementOfInput(IEditorInput input) {
		Object adapted= input.getAdapter(IClassFile.class);
		if (adapted != null) {
			return adapted;
		}

		if (input instanceof IFileEditorInput) {
			IFile file= ((IFileEditorInput)input).getFile();
			IJavaElement javaElement= JavaCore.create(file);
			if (javaElement != null) {
				return javaElement;
			} else {
				return file;
			}
		} else if (input instanceof IStorageEditorInput) {
			try {
				return ((IStorageEditorInput) input).getStorage();
			} catch (CoreException e) {
				return null;
			}
		}
		return null;
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getAdapter(Class<T> adapter) {
		if (adapter == IPropertySheetPage.class) {
			return (T) getPropertySheetPage();
		}
		return super.getAdapter(adapter);
	}

	private PropertySheetPage getPropertySheetPage() {
		if (fPropertySheetPage == null) {
			final PropertySheetPage propertySheetPage= new PropertySheetPage();
			propertySheetPage.setPropertySourceProvider(object -> {
				if (object instanceof IJavaElement)
					return new JavaElementProperties((IJavaElement) object);
				else if (object instanceof IResource)
					return new ResourceProperties((IResource) object);
				else if (object instanceof IMarker)
					return new MarkerProperties((IMarker) object);
				else if (object instanceof IJarEntryResource)
					return new JarEntryResourceProperties((IJarEntryResource) object);
				else if (object instanceof IMemberValuePair)
					return new MemberValuePairProperties((IMemberValuePair) object);
				else if (object instanceof IClasspathEntry)
					return new ClasspathEntryProperties((IClasspathEntry) object);
				else
					return null;
			});
			fPropertySheetPage= propertySheetPage;
		}
		return fPropertySheetPage;
	}
}
