/*******************************************************************************
 * Copyright (c) 2000, 2019 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;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;
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.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Scrollable;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;

import org.eclipse.jface.internal.text.DelayedInputChangeListener;
import org.eclipse.jface.internal.text.InformationControlReplacer;
import org.eclipse.jface.internal.text.InternalAccessor;
import org.eclipse.jface.util.Geometry;

import org.eclipse.jface.text.ITextViewerExtension8.EnrichMode;
import org.eclipse.jface.text.source.AnnotationBarHoverManager;


/**
 * An information control manager that shows information in response to mouse
 * hover events. The mouse hover events are caught by registering a
 * {@link org.eclipse.swt.events.MouseTrackListener} on the manager's subject
 * control. The manager has by default an information control closer that closes
 * the information control as soon as the mouse pointer leaves the subject area,
 * the user presses a key, or the subject control is resized, moved, or
 * deactivated.
 * <p>
 * When being activated by a mouse hover event, the manager disables itself,
 * until the mouse leaves the subject area. Thus, the manager is usually still
 * disabled, when the information control has already been closed by the closer.
 *
 * @see org.eclipse.swt.events.MouseTrackListener
 * @since 2.0
 */
abstract public class AbstractHoverInformationControlManager extends AbstractInformationControlManager {

	/**
	 * The  information control closer for the hover information. Closes the information control as
	 * soon as the mouse pointer leaves the subject area (unless "move into hover" is enabled),
	 * a mouse button is pressed, the user presses a key, or the subject control is resized, moved, or loses focus.
	 */
	class Closer implements IInformationControlCloser, MouseListener, MouseMoveListener, ControlListener, KeyListener, SelectionListener, Listener {

		/** The closer's subject control */
		private Control fSubjectControl;
		/** The subject area */
		private Rectangle fSubjectArea;
		/** Indicates whether this closer is active */
		private boolean fIsActive= false;
		/**
		 * The cached display.
		 * @since 3.1
		 */
		private Display fDisplay;


		/**
		 * Creates a new information control closer.
		 */
		public Closer() {
		}

		@Override
		public void setSubjectControl(Control control) {
			fSubjectControl= control;
		}

		/*
		 * @see IInformationControlCloser#setHoverControl(IHoverControl)
		 */
		@Override
		public void setInformationControl(IInformationControl control) {
			// NOTE: we use getCurrentInformationControl() from the outer class
		}

		@Override
		public void start(Rectangle subjectArea) {

			if (fIsActive)
				return;
			fIsActive= true;
			fWaitForMouseUp= false;

			fSubjectArea= subjectArea;

			if (fSubjectControl != null && !fSubjectControl.isDisposed()) {
				fSubjectControl.addMouseListener(this);
				fSubjectControl.addMouseMoveListener(this);
				fSubjectControl.addControlListener(this);
				fSubjectControl.addKeyListener(this);
				if (fSubjectControl instanceof Scrollable) {
					Scrollable scrollable= (Scrollable) fSubjectControl;
					ScrollBar vBar= scrollable.getVerticalBar();
					if (vBar != null)
						vBar.addSelectionListener(this);
					ScrollBar hBar= scrollable.getHorizontalBar();
					if (hBar != null)
						hBar.addSelectionListener(this);
				}

				fDisplay= fSubjectControl.getDisplay();
				if (!fDisplay.isDisposed()) {
					fDisplay.addFilter(SWT.Activate, this);
					fDisplay.addFilter(SWT.MouseVerticalWheel, this);

					fDisplay.addFilter(SWT.FocusOut, this);

					fDisplay.addFilter(SWT.MouseDown, this);
					fDisplay.addFilter(SWT.MouseUp, this);

					fDisplay.addFilter(SWT.MouseMove, this);
					fDisplay.addFilter(SWT.MouseEnter, this);
					fDisplay.addFilter(SWT.MouseExit, this);
				}
			}
		}

