/*******************************************************************************
 * Copyright (c) 2000, 2015 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
 *     Gunnar Wagenknecht - fix for bug 21756 [PropertiesView] property view sorting
 *     Simon Scholz <simon.scholz@vogella.com> - Bug 460405
 *******************************************************************************/

package org.eclipse.ui.views.properties;

import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.help.IContext;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.util.ConfigureColumns;
import org.eclipse.jface.viewers.CellEditor;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.window.SameShellProvider;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DragSourceAdapter;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.HelpEvent;
import org.eclipse.swt.events.HelpListener;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.ISaveablePart;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.help.IWorkbenchHelpSystem;
import org.eclipse.ui.internal.views.properties.PropertiesMessages;
import org.eclipse.ui.part.CellEditorActionHandler;
import org.eclipse.ui.part.Page;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;

/**
 * The standard implementation of property sheet page which presents
 * a table of property names and values obtained from the current selection
 * in the active workbench part.
 * <p>
 * This page obtains the information about what properties to display from
 * the current selection (which it tracks).
 * </p>
 * <p>
 * The model for this page is a hierarchy of <code>IPropertySheetEntry</code>.
 * The page may be configured with a custom model by setting the root entry.
 * <p>
 * If no root entry is set then a default model is created which uses the
 * <code>IPropertySource</code> interface to obtain the properties of
 * the current selection. This requires that the selected objects provide an
 * <code>IPropertySource</code> adapter (or implement
 * <code>IPropertySource</code> directly). This restiction can be overcome
 * by providing this page with an <code>IPropertySourceProvider</code>. If
 * supplied, this provider will be used by the default model to obtain a
 * property source for the current selection
 * </p>
 * <p>
 * This class may be instantiated; it is not intended to be subclassed.
 * </p>
 *
 * @see IPropertySource
 * @noextend This class is not intended to be subclassed by clients.
 */
public class PropertySheetPage extends Page implements IPropertySheetPage, IAdaptable {
    /**
     * Help context id
     * (value <code>"org.eclipse.ui.property_sheet_page_help_context"</code>).
     */
    public static final String HELP_CONTEXT_PROPERTY_SHEET_PAGE = "org.eclipse.ui.property_sheet_page_help_context"; //$NON-NLS-1$

    private PropertySheetViewer viewer;

    private PropertySheetSorter sorter;

    private IPropertySheetEntry rootEntry;

    private IPropertySourceProvider provider;

    private DefaultsAction defaultsAction;

    private FilterAction filterAction;

    private CategoriesAction categoriesAction;

    private CopyPropertyAction copyAction;

    private ICellEditorActivationListener cellEditorActivationListener;

    private CellEditorActionHandler cellEditorActionHandler;

    private Clipboard clipboard;

	private IWorkbenchPart sourcePart;

	/**
	 * Part listener which cleans up this page when the source part is closed.
	 * This is hooked only when there is a source part.
	 *
	 * @since 3.2
	 */
	private class PartListener implements IPartListener {
		@Override
		public void partActivated(IWorkbenchPart part) {
		}

		@Override
		public void partBroughtToTop(IWorkbenchPart part) {
		}

		@Override
		public void partClosed(IWorkbenchPart part) {
			if (sourcePart == part) {
				if (sourcePart != null)
					sourcePart.getSite().getPage().removePartListener(partListener);
				sourcePart = null;
				if (viewer != null && !viewer.getControl().isDisposed()) {
					viewer.setInput(new Object[0]);
				}
			}
		}

		@Override
		public void partDeactivated(IWorkbenchPart part) {
		}

		@Override
		public void partOpened(IWorkbenchPart part) {
		}
	}

	private PartListener partListener = new PartListener();

	private Action columnsAction;

	private ISelectionChangedListener selectionChangeListener;

    /**
     * Creates a new property sheet page.
     */
    public PropertySheetPage() {
        super();
    }

