/*******************************************************************************
 * Copyright (c) 2004, 2009 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.java.ui;

import java.util.LinkedHashSet;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.ISourceReference;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
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.context.core.IInteractionElement;

/**
 * @author Mik Kersten
 */
public class LandmarkMarkerManager extends AbstractContextListener {

	private static final String ID_MARKER_LANDMARK = "org.eclipse.mylyn.context.ui.markers.landmark"; //$NON-NLS-1$

	private final Map<IInteractionElement, Long> markerMap = new ConcurrentHashMap<IInteractionElement, Long>();

	private final LandmarkUpdateJob updateJob = new LandmarkUpdateJob(
			Messages.LandmarkMarkerManager_Updating_Landmark_Markers);

	public LandmarkMarkerManager() {
		super();
	}

	@Override
	public void contextChanged(ContextChangeEvent event) {
		LinkedHashSet<LandmarkUpdateOperation> runnables = new LinkedHashSet<LandmarkUpdateOperation>();
		switch (event.getEventKind()) {
		case ACTIVATED:
		case DEACTIVATED:
			modelUpdated();
			break;
		case CLEARED:
			if (event.isActiveContext()) {
				modelUpdated();
			}
			break;
		case LANDMARKS_ADDED:
			for (IInteractionElement element : event.getElements()) {
				LandmarkUpdateOperation runnable = createAddLandmarkMarkerOperation(element);
				if (runnable != null) {
					runnables.add(runnable);
				}
			}
			break;
		case ELEMENTS_DELETED:
		case LANDMARKS_REMOVED:
			for (IInteractionElement element : event.getElements()) {
				LandmarkUpdateOperation runnable = createRemoveLandmarkMarkerOperation(element);
				if (runnable != null) {
					runnables.add(runnable);
				}
			}
			break;

		}
		updateJob.updateMarkers(runnables);
	}

	private void modelUpdated() {
		try {
			// remove all known landmark markers
			LinkedHashSet<LandmarkUpdateOperation> runnables = new LinkedHashSet<LandmarkUpdateOperation>();
			for (IInteractionElement node : markerMap.keySet()) {
				LandmarkUpdateOperation runnable = createRemoveLandmarkMarkerOperation(node);
				if (runnable != null) {
					runnables.add(runnable);
				}
			}
			markerMap.clear();
			// add any nodes that are landmarks
			for (IInteractionElement node : ContextCore.getContextManager().getActiveLandmarks()) {
				LandmarkUpdateOperation runnable = createAddLandmarkMarkerOperation(node);
				if (runnable != null) {
					runnables.add(runnable);
				}
			}
			updateJob.updateMarkers(runnables);
		} catch (Throwable t) {
			StatusHandler.log(new Status(IStatus.ERROR, JavaUiBridgePlugin.ID_PLUGIN,
					"Could not update landmark markers", t)); //$NON-NLS-1$
		}
	}

