/*******************************************************************************
 * Copyright (c) 2000, 2003 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package net.sourceforge.phpdt.ui;

import java.util.Iterator;

import net.sourceforge.phpdt.core.ICompilationUnit;
import net.sourceforge.phpdt.core.IJavaElement;
import net.sourceforge.phpdt.core.ISourceRange;
import net.sourceforge.phpdt.core.ISourceReference;
import net.sourceforge.phpdt.core.JavaModelException;
import net.sourceforge.phpdt.internal.ui.PHPUiImages;
import net.sourceforge.phpdt.internal.ui.viewsupport.IProblemChangedListener;
import net.sourceforge.phpdt.internal.ui.viewsupport.ImageDescriptorRegistry;
import net.sourceforge.phpdt.internal.ui.viewsupport.ImageImageDescriptor;
import net.sourceforge.phpeclipse.PHPeclipsePlugin;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.util.ListenerList;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.IDecoration;
import org.eclipse.jface.viewers.ILabelDecorator;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ILightweightLabelDecorator;
import org.eclipse.jface.viewers.LabelProviderChangedEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.MarkerAnnotation;

/**
 * LabelDecorator that decorates an element's image with error and warning
 * overlays that represent the severity of markers attached to the element's
 * underlying resource. To see a problem decoration for a marker, the marker
 * needs to be a subtype of <code>IMarker.PROBLEM</code>.
 * <p>
 * Note: Only images for elements in Java projects are currently updated on
 * marker changes.
 * </p>
 * 
 * @since 2.0
 */
