/*******************************************************************************
 * Copyright (c) 2008, 2018 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
 *     Alexander Kurtakov <akurtako@redhat.com> - bug 415649
 *     Martin Karpisek <martin.karpisek@gmail.com> - Bug 351356
 *******************************************************************************/
package org.eclipse.pde.internal.ua.ui.editor.ctxhelp;

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

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.dialogs.MessageDialog;
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.ViewerDropAdapter;
import org.eclipse.pde.core.IModelChangedEvent;
import org.eclipse.pde.internal.core.text.IDocumentElementNode;
import org.eclipse.pde.internal.ua.core.ctxhelp.ICtxHelpConstants;
import org.eclipse.pde.internal.ua.core.ctxhelp.text.CtxHelpCommand;
import org.eclipse.pde.internal.ua.core.ctxhelp.text.CtxHelpContext;
import org.eclipse.pde.internal.ua.core.ctxhelp.text.CtxHelpDescription;
import org.eclipse.pde.internal.ua.core.ctxhelp.text.CtxHelpModel;
import org.eclipse.pde.internal.ua.core.ctxhelp.text.CtxHelpObject;
import org.eclipse.pde.internal.ua.core.ctxhelp.text.CtxHelpRoot;
import org.eclipse.pde.internal.ua.core.ctxhelp.text.CtxHelpTopic;
import org.eclipse.pde.internal.ua.ui.PDEUserAssistanceUIPlugin;
import org.eclipse.pde.internal.ua.ui.editor.ctxhelp.details.CtxHelpAbstractAddAction;
import org.eclipse.pde.internal.ua.ui.editor.ctxhelp.details.CtxHelpRemoveAction;
import org.eclipse.pde.internal.ua.ui.editor.toc.HelpEditorUtil;
import org.eclipse.pde.internal.ui.editor.PDEFormPage;
import org.eclipse.pde.internal.ui.editor.TreeSection;
import org.eclipse.pde.internal.ui.editor.actions.CollapseAction;
import org.eclipse.pde.internal.ui.editor.plugin.FormFilteredTree;
import org.eclipse.pde.internal.ui.parts.TreePart;
import org.eclipse.pde.internal.ui.util.PDELabelUtility;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.ToolBar;
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.dialogs.PatternFilter;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.Section;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.keys.IBindingService;

/**
 * Tree section for the context help editor. Displays the structure of the xml
 * file and adds actions allowing manipulation of the structure.
 *
 * @since 3.4
 * @see CtxHelpEditor
 */
public class CtxHelpTreeSection extends TreeSection {
	private CtxHelpModel fModel;
	private TreeViewer fTree;
	private FormFilteredTree fFilteredTree;

	/*
	 * The indices for each button attached to the Tree Viewer. This type of UI
	 * form does not permit direct access to each particular button. However,
	 * using these indices, one can perform any typical SWT operation on any
	 * button.
	 */
	private static final int F_BUTTON_ADD_CONTEXT = 0;
	private static final int F_BUTTON_ADD_TOPIC = 2;
	private static final int F_BUTTON_ADD_COMMAND = 3;
	private static final int F_BUTTON_REMOVE = 5;
	private static final int F_BUTTON_UP = 6;
	private static final int F_BUTTON_DOWN = 7;

	// When one of the move buttons is pressed, a flag is used to determine the
	// direction
	private static final int F_UP_FLAG = -1;
	private static final int F_DOWN_FLAG = 1;

	// The actions that will add each type of object
	private CtxHelpAbstractAddAction fAddContextAction;
	private CtxHelpAbstractAddAction fAddTopicAction;
	private CtxHelpAbstractAddAction fAddCommandAction;

	// The object removal action
	private CtxHelpRemoveAction fRemoveObjectAction;

	// The action for opening a link from the context menu
	private OpenLinkAction fOpenLinkAction;

	// Used to temporarily store the target of a drop operation
	// so that it does not have be be recalculated
	private CtxHelpObject fDropTargetParent;
	private CtxHelpObject fDropTargetSibling;

	/**
	 * If items are dragged and dropped within this tree, then this flag
	 * inhibits reselection on the removal (drag) action, thus ensuring that the
	 * selected objects are the ones that were dropped.
	 */
	private boolean fDragFromHere;

