/*******************************************************************************
 * Copyright (c) 2006, 2018 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
 *     Tom Eicher (Avaloq Evolution AG) - block selection mode
 *******************************************************************************/
package org.eclipse.jface.text;

import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;

import org.eclipse.jface.internal.text.SelectionProcessor;

import org.eclipse.jface.text.source.ILineRange;
import org.eclipse.jface.text.source.LineRange;

/**
 * A collection of JFace Text functions.
 * <p>
 * This class is neither intended to be instantiated nor subclassed.
 * </p>
 *
 * @since 3.3
 * @noinstantiate This class is not intended to be instantiated by clients.
 */
public final class JFaceTextUtil {

	private JFaceTextUtil() {
		// Do not instantiate
	}

	/**
	 * Computes the full line height for the text line corresponding to the given widget line,
	 * considering the possible line wrapping.
	 *
	 * @param styledText the widget
	 * @param widgetLine the widget line
	 * @return the full real height of the corresponding line of text (which might wrap to multiple
	 *         widget lines) in the widget
	 * @since 3.11
	 */
	public static int computeLineHeight(StyledText styledText, int widgetLine) {
		boolean isWrapActive= styledText.getWordWrap();
		int lineHeight;
		int offset= styledText.getOffsetAtLine(widgetLine);
		if (!isWrapActive) {
			lineHeight= styledText.getLineHeight(offset);
		} else {
			int offsetEnd= offset + styledText.getLine(widgetLine).length();
			if (offsetEnd == styledText.getCharCount()) {
				lineHeight= styledText.getLineHeight(offset);
			} else {
				Rectangle textBounds= styledText.getTextBounds(offset, offsetEnd);
				lineHeight= textBounds.height;
			}
		}
		return lineHeight;
	}

	/**
	 * Computes the line height for the given line range.
	 *
	 * @param textWidget the <code>StyledText</code> widget
	 * @param startLine the start line
	 * @param endLine the end line (exclusive)
	 * @param lineCount the line count used by the old API
	 * @return the height of all lines starting with <code>startLine</code> and ending above <code>endLime</code>
	 */
	public static int computeLineHeight(StyledText textWidget, int startLine, int endLine, int lineCount) {
		return getLinePixel(textWidget, endLine) - getLinePixel(textWidget, startLine);
	}

	/**
	 * Returns the last fully visible line of the widget. The exact semantics of "last fully visible
	 * line" are:
	 * <ul>
	 * <li>the last line of which the last pixel is visible, if any
	 * <li>otherwise, the only line that is partially visible
	 * </ul>
	 *
	 * @param widget the widget
	 * @return the last fully visible line
	 */
	public static int getBottomIndex(StyledText widget) {
		int lastPixel= computeLastVisiblePixel(widget);

		// bottom is in [0 .. lineCount - 1]
		int bottom= widget.getLineIndex(lastPixel);

		// bottom is the first line - no more checking
		if (bottom == 0)
			return bottom;

		int pixel= widget.getLinePixel(bottom);
		// bottom starts on or before the client area start - bottom is the only visible line
		if (pixel <= 0)
			return bottom;

		int height= computeLineHeight(widget, bottom);

		// bottom is not showing entirely - use the previous line
		if (pixel + height - 1 > lastPixel)
			return bottom - 1;

		// bottom is fully visible and its last line is exactly the last pixel
		return bottom;
	}

	/**
	 * Returns the index of the first (possibly only partially) visible line of the widget
	 *
	 * @param widget the widget
	 * @return the index of the first line of which a pixel is visible
	 */
	public static int getPartialTopIndex(StyledText widget) {
		// see StyledText#getPartialTopIndex()
		int top= widget.getTopIndex();
		int pixels= widget.getLinePixel(top);

		if (pixels > 0)
			top--;

		return top;
	}

	/**
	 * Returns the index of the last (possibly only partially) visible line of the widget
	 *
	 * @param widget the text widget
	 * @return the index of the last line of which a pixel is visible
	 */
	public static int getPartialBottomIndex(StyledText widget) {
		// @see StyledText#getPartialBottomIndex()
		int lastPixel= computeLastVisiblePixel(widget);
		int bottom= widget.getLineIndex(lastPixel);
		return bottom;
	}

	/**
	 * Returns the last visible pixel in the widget's client area.
	 *
	 * @param widget the widget
	 * @return the last visible pixel in the widget's client area
	 */
	private static int computeLastVisiblePixel(StyledText widget) {
		int caHeight= widget.getClientArea().height;
		int lastPixel= caHeight - 1;
		// XXX: what if there is a margin? can't take trim as this includes the scrollbars which are not part of the client area
//		if ((textWidget.getStyle() & SWT.BORDER) != 0)
//			lastPixel -= 4;
		return lastPixel;
	}

	/**
	 * Returns the line index of the first visible model line in the viewer. The line may be only
	 * partially visible.
	 *
	 * @param viewer the text viewer
	 * @return the first line of which a pixel is visible, or -1 for no line
	 */
	public static int getPartialTopIndex(ITextViewer viewer) {
		StyledText widget= viewer.getTextWidget();
		int widgetTop= getPartialTopIndex(widget);
		return widgetLine2ModelLine(viewer, widgetTop);
	}

