/*******************************************************************************
 * Copyright (c) 2000, 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
 *******************************************************************************/
package org.eclipse.jface.text.source;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jface.internal.text.revisions.RevisionPainter;
import org.eclipse.jface.internal.text.source.DiffPainter;
import org.eclipse.jface.viewers.ISelectionProvider;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextListener;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.IViewportListener;
import org.eclipse.jface.text.JFaceTextUtil;
import org.eclipse.jface.text.TextEvent;
import org.eclipse.jface.text.revisions.IRevisionRulerColumn;
import org.eclipse.jface.text.revisions.RevisionInformation;

/**
 * A vertical ruler column displaying line numbers and serving as a UI for quick diff.
 * Clients instantiate and configure object of this class.
 *
 * @since 3.0
 */
public final class ChangeRulerColumn implements IChangeRulerColumn, IRevisionRulerColumn {
	/**
	 * Handles all the mouse interaction in this line number ruler column.
	 */
	private class MouseHandler implements MouseListener, MouseMoveListener {

		@Override
		public void mouseUp(MouseEvent event) {
		}

		@Override
		public void mouseDown(MouseEvent event) {
			fParentRuler.setLocationOfLastMouseButtonActivity(event.x, event.y);
		}

		@Override
		public void mouseDoubleClick(MouseEvent event) {
			fParentRuler.setLocationOfLastMouseButtonActivity(event.x, event.y);
		}

		@Override
		public void mouseMove(MouseEvent event) {
			fParentRuler.setLocationOfLastMouseButtonActivity(event.x, event.y);
		}
	}

	/**
	 * Internal listener class.
	 */
	private class InternalListener implements IViewportListener, ITextListener {

		@Override
		public void viewportChanged(int verticalPosition) {
			if (verticalPosition != fScrollPos)
				redraw();
		}

		@Override
		public void textChanged(TextEvent event) {

			if (!event.getViewerRedrawState())
				return;

			if (fSensitiveToTextChanges || event.getDocumentEvent() == null)
				postRedraw();

		}
	}

	/**
	 * The view(port) listener.
	 */
	private final InternalListener fInternalListener= new InternalListener();
	/**
	 * The mouse handler.
	 * @since 3.2
	 */
	private final MouseHandler fMouseHandler= new MouseHandler();
	/**
	 * The revision painter.
	 * @since 3.2
	 */
	private final RevisionPainter fRevisionPainter;
	/**
	 * The diff info painter.
	 * @since 3.2
	 */
	private final DiffPainter fDiffPainter;

	/** This column's parent ruler */
	private CompositeRuler fParentRuler;
	/** Cached text viewer */
	private ITextViewer fCachedTextViewer;
	/** Cached text widget */
	private StyledText fCachedTextWidget;
	/** The columns canvas */
	private Canvas fCanvas;
	/** The background color */
	private Color fBackground;
	/** The ruler's annotation model. */
	private IAnnotationModel fAnnotationModel;
	/** The width of the change ruler column. */
	private final int fWidth= 5;

	/** Cache for the actual scroll position in pixels */
	private int fScrollPos;
	/** The buffer for double buffering */
	private Image fBuffer;
	/** Indicates whether this column reacts on text change events */
	private boolean fSensitiveToTextChanges= false;

	/**
	 * Creates a new ruler column.
	 *
	 * @deprecated since 3.2 use {@link #ChangeRulerColumn(ISharedTextColors)} instead
	 */
	@Deprecated
	public ChangeRulerColumn() {
		fRevisionPainter= null;
		fDiffPainter= new DiffPainter(this, null);
	}

	/**
	 * Creates a new revision ruler column.
	 *
	 * @param sharedColors the colors to look up RGBs
	 * @since 3.2
	 */
	public ChangeRulerColumn(ISharedTextColors sharedColors) {
		Assert.isNotNull(sharedColors);
		fRevisionPainter= new RevisionPainter(this, sharedColors);
		fDiffPainter= new DiffPainter(this, null); // no shading
	}

