/*******************************************************************************
 * Copyright (c) 2006, 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
 *     anton.leherbauer@windriver.com - bug 212389 [CommonNavigator] working set issues:
 *         missing project, window working set inconsistency
 *     Mickael Istria (Red Hat Inc.) - [266030] Allow "others" working set
 *******************************************************************************/

package org.eclipse.ui.internal.navigator.resources.actions;

import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IAggregateWorkingSet;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IWorkbenchPreferenceConstants;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.IWorkingSetManager;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ResourceWorkingSetFilter;
import org.eclipse.ui.actions.WorkingSetFilterActionGroup;
import org.eclipse.ui.internal.navigator.NavigatorFilterService;
import org.eclipse.ui.internal.navigator.resources.plugin.WorkbenchNavigatorMessages;
import org.eclipse.ui.internal.navigator.resources.plugin.WorkbenchNavigatorPlugin;
import org.eclipse.ui.internal.navigator.workingsets.WorkingSetsContentProvider;
import org.eclipse.ui.navigator.CommonActionProvider;
import org.eclipse.ui.navigator.CommonViewer;
import org.eclipse.ui.navigator.ICommonActionExtensionSite;
import org.eclipse.ui.navigator.IExtensionActivationListener;
import org.eclipse.ui.navigator.IExtensionStateModel;
import org.eclipse.ui.navigator.INavigatorContentService;

/**
 * @since 3.2
 *
 */
public class WorkingSetActionProvider extends CommonActionProvider {

	private static final String TAG_CURRENT_WORKING_SET_NAME = "currentWorkingSetName"; //$NON-NLS-1$

	private static final String WORKING_SET_FILTER_ID = "org.eclipse.ui.navigator.resources.filters.workingSet"; //$NON-NLS-1$

	private boolean contributedToViewMenu = false;

	private CommonViewer viewer;

	private INavigatorContentService contentService;

	private NavigatorFilterService filterService;

	private WorkingSetFilterActionGroup workingSetActionGroup;
	private WorkingSetRootModeActionGroup workingSetRootModeActionGroup;

	private Object originalViewerInput = ResourcesPlugin.getWorkspace().getRoot();

	private IExtensionStateModel extensionStateModel;

	private boolean emptyWorkingSet;
	private IWorkingSet workingSet;

	private IPropertyChangeListener topLevelModeListener;

	private boolean ignoreFilterChangeEvents;

	/**
	 * Provides a smart listener to monitor changes to the Working Set Manager.
	 *
	 */
	public class WorkingSetManagerListener implements IPropertyChangeListener {

		private boolean listening = false;

		@Override
		public void propertyChange(PropertyChangeEvent event) {
			String property = event.getProperty();
			Object newValue = event.getNewValue();
			Object oldValue = event.getOldValue();

			String newLabel = null;
			if (IWorkingSetManager.CHANGE_WORKING_SET_REMOVE.equals(property) && oldValue == workingSet) {
				newLabel = ""; //$NON-NLS-1$
				setWorkingSet(null);
			} else if (IWorkingSetManager.CHANGE_WORKING_SET_NAME_CHANGE.equals(property) && newValue == workingSet) {
				newLabel = workingSet.getLabel();
			} else if (IWorkingSetManager.CHANGE_WORKING_SET_CONTENT_CHANGE.equals(property) && newValue == workingSet) {
				if (workingSet.isAggregateWorkingSet() && workingSet.isEmpty()) {
					// act as if the working set has been made null
					if (!emptyWorkingSet) {
						emptyWorkingSet = true;
						setWorkingSetFilter(null);
						newLabel = null;
					}
				} else if (emptyWorkingSet) {
					// we've gone from empty to non-empty on our set.
					// Restore it.
					emptyWorkingSet = false;
					setWorkingSetFilter(workingSet);
					newLabel = workingSet.getLabel();
				}
			}
			if (viewer != null) {
				if (newLabel != null)
					viewer.getCommonNavigator().setWorkingSetLabel(newLabel);
				viewer.getFrameList().reset();
				viewer.refresh();
			}
		}

		/**
		 * Begin listening to the correct source if not already listening.
		 */
		public synchronized void listen() {
			if (!listening) {
				PlatformUI.getWorkbench().getWorkingSetManager().addPropertyChangeListener(managerChangeListener);
				listening = true;
			}
		}

		/**
		 * Begin listening to the correct source if not already listening.
		 */
		public synchronized void ignore() {
			if (listening) {
				PlatformUI.getWorkbench().getWorkingSetManager().removePropertyChangeListener(managerChangeListener);
				listening = false;
			}
		}
	}

