/*******************************************************************************
 * 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.internal.context.ui;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.StructuredSelection;
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.commons.workbench.DelayedRefreshJob;
import org.eclipse.mylyn.context.core.AbstractContextListener;
import org.eclipse.mylyn.context.core.AbstractContextStructureBridge;
import org.eclipse.mylyn.context.core.ContextChangeEvent;
import org.eclipse.mylyn.context.core.ContextCore;
import org.eclipse.mylyn.context.core.IInteractionElement;
import org.eclipse.mylyn.context.ui.AbstractFocusViewAction;
import org.eclipse.mylyn.context.ui.InterestFilter;
import org.eclipse.mylyn.internal.context.core.ContextCorePlugin;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PlatformUI;

/**
 * Encapsulates the element refresh and expansion state policy for all viewers focused on context.
 * 
 * @author Mik Kersten
 * @author Shawn Minto
 */
public class FocusedViewerManager extends AbstractContextListener implements ISelectionListener {

	private final CopyOnWriteArrayList<StructuredViewer> managedViewers = new CopyOnWriteArrayList<StructuredViewer>();

	private final CopyOnWriteArrayList<StructuredViewer> filteredViewers = new CopyOnWriteArrayList<StructuredViewer>();

	private final Map<StructuredViewer, AbstractFocusViewAction> focusActions = new HashMap<StructuredViewer, AbstractFocusViewAction>();

	private final Map<StructuredViewer, BrowseFilteredListener> listenerMap = new HashMap<StructuredViewer, BrowseFilteredListener>();

	private final Map<IWorkbenchPart, StructuredViewer> partToViewerMap = new HashMap<IWorkbenchPart, StructuredViewer>();

	private final Map<StructuredViewer, FocusedViewerDelayedRefreshJob> fullRefreshJobs = new HashMap<StructuredViewer, FocusedViewerDelayedRefreshJob>();

	// TODO: consider merging in order to discard minors when majors come in, see bug 209846
	private final Map<StructuredViewer, FocusedViewerDelayedRefreshJob> minorRefreshJobs = new HashMap<StructuredViewer, FocusedViewerDelayedRefreshJob>();

	private class FocusedViewerDelayedRefreshJob extends DelayedRefreshJob {

		private boolean minor = false;

		private boolean updateExpansion;

		public FocusedViewerDelayedRefreshJob(StructuredViewer viewer, String name, boolean minor) {
			super(viewer, name);
			this.minor = minor;

		}

		@Override
		protected void doRefresh(Object[] items) {
			try {
				if (viewer == null) {
					return;
				} else if (viewer.getControl().isDisposed()) {
					managedViewers.remove(viewer);
				} else {
					if (items == null || items.length == 0) {
						if (!minor) {
							viewer.refresh(false);
							if (updateExpansion) {
								FocusedViewerManager.this.updateExpansionState(viewer, null);
							}
						} else {
							try {
								viewer.getControl().setRedraw(false);
								viewer.refresh(true);
								if (updateExpansion) {
									FocusedViewerManager.this.updateExpansionState(viewer, null);
								}
							} finally {
								viewer.getControl().setRedraw(true);
							}
						}
					} else {
						if (filteredViewers.contains(viewer)) {
							try {
								viewer.getControl().setRedraw(false);
								viewer.refresh(minor);
								if (updateExpansion) {
									FocusedViewerManager.this.updateExpansionState(viewer, null);
								}
							} finally {
								viewer.getControl().setRedraw(true);
							}
						} else { // don't need to worry about content changes
							try {
								viewer.getControl().setRedraw(false);
								for (Object item : items) {
									Object objectToRefresh = item;
									if (item instanceof IInteractionElement) {
										IInteractionElement node = (IInteractionElement) item;
										AbstractContextStructureBridge structureBridge = ContextCorePlugin.getDefault()
												.getStructureBridge(node.getContentType());
										objectToRefresh = structureBridge.getObjectForHandle(node.getHandleIdentifier());
									}
									if (objectToRefresh != null) {
										viewer.update(objectToRefresh, null);
										if (updateExpansion) {
											FocusedViewerManager.this.updateExpansionState(viewer, objectToRefresh);
										}
									}
								}
							} finally {
								viewer.getControl().setRedraw(true);
							}
						}
					}
				}
			} finally {
				updateExpansion = false;
			}

		}