	/**
	 * Action that allows a linked file to be opened in the editor
	 *
	 * @since 3.4
	 */
	class OpenLinkAction extends Action {
		private CtxHelpTopic fOpenTarget;

		public OpenLinkAction() {
			super(CtxHelpMessages.CtxHelpTreeSection_0);
		}

		public void setTarget(CtxHelpTopic target) {
			fOpenTarget = target;
		}

		@Override
		public void run() {
			if (fOpenTarget != null) {
				open(fOpenTarget);
			}
		}
	}

	public CtxHelpTreeSection(PDEFormPage formPage, Composite parent) {
		super(formPage, parent, Section.DESCRIPTION, new String[] {
				CtxHelpMessages.CtxHelpTreeSection_1, null,
				CtxHelpMessages.CtxHelpTreeSection_2,
				CtxHelpMessages.CtxHelpTreeSection_3, null,
				CtxHelpMessages.CtxHelpTreeSection_4,
				CtxHelpMessages.CtxHelpTreeSection_5,
				CtxHelpMessages.CtxHelpTreeSection_6 });
	}

	@Override
	protected void createClient(Section section, FormToolkit toolkit) {
		fModel = (CtxHelpModel) getPage().getModel();

		Composite container = createClientContainer(section, 2, toolkit);
		createTree(container, toolkit);
		toolkit.paintBordersFor(container);

		section.setText(CtxHelpMessages.CtxHelpTreeSection_7);
		section.setDescription(CtxHelpMessages.CtxHelpTreeSection_8);
		section.setClient(container);

		createCommands();

		initializeTreeViewer();
		createSectionToolbar(section, toolkit);

		// Create the adapted listener for the filter entry field
		fFilteredTree.createUIListenerEntryFilter(this);
	}

	/**
	 * Creates the commands used in this section.
	 */
	private void createCommands() {
		fAddContextAction = new CtxHelpAbstractAddAction() {
			@Override
			public void run() {
				if (fParentObject != null) {
					CtxHelpContext context = fParentObject.getModel()
							.getFactory().createContext();
					String id = PDELabelUtility.generateName(getChildNames(),
							CtxHelpMessages.CtxHelpTreeSection_9);
					context.setID(id);
					addChild(context);
				}
			}
		};
		fAddContextAction.setText(CtxHelpMessages.CtxHelpTreeSection_10);
		fAddTopicAction = new CtxHelpAbstractAddAction() {
			@Override
			public void run() {
				if (fParentObject != null) {
					CtxHelpTopic topic = fParentObject.getModel().getFactory()
							.createTopic();
					String label = PDELabelUtility.generateName(
							getChildNames(),
							CtxHelpMessages.CtxHelpTreeSection_11);
					topic.setLabel(label);
					addChild(topic);
				}
			}
		};
		fAddTopicAction.setText(CtxHelpMessages.CtxHelpTreeSection_12);
		fAddCommandAction = new CtxHelpAbstractAddAction() {
			@Override
			public void run() {
				if (fParentObject != null) {
					CtxHelpCommand command = fParentObject.getModel()
							.getFactory().createCommand();
					String label = PDELabelUtility.generateName(
							getChildNames(),
							CtxHelpMessages.CtxHelpTreeSection_13);
					command.setLabel(label);
					addChild(command);
				}
			}
		};
		fAddCommandAction.setText(CtxHelpMessages.CtxHelpTreeSection_14);

		fRemoveObjectAction = new CtxHelpRemoveAction();
		fOpenLinkAction = new OpenLinkAction();
	}

	/**
	 * Adds a link (with hand cursor) for tree 'Collapse All' action, which
	 * collapses the tree down to the second level
	 *
	 * @param section
	 *            The section that the toolbar will belong to
	 * @param toolkit
	 *            The toolkit that will be used to make the toolbar
	 */
	private void createSectionToolbar(Section section, FormToolkit toolkit) {
		ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
		ToolBar toolbar = toolBarManager.createControl(section);

		final Cursor handCursor = Display.getCurrent().getSystemCursor(
				SWT.CURSOR_HAND);
		toolbar.setCursor(handCursor);

		// Add collapse action to the tool bar
		toolBarManager.add(new CollapseAction(fTree,
				CtxHelpMessages.CtxHelpTreeSection_15, 1, fModel
						.getCtxHelpRoot()));
		toolBarManager.update(true);
		section.setTextClient(toolbar);
	}

