/*******************************************************************************
 * Copyright (c) 2003, 2023 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
 *     anton.leherbauer@windriver.com - bug 220599 make CommonNavigator a ShowInSource
 *     rob.stryker@jboss.com - bug 250198 Slight changes for easier subclassing
 *     Stefan Winkler <stefan@winklerweb.net> - bug 178019 - CNF Tooltip support
 *******************************************************************************/
package org.eclipse.ui.navigator;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.PerformanceStats;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.ISaveablePart;
import org.eclipse.ui.ISaveablesLifecycleListener;
import org.eclipse.ui.ISaveablesSource;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.Saveable;
import org.eclipse.ui.SaveablesLifecycleEvent;
import org.eclipse.ui.actions.ActionGroup;
import org.eclipse.ui.internal.navigator.CommonNavigatorActionGroup;
import org.eclipse.ui.internal.navigator.NavigatorContentService;
import org.eclipse.ui.internal.navigator.NavigatorPlugin;
import org.eclipse.ui.internal.navigator.NavigatorSafeRunnable;
import org.eclipse.ui.part.ISetSelectionTarget;
import org.eclipse.ui.part.IShowInSource;
import org.eclipse.ui.part.IShowInTarget;
import org.eclipse.ui.part.ShowInContext;
import org.eclipse.ui.part.ViewPart;

/**
 * <p>
 * This class provides the IViewPart for the Common Navigator framework in the
 * Eclipse workbench. This class also serves as the backbone for navigational
 * viewers. The following types are used by this class to render the Common
 * Navigator:
 * <ul>
 * <li>
 * <p>
 * {@link org.eclipse.ui.navigator.CommonViewer}: The viewer that renders the
 * extensible tree. Creates and manages the lifecycle of the Navigator Content
 * Service (described below).
 * </p>
 * </li>
 * <li>
 * <p>
 * {@link org.eclipse.ui.navigator.NavigatorActionService}: Manages instances of
 * {@link org.eclipse.ui.navigator.CommonActionProvider}s provided by individual
 * extensions and content extensions.
 * </p>
 * </li>
 * <li>
 * <p>
 * {@link org.eclipse.ui.navigator.INavigatorContentService}: Manages instances
 * of Navigator Content Extensions. Instances are created as needed, and
 * disposed of upon the disposal of the Navigator Content Service.
 * </p>
 * </li>
 * </ul>
 *
 * <p>
 * Clients that wish to define their own custom extensible navigator view using
 * CommonNavigator need to specify an instance of the
 * <b>org.eclipse.ui.views</b> extension point:
 * </p>
 *
 * <pre>
 *          &lt;extension point=&quot;org.eclipse.ui.views&quot;&gt;
 *          	&lt;view
 *          		name=&quot;My Custom View&quot;
 *          		icon=&quot;relative/path/to/icon.gif&quot;
 *          		category=&quot;org.acme.mycategory&quot;
 *          		class=&quot;org.eclipse.ui.navigator.CommonNavigator&quot;
 *          		id=&quot;org.acme.MyCustomNavigatorID&quot;&gt;
 *          	&lt;/view&gt;
 *          &lt;/extension&gt;
 * </pre>
 *
 *
 * <p>
 * CommonNavigator gets its initial input (during initialization) from the
 * Workbench by calling getSite().getPage().getInput(). This is done in
 * {@link #getInitialInput()}. Clients may create a subclass of CommonNavigator
 * to provide their own means of getting the initial input. Or they may access
 * the {@link CommonViewer} and set its input directly after startup.
 * </p>
 *
 * <p>
 * In the IDE scenario, the default page input is IWorkspaceRoot, in the RCP
 * scenario it is null and can be configured in the WorkbenchAdvisor.
 * </p>
 *
 * <p>
 * Clients that wish to extend the view menu provided via the
 * <b>org.eclipse.ui.popupMenu</b>s extension may specify the the
 * <i>popupMenuId</i> specified by <b>org.eclipse.ui.navigator.viewer</b> (or a
 * nested <b>popupMenu</b> element) of their target viewer as their target menu
 * id.
 * </p>
 *
 * <p>
 * This class may be instantiated or subclassed
 * </p>
 *
 * @since 3.2
 */
