/*******************************************************************************
 * Copyright (c) 2000, 2017 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
 *     Dan Rubel (dan_rubel@instantiations.com) - accessor to get context menu ids
 *******************************************************************************/
package org.eclipse.ui.internal;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.Platform;
import org.eclipse.e4.core.contexts.ContextFunction;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.ui.model.application.MApplication;
import org.eclipse.e4.ui.model.application.ui.MElementContainer;
import org.eclipse.e4.ui.model.application.ui.basic.MPart;
import org.eclipse.e4.ui.model.application.ui.basic.MWindow;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IKeyBindingService;
import org.eclipse.ui.IPageService;
import org.eclipse.ui.IPartService;
import org.eclipse.ui.ISelectionService;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.SubActionBars;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.internal.contexts.SlaveContextService;
import org.eclipse.ui.internal.expressions.ActivePartExpression;
import org.eclipse.ui.internal.handlers.LegacyHandlerService;
import org.eclipse.ui.internal.menus.SlaveMenuService;
import org.eclipse.ui.internal.progress.WorkbenchSiteProgressService;
import org.eclipse.ui.internal.registry.IWorkbenchRegistryConstants;
import org.eclipse.ui.internal.services.IServiceLocatorCreator;
import org.eclipse.ui.internal.services.IWorkbenchLocationService;
import org.eclipse.ui.internal.services.ServiceLocator;
import org.eclipse.ui.internal.services.WorkbenchLocationService;
import org.eclipse.ui.internal.testing.WorkbenchPartTestable;
import org.eclipse.ui.menus.IMenuService;
import org.eclipse.ui.progress.IProgressService;
import org.eclipse.ui.progress.IWorkbenchSiteProgressService;
import org.eclipse.ui.services.IServiceScopes;
import org.eclipse.ui.testing.IWorkbenchPartTestable;

/**
 * <code>PartSite</code> is the general implementation for an
 * <code>IWorkbenchPartSite</code>. A site maintains the context for a part,
 * including the part, its pane, active contributions, selection provider, etc.
 * Together, these components make up the complete behavior for a part as if it
 * was implemented by one person.
 *
 * The <code>PartSite</code> lifecycle is as follows ..
 *
 * <ol>
 * <li>a site is constructed</li>
 * <li>a part is constructed and stored in the part</li>
 * <li>the site calls part.init()</li>
 * <li>a pane is constructed and stored in the site</li>
 * <li>the action bars for a part are constructed and stored in the site</li>
 * <li>the pane is added to a presentation</li>
 * <li>the SWT widgets for the pane and part are created</li>
 * <li>the site is activated, causing the actions to become visible</li>
 * </ol>
 */
public abstract class PartSite implements IWorkbenchPartSite {

	/**
	 * This is a helper method for the register context menu functionality. It is
	 * provided so that different implementations of the
	 * <code>IWorkbenchPartSite</code> interface don't have to worry about how
	 * context menus should work.
	 *
	 * @param menuId             the menu id
	 * @param menuManager        the menu manager
	 * @param selectionProvider  the selection provider
	 * @param includeEditorInput whether editor inputs should be included in the
	 *                           structured selection when calculating contributions
	 * @param part               the part for this site
	 * @param menuExtenders      the collection of menu extenders for this site
	 * @see IWorkbenchPartSite#registerContextMenu(MenuManager, ISelectionProvider)
	 */
	public static final void registerContextMenu(final String menuId, final MenuManager menuManager,
			final ISelectionProvider selectionProvider, final boolean includeEditorInput, final IWorkbenchPart part,
			IEclipseContext context, final Collection<PopupMenuExtender> menuExtenders) {
		/*
		 * Check to see if the same menu manager and selection provider have already
		 * been used. If they have, then we can just add another menu identifier to the
		 * existing PopupMenuExtender.
		 */
		final Iterator<PopupMenuExtender> extenderItr = menuExtenders.iterator();
		boolean foundMatch = false;
		while (extenderItr.hasNext()) {
			final PopupMenuExtender existingExtender = extenderItr.next();
			if (existingExtender.matches(menuManager, selectionProvider, part)) {
				existingExtender.addMenuId(menuId);
				foundMatch = true;
				break;
			}
		}

		if (!foundMatch) {
			menuExtenders.add(
					new PopupMenuExtender(menuId, menuManager, selectionProvider, part, context, includeEditorInput));
		}
	}

	private IWorkbenchPartReference partReference;

	private IWorkbenchPart part;

	private ISelectionProvider selectionProvider;

	private SubActionBars actionBars;

	private KeyBindingService keyBindingService;

	private SlavePageService pageService;

	private SlavePartService partService;

	private SlaveSelectionService selectionService;