	/**
	 * Create the tree widget that will contain the structure
	 *
	 * @param container
	 *            The container of the tree widget
	 * @param toolkit
	 *            The toolkit used to create the tree
	 */
	private void createTree(Composite container, FormToolkit toolkit) {
		TreePart treePart = getTreePart();
		createViewerPartControl(container, SWT.MULTI, 2, toolkit);

		fTree = treePart.getTreeViewer();
		fTree.setContentProvider(new CtxHelpContentProvider());
		fTree.setLabelProvider(PDEUserAssistanceUIPlugin.getDefault()
				.getLabelProvider());

		PDEUserAssistanceUIPlugin.getDefault().getLabelProvider().connect(this);

		// Create listener for the outline view 'link with editor' toggle button
		fTree.addPostSelectionChangedListener(getPage().getPDEEditor().new PDEFormEditorChangeListener());
	}

	@Override
	protected TreeViewer createTreeViewer(Composite parent, int style) {
		fFilteredTree = new FormFilteredTree(parent, style, new PatternFilter());
		parent.setData("filtered", Boolean.TRUE); //$NON-NLS-1$
		return fFilteredTree.getViewer();
	}

	/**
	 * Initialize the tree viewer widget and its buttons.
	 */
	private void initializeTreeViewer() {
		if (fModel == null) {
			return;
		}

		CtxHelpRoot root = fModel.getCtxHelpRoot();
		fTree.setInput(root);

		// Buttons must be disabled if file is not editable
		getTreePart().setButtonEnabled(F_BUTTON_ADD_CONTEXT, isEditable());
		getTreePart().setButtonEnabled(F_BUTTON_ADD_COMMAND, isEditable());
		getTreePart().setButtonEnabled(F_BUTTON_ADD_TOPIC, isEditable());

		getTreePart().setButtonEnabled(F_BUTTON_REMOVE, false);
		getTreePart().setButtonEnabled(F_BUTTON_UP, false);
		getTreePart().setButtonEnabled(F_BUTTON_DOWN, false);
	}

	@Override
	public boolean setFormInput(Object object) {
		// This method allows the outline view to select items in the tree
		// (Invoked by org.eclipse.ui.forms.editor.IFormPage.selectReveal(Object
		// object))
		if (object instanceof CtxHelpObject) {
			fTree.setSelection(new StructuredSelection(object), true);

			// Verify that something was actually selected
			ISelection selection = fTree.getSelection();
			if (selection != null && !selection.isEmpty()) {
				return true;
			}
		}
		return false;
	}

	/**
	 * @return the selection of the tree
	 */
	public ISelection getSelection() {
		return fTree.getSelection();
	}

	/**
	 * @param selection
	 *            the new selection for the tree section
	 */
	public void setSelection(ISelection selection) {
		fTree.setSelection(selection);
	}

	/**
	 * Fire a selection change event and refresh the viewer's selection
	 */
	public void fireSelection() {
		fTree.setSelection(fTree.getSelection());
	}

	@Override
	protected void selectionChanged(IStructuredSelection selection) {
		getPage().getPDEEditor().setSelection(selection);
		updateButtons();
	}