		public void refreshElements(Object[] elements, boolean updateExpansion) {
			this.updateExpansion |= updateExpansion;
			super.refreshElements(elements);
		}

		@Override
		public void refreshElements(Object[] elements) {
			refreshElements(elements, true);
		}
	}

	/**
	 * For testing.
	 */
	private boolean syncRefreshMode = false;

//	private boolean internalExpandExceptionLogged;

	public FocusedViewerManager() {
		// NOTE: no longer using viewer part tracker due to bug 162346
//		VIEWER_PART_TRACKER.install(PlatformUI.getWorkbench());
	}

	public void dispose() {
//		VIEWER_PART_TRACKER.dispose(PlatformUI.getWorkbench());
	}

	public void selectionChanged(IWorkbenchPart part, ISelection selection) {
		// ignore
	}

	public void addManagedViewer(StructuredViewer viewer, IWorkbenchPart viewPart) {
		if (viewer != null && !managedViewers.contains(viewer)) {
			managedViewers.add(viewer);
			partToViewerMap.put(viewPart, viewer);
			BrowseFilteredListener listener = new BrowseFilteredListener(viewer);
			listenerMap.put(viewer, listener);
			viewer.getControl().addMouseListener(listener);
			viewer.getControl().addKeyListener(listener);

			try {
				// NOTE: this needs to be done because some views (e.g. Project Explorer) are not
				// correctly initialized on startup and do not have the dummy selection event
				// sent to them.  See PartPluginAction and bug 213545.
				// TODO consider a mechanism to identify only views that provide focus
				FocusedViewerManager.initializeViewerSelection(viewPart);
				Set<IInteractionElement> emptySet = Collections.emptySet();
				refreshViewer(emptySet, true, viewer, true);
			} catch (Exception e) {
				StatusHandler.log(new Status(IStatus.ERROR, ContextUiPlugin.ID_PLUGIN,
						"Could not initialize focused viewer", e)); //$NON-NLS-1$
			}
		}
	}

	private final Map<TreeViewer, FilteredChildrenDecorationDrawer> decorationMap = new HashMap<TreeViewer, FilteredChildrenDecorationDrawer>();

	private void removeFilterDecorations(StructuredViewer viewer) {
		if (viewer instanceof TreeViewer) {
			TreeViewer treeViewer = (TreeViewer) viewer;

			FilteredChildrenDecorationDrawer filterViewDrawer = decorationMap.remove(treeViewer);
			if (filterViewDrawer != null) {
				filterViewDrawer.dispose();
			}
		}
	}

	private void addFilterDecorations(StructuredViewer viewer, BrowseFilteredListener listener) {
		if (viewer instanceof TreeViewer) {
			TreeViewer treeViewer = (TreeViewer) viewer;
			FilteredChildrenDecorationDrawer filteredViewDrawer = new FilteredChildrenDecorationDrawer(treeViewer,
					listener);
			if (filteredViewDrawer.applyToTreeViewer()) {
				decorationMap.put(treeViewer, filteredViewDrawer);
			}

		}
	}

	public void removeManagedViewer(StructuredViewer viewer, IWorkbenchPart viewPart) {
		managedViewers.remove(viewer);
		partToViewerMap.remove(viewPart);
		removeFilterDecorations(viewer);
		BrowseFilteredListener listener = listenerMap.get(viewer);
		if (listener != null && viewer != null && !viewer.getControl().isDisposed()) {
			viewer.getControl().removeMouseListener(listener);
			viewer.getControl().removeKeyListener(listener);
		}
	}

