/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jface.text.source.projection;

import java.util.Iterator;

import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackAdapter;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.AnnotationRulerColumn;
import org.eclipse.jface.text.source.CompositeRuler;
import org.eclipse.jface.text.source.IAnnotationAccess;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelExtension;


/**
 * A ruler column for controlling the behavior of a
 * {@link org.eclipse.jface.text.source.projection.ProjectionViewer}.
 *
 * @since 3.0
 */
class ProjectionRulerColumn extends AnnotationRulerColumn {

	private ProjectionAnnotation fCurrentAnnotation;

	/**
	 * Line number recorded on mouse down.
	 * @since 3.5
	 */
	private int fMouseDownLine;

	/**
	 * Creates a new projection ruler column.
	 *
	 * @param model the column's annotation model
	 * @param width the width in pixels
	 * @param annotationAccess the annotation access
	 */
	public ProjectionRulerColumn(IAnnotationModel model, int width, IAnnotationAccess annotationAccess) {
		super(model, width, annotationAccess);
	}

	/**
	 * Creates a new projection ruler column.
	 *
	 * @param width the width in pixels
	 * @param annotationAccess the annotation access
	 */
	public ProjectionRulerColumn(int width, IAnnotationAccess annotationAccess) {
		super(width, annotationAccess);
	}

	@Override
	protected void mouseClicked(int line) {
		clearCurrentAnnotation();
		if (fMouseDownLine != line)
			return;
		ProjectionAnnotation annotation= findAnnotation(line, true);
		if (annotation != null) {
			ProjectionAnnotationModel model= (ProjectionAnnotationModel) getModel();
			model.toggleExpansionState(annotation);
		}
	}

	@Override
	protected void mouseDown(int rulerLine) {
		fMouseDownLine= rulerLine;
	}

	@Override
	protected void mouseDoubleClicked(int rulerLine) {
		if (findAnnotation(rulerLine, true) != null)
			return;

		ProjectionAnnotation annotation= findAnnotation(rulerLine, false);
		if (annotation != null) {
			ProjectionAnnotationModel model= (ProjectionAnnotationModel)getModel();
			model.toggleExpansionState(annotation);
		}
	}

	/**
	 * Returns the projection annotation of the column's annotation
	 * model that contains the given line.
	 *
	 * @param line the line
	 * @param exact <code>true</code> if the annotation range must match exactly
	 * @return the projection annotation containing the given line
	 */
	private ProjectionAnnotation findAnnotation(int line, boolean exact) {

		ProjectionAnnotation previousAnnotation= null;

		IAnnotationModel model= getModel();
		if (model != null) {
			IDocument document= getCachedTextViewer().getDocument();

			int previousDistance= Integer.MAX_VALUE;

			Iterator<Annotation> e= model.getAnnotationIterator();
			while (e.hasNext()) {
				Object next= e.next();
				if (next instanceof ProjectionAnnotation) {
					ProjectionAnnotation annotation= (ProjectionAnnotation) next;
					Position p= model.getPosition(annotation);
					if (p == null)
						continue;

					int distance= getDistance(annotation, p, document, line);
					if (distance == -1)
						continue;

					if (!exact) {
						if (distance < previousDistance) {
							previousAnnotation= annotation;
							previousDistance= distance;
						}
					} else if (distance == 0) {
						previousAnnotation= annotation;
					}
				}
			}
		}

		return previousAnnotation;
	}

	/**
	 * Returns the distance of the given line to the start line of the given position in the given document. The distance is
	 * <code>-1</code> when the line is not included in the given position.
	 *
	 * @param annotation the annotation
	 * @param position the position
	 * @param document the document
	 * @param line the line
	 * @return <code>-1</code> if line is not contained, a position number otherwise
	 */
	private int getDistance(ProjectionAnnotation annotation, Position position, IDocument document, int line) {
		if (position.getOffset() > -1 && position.getLength() > -1) {
			try {
				int startLine= document.getLineOfOffset(position.getOffset());
				if (startLine <= line) {
					int end= position.getOffset() + position.getLength();
					// Bug 347628: this method expects that the offset after the position range is the
					// offset of the first line after this projected line range. In other words position
					// comprise the whole projected range including the last lines delimiter. If position
					// ends at document end the next offset is not the next line because there is no more content.
					int endLine= end != document.getLength() ? document.getLineOfOffset(end) : document.getNumberOfLines();
					if (line < endLine) {
						if (annotation.isCollapsed()) {
							int captionOffset;
							if (position instanceof IProjectionPosition)
								captionOffset= ((IProjectionPosition) position).computeCaptionOffset(document);
							else
								captionOffset= 0;

							int captionLine= document.getLineOfOffset(position.getOffset() + captionOffset);
							if (startLine <= captionLine && captionLine < endLine)
								return Math.abs(line - captionLine);
						}
						return line - startLine;
					}
				}
			} catch (BadLocationException x) {
			}
		}
		return -1;
	}

	private boolean clearCurrentAnnotation() {
		if (fCurrentAnnotation != null) {
			fCurrentAnnotation.setRangeIndication(false);
			fCurrentAnnotation= null;
			return true;
		}
		return false;
	}

	@Override
	public Control createControl(CompositeRuler parentRuler, Composite parentControl) {
		Control control= super.createControl(parentRuler, parentControl);

		// set background
		Color background= getCachedTextViewer().getTextWidget().getBackground();
		control.setBackground(background);

		// install hover listener
		control.addMouseTrackListener(new MouseTrackAdapter() {
			@Override
			public void mouseExit(MouseEvent e) {
				if (clearCurrentAnnotation())
					redraw();
			}
		});

		// install mouse move listener
		control.addMouseMoveListener(e -> {
			boolean redraw= false;
			ProjectionAnnotation annotation= findAnnotation(toDocumentLineNumber(e.y), false);
			if (annotation != fCurrentAnnotation) {
				if (fCurrentAnnotation != null) {
					fCurrentAnnotation.setRangeIndication(false);
					redraw= true;
				}
				fCurrentAnnotation= annotation;
				if (fCurrentAnnotation != null && !fCurrentAnnotation.isCollapsed()) {
					fCurrentAnnotation.setRangeIndication(true);
					redraw= true;
				}
			}
			if (redraw)
				redraw();
		});
		return control;
	}

	@Override
	public void setModel(IAnnotationModel model) {
		if (model instanceof IAnnotationModelExtension) {
			IAnnotationModelExtension extension= (IAnnotationModelExtension) model;
			model= extension.getAnnotationModel(ProjectionSupport.PROJECTION);
		}
		super.setModel(model);
	}

	@Override
	protected boolean isPropagatingMouseListener() {
		return false;
	}

	@Override
	protected boolean hasAnnotation(int lineNumber) {
		return findAnnotation(lineNumber, true) != null;
	}
}