    @Override
	public void createControl(Composite parent) {
        // create a new viewer
        viewer = new PropertySheetViewer(parent);
        viewer.setSorter(sorter);

        // set the model for the viewer
        if (rootEntry == null) {
            // create a new root
            PropertySheetEntry root = new PropertySheetEntry();
            if (provider != null) {
				// set the property source provider
                root.setPropertySourceProvider(provider);
			}
            rootEntry = root;
        }
        viewer.setRootEntry(rootEntry);
        viewer.addActivationListener(getCellEditorActivationListener());
		selectionChangeListener = new ISelectionChangedListener() {
            @Override
			public void selectionChanged(SelectionChangedEvent event) {
                handleEntrySelection(event.getSelection());
            }
		};
		viewer.addSelectionChangedListener(selectionChangeListener);
        initDragAndDrop();
        makeActions();

        // Create the popup menu for the page.
        MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$
        menuMgr.add(copyAction);
        menuMgr.add(new Separator());
        menuMgr.add(defaultsAction);
        Menu menu = menuMgr.createContextMenu(viewer.getControl());
        viewer.getControl().setMenu(menu);

        // Set help on the viewer
        viewer.getControl().addHelpListener(new HelpListener() {
            /*
             * @see HelpListener#helpRequested(HelpEvent)
             */
            @Override
			public void helpRequested(HelpEvent e) {
                // Get the context for the selected item
                IStructuredSelection selection = (IStructuredSelection) viewer
                        .getSelection();
                if (!selection.isEmpty()) {
                    IPropertySheetEntry entry = (IPropertySheetEntry) selection
                            .getFirstElement();
                    Object helpContextId = entry.getHelpContextIds();
                    if (helpContextId != null) {
                    	IWorkbenchHelpSystem helpSystem = PlatformUI.getWorkbench().getHelpSystem();

                        // Since 2.0 the only valid type for helpContextIds
                        // is a String (a single id).
                        if (helpContextId instanceof String) {
                            helpSystem.displayHelp((String) helpContextId);
                            return;
                        }

                        // For backward compatibility we have to handle
                        // and array of contexts (Strings and/or IContexts)
                        // or a context computer.
                        Object context= getFirstContext(helpContextId, e);
                        if (context instanceof IContext) {
							helpSystem.displayHelp((IContext) context);
						} else if (context instanceof String) {
							helpSystem.displayHelp((String) context);
						}
                        return;
                    }
                }

                // No help for the selection so show page help
                PlatformUI.getWorkbench().getHelpSystem().displayHelp(HELP_CONTEXT_PROPERTY_SHEET_PAGE);
            }

			/**
			 * Returns the first help context.
			 *
			 * @param helpContext the help context which is either an array of contexts (strings
			 *            and/or {@linkplain IContext}s) or an {@link IContextComputer}
			 *
			 * @param e the help event
			 * @return the first context which is either a <code>String</code>, {@link IContext} or
			 *         <code>null</code> if none
			 * @deprecated As of 2.0, nested contexts are no longer supported by the help support
			 *             system
			 */
			@Deprecated
			private Object getFirstContext(Object helpContext, HelpEvent e) {
				Object[] contexts;
				contexts = (Object[]) helpContext;

				if (contexts.length > 0)
					return contexts[0];
				return null;
			}
        });
    }

    /**
     * The <code>PropertySheetPage</code> implementation of this <code>IPage</code> method
     * disposes of this page's entries.
     */
    @Override
	public void dispose() {
        super.dispose();
        if (sourcePart != null) {
        	sourcePart.getSite().getPage().removePartListener(partListener);
			sourcePart = null;
			partListener = null;
        }
        if (rootEntry != null) {
            rootEntry.dispose();
            rootEntry = null;
        }
        if (clipboard != null) {
            clipboard.dispose();
            clipboard = null;
        }
		if (viewer != null) {
			if (selectionChangeListener != null) {
				viewer.removeSelectionChangedListener(selectionChangeListener);
				selectionChangeListener = null;
			}
			viewer.dispose();
			cellEditorActivationListener = null;
			viewer = null;
		}
		if (cellEditorActionHandler != null) {
			cellEditorActionHandler.dispose();
			cellEditorActionHandler = null;
		}
		if (defaultsAction != null) {
			defaultsAction.viewer = null;
			defaultsAction = null;
		}
		if (filterAction != null) {
			filterAction.viewer = null;
			filterAction = null;
		}
		if (categoriesAction != null) {
			categoriesAction.viewer = null;
			categoriesAction = null;
		}
		if (copyAction != null) {
			copyAction.viewer = null;
			copyAction = null;
		}
		sorter = null;
		provider = null;
    }

    /**
     * The <code>PropertySheetPage</code> implementation of this <code>IAdaptable</code> method
     * handles the <code>ISaveablePart</code> adapter by delegating to the source part.
     *
     * @since 3.2
     */
    @Override
	public <T> T getAdapter(Class<T> adapter) {
		if (ISaveablePart.class.equals(adapter)) {
			return adapter.cast(getSaveablePart());
		}
    	return null;
    }

	/**
	 * Returns an <code>ISaveablePart</code> that delegates to the source part
	 * for the current page if it implements <code>ISaveablePart</code>, or
	 * <code>null</code> otherwise.
	 *
	 * @return an <code>ISaveablePart</code> or <code>null</code>
	 * @since 3.2
	 */
	protected ISaveablePart getSaveablePart() {
		if (sourcePart instanceof ISaveablePart) {
			return (ISaveablePart) sourcePart;
		}
		return null;
	}