	/**
	 * Update the buttons in the section based on the current selection
	 */
	public void updateButtons() {
		if (!fModel.isEditable()) {
			return;
		}

		IStructuredSelection selection = fTree.getStructuredSelection();
		CtxHelpObject firstSelectedObject = (CtxHelpObject) selection.getFirstElement();

		// Add Context
		getTreePart().setButtonEnabled(F_BUTTON_ADD_CONTEXT, true);

		// Add Topic
		boolean enableAdd = false;
		if (firstSelectedObject != null) {
			if (firstSelectedObject.canAddSibling(ICtxHelpConstants.TYPE_TOPIC)) {
				enableAdd = true;
			} else if (firstSelectedObject
					.canAddChild(ICtxHelpConstants.TYPE_TOPIC)) {
				enableAdd = true;
			}
		}
		getTreePart().setButtonEnabled(F_BUTTON_ADD_TOPIC, enableAdd);

		// Add Command
		enableAdd = false;
		if (firstSelectedObject != null) {
			if (firstSelectedObject
					.canAddSibling(ICtxHelpConstants.TYPE_COMMAND)) {
				enableAdd = true;
			} else if (firstSelectedObject
					.canAddChild(ICtxHelpConstants.TYPE_COMMAND)) {
				enableAdd = true;
			}
		}
		getTreePart().setButtonEnabled(F_BUTTON_ADD_COMMAND, enableAdd);

		// Remove button
		getTreePart().setButtonEnabled(F_BUTTON_REMOVE,
				getRemovableObjectFromSelection(selection).size() > 0);

		// Up and Down buttons
		boolean canMoveUp = true;
		boolean canMoveDown = true;
		if (firstSelectedObject == null
				|| firstSelectedObject.getType() == ICtxHelpConstants.TYPE_ROOT
				|| firstSelectedObject.getType() == ICtxHelpConstants.TYPE_DESCRIPTION
				|| selection.size() > 1) {
			canMoveUp = false;
			canMoveDown = false;
		} else {
			CtxHelpObject parent = firstSelectedObject.getParent();
			if (parent != null) {
				int index = parent.indexOf(firstSelectedObject);
				if (index == 0) {
					canMoveUp = false;
				}
				if (index >= parent.getChildCount() - 1) {
					canMoveDown = false;
				}
			}
		}
		getTreePart().setButtonEnabled(F_BUTTON_UP, canMoveUp);
		getTreePart().setButtonEnabled(F_BUTTON_DOWN, canMoveDown);
	}

	@Override
	protected void fillContextMenu(IMenuManager manager) {
		IStructuredSelection selection = fTree.getStructuredSelection();
		Object object = selection.getFirstElement();
		// Has to be null or a CtxHelpObject object
		CtxHelpObject firstSelectedObject = (CtxHelpObject) object;

		// Populate the "New" sub-menu
		if (firstSelectedObject != null) {
			MenuManager submenu = new MenuManager(
					CtxHelpMessages.CtxHelpTreeSection_16);
			boolean addMenu = false;
			if (updateAddContextActionWithSelection(firstSelectedObject)) {
				submenu.add(fAddContextAction);
				addMenu = true;
			}
			if (updateAddTopicActionWithSelection(firstSelectedObject)) {
				submenu.add(fAddTopicAction);
				addMenu = true;
			}
			if (updateAddCommandActionWithSelection(firstSelectedObject)) {
				submenu.add(fAddCommandAction);
				addMenu = true;
			}
			if (addMenu) {
				manager.add(submenu);
				manager.add(new Separator());
			}
		}

		// Add the open link and show in actions
		if (firstSelectedObject instanceof CtxHelpTopic
				&& ((CtxHelpTopic) firstSelectedObject).getLocation() != null) {
			fOpenLinkAction.setTarget((CtxHelpTopic) firstSelectedObject);
			manager.add(fOpenLinkAction);
			fillContextMenuShowInAction(manager);
			manager.add(new Separator());
		}

		// Add clipboard actions
		getPage().getPDEEditor().getContributor()
				.contextMenuAboutToShow(manager);
		manager.add(new Separator());

		// Add remove action
		if (updateRemoveActionWithSelection(selection)) {
			manager.add(fRemoveObjectAction);
			manager.add(new Separator());
		}

	}

	/**
	 * Creates and a new submenu in the given menu manager and adds actions to
	 * allow a linked file to be opened in various views.
	 *
	 * @param manager
	 *            menu manager to add the submenu to
	 */
	private void fillContextMenuShowInAction(IMenuManager manager) {
		String showInLabel = CtxHelpMessages.CtxHelpTreeSection_17;

		// Add a label for the keybinding for Show In action, if one exists
		IBindingService bindingService = PlatformUI
				.getWorkbench().getAdapter(IBindingService.class);
		if (bindingService != null) {
			String keyBinding = bindingService
					.getBestActiveBindingFormattedFor("org.eclipse.ui.navigate.showInQuickMenu"); //$NON-NLS-1$
			if (keyBinding != null) {
				showInLabel += '\t' + keyBinding;
			}
		}

		IMenuManager showInMenu = new MenuManager(showInLabel);
		showInMenu.add(ContributionItemFactory.VIEWS_SHOW_IN.create(getPage()
				.getSite().getWorkbenchWindow()));

		manager.add(showInMenu);
	}