	public void addFilteredViewer(StructuredViewer viewer, AbstractFocusViewAction action) {
		addFilteredViewer(viewer);
		if (viewer != null && action != null) {
			focusActions.put(viewer, action);
		}
		BrowseFilteredListener listener = listenerMap.get(viewer);
		if (listener != null) {
			addFilterDecorations(viewer, listener);
		}
	}

	@Deprecated
	public void addFilteredViewer(StructuredViewer viewer) {
		if (viewer != null && !filteredViewers.contains(viewer)) {
			filteredViewers.add(viewer);
		}
	}

	public void removeFilteredViewer(StructuredViewer viewer) {
		removeFilterDecorations(viewer);
		focusActions.remove(viewer);
		filteredViewers.remove(viewer);
	}

	@Override
	public void contextChanged(ContextChangeEvent event) {
		switch (event.getEventKind()) {
		case ACTIVATED:
			refreshViewers();
			break;
		case DEACTIVATED:
			refreshViewers();
			for (StructuredViewer structuredViewer : managedViewers) {
				if (structuredViewer instanceof TreeViewer) {
					((TreeViewer) structuredViewer).collapseAll();
				}
			}
			break;
		case CLEARED:
			if (event.isActiveContext()) {
				// ensure we dont refresh the viewers if a context other than the active one is deleted or cleared
				// bug #265688
				refreshViewers();
				for (StructuredViewer structuredViewer : managedViewers) {
					if (structuredViewer instanceof TreeViewer) {
						((TreeViewer) structuredViewer).collapseAll();
					}
				}
			}
			break;
		case INTEREST_CHANGED:
			if (event.isActiveContext()) {
				refreshViewers(event.getElements(), false, true);
			}
			break;
		case LANDMARKS_ADDED:
			if (event.isActiveContext()) {
				refreshViewers(event.getElements(), true, false);
			}
			break;
		case LANDMARKS_REMOVED:
			if (event.isActiveContext()) {
				refreshViewers(event.getElements(), true, false);
			}
			break;
		case ELEMENTS_DELETED:
			if (event.isActiveContext()) {
				/*
				 * TODO: consider making this work per-element and parent
				 * Should we collect all parents before calling refresh?
				 */
				ArrayList<IInteractionElement> toRefresh = new ArrayList<IInteractionElement>();
				for (IInteractionElement interactionElement : event.getElements()) {
					AbstractContextStructureBridge structureBridge = ContextCore.getStructureBridge(interactionElement.getContentType());
					IInteractionElement parent = ContextCore.getContextManager().getElement(
							structureBridge.getParentHandle(interactionElement.getHandleIdentifier()));
					if (parent != null) {
						toRefresh.add(parent);
					}
				}
				refreshViewers(toRefresh, false, false);
			}
			break;
		}
	}

	protected void refreshViewers() {
		List<IInteractionElement> toRefresh = Collections.emptyList();
		refreshViewers(toRefresh, true, true);
	}

	@Deprecated
	protected void refreshViewers(IInteractionElement node, boolean updateLabels) {
		refreshViewers(node, updateLabels, true);
	}

	protected void refreshViewers(IInteractionElement node, boolean updateLabels, boolean updateExpansion) {
		List<IInteractionElement> toRefresh = new ArrayList<IInteractionElement>();
		toRefresh.add(node);
		refreshViewers(toRefresh, updateLabels, updateExpansion);
	}

	@Deprecated
	protected void refreshViewers(final List<IInteractionElement> nodesToRefresh, final boolean updateLabels) {
		refreshViewers(nodesToRefresh, updateLabels, true);
	}