		@Override
		public void stop() {
			if (!fIsActive)
				return;

			fIsActive= false;

			if (DEBUG)
				System.out.println("AbstractHoverInformationControlManager.Closer stopped"); //$NON-NLS-1$

			if (fSubjectControl != null && !fSubjectControl.isDisposed()) {
				fSubjectControl.removeMouseListener(this);
				fSubjectControl.removeMouseMoveListener(this);
				fSubjectControl.removeControlListener(this);
				fSubjectControl.removeKeyListener(this);
				if (fSubjectControl instanceof Scrollable) {
					Scrollable scrollable= (Scrollable) fSubjectControl;
					ScrollBar vBar= scrollable.getVerticalBar();
					if (vBar != null)
						vBar.removeSelectionListener(this);
					ScrollBar hBar= scrollable.getHorizontalBar();
					if (hBar != null)
						hBar.removeSelectionListener(this);
				}
			}

			if (fDisplay != null && !fDisplay.isDisposed()) {
				fDisplay.removeFilter(SWT.Activate, this);
				fDisplay.removeFilter(SWT.MouseVerticalWheel, this);

				fDisplay.removeFilter(SWT.FocusOut, this);

				fDisplay.removeFilter(SWT.MouseDown, this);
				fDisplay.removeFilter(SWT.MouseUp, this);

				fDisplay.removeFilter(SWT.MouseMove, this);
				fDisplay.removeFilter(SWT.MouseEnter, this);
				fDisplay.removeFilter(SWT.MouseExit, this);
			}
			fDisplay= null;
		}

		@Override
		public void mouseMove(MouseEvent event) {
			if (!hasInformationControlReplacer() || !canMoveIntoInformationControl(getCurrentInformationControl())) {
				if (!fSubjectArea.contains(event.x, event.y)) {
					hideInformationControl();
				}

			} else if (getCurrentInformationControl() != null && !getCurrentInformationControl().isFocusControl()) {
				if (!inKeepUpZone(event.x, event.y, fSubjectControl, fSubjectArea, true)) {
					hideInformationControl();
				}
			}
		}

		@Override
		public void mouseUp(MouseEvent event) {
		}

		@Override
		public void mouseDown(MouseEvent event) {
			hideInformationControl();
		}

		@Override
		public void mouseDoubleClick(MouseEvent event) {
			hideInformationControl();
		}

		@Override
		public void controlResized(ControlEvent event) {
			hideInformationControl();
		}

		@Override
		public void controlMoved(ControlEvent event) {
			hideInformationControl();
		}

		@Override
		public void keyReleased(KeyEvent event) {
		}

		@Override
		public void keyPressed(KeyEvent event) {
			hideInformationControl();
		}

		@Override
		public void widgetSelected(SelectionEvent e) {
			hideInformationControl();
		}

		@Override
		public void widgetDefaultSelected(SelectionEvent e) {
		}

		@Override
		public void handleEvent(Event event) {
			switch (event.type) {
				case SWT.Activate:
				case SWT.MouseVerticalWheel:
					if (!hasInformationControlReplacer())
						hideInformationControl();
					else if (!isReplaceInProgress()) {
						IInformationControl infoControl= getCurrentInformationControl();
						// During isReplaceInProgress(), events can come from the replacing information control
						if (event.widget instanceof Control && infoControl instanceof IInformationControlExtension5) {
							Control control= (Control) event.widget;
							IInformationControlExtension5 iControl5= (IInformationControlExtension5) infoControl;
							if (!(iControl5.containsControl(control)))
								hideInformationControl();
							else if (event.type == SWT.MouseVerticalWheel && cancelReplacingDelay())
								replaceInformationControl(false);
						} else if (infoControl != null && infoControl.isFocusControl() && cancelReplacingDelay()) {
							replaceInformationControl(true);
						}
					}
					break;

				case SWT.MouseUp:
				case SWT.MouseDown:
					if (!hasInformationControlReplacer())
						hideInformationControl();
					else if (!isReplaceInProgress()) {
						IInformationControl infoControl= getCurrentInformationControl();
						if (event.widget instanceof Control && infoControl instanceof IInformationControlExtension5) {
							Control control= (Control) event.widget;
							final IInformationControlExtension5 iControl5= (IInformationControlExtension5) infoControl;
							if (!(iControl5.containsControl(control))) {
								hideInformationControl();
							} else if (cancelReplacingDelay()) {
								if (event.type == SWT.MouseUp) {
									stop(); // avoid that someone else replaces the info control before the async is exec'd
									if (infoControl instanceof IDelayedInputChangeProvider) {
										final IDelayedInputChangeProvider delayedICP= (IDelayedInputChangeProvider) infoControl;
										final IInputChangedListener inputChangeListener= new DelayedInputChangeListener(delayedICP, getInformationControlReplacer());
										delayedICP.setDelayedInputChangeListener(inputChangeListener);
										// cancel automatic input updating after a small timeout:
										control.getShell().getDisplay().timerExec(1000, () -> delayedICP.setDelayedInputChangeListener(null));
									}

									// XXX: workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=212392 :
									control.getShell().getDisplay().asyncExec(() -> replaceInformationControl(true));
								} else {
									fWaitForMouseUp= true;
								}
							}
						} else {
							handleMouseMove(event);
						}
					}
					break;

				case SWT.FocusOut:
					IInformationControl iControl= getCurrentInformationControl();
					if (iControl != null && ! iControl.isFocusControl())
						hideInformationControl();
					break;

				case SWT.MouseMove:
				case SWT.MouseEnter:
				case SWT.MouseExit:
					handleMouseMove(event);
					break;
			}
		}