	private IPropertyChangeListener filterChangeListener = event -> {

		if (ignoreFilterChangeEvents)
			return;

		IWorkingSet newWorkingSet = (IWorkingSet) event.getNewValue();

		setWorkingSet(newWorkingSet);
		if (newWorkingSet != null) {
			if (!contentService.isActive(WorkingSetsContentProvider.EXTENSION_ID)) {
				contentService.getActivationService()
						.activateExtensions(new String[] { WorkingSetsContentProvider.EXTENSION_ID }, false);
				contentService.getActivationService().persistExtensionActivations();
			}
			if (newWorkingSet.isAggregateWorkingSet()) {
				IAggregateWorkingSet agWs = (IAggregateWorkingSet) newWorkingSet;
				IWorkingSet[] comps = agWs.getComponents();
				if (comps.length > 1) {
					viewer.getCommonNavigator().setWorkingSetLabel(
							WorkbenchNavigatorMessages.WorkingSetActionProvider_multipleWorkingSets);
				} else if (comps.length > 0) {
					viewer.getCommonNavigator().setWorkingSetLabel(comps[0].getLabel());
				} else {
					viewer.getCommonNavigator().setWorkingSetLabel(null);
				}
			} else
				viewer.getCommonNavigator().setWorkingSetLabel(workingSet.getLabel());
		} else {
			viewer.getCommonNavigator().setWorkingSetLabel(null);
		}

		viewer.getFrameList().reset();
	};

	private WorkingSetManagerListener managerChangeListener = new WorkingSetManagerListener();

	private IExtensionActivationListener activationListener = new IExtensionActivationListener() {

		private IWorkingSet savedWorkingSet;

		@Override
		public void onExtensionActivation(String aViewerId, String[] theNavigatorExtensionIds, boolean isActive) {

			for (String theNavigatorExtensionId : theNavigatorExtensionIds) {
				if (WorkingSetsContentProvider.EXTENSION_ID.equals(theNavigatorExtensionId)) {
					if (isActive) {
						extensionStateModel = contentService.findStateModel(WorkingSetsContentProvider.EXTENSION_ID);
						workingSetRootModeActionGroup.setStateModel(extensionStateModel);
						extensionStateModel.addPropertyChangeListener(topLevelModeListener);

						if (savedWorkingSet != null) {
							setWorkingSet(savedWorkingSet);
						}
						managerChangeListener.listen();

					} else {
						savedWorkingSet = workingSet;
						setWorkingSet(null);
						viewer.getCommonNavigator().setWorkingSetLabel(null);
						managerChangeListener.ignore();
						workingSetRootModeActionGroup.setShowTopLevelWorkingSets(false);
						extensionStateModel.removePropertyChangeListener(topLevelModeListener);

					}
				}
			}
		}

	};

	@Override
	public void init(ICommonActionExtensionSite aSite) {
		viewer = (CommonViewer) aSite.getStructuredViewer();
		contentService = aSite.getContentService();
		filterService = (NavigatorFilterService) contentService.getFilterService();

		extensionStateModel = contentService.findStateModel(WorkingSetsContentProvider.EXTENSION_ID);

		workingSetActionGroup = new WorkingSetFilterActionGroup(aSite.getViewSite().getShell(), filterChangeListener);
		workingSetRootModeActionGroup = new WorkingSetRootModeActionGroup(viewer, extensionStateModel);

		topLevelModeListener = event -> {
			setWorkingSet(workingSet);
			viewer.getFrameList().reset();
		};

		if (contentService.isActive(WorkingSetsContentProvider.EXTENSION_ID)) {
			managerChangeListener.listen();
			extensionStateModel.addPropertyChangeListener(topLevelModeListener);
		}

		contentService.getActivationService().addExtensionActivationListener(activationListener);
	}

	/**
	 * Restores the working set filter from the persistence store.
	 */
	protected void initWorkingSetFilter(String workingSetName) {
		IWorkingSet workingSet = null;

		if (workingSetName != null && workingSetName.length() > 0) {
			IWorkingSetManager workingSetManager = PlatformUI.getWorkbench().getWorkingSetManager();
			workingSet = workingSetManager.getWorkingSet(workingSetName);
		} else if (PlatformUI.getPreferenceStore().getBoolean(
				IWorkbenchPreferenceConstants.USE_WINDOW_WORKING_SET_BY_DEFAULT)) {
			// use the window set by default if the global preference is set
			workingSet = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getAggregateWorkingSet();
		}

		if (workingSet != null) {
			setWorkingSet(workingSet);
		}
	}

	private void setWorkingSetFilter(IWorkingSet workingSet) {
		setWorkingSetFilter(workingSet, FIRST_TIME);
	}

	private static final boolean FIRST_TIME = true;

	private void setWorkingSetFilter(IWorkingSet workingSet, boolean firstTime) {
		ResourceWorkingSetFilter workingSetFilter = null;
		ViewerFilter[] filters = viewer.getFilters();
		for (ViewerFilter filter : filters) {
			if (filter instanceof ResourceWorkingSetFilter) {
				workingSetFilter = (ResourceWorkingSetFilter) filter;
				break;
			}
		}
		if (workingSetFilter == null) {
			if (firstTime) {
				filterService.addActiveFilterIds(new String[] { WORKING_SET_FILTER_ID });
				filterService.updateViewer();
				setWorkingSetFilter(workingSet, !FIRST_TIME);
				return;
			}
			WorkbenchNavigatorPlugin.log("Required filter " + WORKING_SET_FILTER_ID +  //$NON-NLS-1$
					" is not present. Working set support will not function correctly.",   //$NON-NLS-1$
				new Status(IStatus.ERROR, WorkbenchNavigatorPlugin.PLUGIN_ID, ""));  //$NON-NLS-1$
			return;
		}
		if (extensionStateModel.getBooleanProperty(WorkingSetsContentProvider.SHOW_TOP_LEVEL_WORKING_SETS)) {
			// a filter would hide the "Others" working set content
			workingSetFilter.setWorkingSet(null);
		} else {
			workingSetFilter.setWorkingSet(emptyWorkingSet ? null : workingSet);
		}
	}