	private LandmarkUpdateOperation createAddLandmarkMarkerOperation(final IInteractionElement node) {
		if (node == null || node.getContentType() == null) {
			return null;
		}
		if (JavaStructureBridge.CONTENT_TYPE.equals(node.getContentType())) {
			final IJavaElement element = JavaCore.create(node.getHandleIdentifier());
			if (!element.exists()) {
				return null;
			}
			if (element instanceof IMember) {
				try {
					final ISourceRange range = ((IMember) element).getNameRange();
					IResource resource = element.getUnderlyingResource();
					if (resource instanceof IFile) {
						LandmarkUpdateOperation runnable = new LandmarkUpdateOperation(resource) {
							public void run(IProgressMonitor monitor) throws CoreException {
								IMarker marker = getResource().createMarker(ID_MARKER_LANDMARK);
								if (marker != null && range != null) {
									marker.setAttribute(IMarker.CHAR_START, range.getOffset());
									marker.setAttribute(IMarker.CHAR_END, range.getOffset() + range.getLength());
									marker.setAttribute(IMarker.MESSAGE, "Mylyn Landmark");
									marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_INFO);
									markerMap.put(node, marker.getId());
								}
							}
						};
						return runnable;
					}
				} catch (JavaModelException e) {
					StatusHandler.log(new Status(IStatus.ERROR, JavaUiBridgePlugin.ID_PLUGIN,
							"Could not update marker", e)); //$NON-NLS-1$
				}
			}
		}
		return null;
	}

	private LandmarkUpdateOperation createRemoveLandmarkMarkerOperation(final IInteractionElement node) {
		if (node == null) {
			return null;
		}
		if (JavaStructureBridge.CONTENT_TYPE.equals(node.getContentType())) {
			IJavaElement element = JavaCore.create(node.getHandleIdentifier());
			if (!element.exists()) {
				return null;
			}
			if (element.getAncestor(IJavaElement.COMPILATION_UNIT) != null // stuff
					// from .class files
					&& element instanceof ISourceReference) {
				try {
					IResource resource = element.getUnderlyingResource();
					LandmarkUpdateOperation runnable = new LandmarkUpdateOperation(resource) {
						public void run(IProgressMonitor monitor) throws CoreException {
							if (getResource() != null) {
								try {
									if (markerMap.containsKey(node)) {
										long id = markerMap.get(node);
										IMarker marker = getResource().getMarker(id);
										if (marker != null) {
											marker.delete();
										}
									}
								} catch (NullPointerException e) {
									// FIXME avoid NPE
									StatusHandler.log(new Status(IStatus.ERROR, JavaUiBridgePlugin.ID_PLUGIN,
											"Could not update marker", e)); //$NON-NLS-1$
								}
							}
						}
					};
					return runnable;
				} catch (JavaModelException e) {
					// ignore the Java Model errors
				}
			}
		}
		return null;
	}

	/**
	 * IWorkspaceRunnable that has a reference to the resource that it is operating on
	 */
	private abstract class LandmarkUpdateOperation implements IWorkspaceRunnable {

		private final IResource resource;

		public LandmarkUpdateOperation(IResource resource) {
			Assert.isNotNull(resource);
			this.resource = resource;
		}

		public IResource getResource() {
			return resource;
		}

	}

	/**
	 * Job to handle updating the landmark markers in the background
	 */
	private class LandmarkUpdateJob extends Job {

		private static final int NOT_SCHEDULED = -1;

		private final LinkedHashSet<LandmarkUpdateOperation> queue = new LinkedHashSet<LandmarkUpdateOperation>();

		private long scheduleTime = NOT_SCHEDULED;

		public LandmarkUpdateJob(String name) {
			super(name);
			setSystem(true);
		}

		public synchronized void updateMarkers(LinkedHashSet<LandmarkUpdateOperation> operations) {
			queue.addAll(operations);
			if (queue.size() > 0) {
				if (scheduleTime == NOT_SCHEDULED) {
					scheduleTime = System.currentTimeMillis();
					schedule();
				}
			}
		}

		@Override
		public IStatus run(IProgressMonitor monitor) {
			IWorkspace workspace = ResourcesPlugin.getWorkspace();
			if (workspace == null) {
				return Status.CANCEL_STATUS;
			}
			LinkedHashSet<LandmarkUpdateOperation> operations = null;
			synchronized (this) {
				operations = new LinkedHashSet<LandmarkUpdateOperation>(queue);
				queue.clear();
				scheduleTime = NOT_SCHEDULED;
			}
			if (operations != null) {
				try {
					monitor.beginTask(Messages.LandmarkMarkerManager_Updating_Landmark_Markers, operations.size());
					for (LandmarkUpdateOperation runnable : operations) {
						try {
							workspace.run(runnable, runnable.getResource(), IWorkspace.AVOID_UPDATE, monitor);
						} catch (CoreException e) {
							StatusHandler.log(new Status(IStatus.ERROR, JavaUiBridgePlugin.ID_PLUGIN,
									"Could not update landmark marker", e)); //$NON-NLS-1$
						} catch (OperationCanceledException e) {
							return Status.CANCEL_STATUS;
						}
						monitor.worked(1);
					}
				} finally {
					monitor.done();
				}
			}
			return Status.OK_STATUS;
		}
	}
}