	private SlaveContextService contextService;

	private SlaveMenuService menuService;

	protected ArrayList<PopupMenuExtender> menuExtenders;

	private WorkbenchSiteProgressService progressService;

	protected ServiceLocator serviceLocator;

	protected MPart model;

	private IConfigurationElement element;

	private IEclipseContext e4Context;

	private IWorkbenchWindow workbenchWindow;

	private String extensionId;

	/**
	 * Build the part site.
	 *
	 * @param ref  the part reference
	 * @param part the part
	 */
	public PartSite(MPart model, IWorkbenchPart part, IWorkbenchPartReference ref, IConfigurationElement element) {
		this.model = model;
		this.part = part;
		this.partReference = ref;
		this.element = element;

		MElementContainer<?> parent = (MElementContainer<?>) ((EObject) model).eContainer();
		while (!(parent instanceof MWindow)) {
			parent = (MElementContainer<?>) ((EObject) parent).eContainer(); // parent.getParent();
		}

		setWindow((MWindow) parent);

		e4Context = model.getContext();
		IServiceLocatorCreator slc = e4Context.get(IServiceLocatorCreator.class);
		IWorkbenchWindow workbenchWindow = getWorkbenchWindow();
		this.serviceLocator = (ServiceLocator) slc.createServiceLocator(workbenchWindow, null, () -> {
			// not sure what to do here
		}, e4Context);
		initializeDefaultServices();
	}

	void setExtensionId(String extensionId) {
		this.extensionId = extensionId;
	}

	private void setWindow(MWindow window) {
		MWindow topWindow = getTopLevelModelWindow(window);
		MApplication application = topWindow.getContext().get(MApplication.class);
		Workbench workbench = (Workbench) application.getContext().get(IWorkbench.class);

		workbenchWindow = workbench.createWorkbenchWindow(workbench.getDefaultPageInput(),
				workbench.getPerspectiveRegistry().findPerspectiveWithId(
						workbench.getPerspectiveRegistry().getDefaultPerspective()),
				topWindow, false);
	}

	/**
	 * Initialize the local services.
	 */
	private void initializeDefaultServices() {
		IHandlerService handlerService = new LegacyHandlerService(e4Context, new ActivePartExpression(part));
		e4Context.set(IHandlerService.class, handlerService);

		serviceLocator.registerService(IWorkbenchLocationService.class,
				new WorkbenchLocationService(IServiceScopes.PARTSITE_SCOPE, getWorkbenchWindow().getWorkbench(),
						getWorkbenchWindow(), this, null, null, 2));
		// added back for legacy reasons
		serviceLocator.registerService(IWorkbenchPartSite.class, this);
		serviceLocator.registerService(IWorkbenchPart.class, getPart());

		e4Context.set(IWorkbenchSiteProgressService.class.getName(), new ContextFunction() {
			@Override
			public Object compute(IEclipseContext context, String contextKey) {
				if (progressService == null) {
					progressService = new WorkbenchSiteProgressService(PartSite.this);
				}
				return progressService;
			}
		});
		e4Context.set(IProgressService.class.getName(), new ContextFunction() {
			@Override
			public Object compute(IEclipseContext context, String contextKey) {
				if (progressService == null) {
					progressService = new WorkbenchSiteProgressService(PartSite.this);
				}
				return progressService;
			}
		});
		e4Context.set(IKeyBindingService.class.getName(), new ContextFunction() {
			@Override
			public Object compute(IEclipseContext context, String contextKey) {
				if (keyBindingService == null) {
					keyBindingService = new KeyBindingService(PartSite.this);
				}

				return keyBindingService;
			}
		});
		e4Context.set(IPageService.class.getName(), new ContextFunction() {
			@Override
			public Object compute(IEclipseContext context, String contextKey) {
				if (pageService == null) {
					pageService = new SlavePageService(context.getParent().get(IPageService.class));
				}

				return pageService;
			}
		});
		e4Context.set(IPartService.class.getName(), new ContextFunction() {
			@Override
			public Object compute(IEclipseContext context, String contextKey) {
				if (partService == null) {
					partService = new SlavePartService(context.getParent().get(IPartService.class));
				}
				return partService;
			}
		});
		e4Context.set(ISelectionService.class.getName(), new ContextFunction() {
			@Override
			public Object compute(IEclipseContext context, String contextKey) {
				if (selectionService == null) {
					selectionService = new SlaveSelectionService(context.getParent().get(ISelectionService.class));
				}
				return selectionService;
			}
		});
		e4Context.set(IContextService.class.getName(), new ContextFunction() {
			@Override
			public Object compute(IEclipseContext context, String contextKey) {
				if (contextService == null) {
					contextService = new SlaveContextService(context.getParent().get(IContextService.class),
							new ActivePartExpression(part));
				}
				return contextService;
			}
		});
		e4Context.set(IMenuService.class.getName(), new ContextFunction() {
			@Override
			public Object compute(IEclipseContext context, String contextKey) {
				if (menuService == null) {
					menuService = new SlaveMenuService(context.getParent().get(IMenuService.class), model);
				}
				return menuService;
			}
		});
	}

