/*******************************************************************************
 * Copyright (c) 2009, 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
 *     Matthew Hall - initial API and implementation
 *     Lars Vogel <Lars.Vogel@gmail.com> - Bug 440810
 *     Sergey Prigogin (Google)
 *******************************************************************************/
package org.eclipse.ui.databinding;

import org.eclipse.core.databinding.observable.Diffs;
import org.eclipse.core.databinding.observable.IObservable;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.value.AbstractObservableValue;
import org.eclipse.core.databinding.observable.value.ComputedValue;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IAdapterManager;
import org.eclipse.core.runtime.Platform;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.IPageListener;
import org.eclipse.ui.IPartListener2;
import org.eclipse.ui.IPartService;
import org.eclipse.ui.IPropertyListener;
import org.eclipse.ui.ISelectionService;
import org.eclipse.ui.IWindowListener;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPartConstants;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.services.IServiceLocator;

/**
 * Factory methods for creating observables for Workbench objects
 *
 * @since 3.5
 */
public class WorkbenchObservables {
	/**
	 * Returns an observable with values of the given target type. If the
	 * wrapped observable's value is of the target type, or can be adapted to
	 * the target type, this is taken as the value of the returned observable,
	 * otherwise <code>null</code>.
	 *
	 * @param master
	 *            the observable whose value should be adapted
	 * @param adapter
	 *            the target type
	 * @return an observable with values of the given type, or <code>null</code>
	 *         if the current value of the given observable does not adapt to
	 *         the target type
	 */
	public static <T> IObservableValue<T> observeDetailAdaptedValue(IObservableValue<?> master, Class<T> adapter) {
		return observeDetailAdaptedValue(master, adapter, Platform
				.getAdapterManager());
	}

	/**
	 * Returns an observable with values of the given target type. If the
	 * wrapped observable's value is of the target type, or can be adapted to
	 * the target type, this is taken as the value of the returned observable,
	 * otherwise <code>null</code>.
	 *
	 * @param master
	 *            the observable whose value should be adapted
	 * @param adapter
	 *            the target type
	 * @param adapterManager
	 *            the adapter manager used to adapt the master value
	 * @return an observable with values of the given type, or <code>null</code>
	 *         if the current value of the given observable does not adapt to
	 *         the target type
	 */
	static <T> IObservableValue<T> observeDetailAdaptedValue(IObservableValue<?> master, Class<T> adapter,
			IAdapterManager adapterManager) {
		return WorkbenchProperties.adaptedValue(adapter, adapterManager)
				.observeDetail(master);
	}

	/**
	 * Returns an observable value that tracks the post selection of a selection
	 * service obtained through the given service locator, and adapts the first
	 * element of that selection to the given target type.
	 * <p>
	 * This method can be used by view or editor implementers to tie into the
	 * selection service, for example as follows:
	 *
	 * <pre>
	 * IObservableValue&lt;IResource&gt; selection = WorkbenchObservables.observeAdaptedSingleSelection(getSite(),
	 * 		IResource.class);
	 * </pre>
	 *
	 * </p>
	 *
	 * @param locator
	 *            a service locator with an available {@link ISelectionService}
	 * @param targetType
	 *            the target type
	 * @return an observable value whose value type is the given target type
	 */
	public static <T> IObservableValue<T> observeAdaptedSingleSelection(IServiceLocator locator, Class<T> targetType) {
		ISelectionService selectionService = locator.getService(ISelectionService.class);
		Assert.isNotNull(selectionService);
		return WorkbenchProperties.singleSelection(null, true).value(
				WorkbenchProperties.adaptedValue(targetType)).observe(
				selectionService);
	}

