/*******************************************************************************
 * 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
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 440810, 472654
 *******************************************************************************/
package org.eclipse.ui.internal;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.commands.contexts.Context;
import org.eclipse.e4.ui.services.EContextService;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.commands.ActionHandler;
import org.eclipse.ui.IKeyBindingService;
import org.eclipse.ui.INestableKeyBindingService;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.IWorkbenchSite;
import org.eclipse.ui.handlers.IHandlerActivation;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.internal.actions.CommandAction;
import org.eclipse.ui.internal.handlers.CommandLegacyActionWrapper;

/**
 * This service provides a nestable implementation of a key binding service.
 * This class is provided for backwards compatibility only, and might be removed
 * in the future. All of the functionality is the class can be duplicated by
 * using the commands and contexts API.
 *
 * @since 2.0
 */
public final class KeyBindingService implements INestableKeyBindingService {
	/**
	 * Whether this key binding service has been disposed. A disposed key binding
	 * service should not be used again.
	 */
	private boolean disposed;

	private final Map<IWorkbenchSite, IKeyBindingService> nestedServices = new HashMap<>();

	/**
	 * The set of context identifiers enabled in this key binding service (not
	 * counting any nested services). This set may be empty, but it is never
	 * <code>null</code>.
	 */
	private Set<String> enabledContextIds = Collections.EMPTY_SET;

	/**
	 * The site within the workbench at which this service is provided. This value
	 * should not be <code>null</code>.
	 */
	private IWorkbenchPartSite workbenchPartSite;

	private KeyBindingService parent;

	private IKeyBindingService activeService;

	private Map<IAction, IHandlerActivation> actionToProxy = new HashMap<>();

	/**
	 * Constructs a new instance of <code>KeyBindingService</code> on a given
	 * workbench site. This instance is not nested.
	 *
	 * @param workbenchPartSite The site for which this service will be responsible;
	 *                          should not be <code>null</code>.
	 */
	public KeyBindingService(IWorkbenchPartSite workbenchPartSite) {
		this(workbenchPartSite, null);
	}

	/**
	 * Constructs a new instance of <code>KeyBindingService</code> on a given
	 * workbench site.
	 *
	 * @param workbenchPartSite The site for which this service will be responsible;
	 *                          should not be <code>null</code>.
	 * @param parent            The parent key binding service, if any;
	 *                          <code>null</code> if none.
	 */
	KeyBindingService(IWorkbenchPartSite workbenchPartSite, KeyBindingService parent) {
		this.workbenchPartSite = workbenchPartSite;
		this.parent = parent;
	}

	@Override
	public boolean activateKeyBindingService(IWorkbenchSite nestedSite) {
		if (disposed) {
			return false;
		}

		// Check if we should do a deactivation.
		if (nestedSite == null) {
			// We should do a deactivation, if there is one active.
			if (activeService == null) {
				// There is no active service. Do no work.
				return false;
			}
			// Deactivate the currently active nested service.
			deactivateNestedService();
			return true;
		}

		// Attempt to activate a service.
		final IKeyBindingService service = nestedServices.get(nestedSite);

		if (service == activeService) {
			// The service is already active, or already null
			return false;
		}

		deactivateNestedService();
		if (service != null) {
			activateNestedService(service);
		}
		return true;
	}

	private void activateNestedService(IKeyBindingService service) {
		/*
		 * If I have a parent, and I'm the active service, then deactivate so that I can
		 * make changes.
		 */
		boolean active = false;
		boolean haveParent = (parent != null);
		if (haveParent) {
			active = (parent.activeService == this);
			if (active) {
				parent.deactivateNestedService();
			}
		}

		// Update the active service.
		activeService = service;

		// Check to see that the service isn't null.
		if (service == null) {
			return;
		}

		if (haveParent) {
			if (active) {
				parent.activateNestedService(this);
			}

		} else if (activeService instanceof KeyBindingService) {
			// add all the nested context ids.

			EContextService cs = ((KeyBindingService) activeService).workbenchPartSite
					.getService(EContextService.class);
			for (String id : ((KeyBindingService) activeService).enabledContextIds) {
				cs.activateContext(id);
			}
			/*
			 * add all of the nested handler submissions.
			 */
			IHandlerService hs = ((KeyBindingService) activeService).workbenchPartSite
					.getService(IHandlerService.class);
			Iterator<Entry<IAction, IHandlerActivation>> i = ((KeyBindingService) activeService).actionToProxy
					.entrySet().iterator();
			while (i.hasNext()) {
				Entry<IAction, IHandlerActivation> entry = i.next();
				hs.activateHandler(entry.getValue());
			}
		}
	}

	private void deactivateNestedService() {
		if (disposed) {
			return;
		}

		// Don't do anything if there is no active service.
		if (activeService == null) {
			return;
		}

		// Check to see if there is a parent.
		boolean active = false;
		if (parent != null) {
			// Check if I'm the active service.
			if (parent.activeService == this) {
				active = true;
				// Deactivate myself so I can make changes.
				parent.deactivateNestedService();
			}

		} else if (activeService instanceof KeyBindingService) {
			// Remove all the nested context ids.

			EContextService cs = ((KeyBindingService) activeService).workbenchPartSite
					.getService(EContextService.class);
			for (String id : ((KeyBindingService) activeService).enabledContextIds) {
				cs.deactivateContext(id);
			}
			/*
			 * Remove all of the nested handler submissions. The handlers here weren't
			 * created by this instance (but by the nest instance), and hence can't be
			 * disposed here.
			 */
			IHandlerService hs = ((KeyBindingService) activeService).workbenchPartSite
					.getService(IHandlerService.class);
			hs.deactivateHandlers(((KeyBindingService) activeService).actionToProxy.values());
		}

		// Clear our reference to the active service.
		activeService = null;

		// If necessary, let my parent know that changes have occurred.
		if (active) {
			parent.activateNestedService(this);
		}
	}