public class ProblemsLabelDecorator implements ILabelDecorator,
		ILightweightLabelDecorator {

	/**
	 * This is a special <code>LabelProviderChangedEvent</code> carring
	 * additional information whether the event orgins from a maker change.
	 * <p>
	 * <code>ProblemsLabelChangedEvent</code>s are only generated by <code>
	 * ProblemsLabelDecorator</code>s.
	 * </p>
	 */
	public static class ProblemsLabelChangedEvent extends
			LabelProviderChangedEvent {

		private boolean fMarkerChange;

		/**
		 * Note: This constructor is for internal use only. Clients should not
		 * call this constructor.
		 */
		public ProblemsLabelChangedEvent(IBaseLabelProvider source,
				IResource[] changedResource, boolean isMarkerChange) {
			super(source, changedResource);
			fMarkerChange = isMarkerChange;
		}

		/**
		 * Returns whether this event origins from marker changes. If
		 * <code>false</code> an annotation model change is the origin. In
		 * this case viewers not displaying working copies can ignore these
		 * events.
		 * 
		 * @return if this event origins from a marker change.
		 */
		public boolean isMarkerChange() {
			return fMarkerChange;
		}

	}

	private static final int ERRORTICK_WARNING = JavaElementImageDescriptor.WARNING;

	private static final int ERRORTICK_ERROR = JavaElementImageDescriptor.ERROR;

	private ImageDescriptorRegistry fRegistry;

	private boolean fUseNewRegistry = false;

	private IProblemChangedListener fProblemChangedListener;

	private ListenerList fListeners;

	/**
	 * Creates a new <code>ProblemsLabelDecorator</code>.
	 */
	public ProblemsLabelDecorator() {
		this(null);
		fUseNewRegistry = true;
	}

	/*
	 * Creates decorator with a shared image registry.
	 * 
	 * @param registry The registry to use or <code>null</code> to use the
	 * Java plugin's image registry.
	 */
	/**
	 * Note: This constructor is for internal use only. Clients should not call
	 * this constructor.
	 */
	public ProblemsLabelDecorator(ImageDescriptorRegistry registry) {
		fRegistry = registry;
		fProblemChangedListener = null;
	}

	private ImageDescriptorRegistry getRegistry() {
		if (fRegistry == null) {
			fRegistry = fUseNewRegistry ? new ImageDescriptorRegistry()
					: PHPeclipsePlugin.getImageDescriptorRegistry();
		}
		return fRegistry;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ILabelDecorator#decorateText(String, Object)
	 */
	public String decorateText(String text, Object element) {
		return text;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see ILabelDecorator#decorateImage(Image, Object)
	 */
	public Image decorateImage(Image image, Object obj) {
		int adornmentFlags = computeAdornmentFlags(obj);
		if (adornmentFlags != 0) {
			ImageDescriptor baseImage = new ImageImageDescriptor(image);
			Rectangle bounds = image.getBounds();
			return getRegistry().get(
					new JavaElementImageDescriptor(baseImage, adornmentFlags,
							new Point(bounds.width, bounds.height)));
		}
		return image;
	}

	/**
	 * Note: This method is for internal use only. Clients should not call this
	 * method.
	 */
	protected int computeAdornmentFlags(Object obj) {
		try {
			if (obj instanceof IJavaElement) {
				IJavaElement element = (IJavaElement) obj;
				int type = element.getElementType();
				switch (type) {
				case IJavaElement.JAVA_PROJECT:
				case IJavaElement.PACKAGE_FRAGMENT_ROOT:
					return getErrorTicksFromMarkers(element.getResource(),
							IResource.DEPTH_INFINITE, null);
				case IJavaElement.PACKAGE_FRAGMENT:
				case IJavaElement.CLASS_FILE:
					return getErrorTicksFromMarkers(element.getResource(),
							IResource.DEPTH_ONE, null);
				case IJavaElement.COMPILATION_UNIT:
				case IJavaElement.PACKAGE_DECLARATION:
				case IJavaElement.IMPORT_DECLARATION:
				case IJavaElement.IMPORT_CONTAINER:
				case IJavaElement.TYPE:
				case IJavaElement.INITIALIZER:
				case IJavaElement.METHOD:
				case IJavaElement.FIELD:
					ICompilationUnit cu = (ICompilationUnit) element
							.getAncestor(IJavaElement.COMPILATION_UNIT);
					if (cu != null) {
						ISourceReference ref = (type == IJavaElement.COMPILATION_UNIT) ? null
								: (ISourceReference) element;
						// The assumption is that only source elements in
						// compilation unit can have markers
						if (cu.isWorkingCopy()) {
							// working copy: look at annotation model
							return getErrorTicksFromWorkingCopy(
									(ICompilationUnit) cu.getOriginalElement(),
									ref);
						}
						return getErrorTicksFromMarkers(cu.getResource(),
								IResource.DEPTH_ONE, ref);
					}
					break;
				default:
				}
			} else if (obj instanceof IResource) {
				return getErrorTicksFromMarkers((IResource) obj,
						IResource.DEPTH_INFINITE, null);
			}
		} catch (CoreException e) {
			if (e instanceof JavaModelException) {
				if (((JavaModelException) e).isDoesNotExist()) {
					return 0;
				}
			}
			PHPeclipsePlugin.log(e);
		}
		return 0;
	}

	private int getErrorTicksFromMarkers(IResource res, int depth,
			ISourceReference sourceElement) throws CoreException {
		if (res == null || !res.isAccessible()) {
			return 0;
		}
		int info = 0;

		IMarker[] markers = res.findMarkers(IMarker.PROBLEM, true, depth);
		if (markers != null) {
			for (int i = 0; i < markers.length && (info != ERRORTICK_ERROR); i++) {
				IMarker curr = markers[i];
				if (sourceElement == null
						|| isMarkerInRange(curr, sourceElement)) {
					int priority = curr.getAttribute(IMarker.SEVERITY, -1);
					if (priority == IMarker.SEVERITY_WARNING) {
						info = ERRORTICK_WARNING;
					} else if (priority == IMarker.SEVERITY_ERROR) {
						info = ERRORTICK_ERROR;
					}
				}
			}
		}
		return info;
	}

	private boolean isMarkerInRange(IMarker marker,
			ISourceReference sourceElement) throws CoreException {
		if (marker.isSubtypeOf(IMarker.TEXT)) {
			int pos = marker.getAttribute(IMarker.CHAR_START, -1);
			return isInside(pos, sourceElement);
		}
		return false;
	}

	private int getErrorTicksFromWorkingCopy(ICompilationUnit original,
			ISourceReference sourceElement) throws CoreException {
		int info = 0;
		FileEditorInput editorInput = new FileEditorInput((IFile) original
				.getResource());
		IAnnotationModel model = PHPeclipsePlugin.getDefault()
				.getCompilationUnitDocumentProvider().getAnnotationModel(
						editorInput);
		if (model != null) {
			Iterator iter = model.getAnnotationIterator();
			while ((info != ERRORTICK_ERROR) && iter.hasNext()) {
				Annotation curr = (Annotation) iter.next();
				IMarker marker = isAnnotationInRange(model, curr, sourceElement);
				if (marker != null) {
					int priority = marker.getAttribute(IMarker.SEVERITY, -1);
					if (priority == IMarker.SEVERITY_WARNING) {
						info = ERRORTICK_WARNING;
					} else if (priority == IMarker.SEVERITY_ERROR) {
						info = ERRORTICK_ERROR;
					}
				}
			}
		}
		return info;
	}

	private IMarker isAnnotationInRange(IAnnotationModel model,
			Annotation annot, ISourceReference sourceElement)
			throws CoreException {
		if (annot instanceof MarkerAnnotation) {
			IMarker marker = ((MarkerAnnotation) annot).getMarker();
			if (marker.exists() && marker.isSubtypeOf(IMarker.PROBLEM)) {
				Position pos = model.getPosition(annot);
				if (sourceElement == null
						|| isInside(pos.getOffset(), sourceElement)) {
					return marker;
				}
			}
		}
		return null;
	}

	/**
	 * Tests if a position is inside the source range of an element.
	 * 
	 * @param pos
	 *            Position to be tested.
	 * @param sourceElement
	 *            Source element (must be a IJavaElement)
	 * @return boolean Return <code>true</code> if position is located inside
	 *         the source element.
	 * @throws CoreException
	 *             Exception thrown if element range could not be accessed.
	 * 
	 * @since 2.1
	 */
	protected boolean isInside(int pos, ISourceReference sourceElement)
			throws CoreException {
		ISourceRange range = sourceElement.getSourceRange();
		if (range != null) {
			int rangeOffset = range.getOffset();
			return (rangeOffset <= pos && rangeOffset + range.getLength() > pos);
		}
		return false;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see IBaseLabelProvider#dispose()
	 */
	public void dispose() {
		if (fProblemChangedListener != null) {
			PHPeclipsePlugin.getDefault().getProblemMarkerManager()
					.removeListener(fProblemChangedListener);
			fProblemChangedListener = null;
		}
		if (fRegistry != null && fUseNewRegistry) {
			fRegistry.dispose();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see IBaseLabelProvider#isLabelProperty(Object, String)
	 */
	public boolean isLabelProperty(Object element, String property) {
		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see IBaseLabelProvider#addListener(ILabelProviderListener)
	 */
	public void addListener(ILabelProviderListener listener) {
		if (fListeners == null) {
			fListeners = new ListenerList();
		}
		fListeners.add(listener);
		if (fProblemChangedListener == null) {
			fProblemChangedListener = new IProblemChangedListener() {
				public void problemsChanged(IResource[] changedResources,
						boolean isMarkerChange) {
					fireProblemsChanged(changedResources, isMarkerChange);
				}
			};
			PHPeclipsePlugin.getDefault().getProblemMarkerManager()
					.addListener(fProblemChangedListener);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see IBaseLabelProvider#removeListener(ILabelProviderListener)
	 */
	public void removeListener(ILabelProviderListener listener) {
		if (fListeners != null) {
			fListeners.remove(listener);
			if (fListeners.isEmpty() && fProblemChangedListener != null) {
				PHPeclipsePlugin.getDefault().getProblemMarkerManager()
						.removeListener(fProblemChangedListener);
				fProblemChangedListener = null;
			}
		}
	}

	private void fireProblemsChanged(IResource[] changedResources,
			boolean isMarkerChange) {
		if (fListeners != null && !fListeners.isEmpty()) {
			LabelProviderChangedEvent event = new ProblemsLabelChangedEvent(
					this, changedResources, isMarkerChange);
			Object[] listeners = fListeners.getListeners();
			for (int i = 0; i < listeners.length; i++) {
				((ILabelProviderListener) listeners[i])
						.labelProviderChanged(event);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.jface.viewers.ILightweightLabelDecorator#decorate(java.lang.Object,
	 *      org.eclipse.jface.viewers.IDecoration)
	 */
	public void decorate(Object element, IDecoration decoration) {
		int adornmentFlags = computeAdornmentFlags(element);
		if (adornmentFlags == ERRORTICK_ERROR) {
			decoration.addOverlay(PHPUiImages.DESC_OVR_ERROR);
		} else if (adornmentFlags == ERRORTICK_WARNING) {
			decoration.addOverlay(PHPUiImages.DESC_OVR_WARNING);
		}
	}

}
