/*******************************************************************************
 * Copyright (c) 2009, 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
 *     Oliver Puetter - Bug 423040
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 472654
 ******************************************************************************/
package org.eclipse.e4.ui.internal.workbench;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.contexts.RunAndTrack;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.core.internal.contexts.EclipseContext;
import org.eclipse.e4.core.services.events.IEventBroker;
import org.eclipse.e4.core.services.log.Logger;
import org.eclipse.e4.ui.model.application.ui.basic.MPart;
import org.eclipse.e4.ui.services.IServiceConstants;
import org.eclipse.e4.ui.workbench.UIEvents;
import org.eclipse.e4.ui.workbench.modeling.EPartService;
import org.eclipse.e4.ui.workbench.modeling.ISelectionListener;
import org.osgi.service.event.EventHandler;

public class SelectionAggregator {

	static final String OUT_SELECTION = "org.eclipse.ui.output.selection"; //$NON-NLS-1$
	static final String OUT_POST_SELECTION = "org.eclipse.ui.output.postSelection"; //$NON-NLS-1$

	private ListenerList<ISelectionListener> genericListeners = new ListenerList<>();
	private ListenerList<ISelectionListener> genericPostListeners = new ListenerList<>();
	private Map<String, ListenerList<ISelectionListener>> targetedListeners = new HashMap<>();
	private Map<String, ListenerList<ISelectionListener>> targetedPostListeners = new HashMap<>();
	private Set<IEclipseContext> tracked = new HashSet<>();

	private EventHandler eventHandler = event -> {
		Object element = event.getProperty(UIEvents.EventTags.ELEMENT);
		if (element instanceof MPart) {
			MPart part = (MPart) element;

			String partId = part.getElementId();
			if (targetedListeners.containsKey(partId) || targetedPostListeners.containsKey(partId))
				track(part);
		}
	};

	private MPart activePart;

	private IEclipseContext context;

	private EPartService partService;

	private IEventBroker eventBroker;

	private Logger logger;

	@Inject
	SelectionAggregator(IEclipseContext context, EPartService partService,
			IEventBroker eventBroker, Logger logger) {
		super();
		this.context = context;
		this.partService = partService;
		this.eventBroker = eventBroker;
		this.logger = logger;
	}

	@PreDestroy
	void preDestroy() {
		genericListeners.clear();
		genericPostListeners.clear();
		targetedListeners.clear();
		targetedPostListeners.clear();

		eventBroker.unsubscribe(eventHandler);
	}

	@PostConstruct
	void postConstruct() {
		eventBroker.subscribe(UIEvents.Context.TOPIC_CONTEXT, eventHandler);
	}

	@Inject
	void setPart(@Optional @Named(IServiceConstants.ACTIVE_PART) final MPart part) {
		if (part == null) {
			activePart = null;
			context.set(IServiceConstants.ACTIVE_SELECTION, null);
		} else if (activePart != part) {
			activePart = part;
			IEclipseContext partContext = part.getContext();
			if (partContext.containsKey(OUT_POST_SELECTION)) {
				Object selection = partContext.get(OUT_POST_SELECTION);
				context.set(IServiceConstants.ACTIVE_SELECTION, selection);
			} else if (partContext.containsKey(OUT_SELECTION)) {
				Object selection = partContext.get(OUT_SELECTION);
				context.set(IServiceConstants.ACTIVE_SELECTION, selection);
			}
			track(part);
		}
	}

	private void notifyListeners(final MPart part, final Object selection) {
		for (final ISelectionListener myListener : genericListeners) {
			SafeRunner.run(new ISafeRunnable() {
				@Override
				public void run() throws Exception {
					myListener.selectionChanged(part, selection);
				}

				@Override
				public void handleException(Throwable exception) {
					logger.error(exception);
				}
			});
		}
		notifyTargetedListeners(part, selection);
	}

	private void notifyTargetedListeners(final MPart part, final Object selection) {
		String id = part.getElementId();
		if (id != null) {
			ListenerList<ISelectionListener> listenerList = targetedListeners.get(id);
			if (listenerList != null) {
				for (final ISelectionListener listener : listenerList) {
					final ISelectionListener myListener = listener;
					SafeRunner.run(new ISafeRunnable() {
						@Override
						public void run() throws Exception {
							myListener.selectionChanged(part, selection);
						}

						@Override
						public void handleException(Throwable exception) {
							logger.error(exception);
						}
					});
				}
			}
		}
	}

	private void notifyPostListeners(final MPart part, final Object selection) {
		for (final ISelectionListener myListener : genericPostListeners) {
			SafeRunner.run(new ISafeRunnable() {
				@Override
				public void run() throws Exception {
					myListener.selectionChanged(part, selection);
				}

				@Override
				public void handleException(Throwable exception) {
					logger.error(exception);
				}
			});
		}
		notifyTargetedPostListeners(part, selection);
	}