    /**
     * Returns the cell editor activation listener for this page
     * @return ICellEditorActivationListener the cell editor activation listener for this page
     */
    private ICellEditorActivationListener getCellEditorActivationListener() {
        if (cellEditorActivationListener == null) {
            cellEditorActivationListener = new ICellEditorActivationListener() {
                @Override
				public void cellEditorActivated(CellEditor cellEditor) {
                    if (cellEditorActionHandler != null) {
						cellEditorActionHandler.addCellEditor(cellEditor);
					}
                }

                @Override
				public void cellEditorDeactivated(CellEditor cellEditor) {
                    if (cellEditorActionHandler != null) {
						cellEditorActionHandler.removeCellEditor(cellEditor);
					}
                }
            };
        }
        return cellEditorActivationListener;
    }

    @Override
	public Control getControl() {
        if (viewer == null) {
			return null;
		}
        return viewer.getControl();
    }

    /**
     * Handles a selection change in the entry table.
     *
     * @param selection the new selection
     */
    public void handleEntrySelection(ISelection selection) {
        if (defaultsAction != null) {
            if (selection.isEmpty()) {
                defaultsAction.setEnabled(false);
                return;
            }
            // see if item is editable
            boolean editable = viewer.getActiveCellEditor() != null;
            defaultsAction.setEnabled(editable);
        }
    }

    /**
     * Adds drag and drop support.
     */
    protected void initDragAndDrop() {
        int operations = DND.DROP_COPY;
        Transfer[] transferTypes = new Transfer[] { TextTransfer.getInstance() };
        DragSourceListener listener = new DragSourceAdapter() {
            @Override
			public void dragSetData(DragSourceEvent event) {
                performDragSetData(event);
            }

            @Override
			public void dragFinished(DragSourceEvent event) {
                //Nothing to do here
            }
        };
        DragSource dragSource = new DragSource(
                viewer.getControl(), operations);
        dragSource.setTransfer(transferTypes);
        dragSource.addDragListener(listener);
    }

    /**
     * The user is attempting to drag.  Add the appropriate
     * data to the event.
     * @param event The event sent from the drag and drop support.
     */
    void performDragSetData(DragSourceEvent event) {
        // Get the selected property
        IStructuredSelection selection = (IStructuredSelection) viewer
                .getSelection();
        if (selection.isEmpty()) {
			return;
		}
        // Assume single selection
        IPropertySheetEntry entry = (IPropertySheetEntry) selection
                .getFirstElement();

        // Place text as the data
        StringBuilder buffer = new StringBuilder();
        buffer.append(entry.getDisplayName());
        buffer.append("\t"); //$NON-NLS-1$
        buffer.append(entry.getValueAsString());

        event.data = buffer.toString();
    }

    /**
     * Make action objects.
     */
    private void makeActions() {
        ISharedImages sharedImages = PlatformUI.getWorkbench()
                .getSharedImages();

        // Restore Default Value
        defaultsAction = new DefaultsAction(viewer, "defaults"); //$NON-NLS-1$
        defaultsAction.setText(PropertiesMessages.Defaults_text);
        defaultsAction.setToolTipText(PropertiesMessages.Defaults_toolTip);
        defaultsAction.setImageDescriptor(createImageDescriptor("elcl16/defaults_ps.png")); //$NON-NLS-1$
		defaultsAction.setDisabledImageDescriptor(createImageDescriptor("dlcl16/defaults_ps.png")); //$NON-NLS-1$
        defaultsAction.setEnabled(false);

        // Show Advanced Properties
        filterAction = new FilterAction(viewer, "filter"); //$NON-NLS-1$
        filterAction.setText(PropertiesMessages.Filter_text);
        filterAction.setToolTipText(PropertiesMessages.Filter_toolTip);
		filterAction.setImageDescriptor(createImageDescriptor("elcl16/filter_ps.png")); //$NON-NLS-1$
        filterAction.setChecked(false);

        // Show Categories
        categoriesAction = new CategoriesAction(viewer, "categories"); //$NON-NLS-1$
        categoriesAction.setText(PropertiesMessages.Categories_text);
        categoriesAction.setToolTipText(PropertiesMessages.Categories_toolTip);
		categoriesAction.setImageDescriptor(createImageDescriptor("elcl16/tree_mode.png")); //$NON-NLS-1$
        categoriesAction.setChecked(true);

        // Columns...
        columnsAction = new Action(PropertiesMessages.Columns_text){
        	@Override
			public void run() {
        		Tree tree = (Tree) viewer.getControl();
        		ConfigureColumns.forTree(tree, new SameShellProvider(tree));
        	}
		};
        columnsAction.setToolTipText(PropertiesMessages.Columns_toolTip);

        // Copy
        Shell shell = viewer.getControl().getShell();
        clipboard = new Clipboard(shell.getDisplay());
        copyAction = new CopyPropertyAction(viewer, "copy", clipboard); //$NON-NLS-1$
        copyAction.setText(PropertiesMessages.CopyProperty_text);
		copyAction.setImageDescriptor(sharedImages.getImageDescriptor(ISharedImages.IMG_TOOL_COPY));
    }