	/**
	 * Returns an observable value that tracks the active workbench window for
	 * the given workbench.
	 *
	 * @param workbench
	 *            the workbench to get the observable for
	 * @return an observable value that tracks the active workbench window
	 * @since 3.110
	 */
	public static IObservableValue<IWorkbenchWindow> observeActiveWorkbenchWindow(IWorkbench workbench) {
		Assert.isNotNull(workbench);
		return new ListeningValue<IWorkbenchWindow>() {
			private final IWindowListener listener = new IWindowListener() {
				@Override
				public void windowActivated(IWorkbenchWindow window) {
					protectedSetValue(window);
				}

				@Override
				public void windowDeactivated(IWorkbenchWindow window) {
					if (window == doGetValue()) {
						protectedSetValue(null);
					}
				}

				@Override
				public void windowClosed(IWorkbenchWindow window) {
				}

				@Override
				public void windowOpened(IWorkbenchWindow window) {
				}
			};

			@Override
			protected void startListening() {
				workbench.addWindowListener(listener);
			}

			@Override
			protected void stopListening() {
				workbench.removeWindowListener(listener);
			}

			@Override
			protected IWorkbenchWindow calculate() {
				return workbench.getActiveWorkbenchWindow();
			}
		};
	}

	/**
	 * Returns an observable value that tracks the active workbench page for the
	 * given workbench window.
	 *
	 * @param window
	 *            the workbench window to get the observable for
	 * @return an observable value that tracks the active workbench page
	 * @since 3.110
	 */
	public static IObservableValue<IWorkbenchPage> observeActiveWorkbenchPage(IWorkbenchWindow window) {
		Assert.isNotNull(window);
		return new ListeningValue<IWorkbenchPage>() {
			private final IPageListener listener = new IPageListener() {
				@Override
				public void pageActivated(IWorkbenchPage page) {
					protectedSetValue(page);
				}

				@Override
				public void pageClosed(IWorkbenchPage page) {
					if (page == doGetValue()) {
						protectedSetValue(null);
					}
				}

				@Override
				public void pageOpened(IWorkbenchPage page) {
				}
			};

			@Override
			protected void startListening() {
				window.addPageListener(listener);
			}

			@Override
			protected void stopListening() {
				window.removePageListener(listener);
			}

			@Override
			protected IWorkbenchPage calculate() {
				return window.getActivePage();
			}
		};
	}

	/**
	 * Returns an observable value that tracks the active workbench part for the
	 * given part service.
	 *
	 * @param partService
	 *            the part service to get the observable for, e.g. a workbench
	 *            page
	 * @return an observable value that tracks the active workbench part
	 * @since 3.110
	 */
	public static IObservableValue<IWorkbenchPartReference> observeActivePart(IPartService partService) {
		Assert.isNotNull(partService);
		return new ListeningValue<IWorkbenchPartReference>() {
			private final IPartListener2 listener = new IPartListener2() {
				@Override
				public void partActivated(IWorkbenchPartReference partRef) {
					protectedSetValue(partRef);
				}

				@Override
				public void partDeactivated(IWorkbenchPartReference partRef) {
					if (partRef == doGetValue()) {
						protectedSetValue(null);
					}
				}

				@Override
				public void partBroughtToTop(IWorkbenchPartReference partRef) {
				}

				@Override
				public void partClosed(IWorkbenchPartReference partRef) {
				}

				@Override
				public void partOpened(IWorkbenchPartReference partRef) {
				}

				@Override
				public void partHidden(IWorkbenchPartReference partRef) {
				}

				@Override
				public void partVisible(IWorkbenchPartReference partRef) {
				}

				@Override
				public void partInputChanged(IWorkbenchPartReference partRef) {
				}
			};

			@Override
			protected void startListening() {
				partService.addPartListener(listener);
			}

			@Override
			protected void stopListening() {
				partService.removePartListener(listener);
			}

			@Override
			protected IWorkbenchPartReference calculate() {
				return partService.getActivePartReference();
			}
		};
	}