		/**
		 * Handle mouse movement events.
		 *
		 * @param event the event
		 * @since 3.4
		 */
		private void handleMouseMove(Event event) {
//			if (DEBUG)
//				System.out.println("AbstractHoverInformationControl.Closer.handleMouseMove():" + event); //$NON-NLS-1$

			if (!(event.widget instanceof Control))
				return;
			Control eventControl= (Control) event.widget;

			//transform coordinates to subject control:
			Point mouseLoc= event.display.map(eventControl, fSubjectControl, event.x, event.y);

			if (fSubjectArea.contains(mouseLoc))
				return;

			IInformationControl iControl= getCurrentInformationControl();
			if (!hasInformationControlReplacer() || !canMoveIntoInformationControl(iControl)) {
				if (AbstractHoverInformationControlManager.this instanceof AnnotationBarHoverManager) {
					if (getInternalAccessor().getAllowMouseExit())
						return;
				}
				hideInformationControl();
				return;
			}

			IInformationControlExtension3 iControl3= (IInformationControlExtension3) iControl;
			Rectangle controlBounds= iControl3.getBounds();
			if (controlBounds != null) {
				Rectangle tooltipBounds= event.display.map(null, eventControl, controlBounds);
				if (tooltipBounds.contains(event.x, event.y)) {
					if (!isReplaceInProgress() && event.type != SWT.MouseExit)
						startReplaceInformationControl(event.display);
					return;
				}
				cancelReplacingDelay();
			}

			if (!fSubjectControl.getBounds().contains(mouseLoc)) {
				/*
				 *  Use inKeepUpZone() to make sure it also works when the hover is
				 *  completely outside of the subject control.
				 */
				if (!inKeepUpZone(mouseLoc.x, mouseLoc.y, fSubjectControl, fSubjectArea, true)) {
					hideInformationControl();
					return;
				}
			}
		}
	}

	/**
	 * To be installed on the manager's subject control.  Serves two different purposes:
	 * <ul>
	 * <li> start function: initiates the computation of the information to be presented. This happens on
	 * 		receipt of a mouse hover event and disables the information control manager,
	 * <li> restart function: tracks mouse move and shell activation event to determine when the information
	 * 		control manager needs to be reactivated.
	 * </ul>
	 */
	class MouseTracker extends ShellAdapter implements MouseTrackListener, MouseMoveListener {

		/** Margin around the original hover event location for computing the hover area. */
		private final static int EPSILON= 3;

		/** The area in which the original hover event occurred. */
		private Rectangle fHoverArea;
		/** The area for which is computed information is valid. */
		private Rectangle fSubjectArea;
		/** The tracker's subject control. */
		private Control fSubjectControl;