	// Replacement for the bundle activator, see Bug 481956
	private ImageDescriptor createImageDescriptor(String relativeIconPath) {
		String ICONS_PATH = "$nl$/icons/full/";//$NON-NLS-1$
		Bundle bundle = FrameworkUtil.getBundle(PropertySheetPage.class);
		ImageDescriptor imageDescriptor = AbstractUIPlugin
				.imageDescriptorFromPlugin(bundle.getSymbolicName(), ICONS_PATH + relativeIconPath);
		return imageDescriptor;
    }

    @Override
	public void makeContributions(IMenuManager menuManager,
            IToolBarManager toolBarManager, IStatusLineManager statusLineManager) {

        // add actions to the tool bar
        toolBarManager.add(categoriesAction);
        toolBarManager.add(filterAction);
        toolBarManager.add(defaultsAction);

        // add actions to the menu
        menuManager.add(categoriesAction);
        menuManager.add(filterAction);
        menuManager.add(columnsAction);

        // set status line manager into the viewer
        viewer.setStatusLineManager(statusLineManager);
    }

    /**
     * Updates the model for the viewer.
     * <p>
     * Note that this means ensuring that the model reflects the state
     * of the current viewer input.
     * </p>
     */
    public void refresh() {
        if (viewer == null) {
			return;
		}
        // calling setInput on the viewer will cause the model to refresh
        viewer.setInput(viewer.getInput());
    }

    @Override
	public void selectionChanged(IWorkbenchPart part, ISelection selection) {
        if (viewer == null) {
			return;
		}

        if (sourcePart != null) {
        	sourcePart.getSite().getPage().removePartListener(partListener);
        	sourcePart = null;
        }

        // change the viewer input since the workbench selection has changed.
        if (selection instanceof IStructuredSelection) {
        	sourcePart = part;
            viewer.setInput(((IStructuredSelection) selection).toArray());
        }

        if (sourcePart != null) {
        	sourcePart.getSite().getPage().addPartListener(partListener);
        }
    }

    /**
     * The <code>PropertySheetPage</code> implementation of this <code>IPage</code> method
     * calls <code>makeContributions</code> for backwards compatibility with
     * previous versions of <code>IPage</code>.
     * <p>
     * Subclasses may reimplement.
     * </p>
     */
    @Override
	public void setActionBars(IActionBars actionBars) {
        super.setActionBars(actionBars);
        cellEditorActionHandler = new CellEditorActionHandler(actionBars);
        cellEditorActionHandler.setCopyAction(copyAction);
    }

    /**
     * Sets focus to a part in the page.
     */
    @Override
	public void setFocus() {
        viewer.getControl().setFocus();
    }

    /**
     * Sets the given property source provider as
     * the property source provider.
     * <p>
     * Calling this method is only valid if you are using
     * this page's default root entry.
     * </p>
     * @param newProvider the property source provider
     */
    public void setPropertySourceProvider(IPropertySourceProvider newProvider) {
        provider = newProvider;
        if (rootEntry instanceof PropertySheetEntry) {
            ((PropertySheetEntry) rootEntry)
                    .setPropertySourceProvider(provider);
            // the following will trigger an update
            viewer.setRootEntry(rootEntry);
        }
    }

    /**
     * Sets the given entry as the model for the page.
     *
     * @param entry the root entry
     */
    public void setRootEntry(IPropertySheetEntry entry) {
        rootEntry = entry;
        if (viewer != null) {
			// the following will trigger an update
            viewer.setRootEntry(rootEntry);
		}
    }

    /**
	 * Sets the sorter used for sorting categories and entries in the viewer
	 * of this page.
	 * <p>
	 * The default sorter sorts categories and entries alphabetically.
	 * </p>
	 * @param sorter the sorter to set (<code>null</code> will reset to the
	 * default sorter)
     * @since 3.1
	 */
	protected void setSorter(PropertySheetSorter sorter) {
		this.sorter = sorter;
        if (viewer != null) {
        	viewer.setSorter(sorter);

        	// the following will trigger an update
        	if(null != viewer.getRootEntry()) {
				viewer.setRootEntry(rootEntry);
			}
        }
	}

}