	/**
	 * Dispose the contributions.
	 */
	public void dispose() {
		if (menuExtenders != null) {
			HashSet<MenuManager> managers = new HashSet<>(menuExtenders.size());
			for (PopupMenuExtender ext : menuExtenders) {
				managers.add(ext.getManager());
				ext.dispose();
			}
			if (managers.size() > 0) {
				for (MenuManager mgr : managers) {
					mgr.dispose();
				}
			}
			menuExtenders = null;
		}

		if (keyBindingService != null) {
			keyBindingService.dispose();
			keyBindingService = null;
		}

		if (progressService != null) {
			progressService.dispose();
			progressService = null;
		}

		if (pageService != null) {
			pageService.dispose();
			pageService = null;
		}

		if (partService != null) {
			partService.dispose();
			partService = null;
		}

		if (selectionService != null) {
			selectionService.dispose();
			selectionService = null;
		}

		if (contextService != null) {
			contextService.dispose();
			contextService = null;
		}

		if (serviceLocator != null) {
			serviceLocator.dispose();
			serviceLocator = null;
		}
		if (menuService != null) {
			menuService.dispose();
			menuService = null;
		}
		part = null;
		workbenchWindow = null;
		e4Context = null;
	}

	/**
	 * Returns the action bars for the part. If this part is a view then it has
	 * exclusive use of the action bars. If this part is an editor then the action
	 * bars are shared among this editor and other editors of the same type.
	 */
	public IActionBars getActionBars() {
		return actionBars;
	}

	@Override
	public String getId() {
		return extensionId == null
				? element == null ? model.getElementId() : element.getAttribute(IWorkbenchRegistryConstants.ATT_ID)
				: extensionId;
	}

	@Override
	public String getPluginId() {
		return element == null ? model.getElementId() : element.getNamespaceIdentifier();
	}

	@Override
	public String getRegisteredName() {
		return element == null ? model.getLocalizedLabel() : element.getAttribute(IWorkbenchRegistryConstants.ATT_NAME);
	}

	/**
	 * Returns the page containing this workbench site's part.
	 *
	 * @return the page containing this part
	 */
	@Override
	public IWorkbenchPage getPage() {
		IWorkbenchWindow window = getWorkbenchWindow();
		return window == null ? null : window.getActivePage();
	}

	/**
	 * Returns the part.
	 */
	@Override
	public IWorkbenchPart getPart() {
		return part;
	}

	/**
	 * Returns the part reference.
	 */
	public IWorkbenchPartReference getPartReference() {
		return partReference;
	}

	/**
	 * Returns the selection provider for a part.
	 */
	@Override
	public ISelectionProvider getSelectionProvider() {
		return selectionProvider;
	}

	/**
	 * Returns the shell containing this part.
	 *
	 * @return the shell containing this part
	 */
	@Override
	public Shell getShell() {

		// Compatibility: This method should not be used outside the UI
		// thread... but since this condition
		// was not always in the JavaDoc, we still try to return our best guess
		// about the shell if it is
		// called from the wrong thread.
		Display currentDisplay = Display.getCurrent();
		if (currentDisplay == null) {
			// Uncomment this to locate places that try to access the shell from
			// a background thread
			// WorkbenchPlugin.log(new Exception("Error:
			// IWorkbenchSite.getShell() was called outside the UI thread. Fix
			// this code.")); //$NON-NLS-1$

			return getWorkbenchWindow().getShell();
		}

		Control control = (Control) model.getWidget();
		if (control != null && !control.isDisposed()) {
			return control.getShell();
		}

		// If someone works with disposed parts, we can't help, it is a bug
		if (e4Context == null) {
			WorkbenchPlugin.log(new IllegalStateException(
					"IWorkbenchSite.getShell() was called after part disposal: " + this)); //$NON-NLS-1$
			return PlatformUI.getWorkbench().getDisplay().getActiveShell();
		}

		// likely means the part has been destroyed, return the parent window's
		// shell, we don't just arbitrarily return the workbench window's shell
		// because we may be in a detached window
		MWindow window = e4Context.get(MWindow.class);
		return window == null ? getWorkbenchWindow().getShell() : (Shell) window.getWidget();
	}