public class CommonNavigator extends ViewPart implements ISetSelectionTarget, ISaveablePart, ISaveablesSource, IShowInTarget {
	private static final String PERF_CREATE_PART_CONTROL= "org.eclipse.ui.navigator/perf/explorer/createPartControl"; //$NON-NLS-1$

	private static final String HELP_CONTEXT =  NavigatorPlugin.PLUGIN_ID + ".common_navigator"; //$NON-NLS-1$

	/**
	 * <p>
	 * Used to track changes to the {@link #isLinkingEnabled}&nbsp;property.
	 * </p>
	 *
	 * Make sure this does not conflict with anything in IWorkbenchPartConstants.
	 *
	 */
	public static final int IS_LINKING_ENABLED_PROPERTY = 0x10000;

	private CommonViewer commonViewer;

	private CommonNavigatorManager commonManager;

	private ActionGroup commonActionGroup;

	/**
	 * To allow {@link #createCommonViewer(Composite)} to be subclassed
	 *
	 * @since 3.4
	 */
	protected IMemento memento;

	private boolean isLinkingEnabled;

	private String LINKING_ENABLED = "CommonNavigator.LINKING_ENABLED"; //$NON-NLS-1$

	private LinkHelperService linkService;

	/**
	 *
	 */
	public CommonNavigator() {
		super();
	}

	/**
	 * <p>
	 * Create the CommonViewer part control and setup the default providers as
	 * necessary.
	 * </p>
	 *
	 *
	 * @see org.eclipse.ui.part.WorkbenchPart#createPartControl(org.eclipse.swt.widgets.Composite)
	 */
	@Override
	public void createPartControl(Composite aParent) {

		final PerformanceStats stats= PerformanceStats.getStats(PERF_CREATE_PART_CONTROL, this);
		stats.startRun();

		commonViewer = createCommonViewer(aParent);
		commonViewer.setCommonNavigator(this);

		try {
			commonViewer.getControl().setRedraw(false);

			INavigatorFilterService filterService = commonViewer
					.getNavigatorContentService().getFilterService();
			ViewerFilter[] visibleFilters = filterService.getVisibleFilters(true);
			for (ViewerFilter visibleFilter : visibleFilters) {
				commonViewer.addFilter(visibleFilter);
			}

			commonViewer.setSorter(new CommonViewerSorter());

			/*
			 * make sure input is set after sorters and filters to avoid unnecessary
			 * refreshes
			 */
			commonViewer.setInput(getInitialInput());

			getSite().setSelectionProvider(commonViewer);

			setPartName(getConfigurationElement().getAttribute("name")); //$NON-NLS-1$
		} finally {
			commonViewer.getControl().setRedraw(true);
		}

		commonViewer.createFrameList();

		/*
		 * Create the CommonNavigatorManager last because information about the
		 * state of the CommonNavigator is required for the initialization of
		 * the CommonNavigatorManager
		 */
		commonManager = createCommonManager();
		if (memento != null) {
			commonViewer.getNavigatorContentService().restoreState(memento);
		}

		commonActionGroup = createCommonActionGroup();
		commonActionGroup.fillActionBars(getViewSite().getActionBars());
		if (memento != null && commonActionGroup instanceof IMementoAware mementoAware) {
			mementoAware.restoreState(memento);
		}

		ISaveablesLifecycleListener saveablesLifecycleListener = new ISaveablesLifecycleListener() {
			ISaveablesLifecycleListener siteSaveablesLifecycleListener = getSite()
					.getService(ISaveablesLifecycleListener.class);

			@Override
			public void handleLifecycleEvent(SaveablesLifecycleEvent event) {
				if (event.getEventType() == SaveablesLifecycleEvent.DIRTY_CHANGED) {
					firePropertyChange(PROP_DIRTY);
				}
				siteSaveablesLifecycleListener.handleLifecycleEvent(event);
			}
		};
		commonViewer.getNavigatorContentService()
				.getSaveablesService().init(this, getCommonViewer(),
						saveablesLifecycleListener);

		commonViewer.addSelectionChangedListener(event -> firePropertyChange(PROP_DIRTY));

		String helpContext = commonViewer.getNavigatorContentService().getViewerDescriptor().getHelpContext();
		if (helpContext == null)
			helpContext = HELP_CONTEXT;
		PlatformUI.getWorkbench().getHelpSystem().setHelp(commonViewer.getControl(), helpContext);

		boolean enableToolTipSupport = commonViewer.getNavigatorContentService().getViewerDescriptor()
				.getBooleanConfigProperty(INavigatorViewerDescriptor.PROP_ENABLE_TOOLTIP_SUPPORT);
		if (enableToolTipSupport) {
			ColumnViewerToolTipSupport.enableFor(commonViewer);
		}

		stats.endRun();
	}