		/** Indicates whether the tracker is in restart mode ignoring hover events. */
		private boolean fIsInRestartMode= false;
		/** Indicates whether the tracker is computing the information to be presented. */
		private boolean fIsComputing= false;
		/** Indicates whether the mouse has been lost. */
		private boolean fMouseLostWhileComputing= false;
		/** Indicates whether the subject control's shell has been deactivated. */
		private boolean fShellDeactivatedWhileComputing= false;

		/**
		 * Creates a new mouse tracker.
		 */
		public MouseTracker() {
		}

		/**
		 * Sets this mouse tracker's subject area, the area to be tracked in order
		 * to re-enable the information control manager.
		 *
		 * @param subjectArea the subject area
		 */
		public void setSubjectArea(Rectangle subjectArea) {
			Assert.isNotNull(subjectArea);
			fSubjectArea= subjectArea;
		}

		/**
		 * Starts this mouse tracker. The given control becomes this tracker's subject control.
		 * Installs itself as mouse track listener on the subject control.
		 *
		 * @param subjectControl the subject control
		 */
		public void start(Control subjectControl) {
			start(subjectControl, subjectControl);
		}

		/**
		 * Starts this mouse tracker. The given control becomes this tracker's subject control.
		 * Installs itself as mouse track listener on the subject control. The mouse move listener
		 * gets installed on the area control. The subject control is the element the hover element
		 * is attached to. The area control is the container that holds the subject control. It
		 * denotes the area to be tracked for mouse move events while the hover is open. Usually
		 * this should be the root composite of the current view. If the area is too small, the
		 * hover will not close correctly on some occasions.
		 *
		 * @param subjectControl the subject control
		 * @param areaControl the area control
		 * @since 3.11
		 */
		public void start(Control subjectControl, Control areaControl) {

			fSubjectControl= subjectControl;
			fAreaControl= areaControl;

			if (fSubjectControl != null && !fSubjectControl.isDisposed())
				fSubjectControl.addMouseTrackListener(this);

			fIsInRestartMode= false;
			fIsComputing= false;
			fMouseLostWhileComputing= false;
			fShellDeactivatedWhileComputing= false;
		}

		/**
		 * Stops this mouse tracker. Removes itself  as mouse track, mouse move, and
		 * shell listener from the subject control.
		 */
		public void stop() {
			if (fSubjectControl != null && !fSubjectControl.isDisposed()) {
				fSubjectControl.removeMouseTrackListener(this);
				fSubjectControl.getShell().removeShellListener(this);
			}
			if (fAreaControl != null && !fAreaControl.isDisposed()) {
				fAreaControl.removeMouseMoveListener(this);
			}
		}

		/**
		 * Initiates the computation of the information to be presented. Sets the initial hover area
		 * to a small rectangle around the hover event location. Adds mouse move and shell activation listeners
		 * to track whether the computed information is, after completion, useful for presentation and to
		 * implement the restart function.
		 *
		 * @param event the mouse hover event
		 */
		@Override
		public void mouseHover(MouseEvent event) {
			if (fIsComputing || fIsInRestartMode ||
					(fSubjectControl != null && !fSubjectControl.isDisposed() && fSubjectControl.getShell() != fSubjectControl.getShell().getDisplay().getActiveShell())) {
				if (DEBUG)
					System.out.println("AbstractHoverInformationControlManager...mouseHover: @ " + event.x + "/" + event.y + " : hover cancelled: fIsComputing= " + fIsComputing + ", fIsInRestartMode= " + fIsInRestartMode); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
				return;
			}

			fIsInRestartMode= true;
			fIsComputing= true;
			fMouseLostWhileComputing= false;
			fShellDeactivatedWhileComputing= false;

			fHoverEventStateMask= event.stateMask;
			fHoverEvent= event;
			fHoverArea= new Rectangle(event.x - EPSILON, event.y - EPSILON, 2 * EPSILON, 2 * EPSILON );
			if (fHoverArea.x < 0)
				fHoverArea.x= 0;
			if (fHoverArea.y < 0)
				fHoverArea.y= 0;
			setSubjectArea(fHoverArea);

			if (fSubjectControl != null && !fSubjectControl.isDisposed()) {
				fSubjectControl.getShell().addShellListener(this);
			}
			if (fAreaControl != null && !fAreaControl.isDisposed()) {
				fAreaControl.addMouseMoveListener(this);
			}
			doShowInformation();
		}