	/**
	 * Returns an observable value that tracks the active editor for the given
	 * part service.
	 *
	 * @param partService
	 *            the part service to get the observable for, e.g. a workbench
	 *            page
	 * @return an observable value that tracks the active editor
	 * @since 3.110
	 */
	public static IObservableValue<IEditorReference> observeActiveEditor(IPartService partService) {
		final IObservableValue<IWorkbenchPartReference> partObservable = observeActivePart(partService);
		return ComputedValue.create(() -> {
			IWorkbenchPartReference value = partObservable.getValue();
			return value instanceof IEditorReference ? (IEditorReference) value : null;
		});
	}

	/**
	 * Returns an observable value that tracks the editor input for the given
	 * editor.
	 *
	 * @param editor
	 *            the editor to get the observable for
	 * @return an observable value that tracks the editor input
	 * @since 3.110
	 */
	public static IObservableValue<IEditorInput> observeEditorInput(IEditorPart editor) {
		Assert.isNotNull(editor);
		return new ListeningValue<IEditorInput>() {
			private final IPropertyListener listener = new IPropertyListener() {
				@Override
				public void propertyChanged(Object source, int propId) {
					if (propId == IWorkbenchPartConstants.PROP_INPUT) {
						protectedSetValue(editor.getEditorInput());
					}
				}
			};

			@Override
			protected void startListening() {
				editor.addPropertyListener(listener);
			}

			@Override
			protected void stopListening() {
				editor.removePropertyListener(listener);
			}

			@Override
			protected IEditorInput calculate() {
				return editor.getEditorInput();
			}
		};
	}

	/**
	 * A base class for creating observable values that track the state of a
	 * non-{@link IObservable} objects.
	 */
	private abstract static class ListeningValue<T> extends AbstractObservableValue<T> {
		private T value;
		private boolean isListening;
		private volatile boolean hasListeners;

		@Override
		protected final T doGetValue() {
			// The value is not kept up to date when we are not listening.
			if (isListening) {
				return value;
			}
			return calculate();
		}

		/**
		 * Sets the value. Must be invoked in the {@link Realm} of the
		 * observable. Subclasses must call this method instead of
		 * {@link #setValue} or {@link #doSetValue}.
		 *
		 * @param value
		 *            the value to set
		 */
		protected final void protectedSetValue(T value) {
			checkRealm();
			if (!isListening)
				throw new IllegalStateException();
			if (this.value != value) {
				fireValueChange(Diffs.createValueDiff(this.value, this.value = value));
			}
		}

		@Override
		protected final void firstListenerAdded() {
			if (getRealm().isCurrent()) {
				startListeningInternal();
			} else {
				getRealm().asyncExec(() -> {
					if (hasListeners && !isListening) {
						startListeningInternal();
					}
				});
			}
			hasListeners = true;
			super.firstListenerAdded();
		}

		@Override
		protected final void lastListenerRemoved() {
			if (getRealm().isCurrent()) {
				stopListeningInternal();
			} else {
				getRealm().asyncExec(() -> {
					if (!hasListeners && isListening) {
						stopListeningInternal();
					}
				});
			}
			hasListeners = false;
			super.lastListenerRemoved();
		}

		private void startListeningInternal() {
			isListening = true;
			value = calculate();
			startListening();
		}

		private void stopListeningInternal() {
			isListening = false;
			value = null;
			stopListening();
		}

		/**
		 * Subclasses must override this method to attach listeners to the
		 * non-{@link IObservable} objects the state of which is tracked by this
		 * observable.
		 */
		protected abstract void startListening();

		/**
		 * Subclasses must override this method to detach listeners from the
		 * non-{@link IObservable} objects the state of which is tracked by this
		 * observable.
		 */
		protected abstract void stopListening();

		/**
		 * Subclasses must override this method to provide the object's value
		 * that will be used when the value is not set explicitly by
		 * {@link #doSetValue(Object)}.
		 *
		 * @return the object's value
		 */
		protected abstract T calculate();

		@Override
		public Object getValueType() {
			return null;
		}
	}
}