	/**
	 * Returns the last, possibly partially, visible line in the view port.
	 *
	 * @param viewer the text viewer
	 * @return the last, possibly partially, visible line in the view port
	 */
	public static int getPartialBottomIndex(ITextViewer viewer) {
		StyledText textWidget= viewer.getTextWidget();
		int widgetBottom= getPartialBottomIndex(textWidget);
		return widgetLine2ModelLine(viewer, widgetBottom);
	}

	/**
	 * Returns the range of lines that is visible in the viewer, including any partially visible
	 * lines.
	 *
	 * @param viewer the viewer
	 * @return the range of lines that is visible in the viewer, <code>null</code> if no lines are
	 *         visible
	 */
	public static ILineRange getVisibleModelLines(ITextViewer viewer) {
		int top= getPartialTopIndex(viewer);
		int bottom= getPartialBottomIndex(viewer);
		if (top == -1 || bottom == -1)
			return null;
		return new LineRange(top, bottom - top + 1);
	}

	/**
	 * Converts a widget line into a model (i.e. {@link IDocument}) line using the
	 * {@link ITextViewerExtension5} if available, otherwise by adapting the widget line to the
	 * viewer's {@link ITextViewer#getVisibleRegion() visible region}.
	 *
	 * @param viewer the viewer
	 * @param widgetLine the widget line to convert.
	 * @return the model line corresponding to <code>widgetLine</code> or -1 to signal that there
	 *         is no corresponding model line
	 */
	public static int widgetLine2ModelLine(ITextViewer viewer, int widgetLine) {
		int modelLine;
		if (viewer instanceof ITextViewerExtension5) {
			ITextViewerExtension5 extension= (ITextViewerExtension5) viewer;
			modelLine= extension.widgetLine2ModelLine(widgetLine);
		} else {
			try {
				IRegion r= viewer.getVisibleRegion();
				IDocument d= viewer.getDocument();
				if (d == null)
					return -1;
				modelLine= widgetLine + d.getLineOfOffset(r.getOffset());
			} catch (BadLocationException x) {
				modelLine= widgetLine;
			}
		}
		return modelLine;
	}

	/**
	 * Converts a model (i.e. {@link IDocument}) line into a widget line using the
	 * {@link ITextViewerExtension5} if available, otherwise by adapting the model line to the
	 * viewer's {@link ITextViewer#getVisibleRegion() visible region}.
	 *
	 * @param viewer the viewer
	 * @param modelLine the model line to convert.
	 * @return the widget line corresponding to <code>modelLine</code> or -1 to signal that there
	 *         is no corresponding widget line
	 */
	public static int modelLineToWidgetLine(ITextViewer viewer, final int modelLine) {
		int widgetLine;
		if (viewer instanceof ITextViewerExtension5) {
			ITextViewerExtension5 extension= (ITextViewerExtension5) viewer;
			widgetLine= extension.modelLine2WidgetLine(modelLine);
		} else {
			IRegion region= viewer.getVisibleRegion();
			IDocument document= viewer.getDocument();
			if (document == null)
				return -1;
			try {
				int visibleStartLine= document.getLineOfOffset(region.getOffset());
				int visibleEndLine= document.getLineOfOffset(region.getOffset() + region.getLength());
				if (modelLine < visibleStartLine || modelLine > visibleEndLine)
					widgetLine= -1;
				else
				widgetLine= modelLine - visibleStartLine;
			} catch (BadLocationException x) {
				// ignore and return -1
				widgetLine= -1;
			}
		}
		return widgetLine;
	}


	/**
	 * Returns the number of hidden pixels of the first partially visible line. If there is no
	 * partially visible line, zero is returned.
	 *
	 * @param textWidget the widget
	 * @return the number of hidden pixels of the first partial line, always &gt;= 0
	 */
	public static int getHiddenTopLinePixels(StyledText textWidget) {
		int top= getPartialTopIndex(textWidget);
		return -textWidget.getLinePixel(top);
	}

	/*
	 * @see StyledText#getLinePixel(int)
	 */
	public static int getLinePixel(StyledText textWidget, int line) {
		return textWidget.getLinePixel(line);
	}

	/*
	 * @see StyledText#getLineIndex(int)
	 */
	public static int getLineIndex(StyledText textWidget, int y) {
		int lineIndex= textWidget.getLineIndex(y);
		return lineIndex;
	}

	/**
	 * Returns <code>true</code> if the widget displays the entire contents, i.e. it cannot
	 * be vertically scrolled.
	 *
	 * @param widget the widget
	 * @return <code>true</code> if the widget displays the entire contents, i.e. it cannot
	 *         be vertically scrolled, <code>false</code> otherwise
	 */
	public static boolean isShowingEntireContents(StyledText widget) {
		if (widget.getTopPixel() != 0) {
			// more efficient shortcut
			return false;
		}

		int lastVisiblePixel= computeLastVisiblePixel(widget);
		int bottom= widget.getLineIndex(lastVisiblePixel);
		if (bottom + 1 < widget.getLineCount()) {
			// There's definitely more lines below
			return false;
		}

		// Check whether the last line is fully visible
		int bottomLastPixel= getLinePixel(widget, bottom + 1) - 1;
		return bottomLastPixel <= lastVisiblePixel;
	}