	/**
	 * Set current active working set.
	 *
	 * @param workingSet
	 *            working set to be activated, may be <code>null</code>
	 */
	protected void setWorkingSet(IWorkingSet workingSet) {
		this.workingSet = workingSet;
		emptyWorkingSet = workingSet != null && workingSet.isAggregateWorkingSet() && workingSet.isEmpty();

		ignoreFilterChangeEvents = true;
		try {
			workingSetActionGroup.setWorkingSet(workingSet);
		} finally {
			ignoreFilterChangeEvents = false;
		}

		if (viewer != null) {
			setWorkingSetFilter(workingSet);
			if (workingSet == null || emptyWorkingSet
					|| !extensionStateModel.getBooleanProperty(WorkingSetsContentProvider.SHOW_TOP_LEVEL_WORKING_SETS)) {
				if (viewer.getInput() != originalViewerInput) {
					viewer.setInput(originalViewerInput);
				} else {
					viewer.refresh();
				}
			} else if (!workingSet.isAggregateWorkingSet()) {
				IWorkingSetManager workingSetManager = PlatformUI.getWorkbench().getWorkingSetManager();
				viewer.setInput(workingSetManager.createAggregateWorkingSet(
						"", "", new IWorkingSet[] { workingSet })); //$NON-NLS-1$ //$NON-NLS-2$
			} else {
				viewer.setInput(workingSet);
			}
		}
	}

	@Override
	public void restoreState(final IMemento aMemento) {
		super.restoreState(aMemento);

		// Need to run this async to avoid being reentered when processing a selection change
		viewer.getControl().getShell().getDisplay().asyncExec(() -> {
			boolean showWorkingSets = true;
			if (aMemento != null) {
				Integer showWorkingSetsInt = aMemento
						.getInteger(WorkingSetsContentProvider.SHOW_TOP_LEVEL_WORKING_SETS);
				showWorkingSets = showWorkingSetsInt == null || showWorkingSetsInt.intValue() == 1;
				extensionStateModel.setBooleanProperty(WorkingSetsContentProvider.SHOW_TOP_LEVEL_WORKING_SETS,
						showWorkingSets);
				workingSetRootModeActionGroup.setShowTopLevelWorkingSets(showWorkingSets);

				String lastWorkingSetName = aMemento.getString(TAG_CURRENT_WORKING_SET_NAME);
				initWorkingSetFilter(lastWorkingSetName);
			} else {
				showWorkingSets = false;

				extensionStateModel.setBooleanProperty(WorkingSetsContentProvider.SHOW_TOP_LEVEL_WORKING_SETS,
						showWorkingSets);
				workingSetRootModeActionGroup.setShowTopLevelWorkingSets(showWorkingSets);
			}
		});
	}

	@Override
	public void saveState(IMemento aMemento) {
		super.saveState(aMemento);

		if (aMemento != null) {
			int showWorkingSets = extensionStateModel
					.getBooleanProperty(WorkingSetsContentProvider.SHOW_TOP_LEVEL_WORKING_SETS) ? 1 : 0;
			aMemento.putInteger(WorkingSetsContentProvider.SHOW_TOP_LEVEL_WORKING_SETS, showWorkingSets);

			if (workingSet != null) {
				aMemento.putString(TAG_CURRENT_WORKING_SET_NAME, workingSet.getName());
			}
		}

	}

	@Override
	public void fillActionBars(IActionBars actionBars) {
		if (!contributedToViewMenu) {
			try {
				super.fillActionBars(actionBars);
				workingSetActionGroup.fillActionBars(actionBars);
				if (workingSetRootModeActionGroup != null) {
					workingSetRootModeActionGroup.fillActionBars(actionBars);
				}
			} finally {
				contributedToViewMenu = true;
			}
		}
	}

	@Override
	public void dispose() {
		super.dispose();
		workingSetActionGroup.dispose();
		if (workingSetRootModeActionGroup != null) {
			workingSetRootModeActionGroup.dispose();
		}

		managerChangeListener.ignore();
		extensionStateModel.removePropertyChangeListener(topLevelModeListener);

		contentService.getActivationService().removeExtensionActivationListener(activationListener);
	}

	/**
	 * This is used only for the tests.
	 *
	 * @return a PropertyChangeListener
	 */
	public IPropertyChangeListener getFilterChangeListener() {
		return filterChangeListener;
	}

}