		/**
		 * Deactivates this tracker's restart function and enables the information control
		 * manager. Does not have any effect if the tracker is still executing the start function (i.e.
		 * computing the information to be presented.
		 */
		protected void deactivate() {
			if (fIsComputing)
				return;

			fIsInRestartMode= false;
			if (fSubjectControl != null && !fSubjectControl.isDisposed()) {
				fSubjectControl.getShell().removeShellListener(this);
			}
			if (fAreaControl != null && !fAreaControl.isDisposed()) {
				fAreaControl.removeMouseMoveListener(this);
			}
		}

		@Override
		public void mouseEnter(MouseEvent e) {
		}

		@Override
		public void mouseExit(MouseEvent e) {
			if (!hasInformationControlReplacer() || !canMoveIntoInformationControl(getCurrentInformationControl()) || !inKeepUpZone(e.x, e.y, fSubjectControl, fSubjectArea, false)) {
				fMouseLostWhileComputing= true;
				deactivate();
			}
		}

		@Override
		public void mouseMove(MouseEvent event) {
			if (!hasInformationControlReplacer() || !canMoveIntoInformationControl(getCurrentInformationControl())) {
				if (!fSubjectArea.contains(event.x, event.y))
					deactivate();
			} else {
				if (!inKeepUpZone(event.x, event.y, fSubjectControl, fSubjectArea, false))
					deactivate();
			}
		}

		@Override
		public void shellDeactivated(ShellEvent e) {
			fShellDeactivatedWhileComputing= true;
			deactivate();
		}

		@Override
		public void shellIconified(ShellEvent e) {
			fShellDeactivatedWhileComputing= true;
			deactivate();
		}

		/**
		 * Tells this tracker that the start function processing has been completed.
		 */
		public void computationCompleted() {
			fIsComputing= false;
			fMouseLostWhileComputing= false;
			fShellDeactivatedWhileComputing= false;
		}

		/**
		 * Determines whether the computed information is still useful for presentation.
		 * This is not the case, if the shell of the subject control has been deactivated, the mouse
		 * left the subject control, or the mouse moved on, so that it is no longer in the subject
		 * area.
		 *
		 * @return <code>true</code> if information is still useful for presentation, <code>false</code> otherwise
		 */
		public boolean isMouseLost() {

			if (fMouseLostWhileComputing || fShellDeactivatedWhileComputing)
				return true;

			if (fSubjectControl != null && !fSubjectControl.isDisposed()) {
				Display display= fSubjectControl.getDisplay();
				Point p= display.getCursorLocation();
				p= fSubjectControl.toControl(p);
				if (!fSubjectArea.contains(p) && !fHoverArea.contains(p))
					return true;
			}

			return false;
		}
	}

	/**
	 * The delay in {@link ITextViewerExtension8.EnrichMode#AFTER_DELAY} mode after which
	 * the hover is enriched when the mouse has stopped moving inside the hover.
	 * @since 3.4
	 */
	private static final long HOVER_AUTO_REPLACING_DELAY= 200;

	/** The mouse tracker on the subject control */
	private MouseTracker fMouseTracker= new MouseTracker();
	/**
	 * The remembered hover event.
	 * @since 3.0
	 */
	private MouseEvent fHoverEvent= null;
	/** The remembered hover event state mask of the keyboard modifiers */
	private int fHoverEventStateMask= 0;
	/**
	 * The thread that delays replacing of the hover information control.
	 * To be accessed in the UI thread only!
	 *
	 * @since 3.4
	 */
	private Job fReplacingDelayJob;

	/**
	 * The {@link ITextViewerExtension8.EnrichMode}, may be <code>null</code>.
	 * @since 3.4
	 */
	private EnrichMode fEnrichMode;

	/**
	 * Indicates whether we have received a MouseDown event and are waiting for a MouseUp
	 * (and don't replace the information control until that happened).
	 * @since 3.4
	 */
	private boolean fWaitForMouseUp= false;

	/**
	 * Control area for the mouse tracker.
	 */
	private Control fAreaControl;

