/*******************************************************************************
 * Copyright (c) 2000, 2010 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.viewsupport;

import java.util.List;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.resource.ImageDescriptor;
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.texteditor.MarkerUtilities;

import org.eclipse.cdt.core.language.settings.providers.ILanguageSettingsProvider;
import org.eclipse.cdt.core.language.settings.providers.ILanguageSettingsProvidersKeeper;
import org.eclipse.cdt.core.language.settings.providers.LanguageSettingsManager;
import org.eclipse.cdt.core.language.settings.providers.ScannerDiscoveryLegacySupport;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ISourceRange;
import org.eclipse.cdt.core.model.ISourceReference;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.core.settings.model.ICConfigurationDescription;
import org.eclipse.cdt.core.settings.model.ICLanguageSettingEntry;
import org.eclipse.cdt.core.settings.model.ICProjectDescription;
import org.eclipse.cdt.core.settings.model.ICResourceDescription;
import org.eclipse.cdt.ui.CElementImageDescriptor;
import org.eclipse.cdt.ui.CUIPlugin;

import org.eclipse.cdt.internal.ui.CPluginImages;
import org.eclipse.cdt.internal.ui.util.IProblemChangedListener;
import org.eclipse.cdt.internal.ui.util.ImageDescriptorRegistry;

/**
 * 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= CElementImageDescriptor.WARNING;
	private static final int ERRORTICK_ERROR= CElementImageDescriptor.ERROR;
	private static final int TICK_CONFIGURATION = CElementImageDescriptor.SETTINGS;

	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() : CUIPlugin.getImageDescriptorRegistry();
		}
		return fRegistry;
	}


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

	/* (non-Javadoc)
	 * @see ILabelDecorator#decorateImage(Image, Object)
	 */
	@Override
	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 CElementImageDescriptor(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 ICElement) {
				ICElement element= (ICElement) obj;
				int type= element.getElementType();
				switch (type) {
					case ICElement.C_PROJECT:
					case ICElement.C_CCONTAINER:
						return getErrorTicksFromMarkers(element.getResource(), IResource.DEPTH_INFINITE, null) | getTicks(element.getResource());
					case ICElement.C_UNIT:
						return getErrorTicksFromMarkers(element.getResource(), IResource.DEPTH_ONE, null) | getTicks(element.getResource());
					case ICElement.C_FUNCTION:
					case ICElement.C_CLASS:
					case ICElement.C_UNION:
					case ICElement.C_STRUCT:
					case ICElement.C_VARIABLE:
					case ICElement.C_METHOD:
						ITranslationUnit tu= ((ISourceReference)element).getTranslationUnit();
						if (tu != null && tu.exists()) {
							return getErrorTicksFromMarkers(tu.getResource(), IResource.DEPTH_ONE, (ISourceReference)element);
						}
						break;
				}
			} else if (obj instanceof IResource) {
				return getErrorTicksFromMarkers((IResource) obj, IResource.DEPTH_INFINITE, null) | getTicks((IResource)obj);
			}
		} catch (CoreException e) {
			if (e instanceof CModelException) {
//				if (((CModelException) e).isDoesNotExist()) {
//					return 0;
//				}
			}
			if (e.getStatus().getCode() == IResourceStatus.MARKER_NOT_FOUND) {
				return 0;
			}

			CUIPlugin.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);
			if (pos == -1) {
				int line= MarkerUtilities.getLineNumber(marker);
				if (line >= 0) {
					return isInside( -1, line, sourceElement);
				}
			}
			return isInside(pos, -1, sourceElement);

		}
		return false;
	}


