/*******************************************************************************
 * Copyright (c) 2004, 2010 Tasktop Technologies and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Tasktop Technologies - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.context.ui;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.context.core.AbstractContextListener;
import org.eclipse.mylyn.context.core.ContextChangeEvent;
import org.eclipse.mylyn.context.core.ContextCore;
import org.eclipse.mylyn.internal.context.core.ContextCorePlugin;
import org.eclipse.mylyn.internal.context.ui.ContextUiPlugin;
import org.eclipse.mylyn.internal.context.ui.Messages;
import org.eclipse.mylyn.internal.provisional.commons.ui.CommonImages;
import org.eclipse.mylyn.monitor.ui.MonitorUi;
import org.eclipse.mylyn.tasks.ui.TasksUiImages;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.IActionDelegate2;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IViewActionDelegate;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindowActionDelegate;
import org.eclipse.ui.PlatformUI;

/**
 * Extending this class makes it possible to apply Mylyn management to a structured view (e.g. to provide interest-based
 * filtering).
 * 
 * @author Mik Kersten
 * @since 2.0
 */
public abstract class AbstractFocusViewAction extends Action implements IViewActionDelegate, IActionDelegate2,
		ISelectionListener {

	public static final String PREF_ID_PREFIX = "org.eclipse.mylyn.ui.interest.filter."; //$NON-NLS-1$

	private static Map<IViewPart, AbstractFocusViewAction> partMap = new WeakHashMap<IViewPart, AbstractFocusViewAction>();

	protected String globalPrefId;

	protected IAction initAction = null;

	protected final InterestFilter interestFilter;

	protected IViewPart viewPart;

	protected Map<StructuredViewer, List<ViewerFilter>> previousFilters = new WeakHashMap<StructuredViewer, List<ViewerFilter>>();

	private final boolean manageViewer;

	private final boolean manageFilters;

	private final boolean manageLinking;

	private boolean wasLinkingEnabled = false;

	private boolean wasRun = false;

	private Set<String> cachedPreservedFilters;

	/**
	 * Work-around for suppressing expansion without breaking API. Will be remove post 3.0
	 */
	@Deprecated
	protected boolean internalSuppressExpandAll = false;

	/**
	 * @since 3.0
	 */
	protected boolean showEmptyViewMessage = false;

	private final Map<StructuredViewer, EmptyContextDrawer> viewerToDrawerMap = new HashMap<StructuredViewer, EmptyContextDrawer>();

	private class EmptyContextDrawer implements Listener {

		private final String LABEL = getEmptyViewMessage();

		private final Image IMAGE = CommonImages.getImage(TasksUiImages.CONTEXT_FOCUS);

		private final Tree tree;

		EmptyContextDrawer(Tree tree) {
			this.tree = tree;
		}

		public void handleEvent(Event event) {
			if (tree != null && tree.getItemCount() == 0) {
				switch (event.type) {
				case SWT.Paint: {
					int offset = 7;
					event.gc.drawImage(IMAGE, offset, offset);
					event.gc.drawText(LABEL, offset + IMAGE.getBounds().width + 5, offset);
					break;
				}
				}
			}
		}
	}

	private final AbstractContextListener CONTEXT_LISTENER = new AbstractContextListener() {

		@Override
		public void contextChanged(ContextChangeEvent event) {
			switch (event.getEventKind()) {
			case ACTIVATED:
				if (updateEnablementWithContextActivation()) {
					updateEnablement(initAction);
				}
				break;
			case DEACTIVATED:
				if (updateEnablementWithContextActivation()) {
					updateEnablement(initAction);
					update(false);
				}
				break;
			}
		};
	};

	private final IWorkbenchListener WORKBENCH_LISTENER = new IWorkbenchListener() {

		public boolean preShutdown(IWorkbench workbench, boolean forced) {
			// restore the viewers' previous state
			if (wasRun && manageLinking) {
				setDefaultLinkingEnabled(wasLinkingEnabled);
			}

			List<StructuredViewer> viewers = getViewers();
			Set<String> excludedFilters = getPreservedFilterClasses(false);
			for (StructuredViewer viewer : viewers) {
				if (previousFilters.containsKey(viewer)) {
					for (ViewerFilter filter : previousFilters.get(viewer)) {
						if (!excludedFilters.contains(filter.getClass().getName())) {
							try {
								viewer.addFilter(filter);
							} catch (Throwable t) {
								StatusHandler.log(new Status(IStatus.ERROR, ContextUiPlugin.ID_PLUGIN,
										"Failed to restore filter: " + filter, t)); //$NON-NLS-1$
							}
						}
					}
					previousFilters.remove(viewer);
				}
			}
			return true;
		}

		public void postShutdown(IWorkbench workbench) {
			// ignore
		}
	};

	/**
	 * TODO: not thread safe.
	 */
	public static AbstractFocusViewAction getActionForPart(IViewPart part) {
		return partMap.get(part);
	}

	public IViewPart getPartForAction() {
		if (viewPart == null) {
			if (this instanceof IWorkbenchWindowActionDelegate) {
				if (!PlatformUI.getWorkbench().isClosing()) {
					// ignore, could be called from the Configure Perspective dialog, see bug 2308532
				}
			} else {
				throw new RuntimeException("error: viewPart is null"); //$NON-NLS-1$
			}
		}
		return viewPart;
	}

	public AbstractFocusViewAction(InterestFilter interestFilter, boolean manageViewer, boolean manageFilters,
			boolean manageLinking) {
		super();
		this.interestFilter = interestFilter;
		this.manageViewer = manageViewer;
		this.manageFilters = manageFilters;
		this.manageLinking = manageLinking;
		setText(Messages.AbstractFocusViewAction_Apply_Mylyn);
		setToolTipText(Messages.AbstractFocusViewAction_Apply_Mylyn);
		setImageDescriptor(TasksUiImages.CONTEXT_FOCUS);
		PlatformUI.getWorkbench().addWorkbenchListener(WORKBENCH_LISTENER);
		ContextCore.getContextManager().addListener(CONTEXT_LISTENER);
	}

	public void dispose() {
		partMap.remove(getPartForAction());
		if (viewPart != null && !PlatformUI.getWorkbench().isClosing()) {
			for (StructuredViewer viewer : getViewers()) {
				ContextUiPlugin.getViewerManager().removeManagedViewer(viewer, viewPart);
			}
		}

		MonitorUi.removeWindowPostSelectionListener(this);
		ContextCore.getContextManager().removeListener(CONTEXT_LISTENER);
		PlatformUI.getWorkbench().removeWorkbenchListener(WORKBENCH_LISTENER);
	}

	public void init(IAction action) {
		initAction = action;
		initAction.setChecked(action.isChecked());
	}

	public void init(IViewPart view) {
		String id = view.getSite().getId();
		globalPrefId = PREF_ID_PREFIX + id;
		viewPart = view;
		partMap.put(view, this);
		wasLinkingEnabled = isDefaultLinkingEnabled();
	}

	protected boolean updateEnablementWithContextActivation() {
		return true;
	}

	public void run(IAction action) {
		setChecked(action.isChecked());
		valueChanged(action, action.isChecked(), true);
		wasRun = true;
	}

	public void runWithEvent(IAction action, Event event) {
		run(action);
	}

	/**
	 * Don't update if the preference has not been initialized.
	 */
	public void update() {
		if (globalPrefId != null) {
			update(ContextUiPlugin.getDefault().getPreferenceStore().getBoolean(globalPrefId));
		}
	}

	/**
	 * This operation is expensive.
	 */
	public void update(boolean on) {
		valueChanged(initAction, on, false);
		updateEnablement(initAction);
	}

	protected void valueChanged(IAction action, final boolean on, boolean store) {
		if (PlatformUI.getWorkbench().isClosing()) {
			return;
		}

		boolean wasPaused = ContextCore.getContextManager().isContextCapturePaused();
		try {
			if (!wasPaused) {
				ContextCore.getContextManager().setContextCapturePaused(true);
			}
			setChecked(on);
			if (action != null) {
				action.setChecked(on);
			}
			if (store && ContextCorePlugin.getDefault() != null) {
				ContextUiPlugin.getDefault().getPreferenceStore().setValue(globalPrefId, on);
			}

			List<StructuredViewer> viewers = getViewers();
			for (StructuredViewer viewer : viewers) {
				if (viewPart != null && !viewer.getControl().isDisposed() && manageViewer) {
					ContextUiPlugin.getViewerManager().addManagedViewer(viewer, viewPart);
				}
				updateInterestFilter(on, viewer);
			}

			setManualFilteringAndLinkingEnabled(!on);
			if (manageLinking) {
				updateLinking(on);
			}
		} catch (Throwable t) {
			StatusHandler.log(new Status(IStatus.ERROR, ContextUiPlugin.ID_PLUGIN,
					"Could not install viewer manager on: " + globalPrefId, t)); //$NON-NLS-1$
		} finally {
			if (!wasPaused) {
				ContextCore.getContextManager().setContextCapturePaused(false);
			}
		}
	}

	protected void updateEnablement(IAction action) {
		if (updateEnablementWithContextActivation()) {
			if (action != null) {
				action.setEnabled(ContextCore.getContextManager().isContextActivePropertySet());
			}
		}
	}

	private void updateLinking(boolean on) {
		if (on) {
			wasLinkingEnabled = isDefaultLinkingEnabled();
			MonitorUi.addWindowPostSelectionListener(this);
		} else {
			MonitorUi.removeWindowPostSelectionListener(this);
			setDefaultLinkingEnabled(wasLinkingEnabled);
		}
	}

	public void selectionChanged(IWorkbenchPart part, ISelection selection) {
		if (manageLinking && selection instanceof ITextSelection && part instanceof IEditorPart) {
			try {
				List<StructuredViewer> viewers = getViewers();
				if (viewers.size() == 1) {
					StructuredViewer viewer = getViewers().get(0);
					ITextSelection textSelection = (ITextSelection) selection;
					ISelection toSelect = resolveSelection((IEditorPart) part, textSelection, viewer);
					if (toSelect != null) {
						ISelection currentSelection = viewer.getSelection();
						if (!toSelect.equals(currentSelection)) {
							select(viewer, toSelect);
						}
					}
					// NOTE: if we could make the code below work we could set the selection the first time the elment is shown
//					boolean canSelectElement = true;
//					if (viewer instanceof TreeViewer) {
//						TreeViewer treeViewer = (TreeViewer) viewer;
//						Object[] expanded = treeViewer.getExpandedElements();
//						canSelectElement = false;
//						if (Arrays.asList(expanded).contains(((StructuredSelection) toSelect).getFirstElement())) {
//							canSelectElement = true;
//						}
//					}
				}
			} catch (Throwable t) {
				// ignore, linking failure is not fatal
			}
		}
	}

	protected void select(StructuredViewer viewer, ISelection selection) {
		viewer.setSelection(selection, true);
	}

	/**
	 * Override to provide managed linking
	 */
	protected ISelection resolveSelection(IEditorPart part, ITextSelection selection, StructuredViewer viewer)
			throws CoreException {
		return null;
	}

	/**
	 * Override to provide managed linking
	 */
	protected void setDefaultLinkingEnabled(boolean on) {
		// ignore
	}

	/**
	 * Override to provide managed linking
	 */
	protected boolean isDefaultLinkingEnabled() {
		return false;
	}

	protected void setManualFilteringAndLinkingEnabled(boolean on) {
		// ignore
	}

	public void selectionChanged(IAction action, ISelection selection) {
		updateEnablement(action);
	}

	/**
	 * Public for testing
	 */
	public void updateInterestFilter(final boolean on, StructuredViewer viewer) {
		if (viewer != null) {
			if (on) {
				if (showEmptyViewMessage && viewer instanceof TreeViewer) {
					Tree tree = ((TreeViewer) viewer).getTree();
					Listener drawingListener = viewerToDrawerMap.get(viewer);
					if (drawingListener == null) {
						EmptyContextDrawer drawer = new EmptyContextDrawer(tree);
						viewerToDrawerMap.put(viewer, drawer);
						tree.addListener(SWT.Paint, drawer);
					}
				}

				installInterestFilter(viewer);
				ContextUiPlugin.getViewerManager().addFilteredViewer(viewer, this);
			} else {
				if (showEmptyViewMessage && viewer instanceof TreeViewer) {
					Tree tree = ((TreeViewer) viewer).getTree();
					EmptyContextDrawer drawer = viewerToDrawerMap.remove(viewer);
					if (drawer != null) {
						tree.removeListener(SWT.Paint, drawer);
					}
				}
				ContextUiPlugin.getViewerManager().removeFilteredViewer(viewer);
				uninstallInterestFilter(viewer);
			}
		}
	}

	/**
	 * Public for testing
	 */
	public abstract List<StructuredViewer> getViewers();

	/**
	 * @return filters that should not be removed when the interest filter is installed
	 */
	private Set<String> getPreservedFilterClasses(boolean cacheFilters) {
		if (ContextUiPlugin.getDefault() == null || viewPart == null) {
			return Collections.emptySet();
		}
		if (cachedPreservedFilters == null && cacheFilters) {
			try {
				cachedPreservedFilters = ContextUiPlugin.getDefault().getPreservedFilterClasses(
						viewPart.getSite().getId());
			} catch (Exception e) {
				StatusHandler.log(new Status(IStatus.ERROR, ContextUiPlugin.ID_PLUGIN,
						"Could not determine preserved filters", e)); //$NON-NLS-1$
			}
		}

		if (cachedPreservedFilters != null) {
			return cachedPreservedFilters;
		} else {
			// fall back for if the preserved filters have never been cached or there was a problem getting them from context core
			return Collections.emptySet();
		}
	}

	protected boolean installInterestFilter(StructuredViewer viewer) {
		if (viewer == null) {
			// FIXME Assert.isNotNull(viewer)
			StatusHandler.log(new Status(IStatus.ERROR, ContextUiPlugin.ID_PLUGIN,
					"The viewer to install interest filter is null", new Exception())); //$NON-NLS-1$
			return false;
		} else if (viewer.getControl().isDisposed() && manageViewer) {
			// TODO: do this with part listener, not lazily?
			return false;
		} else if (previousFilters.containsKey(viewer) && hasInterestFilter(viewer)) {
			// install has already run, this can happen if AbstractAutoFocusViewAction.init() executes
			// initialization asynchronously
			return false;
		}

		try {
			viewer.getControl().setRedraw(false);
			previousFilters.put(viewer, Arrays.asList(viewer.getFilters()));

			if (viewPart != null && manageFilters) {
				Set<ViewerFilter> toAdd = new HashSet<ViewerFilter>();
				Set<String> preservedFilterClasses = getPreservedFilterClasses(true);

				for (ViewerFilter filter : previousFilters.get(viewer)) {
					if (preservedFilterClasses.contains(filter.getClass().getName()) || isPreservedFilter(filter)) {
						toAdd.add(filter);
					}
				}

				toAdd.add(interestFilter);
				viewer.setFilters(toAdd.toArray(new ViewerFilter[toAdd.size()]));
			} else {
				viewer.addFilter(interestFilter);
			}

			if (viewer instanceof TreeViewer && !internalSuppressExpandAll) {
				((TreeViewer) viewer).expandAll();
			}
			return true;
		} catch (Throwable t) {
			StatusHandler.log(new Status(IStatus.ERROR, ContextUiPlugin.ID_PLUGIN,
					"Could not install viewer filter on: " + globalPrefId, t)); //$NON-NLS-1$
		} finally {
			viewer.getControl().setRedraw(true);
			internalSuppressExpandAll = false;
		}
		return false;
	}

	private boolean hasInterestFilter(StructuredViewer viewer) {
		for (ViewerFilter filter : viewer.getFilters()) {
			if (filter == getInterestFilter()) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Subclasses can provide additional viewer filters that should not be removed when focusing.
	 * 
	 * @since 3.1
	 */
	protected boolean isPreservedFilter(ViewerFilter filter) {
		return false;
	}

	protected void uninstallInterestFilter(StructuredViewer viewer) {
		if (viewer == null) {
			// FIXME Assert.isNotNull(viewer)
			StatusHandler.log(new Status(IStatus.ERROR, ContextUiPlugin.ID_PLUGIN,
					"The viewer to uninstall interest filter is null", new Exception())); //$NON-NLS-1$
			return;
		} else if (viewer.getControl().isDisposed()) {
			// TODO: do this with part listener, not lazily?
			ContextUiPlugin.getViewerManager().removeManagedViewer(viewer, viewPart);
			return;
		}

		try {
			viewer.getControl().setRedraw(false);

			List<ViewerFilter> restoreFilters = previousFilters.remove(viewer);
			if (restoreFilters != null && viewPart != null && manageFilters) {
				// install all previous filters and all current filters
				Set<ViewerFilter> filters = new HashSet<ViewerFilter>(restoreFilters);
				filters.addAll(Arrays.asList(viewer.getFilters()));
				// ensure that all interest filters are removed
				for (Iterator<ViewerFilter> it = filters.iterator(); it.hasNext();) {
					if (it.next() instanceof InterestFilter) {
						it.remove();
					}
				}
				viewer.setFilters(filters.toArray(new ViewerFilter[filters.size()]));
			}
			viewer.removeFilter(interestFilter);
			interestFilter.resetTemporarilyUnfiltered();
		} catch (Throwable t) {
			StatusHandler.log(new Status(IStatus.ERROR, ContextUiPlugin.ID_PLUGIN,
					"Could not uninstall interest viewer filter on: " + globalPrefId, t)); //$NON-NLS-1$
		} finally {
			viewer.getControl().setRedraw(true);
		}
	}

	public String getGlobalPrefId() {
		return globalPrefId;
	}

	/**
	 * For testing.
	 */
	public InterestFilter getInterestFilter() {
		return interestFilter;
	}

	/**
	 * @since 3.0
	 */
	protected String getEmptyViewMessage() {
		return Messages.AbstractFocusViewAction_Empty_task_context;
	}
}