	/**
	 * Creates a new hover information control manager using the given information control creator.
	 * By default a <code>Closer</code> instance is set as this manager's closer.
	 *
	 * @param creator the information control creator
	 */
	protected AbstractHoverInformationControlManager(IInformationControlCreator creator) {
		super(creator);
		setCloser(new Closer());
		setHoverEnrichMode(ITextViewerExtension8.EnrichMode.AFTER_DELAY);
	}

	/**
	 * Tests whether a given mouse location is within the keep-up zone.
	 * The hover should not be hidden as long as the mouse stays inside this zone.
	 *
	 * @param x the x coordinate, relative to the <em>subject control</em>
	 * @param y the y coordinate, relative to the <em>subject control</em>
	 * @param subjectControl the subject control
	 * @param subjectArea the area for which the presented information is valid
	 * @param blowUp If <code>true</code>, then calculate for the closer, i.e. blow up the keepUp area.
	 *        If <code>false</code>, then use tight bounds for hover detection.
	 *
	 * @return <code>true</code> iff the mouse event occurred in the keep-up zone
	 * @since 3.4
	 */
	private boolean inKeepUpZone(int x, int y, Control subjectControl, Rectangle subjectArea, boolean blowUp) {
		if (subjectArea.contains(x, y))
			return true;

		IInformationControl iControl= getCurrentInformationControl();
		if ((iControl instanceof IInformationControlExtension5 && !((IInformationControlExtension5) iControl).isVisible())) {
			iControl= null;
			if (getInformationControlReplacer() != null) {
				iControl= getInformationControlReplacer().getCurrentInformationControl2();
				if ((iControl instanceof IInformationControlExtension5 && !((IInformationControlExtension5) iControl).isVisible())) {
					return false;
				}
			}
		}
		if (iControl instanceof IInformationControlExtension3) {
			IInformationControlExtension3 iControl3= (IInformationControlExtension3) iControl;

			Rectangle iControlBounds= subjectControl.getDisplay().map(null, subjectControl, iControl3.getBounds());
			Rectangle totalBounds= Geometry.copy(iControlBounds);
			if (blowUp && isReplaceInProgress()) {
				//Problem: blown up iControl overlaps rest of subjectArea's line
				// solution for now: only blow up for keep up (closer), but not for further hover detection
				int margin= getInformationControlReplacer().getKeepUpMargin();
				Geometry.expand(totalBounds, margin, margin, margin, margin);
			}

			if (!blowUp) {
				if (iControlBounds.contains(x, y))
					return true;

				if (subjectArea.y + subjectArea.height < iControlBounds.y) {
					// special case for hover events: subjectArea totally above iControl:
					//  +-----------+
					//  |subjectArea|
					//  +-----------+
					//  |also keepUp|
					// ++-----------+-------+
					// | InformationControl |
					// +--------------------+
					if (subjectArea.y + subjectArea.height <= y && y <= totalBounds.y) {
						// is vertically between subject area and iControl
						if (subjectArea.x <= x && x <= subjectArea.x + subjectArea.width) {
							// is below subject area (in a vertical projection)
							return true;
						}
						// FIXME: cases when subjectArea extends to left or right of iControl?
					}
					return false;

				} else if (iControlBounds.x + iControlBounds.width < subjectArea.x) {
					// special case for hover events (e.g. in overview ruler): iControl totally left of subjectArea
					// +--------------------+-----------+
					// |                    |           +-----------+
					// | InformationControl |also keepUp|subjectArea|
					// |                    |           +-----------+
					// +--------------------+-----------+
					if (iControlBounds.x + iControlBounds.width <= x && x <= subjectArea.x) {
						// is horizontally between iControl and subject area
						if (iControlBounds.y <= y && y <= iControlBounds.y + iControlBounds.height) {
							// is to the right of iControl (in a horizontal projection)
							return true;
						}
					}
					return false;

				} else if (subjectArea.x + subjectArea.width < iControlBounds.x) {
					// special case for hover events (e.g. in annotation ruler): subjectArea totally left of iControl
					//             +-----------+--------------------+
					// +-----------+           |                    |
					// |subjectArea|also keepUp| InformationControl |
					// +-----------+           |                    |
					//             +-----------+--------------------+
					if (subjectArea.x + subjectArea.width <= x && x <= iControlBounds.x) {
						// is horizontally between subject area and iControl
						if (iControlBounds.y <= y && y <= iControlBounds.y + iControlBounds.height) {
							// is to the left of iControl (in a horizontal projection)
							return true;
						}
					}
					return false;
				}
			}

			// FIXME: should maybe use convex hull, not bounding box
			totalBounds.add(subjectArea);
			if (totalBounds.contains(x, y))
				return true;
		}
		return false;
	}