//	private int getErrorTicksFromWorkingCopy(ITranslationUnit original, ISourceReference sourceElement) throws CoreException {
//		int info= 0;
//		FileEditorInput editorInput= new FileEditorInput((IFile) original.getResource());
//		IAnnotationModel model= CUIPlugin.getDefault().getTranslationUnitDocumentProvider().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 (pos != null && (sourceElement == null || isInside(pos.getOffset(), -1, sourceElement))) {
//					return marker;
//				}
//			}
//		}
//		return null;
//	}

	/**
	 * Tests if a position is inside the source range of an element. Usually this is done
	 * by looking at the offset. In case the offset equals <code>-1</code>, the line is
	 * tested.
	 * @param offSet offset to be tested
	 * @param line line to be tested
	 * @param sourceElement Source element (must be a ICElement)
	 * @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 offSet, int line, ISourceReference sourceElement) throws CoreException {
		ISourceRange range= sourceElement.getSourceRange();
		if (range != null) {
			if (offSet ==-1) {
				return (line >= range.getStartLine() && line <= range.getEndLine());
			}
			int rangeOffset= range.getStartPos();
			return (rangeOffset <= offSet && rangeOffset + range.getLength() > offSet);
		}
		return false;
	}

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

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

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

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

	protected void fireProblemsChanged(IResource[] changedResources, boolean isMarkerChange) {
		if (fListeners != null && !fListeners.isEmpty()) {
			LabelProviderChangedEvent event= new ProblemsLabelChangedEvent(this, changedResources, isMarkerChange);
			Object[] listeners= fListeners.getListeners();
			for (Object listener : listeners) {
				((ILabelProviderListener) listener).labelProviderChanged(event);
			}
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jface.viewers.ILightweightLabelDecorator#decorate(java.lang.Object, org.eclipse.jface.viewers.IDecoration)
	 */
	@Override
	public void decorate(Object element, IDecoration decoration) {
		int adornmentFlags= computeAdornmentFlags(element);

		if ((adornmentFlags & TICK_CONFIGURATION) != 0) {
			decoration.addOverlay(CPluginImages.DESC_OVR_SETTING);
			adornmentFlags &= ~TICK_CONFIGURATION;
		}

		if (adornmentFlags == ERRORTICK_ERROR) {
			decoration.addOverlay(CPluginImages.DESC_OVR_ERROR);
		} else if (adornmentFlags == ERRORTICK_WARNING) {
			decoration.addOverlay(CPluginImages.DESC_OVR_WARNING);
		}
	}

	private static boolean isCustomizedResource(ICConfigurationDescription cfgDescription, IResource rc) {
		if (rc instanceof IProject)
			return false;

		if (!ScannerDiscoveryLegacySupport.isLanguageSettingsProvidersFunctionalityEnabled(rc.getProject())) {
			ICResourceDescription rcDescription = cfgDescription.getResourceDescription(rc.getProjectRelativePath(), true);
			return rcDescription != null;
		}

		if (cfgDescription instanceof ILanguageSettingsProvidersKeeper) {
			for (ILanguageSettingsProvider provider: ((ILanguageSettingsProvidersKeeper) cfgDescription).getLanguageSettingProviders()) {
				for (String languageId : LanguageSettingsManager.getLanguages(rc, cfgDescription)) {
					List<ICLanguageSettingEntry> list = provider.getSettingEntries(cfgDescription, rc, languageId);
					if (list != null) {
						List<ICLanguageSettingEntry> listDefault = provider.getSettingEntries(cfgDescription, rc.getParent(), languageId);
						// != is OK here due as the equal lists will have the same reference in WeakHashSet
						if (list != listDefault)
							return true;
					}
				}
			}
		}
		return false;
	}

	/**
	 * @param rc - resource to check
	 * @return flags {@link TICK_CONFIGURATION} if the resource has custom settings and possibly needs
	 * to be adorned or 0 otherwise.
	 */
	private int getTicks (IResource rc) {
		if (rc == null || rc instanceof IProject)
			return 0;

		int result = 0;
		ICProjectDescription prjDescription = CoreModel.getDefault().getProjectDescription(rc.getProject(), false);
		if (prjDescription != null) {
			ICConfigurationDescription cfgDescription = prjDescription.getDefaultSettingConfiguration();
			if (cfgDescription != null) {
				if (isCustomizedResource(cfgDescription, rc))
					result |= TICK_CONFIGURATION;
			}
		}
		return result;
	}
}