	/**
	 * <p>
	 * Updates the title text and title tool tip. Called whenever the input of
	 * the viewer changes.
	 * </p>
	 * @since 3.4
	 */
	public void updateTitle() {
		Object input = commonViewer.getInput();
		if (input == null) {
			setTitleToolTip(""); //$NON-NLS-1$
		} else {
			String inputToolTip = getFrameToolTipText(input);
			setTitleToolTip(inputToolTip);
		}
	}

	/**
	 * <p>
	 * Returns the tool tip text for the given element. Used as the tool tip text
	 * for the current frame, and for the view title tooltip.
	 * </p>
	 *
	 * @param anElement element to get text for
	 * @return the tool tip text
	 * @since 3.4
	 *
	 */
	public String getFrameToolTipText(Object anElement) {
		if (commonViewer == null)
			return ""; //$NON-NLS-1$
		return ((ILabelProvider) commonViewer.getLabelProvider())
					.getText(anElement);
	}


	/**
	 * <p>
	 * Note: This method is for internal use only. Clients should not call this
	 * method.
	 * </p>
	 * <p>
	 * This method will be invoked when the DisposeListener is notified of the
	 * disposal of the Eclipse view part.
	 * </p>
	 *
	 * @see org.eclipse.ui.part.WorkbenchPart#dispose()
	 */
	@Override
	public void dispose() {
		if (commonManager != null) {
			commonManager.dispose();
		}
		if (commonActionGroup != null) {
			commonActionGroup.dispose();
		}
		super.dispose();
	}

	/**
	 * <p>
	 * Note: This method is for internal use only. Clients should not call this
	 * method.
	 * </p>
	 * @see org.eclipse.ui.part.ViewPart#init(org.eclipse.ui.IViewSite,
	 *      org.eclipse.ui.IMemento)
	 */
	@Override
	public void init(IViewSite aSite, IMemento aMemento)
			throws PartInitException {
		super.init(aSite, aMemento);
		memento = aMemento;
		if (memento != null) {
			Integer linkingEnabledInteger = memento.getInteger(LINKING_ENABLED);
			setLinkingEnabled(((linkingEnabledInteger != null) ? linkingEnabledInteger
					.intValue() == 1
					: false));
		}
	}

	/**
	 *
	 * <p>
	 * Note: This method is for internal use only. Clients should not call this
	 * method.
	 * </p>
	 *
	 * @see org.eclipse.ui.part.ViewPart#saveState(org.eclipse.ui.IMemento)
	 */
	@Override
	public void saveState(IMemento aMemento) {
		aMemento.putInteger(LINKING_ENABLED, (isLinkingEnabled) ? 1 : 0);
		super.saveState(aMemento);
		commonManager.saveState(aMemento);
		commonViewer.getNavigatorContentService().saveState(aMemento);
		if (commonActionGroup instanceof IMementoAware mementoAware) {
			mementoAware.saveState(aMemento);
		}
	}