	/**
	 * Tests whether the given information control allows the mouse to be moved
	 * into it.
	 *
	 * @param iControl information control or <code>null</code> if none
	 * @return <code>true</code> if information control allows mouse move into
	 *         control, <code>false</code> otherwise
	 */
	boolean canMoveIntoInformationControl(IInformationControl iControl) {
		return fEnrichMode != null && canReplace(iControl);
	}

	@Override
	protected void hideInformationControl() {
		cancelReplacingDelay();
		super.hideInformationControl();
	}

	/**
	 * Sets the hover enrich mode. Only applicable when an information
	 * control replacer has been set with
	 * {@link #setInformationControlReplacer(InformationControlReplacer)} .
	 *
	 * @param mode the enrich mode
	 * @since 3.4
	 * @see ITextViewerExtension8#setHoverEnrichMode(org.eclipse.jface.text.ITextViewerExtension8.EnrichMode)
	 */
	void setHoverEnrichMode(EnrichMode mode) {
		fEnrichMode= mode;
	}

	@Override
	void replaceInformationControl(boolean takeFocus) {
		fWaitForMouseUp= false;
		super.replaceInformationControl(takeFocus);
	}

	/**
	 * Cancels the replacing delay job.
	 * @return <code>true</code> iff canceling was successful, <code>false</code> if replacing has already started
	 */
	boolean cancelReplacingDelay() {
		fWaitForMouseUp= false;
		if (fReplacingDelayJob != null && fReplacingDelayJob.getState() != Job.RUNNING) {
			boolean cancelled= fReplacingDelayJob.cancel();
			fReplacingDelayJob= null;
//			if (DEBUG)
//				System.out.println("AbstractHoverInformationControlManager.cancelReplacingDelay(): cancelled=" + cancelled); //$NON-NLS-1$
			return cancelled;
		}
//		if (DEBUG)
//			System.out.println("AbstractHoverInformationControlManager.cancelReplacingDelay(): not delayed"); //$NON-NLS-1$
		return true;
	}

	/**
	 * Starts replacing the information control, considering the current
	 * {@link ITextViewerExtension8.EnrichMode}.
	 * If set to {@link ITextViewerExtension8.EnrichMode#AFTER_DELAY}, this
	 * method cancels previous requests and restarts the delay timer.
	 *
	 * @param display the display to be used for the call to
	 *        {@link #replaceInformationControl(boolean)} in the UI thread
	 */
	private void startReplaceInformationControl(final Display display) {
		if (fEnrichMode == EnrichMode.ON_CLICK)
			return;

		if (fReplacingDelayJob != null) {
			if (fReplacingDelayJob.getState() != Job.RUNNING) {
				if (fReplacingDelayJob.cancel()) {
					if (fEnrichMode == EnrichMode.IMMEDIATELY) {
						fReplacingDelayJob= null;
						if (! fWaitForMouseUp)
							replaceInformationControl(false);
					} else {
//						if (DEBUG)
//							System.out.println("AbstractHoverInformationControlManager.startReplaceInformationControl(): rescheduled"); //$NON-NLS-1$
						fReplacingDelayJob.schedule(HOVER_AUTO_REPLACING_DELAY);
					}
				}
			}
			return;
		}

		fReplacingDelayJob= new Job("AbstractHoverInformationControlManager Replace Delayer") { //$NON-NLS-1$
			@Override
			public IStatus run(final IProgressMonitor monitor) {
				if (monitor.isCanceled() || display.isDisposed()) {
					return Status.CANCEL_STATUS;
				}
				display.syncExec(() -> {
					fReplacingDelayJob= null;
					if (monitor.isCanceled())
						return;
					if (!fWaitForMouseUp)
						replaceInformationControl(false);
				});
				return Status.OK_STATUS;
			}
		};
		fReplacingDelayJob.setSystem(true);
		fReplacingDelayJob.setPriority(Job.INTERACTIVE);
//		if (DEBUG)
//			System.out.println("AbstractHoverInformationControlManager.startReplaceInformationControl(): scheduled"); //$NON-NLS-1$
		fReplacingDelayJob.schedule(HOVER_AUTO_REPLACING_DELAY);
	}