	/**
	 * Determines the graphical area covered by the given text region in
	 * the given viewer.
	 *
	 * @param region the region whose graphical extend must be computed
	 * @param textViewer the text viewer containing the region
	 * @return the graphical extend of the given region in the given viewer
	 *
	 * @since 3.4
	 */
	public static Rectangle computeArea(IRegion region, ITextViewer textViewer) {
		int start= 0;
		int end= 0;
		IRegion widgetRegion= modelRange2WidgetRange(region, textViewer);
		if (widgetRegion != null) {
			start= widgetRegion.getOffset();
			end= start + widgetRegion.getLength();
		}

		StyledText styledText= textViewer.getTextWidget();
		Rectangle bounds;
		if (end > 0 && start < end)
			bounds= styledText.getTextBounds(start, end - 1);
		else {
			Point loc= styledText.getLocationAtOffset(start);
			bounds= new Rectangle(loc.x, loc.y, getAverageCharWidth(textViewer.getTextWidget()),
					computeLineHeight(styledText, styledText.getLineAtOffset(start)));
		}

		return new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height);
	}

	/**
	 * Translates a given region of the text viewer's document into
	 * the corresponding region of the viewer's widget.
	 *
	 * @param region the document region
	 * @param textViewer the viewer containing the region
	 * @return the corresponding widget region
	 *
	 * @since 3.4
	 */
	private static IRegion modelRange2WidgetRange(IRegion region, ITextViewer textViewer) {
		if (textViewer instanceof ITextViewerExtension5) {
			ITextViewerExtension5 extension= (ITextViewerExtension5) textViewer;
			return extension.modelRange2WidgetRange(region);
		}

		IRegion visibleRegion= textViewer.getVisibleRegion();
		int start= region.getOffset() - visibleRegion.getOffset();
		int end= start + region.getLength();
		if (end > visibleRegion.getLength())
			end= visibleRegion.getLength();

		return new Region(start, end - start);
	}

	/**
	 * Returns the average character width of the given control's font.
	 *
	 * @param control the control to calculate the average char width for
	 * @return the average character width of the controls font
	 *
	 * @since 3.4
	 */
	public static int getAverageCharWidth(Control control) {
		GC gc= new GC(control);
		gc.setFont(control.getFont());
		double increment= gc.getFontMetrics().getAverageCharacterWidth();
		gc.dispose();
		return (int) increment;
	}

	/**
	 * Returns <code>true</code> if the text covered by <code>selection</code> does not contain any
	 * characters in the given viewer. Note the difference to {@link ITextSelection#isEmpty()},
	 * which returns <code>true</code> only for invalid selections.
	 *
	 * @param viewer the viewer
	 * @param selection the selection
	 * @return <code>true</code> if <code>selection</code> does not contain any text,
	 *         <code>false</code> otherwise
	 * @throws BadLocationException if accessing the document failed
	 * @since 3.5
	 */
	public static boolean isEmpty(ITextViewer viewer, ITextSelection selection) throws BadLocationException {
		return new SelectionProcessor(viewer).isEmpty(selection);
	}

	/**
	 * Returns the text regions covered by the given selection in the given viewer.
	 *
	 * @param viewer the viewer
	 * @param selection the selection
	 * @return the text regions corresponding to <code>selection</code>
	 * @throws BadLocationException if accessing the document failed
	 * @since 3.5
	 */
	public static IRegion[] getCoveredRanges(ITextViewer viewer, ITextSelection selection) throws BadLocationException {
		return new SelectionProcessor(viewer).getRanges(selection);
	}

	/**
	 * Returns the offset in the given viewer that corresponds to the current cursor location.
	 *
	 * @param viewer the viewer
	 * @return the offset for the current cursor location or -1 if not available
	 * @since 3.5
	 */
	public static int getOffsetForCursorLocation(ITextViewer viewer) {
		try {
			StyledText text= viewer.getTextWidget();
			if (text == null || text.isDisposed()) {
				return -1;
			}

			Display display= text.getDisplay();
			Point absolutePosition= display.getCursorLocation();
			Point relativePosition= text.toControl(absolutePosition);

			int widgetOffset= text.getOffsetAtPoint(relativePosition);
			if (widgetOffset == -1) {
				return -1;
			}
			Point p= text.getLocationAtOffset(widgetOffset);
			if (p.x > relativePosition.x) {
				widgetOffset--;
			}

			if (viewer instanceof ITextViewerExtension5) {
				ITextViewerExtension5 extension= (ITextViewerExtension5)viewer;
				return extension.widgetOffset2ModelOffset(widgetOffset);
			}

			return widgetOffset + viewer.getVisibleRegion().getOffset();
		} catch (IllegalArgumentException e) {
			return -1;
		}
	}
}
