/**********************************************************************
 Copyright (c) 2000, 2002 IBM Corp. 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 implementation
 **********************************************************************/

package net.sourceforge.phpeclipse.phpeditor;

import java.util.ArrayList;
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 net.sourceforge.phpeclipse.PHPeclipsePlugin;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelListener;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;

/**
 * Highlights the temporary problems.
 */
public class ProblemPainter implements IPainter, PaintListener,
		IAnnotationModelListener {

	private static class ProblemPosition {
		Position fPosition;

		Color fColor;

		boolean fMultiLine;
	};

	private boolean fIsActive = false;

	private boolean fIsPainting = false;

	private boolean fIsSettingModel = false;

	private ITextEditor fTextEditor;

	private ISourceViewer fSourceViewer;

	private StyledText fTextWidget;

	private IAnnotationModel fModel;

	private List fProblemPositions = new ArrayList();

	private Map fColorTable = new HashMap();

	private Set fAnnotationSet = new HashSet();

	public ProblemPainter(ITextEditor textEditor, ISourceViewer sourceViewer) {
		fTextEditor = textEditor;
		fSourceViewer = sourceViewer;
		fTextWidget = sourceViewer.getTextWidget();
	}

	private boolean hasProblems() {
		return !fProblemPositions.isEmpty();
	}

	private void enablePainting() {
		if (!fIsPainting && hasProblems()) {
			fIsPainting = true;
			fTextWidget.addPaintListener(this);
			handleDrawRequest(null);
		}
	}

	private void disablePainting(boolean redraw) {
		if (fIsPainting) {
			fIsPainting = false;
			fTextWidget.removePaintListener(this);
			if (redraw && hasProblems())
				handleDrawRequest(null);
		}
	}

	private void setModel(IAnnotationModel model) {
		if (fModel != model) {
			if (fModel != null)
				fModel.removeAnnotationModelListener(this);
			fModel = model;
			if (fModel != null) {
				try {
					fIsSettingModel = true;
					fModel.addAnnotationModelListener(this);
				} finally {
					fIsSettingModel = false;
				}
			}
		}
	}

	private void catchupWithModel() {
		if (fProblemPositions != null) {
			fProblemPositions.clear();
			if (fModel != null) {

				Iterator e = new ProblemAnnotationIterator(fModel, true);
				while (e.hasNext()) {
					IProblemAnnotation pa = (IProblemAnnotation) e.next();
					Annotation a = (Annotation) pa;

					Color color = null;
					AnnotationType type = pa.getAnnotationType();
					if (fAnnotationSet.contains(type))
						color = (Color) fColorTable.get(type);

					if (color != null) {
						ProblemPosition pp = new ProblemPosition();
						pp.fPosition = fModel.getPosition(a);
						pp.fColor = color;
						pp.fMultiLine = true;
						fProblemPositions.add(pp);
					}
				}
			}
		}
	}

	private void updatePainting() {
		disablePainting(true);
		catchupWithModel();
		enablePainting();
	}

	/*
	 * @see IAnnotationModelListener#modelChanged(IAnnotationModel)
	 */
	public void modelChanged(final IAnnotationModel model) {
		if (fTextWidget != null && !fTextWidget.isDisposed()) {
			if (fIsSettingModel) {
				// inside the ui thread -> no need for posting
				updatePainting();
			} else {
				Display d = fTextWidget.getDisplay();
				if (d != null) {
					d.asyncExec(new Runnable() {
						public void run() {
							if (fTextWidget != null
									&& !fTextWidget.isDisposed())
								updatePainting();
						}
					});
				}
			}
		}
	}

	public void setColor(AnnotationType annotationType, Color color) {
		if (color != null)
			fColorTable.put(annotationType, color);
		else
			fColorTable.remove(annotationType);
	}

	public void paintAnnotations(AnnotationType annotationType, boolean paint) {
		if (paint)
			fAnnotationSet.add(annotationType);
		else
			fAnnotationSet.remove(annotationType);
	}

	public boolean isPaintingAnnotations() {
		return !fAnnotationSet.isEmpty();
	}

	/*
	 * @see IPainter#dispose()
	 */
	public void dispose() {

		if (fColorTable != null)
			fColorTable.clear();
		fColorTable = null;

		if (fAnnotationSet != null)
			fAnnotationSet.clear();
		fAnnotationSet = null;

		fTextWidget = null;
		fModel = null;
		fProblemPositions = null;
	}

	/*
	 * Returns the document offset of the upper left corner of the widgets
	 * viewport, possibly including partially visible lines.
	 */
	private int getInclusiveTopIndexStartOffset() {

		if (fTextWidget != null && !fTextWidget.isDisposed()) {
			int top = fSourceViewer.getTopIndex();
			if ((fTextWidget.getTopPixel() % fTextWidget.getLineHeight()) != 0)
				top--;
			try {
				IDocument document = fSourceViewer.getDocument();
				return document.getLineOffset(top);
			} catch (BadLocationException ex) {
			}
		}

		return -1;
	}

	/*
	 * @see PaintListener#paintControl(PaintEvent)
	 */
	public void paintControl(PaintEvent event) {
		if (fTextWidget != null)
			handleDrawRequest(event.gc);
	}

	private void handleDrawRequest(GC gc) {

		int vOffset = getInclusiveTopIndexStartOffset();
		// http://bugs.eclipse.org/bugs/show_bug.cgi?id=17147
		int vLength = fSourceViewer.getBottomIndexEndOffset() + 1;

		for (Iterator e = fProblemPositions.iterator(); e.hasNext();) {
			ProblemPosition pp = (ProblemPosition) e.next();
			Position p = pp.fPosition;
			if (p.overlapsWith(vOffset, vLength)) {

				if (!pp.fMultiLine) {

					IRegion widgetRange = getWidgetRange(p);
					if (widgetRange != null)
						draw(gc, widgetRange.getOffset(), widgetRange
								.getLength(), pp.fColor);

				} else {

					IDocument document = fSourceViewer.getDocument();
					try {

						int startLine = document.getLineOfOffset(p.getOffset());
						int lastInclusive = Math.max(p.getOffset(), p
								.getOffset()
								+ p.getLength() - 1);
						int endLine = document.getLineOfOffset(lastInclusive);

						for (int i = startLine; i <= endLine; i++) {
							IRegion line = document.getLineInformation(i);
							int paintStart = Math.max(line.getOffset(), p
									.getOffset());
							int paintEnd = Math.min(line.getOffset()
									+ line.getLength(), p.getOffset()
									+ p.getLength());
							if (paintEnd > paintStart) {
								// otherwise inside a line delimiter
								IRegion widgetRange = getWidgetRange(new Position(
										paintStart, paintEnd - paintStart));
								if (widgetRange != null)
									draw(gc, widgetRange.getOffset(),
											widgetRange.getLength(), pp.fColor);
							}
						}

					} catch (BadLocationException x) {
					}
				}
			}
		}
	}

	private IRegion getWidgetRange(Position p) {
		if (fSourceViewer instanceof ITextViewerExtension5) {
			ITextViewerExtension5 extension = (ITextViewerExtension5) fSourceViewer;
			return extension.modelRange2WidgetRange(new Region(p.getOffset(), p
					.getLength()));

		} else {

			IRegion region = fSourceViewer.getVisibleRegion();
			int offset = region.getOffset();
			int length = region.getLength();

			if (p.overlapsWith(offset, length)) {
				int p1 = Math.max(offset, p.getOffset());
				int p2 = Math.min(offset + length, p.getOffset()
						+ p.getLength());
				return new Region(p1 - offset, p2 - p1);
			}
		}

		return null;
	}

	private int[] computePolyline(Point left, Point right, int height) {

		final int WIDTH = 4; // must be even
		final int HEIGHT = 2; // can be any number
		// final int MINPEEKS= 2; // minimal number of peeks

		int peeks = (right.x - left.x) / WIDTH;
		// if (peeks < MINPEEKS) {
		// int missing= (MINPEEKS - peeks) * WIDTH;
		// left.x= Math.max(0, left.x - missing/2);
		// peeks= MINPEEKS;
		// }

		int leftX = left.x;

		// compute (number of point) * 2
		int length = ((2 * peeks) + 1) * 2;
		if (length < 0)
			return new int[0];

		int[] coordinates = new int[length];

		// cache peeks' y-coordinates
		int bottom = left.y + height - 1;
		int top = bottom - HEIGHT;

		// populate array with peek coordinates
		for (int i = 0; i < peeks; i++) {
			int index = 4 * i;
			coordinates[index] = leftX + (WIDTH * i);
			coordinates[index + 1] = bottom;
			coordinates[index + 2] = coordinates[index] + WIDTH / 2;
			coordinates[index + 3] = top;
		}

		// the last down flank is missing
		coordinates[length - 2] = left.x + (WIDTH * peeks);
		coordinates[length - 1] = bottom;

		return coordinates;
	}

	private void draw(GC gc, int offset, int length, Color color) {
		if (gc != null) {

			Point left = fTextWidget.getLocationAtOffset(offset);
			Point right = fTextWidget.getLocationAtOffset(offset + length);

			gc.setForeground(color);
			int[] polyline = computePolyline(left, right, gc.getFontMetrics()
					.getHeight());
			gc.drawPolyline(polyline);

		} else {
			fTextWidget.redrawRange(offset, length, true);
		}
	}

	/*
	 * @see IPainter#deactivate(boolean)
	 */
	public void deactivate(boolean redraw) {
		if (fIsActive) {
			fIsActive = false;
			disablePainting(redraw);
			setModel(null);
			catchupWithModel();
		}
	}

	/*
	 * @see IPainter#paint(int)
	 */
	public void paint(int reason) {
		if (!fIsActive) {
			fIsActive = true;
			IDocumentProvider provider = PHPeclipsePlugin.getDefault()
					.getCompilationUnitDocumentProvider();
			setModel(provider.getAnnotationModel(fTextEditor.getEditorInput()));
		} else if (CONFIGURATION == reason || INTERNAL == reason)
			updatePainting();
	}

	/*
	 * @see IPainter#setPositionManager(IPositionManager)
	 */
	public void setPositionManager(IPositionManager manager) {
	}
}