	@Override
	protected void presentInformation() {
		if (fMouseTracker == null) {
			super.presentInformation();
			return;
		}

		Rectangle area= getSubjectArea();
		if (area != null)
			fMouseTracker.setSubjectArea(area);

		if (fMouseTracker.isMouseLost()) {
			fMouseTracker.computationCompleted();
			fMouseTracker.deactivate();
		} else {
			fMouseTracker.computationCompleted();
			super.presentInformation();
		}
	}

	/**
	 * {@inheritDoc}
	 * @deprecated visibility will be changed to protected
	 */
	@Deprecated
	@Override
	public void setEnabled(boolean enabled) {

		boolean was= isEnabled();
		super.setEnabled(enabled);
		boolean is= isEnabled();

		if (was != is && fMouseTracker != null) {
			if (is)
				fMouseTracker.start(getSubjectControl(), fAreaControl);
			else
				fMouseTracker.stop();
		}
	}

	/**
	 * Disposes this manager's information control.
	 */
	@Override
	public void dispose() {
		if (fMouseTracker != null) {
			fMouseTracker.stop();
			fMouseTracker.fSubjectControl= null;
			fMouseTracker.fSubjectArea= null;
			fMouseTracker= null;
		}
		super.dispose();
	}

	/**
	 * Returns the location at which the most recent mouse hover event
	 * has been issued.
	 *
	 * @return the location of the most recent mouse hover event
	 */
	protected Point getHoverEventLocation() {
		return fHoverEvent != null ? new Point(fHoverEvent.x, fHoverEvent.y) : new Point(-1, -1);
	}

	/**
	 * Returns the most recent mouse hover event.
	 *
	 * @return the most recent mouse hover event or <code>null</code>
	 * @since 3.0
	 */
	protected MouseEvent getHoverEvent() {
		return fHoverEvent;
	}

	/**
	 * Returns the SWT event state of the most recent mouse hover event.
	 *
	 * @return the SWT event state of the most recent mouse hover event
	 */
	protected int getHoverEventStateMask() {
		return fHoverEventStateMask;
	}

	/**
	 * Returns an adapter that gives access to internal methods.
	 * <p>
	 * <strong>Note:</strong> This method is not intended to be referenced or overridden by clients.</p>
	 *
	 * @return the replaceable information control accessor
	 * @since 3.4
	 * @noreference This method is not intended to be referenced by clients.
	 * @nooverride This method is not intended to be re-implemented or extended by clients.
	 */
	@Override
	public InternalAccessor getInternalAccessor() {
		return new MyInternalAccessor() {
			@Override
			public void setHoverEnrichMode(EnrichMode mode) {
				AbstractHoverInformationControlManager.this.setHoverEnrichMode(mode);
			}
		};
	}

	/**
	 * Installs this manager on the given subject control. The hover control is now taking the role
	 * of the subject control. This implementation sets the control also as the information control
	 * closer's subject control and automatically enables this manager. The area control typically
	 * is the root composite of the display element (e.g. a window or a dialog) the subject control
	 * is embedded in. It is needed to correctly close popups when the mouse pointer leaves the
	 * popup area.
	 *
	 * @param subjectControl the subject control
	 * @param areaControl the area control
	 * @since 3.11
	 */
	public void install(Control subjectControl, Control areaControl) {
		fAreaControl= (areaControl != null) ? areaControl : subjectControl;
		super.install(subjectControl);
	}

	@Override
	public void install(Control subjectControl) {
		install(subjectControl, subjectControl);
	}
}