	/**
	 * Updates the add context action if the action should be available for the
	 * selection. Updates enablement, parent object and target object. Returns
	 * true if the action should be available to the selection.
	 *
	 * @param selectedObject
	 *            selected object
	 * @return true if the action should be available for the current selection,
	 *         false otherwise
	 */
	private boolean updateAddContextActionWithSelection(
			CtxHelpObject selectedObject) {
		if (selectedObject != null
				&& selectedObject.canAddSibling(ICtxHelpConstants.TYPE_CONTEXT)) {
			fAddContextAction.setParentObject(selectedObject.getParent());
			fAddContextAction.setTargetObject(selectedObject);
			fAddContextAction.setEnabled(fModel.isEditable());
			return true;
		} else if (selectedObject != null
				&& selectedObject.canAddChild(ICtxHelpConstants.TYPE_CONTEXT)) {
			fAddContextAction.setParentObject(selectedObject);
			fAddContextAction.setTargetObject(null);
			fAddContextAction.setEnabled(fModel.isEditable());
			return true;
		} else if (fModel.getCtxHelpRoot().canAddChild(
				ICtxHelpConstants.TYPE_CONTEXT)) {
			fAddContextAction.setParentObject(fModel.getCtxHelpRoot());
			fAddContextAction.setTargetObject(null);
			fAddContextAction.setEnabled(fModel.isEditable());
			return true;
		}
		return false;
	}

	/**
	 * Updates the add topic action if the action should be available for the
	 * selection. Updates enablement, parent object and target object. Returns
	 * true if the action should be available to the selection.
	 *
	 * @param selectedObject
	 *            selected object
	 * @return true if the action should be available for the current selection,
	 *         false otherwise
	 */
	private boolean updateAddTopicActionWithSelection(
			CtxHelpObject selectedObject) {
		if (selectedObject != null) {
			if (selectedObject.canAddSibling(ICtxHelpConstants.TYPE_TOPIC)) {
				fAddTopicAction.setParentObject(selectedObject.getParent());
				fAddTopicAction.setTargetObject(selectedObject);
				fAddTopicAction.setEnabled(fModel.isEditable());
				return true;
			} else if (selectedObject.canAddChild(ICtxHelpConstants.TYPE_TOPIC)) {
				fAddTopicAction.setParentObject(selectedObject);
				fAddTopicAction.setTargetObject(null);
				fAddTopicAction.setEnabled(fModel.isEditable());
				return true;
			}
		}
		return false;
	}

	/**
	 * Updates the add topic action if the action should be available for the
	 * selection. Updates enablement, parent object and target object. Returns
	 * true if the action should be available to the selection.
	 *
	 * @param selectedObject
	 *            selected object
	 * @return true if the action should be available for the current selection,
	 *         false otherwise
	 */
	private boolean updateAddCommandActionWithSelection(
			CtxHelpObject selectedObject) {
		if (selectedObject != null) {
			if (selectedObject.canAddSibling(ICtxHelpConstants.TYPE_COMMAND)) {
				fAddCommandAction.setParentObject(selectedObject.getParent());
				fAddCommandAction.setTargetObject(selectedObject);
				fAddCommandAction.setEnabled(fModel.isEditable());
				return true;
			} else if (selectedObject
					.canAddChild(ICtxHelpConstants.TYPE_COMMAND)) {
				fAddCommandAction.setParentObject(selectedObject);
				fAddCommandAction.setTargetObject(null);
				fAddCommandAction.setEnabled(fModel.isEditable());
				return true;
			}
		}
		return false;
	}

	/**
	 * Updates the remove action if the action should be available for the
	 * selection. Updates enablement, parent object and target object. Returns
	 * true if the action should be available to the selection.
	 *
	 * @param selectedObject
	 *            selected object
	 * @return true if the action should be available for the current selection,
	 *         false otherwise
	 */
	private boolean updateRemoveActionWithSelection(
			IStructuredSelection selection) {
		List<CtxHelpObject> objectsToRemove = getRemovableObjectFromSelection(selection);
		fRemoveObjectAction.setToRemove(objectsToRemove
				.toArray(new CtxHelpObject[objectsToRemove.size()]));
		fRemoveObjectAction.setEnabled(fModel.isEditable());
		return objectsToRemove.size() > 0;
	}