	/**
	 * <p>
	 * Force the workbench to focus on the Common Navigator tree.
	 * </p>
	 *
	 * @see org.eclipse.ui.part.WorkbenchPart#setFocus()
	 */
	@Override
	public void setFocus() {
		if (commonViewer != null) {
			commonViewer.getTree().setFocus();
		}
	}

	/**
	 * <p>
	 * Set the selection to the Common Navigator tree, and expand nodes if
	 * necessary. Use caution when invoking this method as it can cause
	 * Navigator Content Extensions to load, thus causing plugin activation.
	 * </p>
	 *
	 * @see org.eclipse.ui.part.ISetSelectionTarget#selectReveal(org.eclipse.jface.viewers.ISelection)
	 */
	@Override
	public void selectReveal(ISelection selection) {
		if (commonViewer != null) {
			commonViewer.setSelection(selection, true);
		}
	}

	/**
	 * <p>
	 * Linking is handled by an action which listens for
	 * changes to the {@link CommonNavigator#IS_LINKING_ENABLED_PROPERTY}.
	 * Custom implementations that wish to override this functionality, need to
	 * override the action used by the default ActionGroup and listen for
	 * changes to the above property.
	 *
	 * @param toEnableLinking
	 *            True enables linking the current selection with open editors
	 */
	public final void setLinkingEnabled(boolean toEnableLinking) {
		isLinkingEnabled = toEnableLinking;
		firePropertyChange(IS_LINKING_ENABLED_PROPERTY);
	}

	/**
	 * @return Whether linking the current selection with open editors is
	 *         enabled.
	 */
	public final boolean isLinkingEnabled() {
		return isLinkingEnabled;
	}

	/**
	 * <p>
	 * Provides access to the commonViewer used by the current CommonNavigator.
	 * The field will not be valid until after
	 * {@link #init(IViewSite, IMemento)}&nbsp;has been called by the
	 * Workbench.
	 * </p>
	 *
	 * @return The (already created) instance of Common Viewer.
	 */
	public CommonViewer getCommonViewer() {
		return commonViewer;
	}

	/**
	 * @return The Navigator Content Service which populates this instance of
	 *         Common Navigator
	 */
	public INavigatorContentService getNavigatorContentService() {
		return getCommonViewer().getNavigatorContentService();
	}

	/**
	 * Returns an object which is an instance of the given class
	 * associated with this object. Returns <code>null</code> if
	 * no such object can be found.
	 *
	 * @param adapter the adapter class to look up
	 * @return a object castable to the given class,
	 *    or <code>null</code> if this object does not
	 *    have an adapter for the given class
	 */
	@Override
	public <T> T getAdapter(Class<T> adapter) {
		if (adapter == CommonViewer.class) {
			return adapter.cast(getCommonViewer());
		} else if (adapter == INavigatorContentService.class) {
			return adapter.cast(getCommonViewer().getNavigatorContentService());
		} else if (adapter == IShowInTarget.class) {
			return adapter.cast(this);
		} else if (adapter == IShowInSource.class) {
			return adapter.cast(getShowInSource());
		}
		return super.getAdapter(adapter);
	}

	/**
	 * Returns the <code>IShowInSource</code> for this view.
	 */
	private IShowInSource getShowInSource() {
		return () -> new ShowInContext(getCommonViewer().getInput(), getCommonViewer().getSelection());
	}

	/**
	 * @return The Navigator Action Service which populates this instance of Common
	 *         Navigator
	 */
	public NavigatorActionService getNavigatorActionService() {
		return commonManager.getNavigatorActionService();
	}

