/**
 * Copyright (c) 2019 Laurent CARON.
 *
 * 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:
 * Laurent CARON (laurent.caron at gmail dot com) - initial API and implementation (bug 542777)
 */
package org.eclipse.swt.custom;

import org.eclipse.swt.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.widgets.*;

/**
 * This class add the following behaviour to <code>StyledText</code> widgets:
 * <p>
 * When the user clicks on the wheel, a circle with arrows appears. When the user moves the mouse,
 * the StyledText content is scrolled (on the right or on the left for horizontal movements, up or down for vertical movements).
 * </p>
 *
 * @since 3.110
 */
class MouseNavigator {
	private final StyledText parent;
	boolean navigationActivated = false;
	private GC gc;
	private static final int CIRCLE_RADIUS = 15;
	private static final int CENTRAL_POINT_RADIUS = 2;
	private Point originalMouseLocation;
	private final Listener mouseDownListener, mouseUpListener, paintListener, mouseMoveListener, focusOutListener;
	private boolean hasHBar, hasVBar;
	private Cursor previousCursor;

	MouseNavigator(final StyledText styledText) {
		if (styledText == null) {
			SWT.error(SWT.ERROR_NULL_ARGUMENT);
		}
		if (styledText.isDisposed()) {
			SWT.error(SWT.ERROR_WIDGET_DISPOSED);
		}
		parent = styledText;

		mouseDownListener = event -> {
			onMouseDown(event);
		};
		parent.addListener(SWT.MouseDown, mouseDownListener);

		mouseUpListener = event -> {
			onMouseUp(event);
		};
		parent.addListener(SWT.MouseUp, mouseUpListener);

		paintListener = event -> {
			onPaint(event);
		};
		parent.addListener(SWT.Paint, paintListener);

		mouseMoveListener = event -> {
			onMouseMove(event);
		};
		parent.addListener(SWT.MouseMove, mouseMoveListener);

		focusOutListener = event -> {
			onFocusOut(event);
		};
		parent.addListener(SWT.FocusOut, focusOutListener);
	}

	void onMouseDown(Event e) {
		if ((e.button != 2) || navigationActivated) {
			return;
		}

		if (!parent.isVisible() || !parent.getEnabled() || parent.middleClickPressed) {
			return;
		}

		// Widget has no bar or bars are not enabled
		initBarState();

		if (!hasHBar && !hasVBar) {
			return;
		}

		navigationActivated = true;
		previousCursor = parent.getCursor();
		parent.setCursor(parent.getDisplay().getSystemCursor(SWT.CURSOR_ARROW));
		originalMouseLocation = getMouseLocation();
		parent.redraw();
	}

	private void initBarState() {
		hasHBar = computeHasHorizontalBar();
		hasVBar = computeHasVerticalBar();
	}

	private boolean computeHasHorizontalBar() {
		final ScrollBar horizontalBar = parent.getHorizontalBar();
		final boolean hasHorizontalBar = horizontalBar != null && horizontalBar.isVisible();
		final boolean exceedHorizontalSpace = parent.computeSize(SWT.DEFAULT, SWT.DEFAULT).x > parent.getSize().x;
		return hasHorizontalBar && exceedHorizontalSpace;
	}

	private boolean computeHasVerticalBar() {
		final ScrollBar verticalBar = parent.getVerticalBar();
		final boolean hasVerticalBar = verticalBar != null && verticalBar.isEnabled();
		final boolean exceedVerticalSpace = parent.computeSize(SWT.DEFAULT, SWT.DEFAULT).y > parent.getSize().y;
		return hasVerticalBar && exceedVerticalSpace;
	}

	private void onMouseUp(Event e) {
		if ((computeDist() < CIRCLE_RADIUS) && (computeDist() >= 0)) {
			return;
		}
		deactivate();
	}

	public int computeDist() {
		if (originalMouseLocation == null) {
			return -1;
		}
		final Point mouseLocation = getMouseLocation();
		final int deltaX = originalMouseLocation.x - mouseLocation.x;
		final int deltaY = originalMouseLocation.y - mouseLocation.y;
		final int dist = (int) Math.sqrt(deltaX * deltaX + deltaY * deltaY);
		return dist;
	}

	private void deactivate() {
		parent.setCursor(previousCursor);
		navigationActivated = false;
		originalMouseLocation = null;
		parent.redraw();
	}

	private void onFocusOut(Event e) {
		deactivate();
	}

	private void onMouseMove(Event e) {
		if (!navigationActivated) {
			return;
		}

		final Point mouseLocation = getMouseLocation();
		final int deltaX = originalMouseLocation.x - mouseLocation.x;
		final int deltaY = originalMouseLocation.y - mouseLocation.y;
		final int dist = (int) Math.sqrt(deltaX * deltaX + deltaY * deltaY);
		if (dist < CIRCLE_RADIUS) {
			return;
		}

		parent.setRedraw(false);
		if (hasHBar) {
			final ScrollBar bar = parent.getHorizontalBar();
			bar.setSelection((int) (bar.getSelection() - deltaX * .1));
			fireSelectionEvent(e, bar);
		}

		if (hasVBar) {
			final ScrollBar bar = parent.getVerticalBar();
			bar.setSelection((int) (bar.getSelection() - deltaY * .1));
			fireSelectionEvent(e, bar);
		}
		parent.setRedraw(true);
		parent.redraw();
	}