	private MWindow getTopLevelModelWindow(MWindow window) {
		EObject previousParent = (EObject) window;
		EObject parent = previousParent.eContainer();
		// we can't simply stop at an MWindow because the part may be in a detached
		// window
		while (!(parent instanceof MApplication)) {
			previousParent = parent;
			parent = parent.eContainer();
		}

		return (MWindow) previousParent;
	}

	/**
	 * Returns the workbench window containing this part.
	 *
	 * @return the workbench window containing this part
	 */
	@Override
	public IWorkbenchWindow getWorkbenchWindow() {
		return workbenchWindow;
	}

	/**
	 * Register a popup menu for extension.
	 */
	@Override
	public void registerContextMenu(String menuID, MenuManager menuMgr, ISelectionProvider selProvider) {
		if (menuExtenders == null) {
			menuExtenders = new ArrayList<>(1);
		}

		registerContextMenu(menuID, menuMgr, selProvider, true, getPart(), e4Context, menuExtenders);
	}

	/**
	 * Register a popup menu with the default id for extension.
	 */
	@Override
	public void registerContextMenu(MenuManager menuMgr, ISelectionProvider selProvider) {
		registerContextMenu(getId(), menuMgr, selProvider);
	}

	// getContextMenuIds() added by Dan Rubel (dan_rubel@instantiations.com)
	/**
	 * Get the registered popup menu identifiers
	 */
	public String[] getContextMenuIds() {
		if (menuExtenders == null) {
			return new String[0];
		}
		ArrayList<String> menuIds = new ArrayList<>(menuExtenders.size());
		for (PopupMenuExtender extender : menuExtenders) {
			menuIds.addAll(extender.getMenuIds());
		}
		return menuIds.toArray(new String[menuIds.size()]);
	}

	/**
	 * Sets the action bars for the part.
	 */
	public void setActionBars(SubActionBars bars) {
		actionBars = bars;
	}

	/**
	 * Sets the part.
	 */
	public void setPart(IWorkbenchPart newPart) {
		part = newPart;
	}

	/**
	 * Set the selection provider for a part.
	 */
	@Override
	public void setSelectionProvider(ISelectionProvider provider) {
		selectionProvider = provider;
	}

	/*
	 * @see IWorkbenchPartSite#getKeyBindingService()
	 */
	@Override
	public IKeyBindingService getKeyBindingService() {
		return e4Context.get(IKeyBindingService.class);
	}

	protected String getInitialScopeId() {
		return null;
	}

	/**
	 * Get an adapter for this type.
	 *
	 * @param adapter
	 * @return
	 */
	@Override
	public final <T> T getAdapter(Class<T> adapter) {

		if (IWorkbenchSiteProgressService.class == adapter) {
			return adapter.cast(getService(adapter));
		}

		if (IWorkbenchPartTestable.class == adapter) {
			return adapter.cast(new WorkbenchPartTestable(this));
		}

		return Platform.getAdapterManager().getAdapter(this, adapter);
	}

	public void activateActionBars(boolean forceVisibility) {
		if (serviceLocator != null) {
			serviceLocator.activate();
		}

		if (actionBars != null) {
			actionBars.activate(forceVisibility);
		}
	}

	public void deactivateActionBars(boolean forceHide) {
		if (actionBars != null) {
			actionBars.deactivate(forceHide);
		}
		if (serviceLocator != null) {
			serviceLocator.deactivate();
		}
	}

	/**
	 * Get a progress service for the receiver.
	 *
	 * @return WorkbenchSiteProgressService
	 */
	IWorkbenchSiteProgressService getSiteProgressService() {
		return e4Context.get(IWorkbenchSiteProgressService.class);
	}

	@Override
	public final <T> T getService(final Class<T> key) {
		return serviceLocator == null ? null : serviceLocator.getService(key);
	}

	@Override
	public final boolean hasService(final Class<?> key) {
		return serviceLocator == null ? false : serviceLocator.hasService(key);
	}

	/**
	 * Prints out the identifier, the plug-in identifier and the registered name.
	 * This is for debugging purposes only.
	 *
	 * @since 3.2
	 */
	@Override
	public String toString() {
		final StringBuilder buffer = new StringBuilder();
		buffer.append("PartSite(id="); //$NON-NLS-1$
		buffer.append(getId());
		buffer.append(",pluginId="); //$NON-NLS-1$
		buffer.append(getPluginId());
		buffer.append(",registeredName="); //$NON-NLS-1$
		buffer.append(getRegisteredName());
		buffer.append(",hashCode="); //$NON-NLS-1$
		buffer.append(hashCode());
		buffer.append(')');
		return buffer.toString();
	}

	public MPart getModel() {
		return model;
	}

	public IEclipseContext getContext() {
		return e4Context;
	}
}