	/**
	 * Creates and initializes an instance of {@link CommonViewer}. The ID of
	 * the Eclipse view part will be used to create the viewer. The ID is
	 * important as some extensions indicate they should only be used with a
	 * particular viewer ID.
	 *
	 * @param aParent
	 *            A composite parent to contain the Common Viewer
	 * @return An initialized instance of CommonViewer
	 */
	protected CommonViewer createCommonViewer(Composite aParent) {
		CommonViewer aViewer = createCommonViewerObject(aParent);
		initListeners(aViewer);
		aViewer.getNavigatorContentService().restoreState(memento);
		return aViewer;
	}

	/**
	 * Constructs and returns an instance of {@link CommonViewer}. The ID of
	 * the Eclipse view part will be used to create the viewer.
	 *
	 * Override this method if you want a subclass of the CommonViewer
	 *
	 * @param aParent
	 *            A composite parent to contain the CommonViewer
	 * @return An instance of CommonViewer
	 * @since 3.4
	 */
	protected CommonViewer createCommonViewerObject(Composite aParent) {
		return new CommonViewer(getViewSite().getId(), aParent,
				SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
	}

	/**
	 * <p>
	 * Adds the listeners to the Common Viewer.
	 * </p>
	 *
	 * @param viewer
	 *            The viewer
	 * @since 2.0
	 */
	protected void initListeners(TreeViewer viewer) {

		viewer.addDoubleClickListener(event -> SafeRunner.run(new NavigatorSafeRunnable() {
			@Override
			public void run() throws Exception {
				handleDoubleClick(event);
			}
		}));
	}

	/**
	 * <p>
	 * Note: This method is for internal use only. Clients should not call this
	 * method.
	 * </p>
	 *
	 * @param anEvent
	 *            Supplied by the DoubleClick listener.
	 */
	protected void handleDoubleClick(DoubleClickEvent anEvent) {

		IAction openHandler = getViewSite().getActionBars().getGlobalActionHandler(ICommonActionConstants.OPEN);

		if(openHandler == null) {
			IStructuredSelection selection = (IStructuredSelection) anEvent
					.getSelection();
			Object element = selection.getFirstElement();

			TreeViewer viewer = getCommonViewer();
			if (viewer.isExpandable(element)) {
				viewer.setExpandedState(element, !viewer.getExpandedState(element));
			}
		}
	}

	/**
	 * <p>
	 * The Common Navigator Manager handles the setup of the Common Navigator
	 * Menu, manages updates to the ActionBars from
	 * {@link CommonActionProvider}&nbsp; extensions as the user's selection
	 * changes, and also updates the status bar based on the current selection.
	 *
	 * @return The Common Navigator Manager class which handles menu population
	 *         and ActionBars
	 *
	 * @since 3.4
	 */
	protected CommonNavigatorManager createCommonManager() {
		return new CommonNavigatorManager(this, memento);
	}

	/**
	 * <p>
	 * The ActionGroup is used to populate the ActionBars of Common Navigator
	 * View Part, and the returned implementation will have an opportunity to
	 * fill the ActionBars of the view as soon as it is created. ({@link ActionGroup#fillActionBars(org.eclipse.ui.IActionBars)}.
	 * </p>
	 * <p>
	 * The default implementation returns an action group which will add the
	 * following actions:
	 * <ul>
	 * <li>
	 * <p>
	 * Link with editor support. Allows the user to toggling linking the current
	 * selection with the active editors.
	 * </p>
	 * <li>
	 * <p>
	 * Collapse all. Collapses all expanded nodes.
	 * </p>
	 * <li>
	 * <p>
	 * Select Filters. Provides access to the "Select Filters" dialog that
	 * allows users to enable/disable filters and also the Content Extension
	 * activations.
	 * </p>
	 * </ul>
	 *
	 * @return The Action Group to be associated with the Common Navigator View
	 *         Part.
	 */
	protected ActionGroup createCommonActionGroup() {
		return new CommonNavigatorActionGroup(this, commonViewer, getLinkHelperService());
	}

	/**
	 * Used to provide the initial input for the {@link CommonViewer}.  By default
	 * getSite().getPage().getInput() is used.  Subclass this to return your desired
	 * input.
	 *
	 * @return The initial input for the viewer. Defaults to
	 *         getSite().getPage().getInput()
	 * @since 3.4
	 */
	protected Object getInitialInput() {
		return getSite().getPage().getInput();
	}

	@Override
	public Saveable[] getSaveables() {
		Saveable[] saveables = getNavigatorContentService().getSaveablesService().getSaveables();
		if (saveables == null) {
			saveables = new Saveable[0];
		}
		return saveables;
	}

	@Override
	public Saveable[] getActiveSaveables() {
		Saveable[] activeSaveables = getNavigatorContentService().getSaveablesService().getActiveSaveables();
		if (activeSaveables == null) {
			activeSaveables = new Saveable[0];
		}
		return activeSaveables;
	}

	/**
	 * @since 3.9
	 */
	protected boolean hasSaveablesProvider() {
		return getNavigatorContentService().getSaveablesService().hasSaveablesProvider();
	}

	@Override
	public void doSave(IProgressMonitor monitor) {
		// Ignore. This method is not called because CommonNavigator implements
		// ISaveablesSource. All saves will go through the ISaveablesSource /
		// Saveable protocol.
	}

	@Override
	public void doSaveAs() {
		// ignore
	}

	@Override
	public boolean isDirty() {
		Saveable[] saveables = getSaveables();
		for (Saveable saveable : saveables) {
			if(saveable.isDirty()) {
				return true;
			}
		}
		return false;
	}

	@Override
	public boolean isSaveAsAllowed() {
		return false;
	}

	@Override
	public boolean isSaveOnCloseNeeded() {
		return isDirty();
	}

	@Override
	public boolean show(ShowInContext context) {
		if (context == null)
			return false;
		ISelection selection = context.getSelection();
		if (selection instanceof IStructuredSelection && !selection.isEmpty()) {
			selectReveal(selection);
			selection = commonViewer.getSelection();
			if (selection != null && !selection.isEmpty()) {
				return true;
			}
		}
		Object input = context.getInput();
		if (input instanceof IEditorInput editorInput) {
			LinkHelperService lhs = getLinkHelperService();
			selection = lhs.getSelectionFor(editorInput);
			if (selection != null && !selection.isEmpty()) {
				selectReveal(selection);
				return true;
			}
		}
		if (input != null) {
			selectReveal(new StructuredSelection(input));
			return true;

		}

		return false;
	}

	/**
	 * @since 3.4
	 */
	protected synchronized LinkHelperService getLinkHelperService() {
		if (linkService == null)
			linkService = new LinkHelperService((NavigatorContentService)getCommonViewer().getNavigatorContentService());
		return linkService;
	}

	/**
	 * @since 3.4
	 */
	protected IMemento getMemento() {
		return memento;
	}


	/**
	 * @param mode new root mode
	 * @noreference This method is not intended to be referenced by clients.
	 * @nooverride This method is not intended to be re-implemented or extended by clients.
	 * @since 3.4
	 */
	public void setRootMode(int mode) {
		// For subclassing
	}

	/**
	 * @return the root mode
	 * @noreference This method is not intended to be referenced by clients.
	 * @nooverride This method is not intended to be re-implemented or extended by clients.
	 * @since 3.4
	 */
	public int getRootMode() {
		// For subclassing
		return 0;
	}

	/**
	 * @param label new working set label
	 * @noreference This method is not intended to be referenced by clients.
	 * @nooverride This method is not intended to be re-implemented or extended by clients.
	 * @since 3.4
	 */
	public void setWorkingSetLabel(String label) {
		// For subclassing
	}

	/**
	 * @return the working set label
	 * @noreference This method is not intended to be referenced by clients.
	 * @nooverride This method is not intended to be re-implemented or extended by clients.
	 * @since 3.4
	 */
	public String getWorkingSetLabel() {
		// For subclassing
		return null;
	}


}