	/**
	 * Returns a list of objects that is the subset of objects in the selection
	 * that can be removed.
	 *
	 * @param selection
	 *            the selection
	 * @return list of {@link CtxHelpObject}s that can be removed, possibly
	 *         empty.
	 */
	private List<CtxHelpObject> getRemovableObjectFromSelection(IStructuredSelection selection) {
		List<CtxHelpObject> objectsToRemove = new ArrayList<>();
		for (Iterator<?> iterator = selection.iterator(); iterator.hasNext();) {
			Object currentObject = iterator.next();
			if (currentObject instanceof CtxHelpObject
					&& ((CtxHelpObject) currentObject).canBeRemoved()) {
				objectsToRemove.add((CtxHelpObject) currentObject);
			}
		}
		return objectsToRemove;
	}

	@Override
	public boolean doGlobalAction(String actionId) {
		boolean cutAction = actionId.equals(ActionFactory.CUT.getId());

		if (cutAction || actionId.equals(ActionFactory.DELETE.getId())) {
			updateRemoveActionWithSelection(fTree.getStructuredSelection());
			fRemoveObjectAction.run();
			return !cutAction;
		}

		if (actionId.equals(ActionFactory.PASTE.getId())) {
			doPaste();
			return true;
		}

		return super.doGlobalAction(actionId);
	}

	@Override
	protected boolean canPaste(Object targetObject, Object[] sourceObjects) {
		return canDropCopy(targetObject, sourceObjects,
				ViewerDropAdapter.LOCATION_ON);
	}

	@Override
	protected void doPaste(Object targetObject, Object[] sourceObjects) {
		doDropCopy(targetObject, sourceObjects, ViewerDropAdapter.LOCATION_ON);
	}

	@Override
	protected void handleDoubleClick(IStructuredSelection selection) {
		Object selected = selection.getFirstElement();
		if (selected instanceof CtxHelpTopic) {
			open((CtxHelpTopic) selected);
		} else if (selected instanceof CtxHelpObject) {
			fTree.setExpandedState(selected, !fTree.getExpandedState(selected));
		}
	}

	/**
	 * Opens the file that is linked in the given topic.
	 *
	 * @param topic
	 *            the topic containing a link to a file
	 */
	public void open(CtxHelpTopic topic) {
		IPath resourcePath = topic.getLocation();
		if (!isEditable() || resourcePath == null || resourcePath.isEmpty()) {
			MessageDialog.openWarning(
					PDEUserAssistanceUIPlugin.getActiveWorkbenchShell(),
					CtxHelpMessages.CtxHelpTreeSection_18,
					CtxHelpMessages.CtxHelpTreeSection_19);
			return;
		}

		IResource resource = fModel.getUnderlyingResource().getProject()
				.findMember(resourcePath);
		if (resource != null && resource instanceof IFile) {
			IPath path = resource.getFullPath();
			if (HelpEditorUtil.hasValidPageExtension(path)) {
				try {
					IDE.openEditor(PDEUserAssistanceUIPlugin.getActivePage(),
							(IFile) resource, true);
				} catch (PartInitException e) { // suppress exception
				}
			} else {
				MessageDialog.openWarning(
						PDEUserAssistanceUIPlugin.getActiveWorkbenchShell(),
						CtxHelpMessages.CtxHelpTreeSection_20,
						CtxHelpMessages.CtxHelpTreeSection_21);
			}
		} else {
			MessageDialog.openWarning(
					PDEUserAssistanceUIPlugin.getActiveWorkbenchShell(),
					CtxHelpMessages.CtxHelpTreeSection_22,
					CtxHelpMessages.CtxHelpTreeSection_23);
		}
	}

	@Override
	protected void buttonSelected(int index) {
		IStructuredSelection selection = fTree.getStructuredSelection();
		Object object = selection.getFirstElement();
		CtxHelpObject firstSelectedObject = (CtxHelpObject) object;
		switch (index) {
		case F_BUTTON_ADD_CONTEXT:
			updateAddContextActionWithSelection(firstSelectedObject);
			fAddContextAction.run();
			break;
		case F_BUTTON_ADD_TOPIC:
			updateAddTopicActionWithSelection(firstSelectedObject);
			fAddTopicAction.run();
			break;
		case F_BUTTON_ADD_COMMAND:
			updateAddCommandActionWithSelection(firstSelectedObject);
			fAddCommandAction.run();
			break;
		case F_BUTTON_REMOVE:
			updateRemoveActionWithSelection(selection);
			fRemoveObjectAction.run();
			break;
		case F_BUTTON_UP:
			handleMoveAction(F_UP_FLAG);
			break;
		case F_BUTTON_DOWN:
			handleMoveAction(F_DOWN_FLAG);
			break;
		}
	}