	protected void refreshViewers(final List<IInteractionElement> nodesToRefresh, final boolean updateLabels,
			final boolean updateExpansion) {
		// TODO replace by Assert.isNotNull(nodesToRefresh);
		if (nodesToRefresh == null) {
			return;
		}

		if (syncRefreshMode) {
			internalRefresh(new HashSet<IInteractionElement>(nodesToRefresh), updateLabels, updateExpansion);
		} else {
			PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
				public void run() {
					internalRefresh(new HashSet<IInteractionElement>(nodesToRefresh), updateLabels, updateExpansion);
				}
			});
		}
	}

	private void internalRefresh(final Set<IInteractionElement> nodesToRefresh, final boolean updateLabels,
			final boolean updateExpansion) {
		try {
			for (StructuredViewer viewer : managedViewers) {
				refreshViewer(nodesToRefresh, updateLabels, viewer, updateExpansion);
			}
		} catch (Throwable t) {
			StatusHandler.log(new Status(IStatus.ERROR, ContextUiPlugin.ID_PLUGIN, "Could not refresh viewer", t)); //$NON-NLS-1$
		}
	}

	@Deprecated
	public void refreshViewer(final Set<IInteractionElement> nodesToRefresh, final boolean updateLabels,
			StructuredViewer viewer) {
		refreshViewer(nodesToRefresh, updateLabels, viewer, true);
	}

	public void refreshViewer(final Set<IInteractionElement> nodesToRefresh, final boolean updateLabels,
			StructuredViewer viewer, boolean updateExpansion) {

		Map<StructuredViewer, FocusedViewerDelayedRefreshJob> refreshJobs = null;
		if (updateLabels) {
			refreshJobs = minorRefreshJobs;
		} else {
			refreshJobs = fullRefreshJobs;
		}
		FocusedViewerDelayedRefreshJob job = refreshJobs.get(viewer);
		if (job == null) {
			job = new FocusedViewerDelayedRefreshJob(viewer, "refresh viewer", updateLabels); //$NON-NLS-1$
			refreshJobs.put(viewer, job);
		}
		job.refreshElements(nodesToRefresh.toArray(), updateExpansion);

	}

	private void updateExpansionState(StructuredViewer viewer, Object objectToRefresh) {
		if (viewer instanceof TreeViewer
				&& filteredViewers.contains(viewer)
				&& hasInterestFilter(viewer, true)
				&& ContextUiPlugin.getDefault()
						.getPreferenceStore()
						.getBoolean(IContextUiPreferenceContstants.AUTO_MANAGE_EXPANSION)) {
			TreeViewer treeViewer = (TreeViewer) viewer;

			// HACK to fix bug 278569: [context] errors with Markers view and active Mylyn task
			if ("org.eclipse.ui.internal.views.markers.MarkersTreeViewer".equals(treeViewer.getClass() //$NON-NLS-1$
					.getCanonicalName())) {
				objectToRefresh = null;
			}

			if (objectToRefresh == null) {
				treeViewer.expandAll();
			} else {
				treeViewer.expandToLevel(objectToRefresh, AbstractTreeViewer.ALL_LEVELS);
			}
		}
	}

	private boolean hasInterestFilter(StructuredViewer viewer, boolean tryToReinstall) {
		for (ViewerFilter filter : viewer.getFilters()) {
			if (filter instanceof InterestFilter) {
				return true;
			}
		}
		if (tryToReinstall) {
			AbstractFocusViewAction action = focusActions.get(viewer);
			if (action != null) {
				action.run(action);
				viewer.refresh();
				return hasInterestFilter(viewer, false);
			}
		}
		return false;
	}

	/**
	 * Set to true for testing
	 */
	public void setSyncRefreshMode(boolean syncRefreshMode) {
		this.syncRefreshMode = syncRefreshMode;
	}

	public void forceRefresh() {
		refreshViewers();
	}

	public static void initializeViewerSelection(IWorkbenchPart part) {
		ISelectionProvider selectionProvider = part.getSite().getSelectionProvider();
		if (selectionProvider != null) {
			ISelection selection = selectionProvider.getSelection();
			try {
				if (selection != null) {
					selectionProvider.setSelection(selection);
				} else {
					selectionProvider.setSelection(StructuredSelection.EMPTY);
				}
			} catch (UnsupportedOperationException e) {
				// ignore if the selection does not support setting a selection, see bug 217634
			}
		}
	}

}