	private void fireSelectionEvent(final Event e, final ScrollBar bar) {
		final Event event = new Event();
		event.widget = bar;
		event.display = parent.getDisplay();
		event.type = SWT.Selection;
		event.time = e.time;

		for (final Listener selectionListener : bar.getListeners(SWT.Selection)) {
			selectionListener.handleEvent(event);
		}
	}

	private Point getMouseLocation() {
		final Point cursorLocation = Display.getCurrent().getCursorLocation();
		final Point relativeCursorLocation = parent.toControl(cursorLocation);
		return relativeCursorLocation;
	}

	private void onPaint(final Event e) {
		if (!navigationActivated) {
			return;
		}

		final Rectangle rect = parent.getClientArea();
		if (rect.width == 0 || rect.height == 0) {
			return;
		}
		gc = e.gc;
		gc.setAntialias(SWT.ON);
		gc.setAdvanced(true);

		final Color oldForegroundColor = gc.getForeground();
		final Color oldBackgroundColor = gc.getBackground();
		gc.setBackground(parent.getForeground());

		drawCircle();
		drawCentralPoint();

		drawArrows();

		gc.setForeground(oldForegroundColor);
		gc.setBackground(oldBackgroundColor);
	}

	private void drawCircle() {
		gc.setBackground(parent.getBackground());
		gc.setForeground(parent.getForeground());
		gc.setAlpha(200);
		gc.fillOval(originalMouseLocation.x - CIRCLE_RADIUS, originalMouseLocation.y - CIRCLE_RADIUS, CIRCLE_RADIUS * 2, CIRCLE_RADIUS * 2);
		gc.setBackground(parent.getForeground());
		gc.setAlpha(255);
		gc.drawOval(originalMouseLocation.x - CIRCLE_RADIUS, originalMouseLocation.y - CIRCLE_RADIUS, CIRCLE_RADIUS * 2, CIRCLE_RADIUS * 2);
	}

	private void drawCentralPoint() {
		gc.fillOval(originalMouseLocation.x - CENTRAL_POINT_RADIUS, originalMouseLocation.y - CENTRAL_POINT_RADIUS, CENTRAL_POINT_RADIUS * 2, CENTRAL_POINT_RADIUS * 2);
	}

	private void drawArrows() {
		gc.setLineWidth(2);
		if (hasHBar) {
			drawHorizontalArrows();
		}
		if (hasVBar) {
			drawVerticalArrows();
		}
	}

	private void drawHorizontalArrows() {
		final int[] points = new int[6];
		// Left
		points[0] = originalMouseLocation.x - 6;
		points[1] = originalMouseLocation.y + 3;
		points[2] = originalMouseLocation.x - 9;
		points[3] = originalMouseLocation.y;
		points[4] = originalMouseLocation.x - 6;
		points[5] = originalMouseLocation.y - 3;
		gc.drawPolyline(points);

		// Right
		points[0] = originalMouseLocation.x + 7;
		points[1] = originalMouseLocation.y + 3;
		points[2] = originalMouseLocation.x + 10;
		points[3] = originalMouseLocation.y;
		points[4] = originalMouseLocation.x + 7;
		points[5] = originalMouseLocation.y - 3;
		gc.drawPolyline(points);
	}

	private void drawVerticalArrows() {
		final int[] points = new int[6];
		// Upper
		points[0] = originalMouseLocation.x - 3;
		points[1] = originalMouseLocation.y - 6;
		points[2] = originalMouseLocation.x;
		points[3] = originalMouseLocation.y - 10;
		points[4] = originalMouseLocation.x + 3;
		points[5] = originalMouseLocation.y - 6;
		gc.drawPolyline(points);

		// Lower
		points[0] = originalMouseLocation.x - 3;
		points[1] = originalMouseLocation.y + 7;
		points[2] = originalMouseLocation.x;
		points[3] = originalMouseLocation.y + 11;
		points[4] = originalMouseLocation.x + 3;
		points[5] = originalMouseLocation.y + 7;
		gc.drawPolyline(points);

	}

	void dispose() {
		if (parent.isDisposed()) {
			return;
		}
		parent.removeListener(SWT.MouseDown, mouseDownListener);
		parent.removeListener(SWT.MouseUp, mouseUpListener);
		parent.removeListener(SWT.Paint, paintListener);
		parent.removeListener(SWT.MouseMove, mouseMoveListener);
		parent.removeListener(SWT.MouseExit, focusOutListener);
	}
}