	/**
	 * Move an object within the structure.
	 *
	 * @param positionFlag
	 *            The direction that the object will move, either F_UP_FLAG or
	 *            F_DOWN_FLAG
	 */
	private void handleMoveAction(int positionFlag) {
		IStructuredSelection sel = fTree.getStructuredSelection();

		Object object = sel.getFirstElement();
		if (object == null) {
			return;
		} else if (object instanceof CtxHelpObject) {
			CtxHelpObject ctxHelpObject = (CtxHelpObject) object;
			CtxHelpObject parent = ctxHelpObject.getParent();
			if (parent != null) {
				parent.moveChild(ctxHelpObject, positionFlag);
				fTree.setSelection(new StructuredSelection(ctxHelpObject), true);
			}
		}

	}

	@Override
	public void dispose() {
		PDEUserAssistanceUIPlugin.getDefault().getLabelProvider()
				.disconnect(this);
		super.dispose();
	}

	@Override
	protected boolean isDragAndDropEnabled() {
		return true;
	}

	@Override
	public int getSupportedDNDOperations() {
		return DND.DROP_MOVE | DND.DROP_COPY;
	}

	@Override
	public void doDropCopy(Object targetObject, Object[] sourceObjects,
			int targetLocation) {
		if (fDropTargetParent != null) {
			if (fDropTargetSibling != null) {
				if (targetLocation == ViewerDropAdapter.LOCATION_BEFORE) {
					for (Object sourceObject : sourceObjects) {
						((CtxHelpObject) sourceObject).reconnect(
								fDropTargetParent, fModel);
						fDropTargetParent.addChild(
								(CtxHelpObject) sourceObject,
								fDropTargetSibling, true);
					}
				} else {
					for (int i = sourceObjects.length - 1; i >= 0; i--) {
						((CtxHelpObject) sourceObjects[i]).reconnect(
								fDropTargetParent, fModel);
						fDropTargetParent.addChild(
								(CtxHelpObject) sourceObjects[i],
								fDropTargetSibling, false);
					}
				}
			} else {
				for (Object sourceObject : sourceObjects) {
					((CtxHelpObject) sourceObject).reconnect(
							fDropTargetParent, fModel);
					fDropTargetParent
							.addChild((CtxHelpObject) sourceObject);
				}
			}
		}
		fDropTargetParent = null;
		fDropTargetSibling = null;
	}

	@Override
	public void doDropMove(Object targetObject, Object[] sourceObjects,
			int targetLocation) {
		doDropCopy(targetObject, sourceObjects, targetLocation);
	}

	@Override
	public boolean canDropCopy(Object targetObject, Object[] sourceObjects,
			int targetLocation) {
		// Add as a child of the root
		if (targetObject == null
				|| !(targetObject instanceof CtxHelpObject || ((CtxHelpObject) targetObject)
						.getType() == ICtxHelpConstants.TYPE_ROOT)) {
			for (int i = 0; i < sourceObjects.length; i++) {
				if (!(sourceObjects[i] instanceof CtxHelpObject)
						|| !fModel.getCtxHelpRoot().canAddChild(
								((CtxHelpObject) sourceObjects[i]).getType())) {
					return false;
				}
			}
			fDropTargetParent = fModel.getCtxHelpRoot();
			fDropTargetSibling = null;
			return true;
		}

		CtxHelpObject dropTarget = (CtxHelpObject) targetObject;

		// Add as a child of the target
		if (targetLocation == ViewerDropAdapter.LOCATION_ON) {
			boolean result = true;
			for (int i = 0; i < sourceObjects.length; i++) {
				if (!(sourceObjects[i] instanceof CtxHelpObject)
						|| !dropTarget
								.canAddChild(((CtxHelpObject) sourceObjects[i])
										.getType())) {
					result = false;
					break;
				}
			}
			// If adding as a child works, do so, otherwise try as a sibling
			if (result) {
				fDropTargetParent = dropTarget;
				fDropTargetSibling = null;
				return true;
			}
		}

		// Add as a sibling of the target
		for (int i = 0; i < sourceObjects.length; i++) {
			if (!(sourceObjects[i] instanceof CtxHelpObject)
					|| !dropTarget
							.canAddSibling(((CtxHelpObject) sourceObjects[i])
									.getType())) {
				return false;
			}
		}
		fDropTargetParent = dropTarget.getParent();
		fDropTargetSibling = dropTarget;
		return true;
	}