	private void notifyTargetedPostListeners(final MPart part, final Object selection) {
		String id = part.getElementId();
		if (id != null) {
			ListenerList<ISelectionListener> listenerList = targetedPostListeners.get(id);
			if (listenerList != null) {
				for (final ISelectionListener myListener : listenerList) {
					SafeRunner.run(new ISafeRunnable() {
						@Override
						public void run() throws Exception {
							myListener.selectionChanged(part, selection);
						}

						@Override
						public void handleException(Throwable exception) {
							logger.error(exception);
						}
					});
				}
			}
		}
	}

	private void track(final MPart part) {
		final IEclipseContext myContext = this.context;
		IEclipseContext context = part.getContext();
		if (context != null && tracked.add(context)) {
			if (context instanceof EclipseContext) {
				((EclipseContext) context).notifyOnDisposal(context1 -> tracked.remove(context1));
			}

			context.runAndTrack(new RunAndTrack() {
				private boolean initial = true;

				@Override
				public boolean changed(IEclipseContext context) {
					final Object selection = context.get(OUT_SELECTION);
					if (initial) {
						initial = false;
						if (selection == null) {
							return true;
						}
					}

					if (activePart == part) {
						myContext.set(IServiceConstants.ACTIVE_SELECTION, selection);
						runExternalCode(() -> notifyListeners(part, selection));
					} else {
						runExternalCode(() -> notifyTargetedListeners(part, selection));
						// we don't need to keep tracking non-active parts unless
						// they have targeted listeners
						String partId = part.getElementId();
						boolean continueTracking = targetedListeners.containsKey(partId)
								|| targetedPostListeners.containsKey(partId);
						if (!continueTracking) {
							tracked.remove(part.getContext());
						}
						return continueTracking;
					}
					return true;
				}
			});
			context.runAndTrack(new RunAndTrack() {
				private boolean initial = true;

				@Override
				public boolean changed(IEclipseContext context) {
					final Object postSelection = context.get(OUT_POST_SELECTION);
					if (initial) {
						initial = false;
						if (postSelection == null) {
							return true;
						}
					}

					if (activePart == part) {
						runExternalCode(() -> notifyPostListeners(part, postSelection));
					} else {
						runExternalCode(() -> notifyTargetedPostListeners(part, postSelection));
						// we don't need to keep tracking non-active parts unless
						// they have targeted listeners
						String partId = part.getElementId();
						boolean continueTracking = targetedListeners.containsKey(partId)
								|| targetedPostListeners.containsKey(partId);
						if (!continueTracking) {
							tracked.remove(part.getContext());
						}
						return continueTracking;
					}
					return true;
				}
			});
		}
	}

	public Object getSelection() {
		return context.get(IServiceConstants.ACTIVE_SELECTION);
	}

	public void addSelectionListener(ISelectionListener listener) {
		genericListeners.add(listener);
	}

	public void addPostSelectionListener(ISelectionListener listener) {
		genericPostListeners.add(listener);
	}

	public void removeSelectionListener(ISelectionListener listener) {
		// we may have been destroyed already, see bug 310113
		if (context != null) {
			genericListeners.remove(listener);
		}
	}

	public void removePostSelectionListener(ISelectionListener listener) {
		// we may have been destroyed already, see bug 310113
		if (context != null) {
			genericPostListeners.remove(listener);
		}
	}

	public void addSelectionListener(String partId, ISelectionListener listener) {
		ListenerList<ISelectionListener> listeners = targetedListeners.get(partId);
		if (listeners == null) {
			listeners = new ListenerList<>();
			targetedListeners.put(partId, listeners);
		}
		listeners.add(listener);

		MPart part = partService.findPart(partId);
		if (part != null)
			track(part);
	}

	public void addPostSelectionListener(String partId, ISelectionListener listener) {
		ListenerList<ISelectionListener> listeners = targetedPostListeners.get(partId);
		if (listeners == null) {
			listeners = new ListenerList<>();
			targetedPostListeners.put(partId, listeners);
		}
		listeners.add(listener);

		MPart part = partService.findPart(partId);
		if (part != null)
			track(part);
	}

	public void removeSelectionListener(String partId, ISelectionListener listener) {
		// we may have been destroyed already, see bug 310113
		if (context != null) {
			ListenerList<ISelectionListener> listeners = targetedListeners.get(partId);
			if (listeners != null) {
				listeners.remove(listener);
			}
		}
	}

	public void removePostSelectionListener(String partId, ISelectionListener listener) {
		// we may have been destroyed already, see bug 310113
		if (context != null) {
			ListenerList<ISelectionListener> listeners = targetedPostListeners.get(partId);
			if (listeners != null) {
				listeners.remove(listener);
			}
		}
	}

	public Object getSelection(String partId) {
		MPart part = partService.findPart(partId);
		if (part == null) {
			return null;
		}

		IEclipseContext partContext = part.getContext();
		if (partContext == null) {
			return null;
		}
		return partContext.get(OUT_SELECTION);
	}

}