	/**
	 * Disposes this key binding service. This clears out all of the submissions
	 * held by this service, and its nested services.
	 */
	public void dispose() {
		if (!disposed) {
			disposed = true;
			deactivateNestedService();
			EContextService cs = workbenchPartSite.getService(EContextService.class);
			for (String id : enabledContextIds) {
				cs.deactivateContext(id);
			}
			enabledContextIds.clear();
			/*
			 * Remove all of the nested handler submissions. The handlers here weren't
			 * created by this instance (but by the nest instance), and hence can't be
			 * disposed here.
			 */
			IHandlerService hs = workbenchPartSite.getService(IHandlerService.class);
			hs.deactivateHandlers(actionToProxy.values());
			actionToProxy.clear();
		}

	}

	@Override
	public IKeyBindingService getKeyBindingService(IWorkbenchSite nestedSite) {
		if (disposed) {
			return null;
		}

		if (nestedSite == null) {
			return null;
		}

		IKeyBindingService service = nestedServices.get(nestedSite);
		if (service == null) {
			// TODO the INestedKeyBindingService API should be based on
			// IWorkbenchPartSite..
			if (nestedSite instanceof IWorkbenchPartSite) {
				service = new KeyBindingService((IWorkbenchPartSite) nestedSite, this);
			} else {
				service = new KeyBindingService(null, this);
			}

			nestedServices.put(nestedSite, service);
		}

		return service;
	}

	@Override
	public String[] getScopes() {
		if (disposed) {
			return null;
		}

		// Build the list of active scopes
		final Set<String> activeScopes = new HashSet<>();
		activeScopes.addAll(enabledContextIds);
		if (activeService instanceof KeyBindingService) {
			activeScopes.addAll(((KeyBindingService) activeService).enabledContextIds);
		}

		return activeScopes.toArray(new String[activeScopes.size()]);
	}

	@Override
	public void registerAction(IAction action) {
		if (disposed) {
			return;
		}

		if (action instanceof CommandLegacyActionWrapper) {
			// this is a registration of a fake action for an already
			// registered handler
			WorkbenchPlugin.log("Cannot register a CommandLegacyActionWrapper back into the system"); //$NON-NLS-1$
			return;
		}

		if (action instanceof CommandAction) {
			// we unfortunately had to allow these out into the wild, but they
			// still must not feed back into the system
			return;
		}

		unregisterAction(action);

		String commandId = action.getActionDefinitionId();
		if (commandId != null) {
			for (IAction registeredAction : actionToProxy.keySet()) {
				// we also need to unregister any other action that may have
				// been registered with the same definition id
				if (commandId.equals(registeredAction.getActionDefinitionId())) {
					unregisterAction(registeredAction);
					break;
				}
			}

			IHandlerService hs = workbenchPartSite.getService(IHandlerService.class);
			actionToProxy.put(action, hs.activateHandler(commandId, new ActionHandler(action)));
		}
	}

	@Override
	public boolean removeKeyBindingService(IWorkbenchSite nestedSite) {
		if (disposed) {
			return false;
		}

		final IKeyBindingService service = nestedServices.remove(nestedSite);
		if (service == null) {
			return false;
		}

		if (service.equals(activeService)) {
			deactivateNestedService();
		}

		return true;
	}

	@Override
	public void setScopes(String[] scopes) {
		if (disposed) {
			return;
		}
		Set<String> oldContextIds = enabledContextIds;
		enabledContextIds = new HashSet<>(Arrays.asList(scopes));
		EContextService cs = workbenchPartSite.getService(EContextService.class);
		addParents(cs, scopes);

		for (String id : oldContextIds) {
			if (!enabledContextIds.contains(id)) {
				cs.deactivateContext(id);
			}
		}
		for (String id : enabledContextIds) {
			if (!oldContextIds.contains(id)) {
				cs.activateContext(id);
			}
		}
	}

	/**
	 * @param cs
	 * @param scopes
	 */
	private void addParents(EContextService cs, String[] scopes) {
		for (String id : scopes) {
			try {
				Context current = cs.getContext(id);
				String parentId = current.getParentId();
				while (parentId != null && !enabledContextIds.contains(parentId)) {
					enabledContextIds.add(parentId);
					current = cs.getContext(parentId);
					parentId = current.getParentId();
				}
			} catch (NotDefinedException e) {
				WorkbenchPlugin.log(e);
			}
		}
	}

	@Override
	public void unregisterAction(IAction action) {
		if (disposed) {
			return;
		}

		if (action instanceof CommandLegacyActionWrapper) {
			// this is a registration of a fake action for an already
			// registered handler
			WorkbenchPlugin.log("Cannot unregister a CommandLegacyActionWrapper out of the system"); //$NON-NLS-1$
			return;
		}

		IHandlerActivation activation = actionToProxy.remove(action);
		if (activation == null) {
			return;
		}
		IHandlerService hs = workbenchPartSite.getService(IHandlerService.class);
		hs.deactivateHandler(activation);
	}
}