	@Override
	public boolean canDropMove(Object targetObject, Object[] sourceObjects,
			int targetLocation) {
		// Same as drop copy operation
		return canDropCopy(targetObject, sourceObjects, targetLocation);
	}

	@Override
	public boolean canDragCopy(Object[] sourceObjects) {
		// Allow anything to be drag copied
		return true;
	}

	@Override
	public boolean canDragMove(Object[] sourceObjects) {
		for (int i = 0; i < sourceObjects.length; i++) {
			if (!(sourceObjects[i] instanceof CtxHelpObject)
					|| !((CtxHelpObject) sourceObjects[i]).canBeRemoved()) {
				return false;
			}
		}
		fDragFromHere = true;
		return true;
	}

	@Override
	public void doDragRemove(Object[] sourceObjects) {
		updateRemoveActionWithSelection(new StructuredSelection(sourceObjects));
		fRemoveObjectAction.run();
		fDragFromHere = false;
	}

	@Override
	public void modelChanged(IModelChangedEvent event) {
		// No need to call super, world changed event handled here
		if (event.getChangeType() == IModelChangedEvent.WORLD_CHANGED) {
			handleModelEventWorldChanged(event);
		} else if (event.getChangeType() == IModelChangedEvent.INSERT) {
			handleModelInsertType(event);
		} else if (event.getChangeType() == IModelChangedEvent.REMOVE) {
			handleModelRemoveType(event);
		} else if ((event.getChangeType() == IModelChangedEvent.CHANGE)
				&& (event.getChangedProperty()
						.equals(IDocumentElementNode.F_PROPERTY_CHANGE_TYPE_SWAP))) {
			handleModelChangeTypeSwap(event);
		} else if (event.getChangeType() == IModelChangedEvent.CHANGE) {
			handleModelChangeType(event);
		}
	}

	/**
	 * Handles the swap event
	 *
	 * @param event
	 *            the swap event
	 */
	private void handleModelChangeTypeSwap(IModelChangedEvent event) {
		Object[] objects = event.getChangedObjects();
		CtxHelpObject object = (CtxHelpObject) objects[0];

		if (object != null) {
			fTree.refresh(object);
		}
	}

	/**
	 * The model is stale, refresh the UI
	 *
	 * @param event
	 *            The world-change event
	 */
	private void handleModelEventWorldChanged(IModelChangedEvent event) {
		markStale();
	}

	/**
	 * Handle insertions in the model
	 *
	 * @param event
	 *            the insertion event
	 */
	private void handleModelInsertType(IModelChangedEvent event) {
		Object[] objects = event.getChangedObjects();
		for (Object o : objects) {
			if (o instanceof CtxHelpObject) {
				CtxHelpObject object = (CtxHelpObject) o;
				if (object.getType() != ICtxHelpConstants.TYPE_ROOT) {
					fTree.refresh(object.getParent());
					// Select the new object in the tree, unless it is a
					// description node
					if (!(object instanceof CtxHelpDescription)) {
						fTree.setSelection(new StructuredSelection(object),
								true);
					}
				}
			}
		}
	}

	/**
	 * Handle removals in the model
	 *
	 * @param event
	 *            the removal event
	 */
	private void handleModelRemoveType(IModelChangedEvent event) {
		Object[] objects = event.getChangedObjects();
		for (Object o : objects) {
			if (o instanceof CtxHelpObject) {
				CtxHelpObject object = (CtxHelpObject) o;
				fTree.remove(object);
				CtxHelpObject nextSelection = fRemoveObjectAction
						.getNextSelection();
				if (nextSelection != null) {
					fTree.refresh(object.getParent());
					if (!fDragFromHere) {
						fTree.setSelection(new StructuredSelection(
								nextSelection), true);
					}
					fRemoveObjectAction.clearNextSelection();
				}
			}
		}
	}

	/**
	 * Handle an update to an object's properties
	 *
	 * @param event
	 *            the update event
	 */
	private void handleModelChangeType(IModelChangedEvent event) {
		Object[] objects = event.getChangedObjects();
		if (objects[0] != null) {
			fTree.update(objects[0], null);
		}
	}

}