	/**
	 * Returns the System background color for list widgets.
	 *
	 * @return the System background color for list widgets
	 */
	private Color getBackground() {
		if (fBackground == null)
			return fCachedTextWidget.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND);
		return fBackground;
	}

	@Override
	public Control createControl(CompositeRuler parentRuler, Composite parentControl) {

		fParentRuler= parentRuler;
		fCachedTextViewer= parentRuler.getTextViewer();
		fCachedTextWidget= fCachedTextViewer.getTextWidget();

		fCanvas= new Canvas(parentControl, SWT.NONE);
		fCanvas.setBackground(getBackground());

		fCanvas.addPaintListener(event -> {
			if (fCachedTextViewer != null)
				doubleBufferPaint(event.gc);
		});

		fCanvas.addDisposeListener(e -> {
			handleDispose();
			fCachedTextViewer= null;
			fCachedTextWidget= null;
		});

		fCanvas.addMouseListener(fMouseHandler);
		fCanvas.addMouseMoveListener(fMouseHandler);

		if (fCachedTextViewer != null) {

			fCachedTextViewer.addViewportListener(fInternalListener);
			fCachedTextViewer.addTextListener(fInternalListener);
		}

		fRevisionPainter.setParentRuler(parentRuler);
		fDiffPainter.setParentRuler(parentRuler);

		return fCanvas;
	}

	/**
	 * Disposes the column's resources.
	 */
	protected void handleDispose() {

		if (fCachedTextViewer != null) {
			fCachedTextViewer.removeViewportListener(fInternalListener);
			fCachedTextViewer.removeTextListener(fInternalListener);
		}

		if (fBuffer != null) {
			fBuffer.dispose();
			fBuffer= null;
		}
	}

	/**
	 * Double buffer drawing.
	 *
	 * @param dest the GC to draw into
	 */
	private void doubleBufferPaint(GC dest) {

		Point size= fCanvas.getSize();

		if (size.x <= 0 || size.y <= 0)
			return;

		if (fBuffer != null) {
			Rectangle r= fBuffer.getBounds();
			if (r.width != size.x || r.height != size.y) {
				fBuffer.dispose();
				fBuffer= null;
			}
		}
		if (fBuffer == null)
			fBuffer= new Image(fCanvas.getDisplay(), size.x, size.y);

		GC gc= new GC(fBuffer);
		gc.setFont(fCanvas.getFont());

		try {
			gc.setBackground(getBackground());
			gc.fillRectangle(0, 0, size.x, size.y);

			doPaint(gc);
		} finally {
			gc.dispose();
		}

		dest.drawImage(fBuffer, 0, 0);
	}

	/**
	 * Returns the view port height in lines.
	 *
	 * @return the view port height in lines
	 * @deprecated as of 3.2 the number of lines in the viewport cannot be computed because
	 *             StyledText supports variable line heights
	 */
	@Deprecated
	protected int getVisibleLinesInViewport() {
		// Hack to reduce amount of copied code.
		return LineNumberRulerColumn.getVisibleLinesInViewport(fCachedTextWidget);
	}

	/**
	 * Returns <code>true</code> if the viewport displays the entire viewer contents, i.e. the
	 * viewer is not vertically scrollable.
	 *
	 * @return <code>true</code> if the viewport displays the entire contents, <code>false</code> otherwise
	 * @since 3.2
	 */
	protected final boolean isViewerCompletelyShown() {
		return JFaceTextUtil.isShowingEntireContents(fCachedTextWidget);
	}

	/**
	 * Draws the ruler column.
	 *
	 * @param gc the GC to draw into
	 */
	private void doPaint(GC gc) {
		ILineRange visibleModelLines= computeVisibleModelLines();
		if (visibleModelLines == null)
			return;

		fSensitiveToTextChanges= isViewerCompletelyShown();

		fScrollPos= fCachedTextWidget.getTopPixel();

		fRevisionPainter.paint(gc, visibleModelLines);
		if (!fRevisionPainter.hasInformation()) // don't paint quick diff colors if revisions are painted
			fDiffPainter.paint(gc, visibleModelLines);
	}

	@Override
	public void redraw() {

		if (fCachedTextViewer != null && fCanvas != null && !fCanvas.isDisposed()) {
			if (VerticalRuler.AVOID_NEW_GC) {
				fCanvas.redraw();
			} else {
				GC gc= new GC(fCanvas);
				doubleBufferPaint(gc);
				gc.dispose();
			}
		}
	}

	@Override
	public void setFont(Font font) {
	}

	/**
	 * Returns the parent (composite) ruler of this ruler column.
	 *
	 * @return the parent ruler
	 * @since 3.0
	 */
	private CompositeRuler getParentRuler() {
		return fParentRuler;
	}

	@Override
	public int getLineOfLastMouseButtonActivity() {
		return getParentRuler().getLineOfLastMouseButtonActivity();
	}

	@Override
	public int toDocumentLineNumber(int y_coordinate) {
		return getParentRuler().toDocumentLineNumber(y_coordinate);
	}

	@Override
	public IAnnotationHover getHover() {
		int activeLine= getParentRuler().getLineOfLastMouseButtonActivity();
		if (fRevisionPainter.hasHover(activeLine))
			return fRevisionPainter.getHover();
		if (fDiffPainter.hasHover(activeLine))
			return fDiffPainter.getHover();
		return null;
	}

	@Override
	public void setHover(IAnnotationHover hover) {
		fRevisionPainter.setHover(hover);
		fDiffPainter.setHover(hover);
	}

	@Override
	public void setModel(IAnnotationModel model) {
		setAnnotationModel(model);
		fRevisionPainter.setModel(model);
		fDiffPainter.setModel(model);
	}

	private void setAnnotationModel(IAnnotationModel model) {
		if (fAnnotationModel != model)
			fAnnotationModel= model;
	}

	@Override
	public void setBackground(Color background) {
		fBackground= background;
		if (fCanvas != null && !fCanvas.isDisposed())
			fCanvas.setBackground(getBackground());
		fRevisionPainter.setBackground(background);
		fDiffPainter.setBackground(background);
	}

	@Override
	public void setAddedColor(Color addedColor) {
		fDiffPainter.setAddedColor(addedColor);
	}

	@Override
	public void setChangedColor(Color changedColor) {
		fDiffPainter.setChangedColor(changedColor);
	}

	@Override
	public void setDeletedColor(Color deletedColor) {
		fDiffPainter.setDeletedColor(deletedColor);
	}

	@Override
	public IAnnotationModel getModel() {
		return fAnnotationModel;
	}

	@Override
	public Control getControl() {
		return fCanvas;
	}

	@Override
	public int getWidth() {
		return fWidth;
	}

	/**
	 * Triggers a redraw in the display thread.
	 */
	protected final void postRedraw() {
		if (fCanvas != null && !fCanvas.isDisposed()) {
			Display d= fCanvas.getDisplay();
			if (d != null) {
				d.asyncExec(this::redraw);
			}
		}
	}

	@Override
	public void addVerticalRulerListener(IVerticalRulerListener listener) {
		throw new UnsupportedOperationException();
	}

	@Override
	public void removeVerticalRulerListener(IVerticalRulerListener listener) {
		throw new UnsupportedOperationException();
	}

	/**
	 * Computes the document based line range visible in the text widget.
	 *
	 * @return the document based line range visible in the text widget
	 * @since 3.2
	 */
	private final ILineRange computeVisibleModelLines() {
		IDocument doc= fCachedTextViewer.getDocument();
		if (doc == null)
			return null;

		int topLine;
		IRegion coverage;

		if (fCachedTextViewer instanceof ITextViewerExtension5) {
			ITextViewerExtension5 extension= (ITextViewerExtension5) fCachedTextViewer;

			// ITextViewer.getTopIndex returns the fully visible line, but we want the partially
			// visible one
			int widgetTopLine= JFaceTextUtil.getPartialTopIndex(fCachedTextWidget);
			topLine= extension.widgetLine2ModelLine(widgetTopLine);

			coverage= extension.getModelCoverage();

		} else {
			topLine= JFaceTextUtil.getPartialTopIndex(fCachedTextViewer);
			coverage= fCachedTextViewer.getVisibleRegion();
		}

		int bottomLine= fCachedTextViewer.getBottomIndex();
		if (bottomLine != -1)
			++ bottomLine;

		// clip by coverage window
		try {
			int firstLine= doc.getLineOfOffset(coverage.getOffset());
			if (firstLine > topLine)
				topLine= firstLine;

			int lastLine= doc.getLineOfOffset(coverage.getOffset() + coverage.getLength());
			if (lastLine < bottomLine || bottomLine == -1)
				bottomLine= lastLine;
		} catch (BadLocationException x) {
			return null;
		}

		ILineRange visibleModelLines= new LineRange(topLine, bottomLine - topLine + 1);
		return visibleModelLines;
	}

	@Override
	public void setRevisionInformation(RevisionInformation info) {
		fRevisionPainter.setRevisionInformation(info);
		fRevisionPainter.setBackground(getBackground());
	}

	/**
	 * Returns the revision selection provider.
	 *
	 * @return the revision selection provider
	 * @since 3.2
	 */
	public ISelectionProvider getRevisionSelectionProvider() {
		return fRevisionPainter.getRevisionSelectionProvider();
	}
}
