/*******************************************************************************
 * Copyright (c) 2000, 2022 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
 *     Danail Nachev <d.nachev@gmail.com> - http://bugs.eclipse.org/348608
 *******************************************************************************/
package org.eclipse.jface.text.link;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.VerifyKeyListener;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.events.ShellListener;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.runtime.Assert;

import org.eclipse.text.undo.DocumentUndoEvent;
import org.eclipse.text.undo.DocumentUndoManagerRegistry;
import org.eclipse.text.undo.IDocumentUndoListener;
import org.eclipse.text.undo.IDocumentUndoManager;

import org.eclipse.jface.internal.text.link.contentassist.ContentAssistant2;
import org.eclipse.jface.internal.text.link.contentassist.IProposalListener;
import org.eclipse.jface.viewers.IPostSelectionProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPartitioningException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DefaultPositionUpdater;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IAutoEditStrategy;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension3;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IEditingSupport;
import org.eclipse.jface.text.IEditingSupportRegistry;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.IRewriteTarget;
import org.eclipse.jface.text.ITextInputListener;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension;
import org.eclipse.jface.text.ITextViewerExtension2;
import org.eclipse.jface.text.ITextViewerExtension5;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension6;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelExtension;
import org.eclipse.jface.text.source.ISourceViewer;


/**
 * The UI for linked mode. Detects events that influence behavior of the linked mode
 * UI and acts upon them.
 * <p>
 * <code>LinkedModeUI</code> relies on all added
 * <code>LinkedModeUITarget</code>s to provide implementations of
 * <code>ITextViewer</code> that implement <code>ITextViewerExtension</code>,
 * and the documents being edited to implement <code>IDocumentExtension3</code>.
 * </p>
 * <p>
 * Clients may instantiate and extend this class.
 * </p>
 *
 * @since 3.0
 */
public class LinkedModeUI {

	/* cycle constants */
	/**
	 * Constant indicating that this UI should never cycle from the last
	 * position to the first and vice versa.
	 */
	public static final Object CYCLE_NEVER= new Object();
	/**
	 * Constant indicating that this UI should always cycle from the last
	 * position to the first and vice versa.
	 */
	public static final Object CYCLE_ALWAYS= new Object();
	/**
	 * Constant indicating that this UI should cycle from the last position to
	 * the first and vice versa if its model is not nested.
	 */
	public static final Object CYCLE_WHEN_NO_PARENT= new Object();

	/**
	 * Listener that gets notified when the linked mode UI switches its focus position.
	 * <p>
	 * Clients may implement this interface.
	 * </p>
	 */
	public interface ILinkedModeUIFocusListener {
		/**
		 * Called when the UI for the linked mode leaves a linked position.
		 *
		 * @param position the position being left
		 * @param target the target where <code>position</code> resides in
		 */
		void linkingFocusLost(LinkedPosition position, LinkedModeUITarget target);
		/**
		 * Called when the UI for the linked mode gives focus to a linked position.
		 *
		 * @param position the position being entered
		 * @param target the target where <code>position</code> resides in
		 */
		void linkingFocusGained(LinkedPosition position, LinkedModeUITarget target);
	}

	/**
	 * Null object implementation of focus listener.
	 */
	private static final class EmtpyFocusListener implements ILinkedModeUIFocusListener {

		@Override
		public void linkingFocusGained(LinkedPosition position, LinkedModeUITarget target) {
			// ignore
		}

		@Override
		public void linkingFocusLost(LinkedPosition position, LinkedModeUITarget target) {
			// ignore
		}
	}

	/**
	 * A link target consists of a viewer and gets notified if the linked mode UI on
	 * it is being shown.
	 * <p>
	 * Clients may extend this class.
	 * </p>
	 * @since 3.0
	 */
	public static abstract class LinkedModeUITarget implements ILinkedModeUIFocusListener {
		/**
		 * Returns the viewer represented by this target, never <code>null</code>.
		 *
		 * @return the viewer associated with this target.
		 */
		public abstract ITextViewer getViewer();

		/**
		 * The viewer's text widget is initialized when the UI first connects
		 * to the viewer and never changed thereafter. This is to keep the
		 * reference of the widget that we have registered our listeners with,
		 * as the viewer, when it gets disposed, does not remember it, resulting
		 * in a situation where we cannot uninstall the listeners and a memory leak.
		 */
		StyledText fWidget;

		/** The cached shell - same reason as fWidget. */
		Shell fShell;

		/** The registered listener, or <code>null</code>. */
		KeyListener fKeyListener;

		/** The cached custom annotation model. */
		LinkedPositionAnnotations fAnnotationModel;
	}

	private static final class EmptyTarget extends LinkedModeUITarget {

		private ITextViewer fTextViewer;

		/**
		 * @param viewer the viewer
		 */
		public EmptyTarget(ITextViewer viewer) {
			Assert.isNotNull(viewer);
			fTextViewer= viewer;
		}

		@Override
		public ITextViewer getViewer() {
			return fTextViewer;
		}

		@Override
		public void linkingFocusLost(LinkedPosition position, LinkedModeUITarget target) {
		}

		@Override
		public void linkingFocusGained(LinkedPosition position, LinkedModeUITarget target) {
		}

	}

	/**
	 * Listens for state changes in the model.
	 */
	private final class ExitListener implements ILinkedModeListener {
		@Override
		public void left(LinkedModeModel model, int flags) {
			leave(ILinkedModeListener.EXIT_ALL | flags);
		}

		@Override
		public void suspend(LinkedModeModel model) {
			disconnect();
			redraw();
		}

		@Override
		public void resume(LinkedModeModel model, int flags) {
			if ((flags & ILinkedModeListener.EXIT_ALL) != 0) {
				leave(flags);
			} else {
				connect();
				if ((flags & ILinkedModeListener.SELECT) != 0)
					select();
				ensureAnnotationModelInstalled();
				redraw();
			}
		}
	}

	/**
	 * Exit flags returned if a custom exit policy wants to exit linked mode.
	 * <p>
	 * Clients may instantiate this class.
	 * </p>
	 */
	public static class ExitFlags {
		/** The flags to return in the <code>leave</code> method. */
		public int flags;
		/** The doit flag of the checked <code>VerifyKeyEvent</code>. */
		public boolean doit;
		/**
		 * Creates a new instance.
		 *
		 * @param flags the exit flags
		 * @param doit the doit flag for the verify event
		 */
		public ExitFlags(int flags, boolean doit) {
			this.flags= flags;
			this.doit= doit;
		}
	}

	/**
	 * An exit policy can be registered by a caller to get custom exit
	 * behavior.
	 * <p>
	 * Clients may implement this interface.
	 * </p>
	 */
	public interface IExitPolicy {
		/**
		 * Checks whether the linked mode should be left after receiving the
		 * given <code>VerifyEvent</code> and selection. Note that the event
		 * carries widget coordinates as opposed to <code>offset</code> and
		 * <code>length</code> which are document coordinates.
		 *
		 * @param model the linked mode model
		 * @param event the verify event
		 * @param offset the offset of the current selection
		 * @param length the length of the current selection
		 * @return valid exit flags or <code>null</code> if no special action
		 *         should be taken
		 */
		ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length);

		/**
		 * Checks whether the linked mode should be left after receiving the given
		 * <code>DocumentEvent</code>, especially allowing to control Copy-Paste operations.
		 *
		 * @param model the linked mode model
		 * @param event the document event
		 * @return valid exit flags or <code>null</code> if no special action should be taken
		 * @since 3.22
		 */
		default ExitFlags doExit(LinkedModeModel model, DocumentEvent event) {
			return null;
		}
	}

	/**
	 * A NullObject implementation of <code>IExitPolicy</code>.
	 */
	private static class NullExitPolicy implements IExitPolicy {
		@Override
		public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length) {
			return null;
		}
	}

	/**
	 * Listens for shell events and acts upon them.
	 */
	private class Closer implements ShellListener, ITextInputListener, IDocumentUndoListener {

		@Override
		public void shellActivated(ShellEvent e) {
		}

		@Override
		public void shellClosed(ShellEvent e) {
			leave(ILinkedModeListener.EXIT_ALL);
		}

		@Override
		public void shellDeactivated(ShellEvent e) {
// 			TODO re-enable after debugging
//			if (true) return;

			// from LinkedPositionUI:

			// don't deactivate on focus lost, since the proposal popups may take focus
			// plus: it doesn't hurt if you can check with another window without losing linked mode
			// since there is no intrusive popup sticking out.

			// need to check first what happens on reentering based on an open action
			// Seems to be no problem

			// Better:
			// Check with content assistant and only leave if its not the proposal shell that took the
			// focus away.

			StyledText text;
			final ITextViewer viewer;
			Display display;

			if (fCurrentTarget == null || (text= fCurrentTarget.fWidget) == null
					|| text.isDisposed() || (display= text.getDisplay()) == null
					|| display.isDisposed()
					|| (viewer= fCurrentTarget.getViewer()) == null)
			{
				leave(ILinkedModeListener.EXIT_ALL);
			}
			else
			{
				// Post in UI thread since the assistant popup will only get the focus after we lose it.
				display.asyncExec(() -> {
					if (fIsActive && viewer instanceof IEditingSupportRegistry) {
						IEditingSupport[] helpers= ((IEditingSupportRegistry) viewer).getRegisteredSupports();
						for (IEditingSupport helper : helpers) {
							if (helper.ownsFocusShell())
								return;
						}
					}

					// else
					leave(ILinkedModeListener.EXIT_ALL);

				});
			}
		}

		@Override
		public void shellDeiconified(ShellEvent e) {
		}

		@Override
		public void shellIconified(ShellEvent e) {
			leave(ILinkedModeListener.EXIT_ALL);
		}

		@Override
		public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
			leave(ILinkedModeListener.EXIT_ALL);
		}

		@Override
		public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
		}

		@Override
		public void documentUndoNotification(DocumentUndoEvent event) {
			int type= event.getEventType();
			if ((type & DocumentUndoEvent.ABOUT_TO_UNDO) != 0 || (type & DocumentUndoEvent.ABOUT_TO_REDO) != 0) {
				// default behavior: any document change outside a linked position
				// causes us to exit
				String textRemoved= event.getPreservedText();
				int end= event.getOffset() + (textRemoved != null ? textRemoved.length() : 0);
				for (int offset= event.getOffset(); offset <= end; offset++) {
					if (!fModel.anyPositionContains(offset)) {
						ITextViewer viewer= fCurrentTarget.getViewer();
						if (fFramePosition != null && viewer instanceof IEditingSupportRegistry) {
							IEditingSupport[] helpers= ((IEditingSupportRegistry) viewer).getRegisteredSupports();
							for (IEditingSupport helper : helpers) {
								if (helper.isOriginator(null, new Region(fFramePosition.getOffset(), fFramePosition.getLength())))
									return;
							}
						}

						leave(ILinkedModeListener.EXTERNAL_MODIFICATION);
						return;
					}
				}

				// Make sure that any document compound change is done committed
				endCompoundChangeIfNeeded();
			}
		}
	}

	/**
	 * @since 3.1
	 */
	private class DocumentListener implements IDocumentListener {
		@Override
		public void documentAboutToBeChanged(DocumentEvent event) {

			// default behavior: any document change outside a linked position
			// causes us to exit
			int end= event.getOffset() + event.getLength();
			for (int offset= event.getOffset(); offset <= end; offset++) {
				if (!fModel.anyPositionContains(offset)) {
					ITextViewer viewer= fCurrentTarget.getViewer();
					if (fFramePosition != null && viewer instanceof IEditingSupportRegistry) {
						IEditingSupport[] helpers= ((IEditingSupportRegistry) viewer).getRegisteredSupports();
						for (IEditingSupport helper : helpers) {
							if (helper.isOriginator(null, new Region(fFramePosition.getOffset(), fFramePosition.getLength())))
								return;
						}
					}

					leave(ILinkedModeListener.EXTERNAL_MODIFICATION);
					return;
				}
			}

			// Apply  ExitPolicy to any inserted text if the insertion is made inside a linked region
			if (fExitPolicy != null) {
				ExitFlags flags= fExitPolicy.doExit(fModel, event);
				if (flags != null) {
					leave(flags.flags);
					return;
				}
			}

			// Make sure that any document change is done inside a compound change
			beginCompoundChangeIfNeeded();

		}

		@Override
		public void documentChanged(DocumentEvent event) {
		}
	}

	/**
	 * Listens for key events, checks the exit policy for custom exit
	 * strategies but defaults to handling Tab, Enter, and Escape.
	 */
	private class KeyListener implements VerifyKeyListener {

		private boolean fIsEnabled= true;

		@Override
		public void verifyKey(VerifyEvent event) {

			if (!event.doit || !fIsEnabled)
				return;

			Point selection= fCurrentTarget.getViewer().getSelectedRange();
			int offset= selection.x;
			int length= selection.y;

			// if the custom exit policy returns anything, use that
			ExitFlags exitFlags= fExitPolicy.doExit(fModel, event, offset, length);
			if (exitFlags != null) {
				leave(exitFlags.flags);
				event.doit= exitFlags.doit;
				return;
			}

			// standard behavior:
			// (Shift+)Tab:	jumps from position to position, depending on cycle mode
			// Enter:		accepts all entries and leaves all (possibly stacked) environments, the last sets the caret
			// Esc:			accepts all entries and leaves all (possibly stacked) environments, the caret stays
			// ? what do we do to leave one level of a cycling model that is stacked?
			// -> This is only the case if the level was set up with forced cycling (CYCLE_ALWAYS), in which case
			// the caller is sure that one does not need by-level exiting.
			switch (event.character) {
				// [SHIFT-]TAB = hop between edit boxes
				case 0x09:
					if (!(fExitPosition != null && fExitPosition.includes(offset)) && !fModel.anyPositionContains(offset)) {
						// outside any edit box -> leave (all? TODO should only leave the affected, level and forward to the next upper)
						leave(ILinkedModeListener.EXIT_ALL);
						break;
					}

					if (event.stateMask == SWT.SHIFT)
						previous();
					else
						next();

					event.doit= false;
					break;

				// ENTER
				case 0x0A:
				// Ctrl+Enter on WinXP
				case 0x0D:
//					if ((fExitPosition != null && fExitPosition.includes(offset)) || !fModel.anyPositionContains(offset)) {
					if (!fModel.anyPositionContains(offset)) {
//					if ((fExitPosition == null || !fExitPosition.includes(offset)) && !fModel.anyPositionContains(offset)) {
						// outside any edit box or on exit position -> leave (all? TODO should only leave the affected, level and forward to the next upper)
						leave(ILinkedModeListener.EXIT_ALL);
						break;
					}

					// normal case: exit entire stack and put caret to final position
					leave(ILinkedModeListener.EXIT_ALL | ILinkedModeListener.UPDATE_CARET);
					event.doit= false;
					break;

				// ESC
				case 0x1B:
					// exit entire stack and leave caret
					leave(ILinkedModeListener.EXIT_ALL);
					event.doit= false;
					break;

				default:
					if (event.character != 0) {
						if (!controlUndoBehavior(offset, length)) {
							leave(ILinkedModeListener.EXIT_ALL);
							break;
						}
					}
			}
		}

		private boolean controlUndoBehavior(int offset, int length) {
			LinkedPosition position= fModel.findPosition(new LinkedPosition(fCurrentTarget.getViewer().getDocument(), offset, length, LinkedPositionGroup.NO_STOP));
			if (position != null) {

				// if the last position is not the same and there is an open change: close it.
				if (!position.equals(fPreviousPosition))
					endCompoundChangeIfNeeded();

				beginCompoundChangeIfNeeded();
			}

			fPreviousPosition= position;
			return fPreviousPosition != null;
		}

		/**
		 * @param enabled the new enabled state
		 */
		public void setEnabled(boolean enabled) {
			fIsEnabled= enabled;
		}

	}

	/**
	 * Installed as post selection listener on the watched viewer. Updates the
	 * linked position after cursor movement, even to positions not in the
	 * iteration list.
	 */
	private class MySelectionListener implements ISelectionChangedListener {

		@Override
		public void selectionChanged(SelectionChangedEvent event) {
			ISelection selection= event.getSelection();
			if (selection instanceof ITextSelection) {
				ITextSelection textsel= (ITextSelection) selection;
				if (event.getSelectionProvider() instanceof ITextViewer) {
					IDocument doc= ((ITextViewer) event.getSelectionProvider()).getDocument();
					if (doc != null) {
						int offset= textsel.getOffset();
						int length= textsel.getLength();
						if (offset >= 0 && length >= 0) {
							LinkedPosition find= new LinkedPosition(doc, offset, length, LinkedPositionGroup.NO_STOP);
							LinkedPosition pos= fModel.findPosition(find);
							if (pos == null && fExitPosition != null && fExitPosition.includes(find))
								pos= fExitPosition;

							if (pos != null)
								switchPosition(pos, false, false);
						}
					}
				}
			}
		}

	}

	private class ProposalListener implements IProposalListener {

		@Override
		public void proposalChosen(ICompletionProposal proposal) {
			next();
		}
	}

	/** The current viewer. */
	private LinkedModeUITarget fCurrentTarget;
	/**
	 * The manager of the linked positions we provide a UI for.
	 * @since 3.1
	 */
	private LinkedModeModel fModel;
	/** The set of viewers we manage. */
	private LinkedModeUITarget[] fTargets;
	/** The iterator over the tab stop positions. */
	private TabStopIterator fIterator;

	/* Our team of event listeners */
	/** The shell listener. */
	private Closer fCloser= new Closer();
	/** The linked mode listener. */
	private ILinkedModeListener fLinkedListener= new ExitListener();
	/** The selection listener. */
	private MySelectionListener fSelectionListener= new MySelectionListener();
	/** The content assist listener. */
	private ProposalListener fProposalListener= new ProposalListener();
	/**
	 * The document listener.
	 * @since 3.1
	 */
	private IDocumentListener fDocumentListener= new DocumentListener();

	/** The last caret position, used by fCaretListener. */
	private final Position fCaretPosition= new Position(0, 0);
	/** The exit policy to control custom exit behavior */
	private IExitPolicy fExitPolicy= new NullExitPolicy();
	/** The current frame position shown in the UI, or <code>null</code>. */
	private LinkedPosition fFramePosition;
	/** The last visited position, used for undo / redo. */
	private LinkedPosition fPreviousPosition;
	/** The content assistant used to show proposals. */
	private ContentAssistant2 fAssistant;
	/** The exit position. */
	private LinkedPosition fExitPosition;
	/** State indicator to prevent multiple invocation of leave. */
	private boolean fIsActive= false;
	/** The position updater for the exit position. */
	private IPositionUpdater fPositionUpdater= new DefaultPositionUpdater(getCategory());
	/** Whether to show context info. */
	private boolean fDoContextInfo= false;
	/** Whether we have begun a compound change, but not yet closed. */
	private boolean fHasOpenCompoundChange= false;
	/** The position listener. */
	private ILinkedModeUIFocusListener fPositionListener= new EmtpyFocusListener();

	private IAutoEditStrategy fAutoEditVetoer= (document, command) -> {
		// invalidate the change to ensure that the change is performed on the document only.
		if (fModel.anyPositionContains(command.offset)) {
			command.doit= false;
			command.caretOffset= command.offset + command.length;
		}

	};


	/** Whether this UI is in simple highlighting mode or not. */
	private boolean fSimple;

	/**
	 * Creates a new UI on the given model and the set of viewers. The model
	 * must provide a tab stop sequence with a non-empty list of tab stops.
	 *
	 * @param model the linked mode model
	 * @param targets the non-empty list of targets upon which the linked mode
	 *        UI should act
	 */
	public LinkedModeUI(LinkedModeModel model, LinkedModeUITarget[] targets) {
		constructor(model, targets);
	}

	/**
	 * Convenience constructor for just one viewer.
	 *
	 * @param model the linked mode model
	 * @param viewer the viewer upon which the linked mode UI should act
	 */
	public LinkedModeUI(LinkedModeModel model, ITextViewer viewer) {
		constructor(model, new LinkedModeUITarget[]{new EmptyTarget(viewer)});
	}

	/**
	 * Convenience constructor for multiple viewers.
	 *
	 * @param model the linked mode model
	 * @param viewers the non-empty list of viewers upon which the linked mode
	 *        UI should act
	 */
	public LinkedModeUI(LinkedModeModel model, ITextViewer[] viewers) {
		LinkedModeUITarget[] array= new LinkedModeUITarget[viewers.length];
		for (int i= 0; i < array.length; i++) {
			array[i]= new EmptyTarget(viewers[i]);
		}
		constructor(model, array);
	}

	/**
	 * Convenience constructor for one target.
	 *
	 * @param model the linked mode model
	 * @param target the target upon which the linked mode UI should act
	 */
	public LinkedModeUI(LinkedModeModel model, LinkedModeUITarget target) {
		constructor(model, new LinkedModeUITarget[]{target});
	}

	/**
	 * This does the actual constructor work.
	 *
	 * @param model the linked mode model
	 * @param targets the non-empty array of targets upon which the linked mode UI
	 *        should act
	 */
	private void constructor(LinkedModeModel model, LinkedModeUITarget[] targets) {
		Assert.isNotNull(model);
		Assert.isNotNull(targets);
		Assert.isTrue(targets.length > 0);
		Assert.isTrue(!model.getTabStopSequence().isEmpty());

		fModel= model;
		fTargets= targets;
		fCurrentTarget= targets[0];
		fIterator= new TabStopIterator(fModel.getTabStopSequence());
		fIterator.setCycling(!fModel.isNested());
		fModel.addLinkingListener(fLinkedListener);

		fAssistant= new ContentAssistant2();
		fAssistant.addProposalListener(fProposalListener);
		// TODO find a way to set up content assistant.
//		fAssistant.setDocumentPartitioning(IJavaPartitions.JAVA_PARTITIONING);
		fCaretPosition.delete();
	}

	/**
	 * Starts this UI on the first position.
	 */
	public void enter() {
		fIsActive= true;
		connect();
		next();
	}

	/**
	 * Sets an <code>IExitPolicy</code> to customize the exit behavior of
	 * this linked mode UI.
	 *
	 * @param policy the exit policy to use.
	 */
	public void setExitPolicy(IExitPolicy policy) {
		fExitPolicy= policy;
	}

	/**
	 * Sets the exit position to move the caret to when linked mode mode is
	 * exited.
	 *
	 * @param target the target where the exit position is located
	 * @param offset the offset of the exit position
	 * @param length the length of the exit position (in case there should be a
	 *        selection)
	 * @param sequence set to the tab stop position of the exit position, or
	 *        <code>LinkedPositionGroup.NO_STOP</code> if there should be no
	 *        tab stop.
	 * @throws BadLocationException if the position is not valid in the viewer's
	 *         document
	 */
	public void setExitPosition(LinkedModeUITarget target, int offset, int length, int sequence) throws BadLocationException {
		// remove any existing exit position
		if (fExitPosition != null) {
			fExitPosition.getDocument().removePosition(fExitPosition);
			fIterator.removePosition(fExitPosition);
			fExitPosition= null;
		}

		IDocument doc= target.getViewer().getDocument();
		if (doc == null)
			return;

		fExitPosition= new LinkedPosition(doc, offset, length, sequence);
		doc.addPosition(fExitPosition); // gets removed in leave()
		if (sequence != LinkedPositionGroup.NO_STOP)
			fIterator.addPosition(fExitPosition);

	}

	/**
	 * Sets the exit position to move the caret to when linked mode is exited.
	 *
	 * @param viewer the viewer where the exit position is located
	 * @param offset the offset of the exit position
	 * @param length the length of the exit position (in case there should be a
	 *        selection)
	 * @param sequence set to the tab stop position of the exit position, or
	 * 		  <code>LinkedPositionGroup.NO_STOP</code> if there should be no tab stop.
	 * @throws BadLocationException if the position is not valid in the
	 *         viewer's document
	 */
	public void setExitPosition(ITextViewer viewer, int offset, int length, int sequence) throws BadLocationException {
		setExitPosition(new EmptyTarget(viewer), offset, length, sequence);
	}

	/**
	 * Sets the cycling mode to either of <code>CYCLING_ALWAYS</code>,
	 * <code>CYCLING_NEVER</code>, or <code>CYCLING_WHEN_NO_PARENT</code>,
	 * which is the default.
	 *
	 * @param mode the new cycling mode.
	 */
	public void setCyclingMode(Object mode) {
		if (mode != CYCLE_ALWAYS && mode != CYCLE_NEVER && mode != CYCLE_WHEN_NO_PARENT)
			throw new IllegalArgumentException();

		if (mode == CYCLE_ALWAYS || mode == CYCLE_WHEN_NO_PARENT && !fModel.isNested())
			fIterator.setCycling(true);
		else
			fIterator.setCycling(false);
	}

	void next() {
		if (fIterator.hasNext(fFramePosition)) {
			switchPosition(fIterator.next(fFramePosition), true, true);
			return;
		}
		leave(ILinkedModeListener.UPDATE_CARET);
	}

	void previous() {
		if (fIterator.hasPrevious(fFramePosition)) {
			switchPosition(fIterator.previous(fFramePosition), true, true);
		} else
			// dont't update caret, but rather select the current frame
			leave(ILinkedModeListener.SELECT);
	}

	private void triggerContextInfo() {
		fAssistant.showContextInformation();
	}

	/** Trigger content assist on choice positions */
	private void triggerContentAssist() {
		if (fFramePosition instanceof ProposalPosition) {
			ProposalPosition pp= (ProposalPosition) fFramePosition;
			ICompletionProposal[] choices= pp.getChoices();
			if (choices != null && choices.length > 0) {
				fAssistant.setCompletions(choices);
				fAssistant.showPossibleCompletions();
				return;
			}
		}

		fAssistant.setCompletions(new ICompletionProposal[0]);
		fAssistant.hidePossibleCompletions();
	}

	private void switchPosition(LinkedPosition pos, boolean select, boolean showProposals) {
		Assert.isNotNull(pos);
		if (pos.equals(fFramePosition))
			return;

		if (fFramePosition != null && fCurrentTarget != null)
			fPositionListener.linkingFocusLost(fFramePosition, fCurrentTarget);

		// undo
		endCompoundChangeIfNeeded();

		redraw(); // redraw current position being left - usually not needed
		IDocument oldDoc= fFramePosition == null ? null : fFramePosition.getDocument();
		IDocument newDoc= pos.getDocument();

		switchViewer(oldDoc, newDoc, pos);
		fFramePosition= pos;

		if (select)
			select();
		if (fFramePosition == fExitPosition && !fIterator.isCycling())
			leave(ILinkedModeListener.NONE);
		else {
			redraw(); // redraw new position
			ensureAnnotationModelInstalled();
		}
		if (showProposals)
			triggerContentAssist();
		if (fFramePosition != fExitPosition && fDoContextInfo)
			triggerContextInfo();

		if (fFramePosition != null && fCurrentTarget != null)
			fPositionListener.linkingFocusGained(fFramePosition, fCurrentTarget);

	}

	private void ensureAnnotationModelInstalled() {
		LinkedPositionAnnotations lpa= fCurrentTarget.fAnnotationModel;
		if (lpa != null) {
			ITextViewer viewer= fCurrentTarget.getViewer();
			if (viewer instanceof ISourceViewer) {
				ISourceViewer sv= (ISourceViewer) viewer;
				IAnnotationModel model= sv.getAnnotationModel();
				if (model instanceof IAnnotationModelExtension) {
					IAnnotationModelExtension ext= (IAnnotationModelExtension) model;
					IAnnotationModel ourModel= ext.getAnnotationModel(getUniqueKey());
					if (ourModel == null) {
						ext.addAnnotationModel(getUniqueKey(), lpa);
					}
				}
			}
		}
	}

	private void uninstallAnnotationModel(LinkedModeUITarget target) {
		ITextViewer viewer= target.getViewer();
		if (viewer instanceof ISourceViewer) {
			ISourceViewer sv= (ISourceViewer) viewer;
			IAnnotationModel model= sv.getAnnotationModel();
			if (model instanceof IAnnotationModelExtension) {
				IAnnotationModelExtension ext= (IAnnotationModelExtension) model;
				ext.removeAnnotationModel(getUniqueKey());
			}
		}
	}

	private void switchViewer(IDocument oldDoc, IDocument newDoc, LinkedPosition pos) {
		if (oldDoc != newDoc) {

			// redraw current document with new position before switching viewer
			if (fCurrentTarget.fAnnotationModel != null)
				fCurrentTarget.fAnnotationModel.switchToPosition(fModel, pos);

			LinkedModeUITarget target= null;
			for (LinkedModeUITarget fTarget : fTargets) {
				if (fTarget.getViewer().getDocument() == newDoc) {
					target= fTarget;
					break;
				}
			}
			if (target != fCurrentTarget) {
				disconnect();
				fCurrentTarget= target;
				target.linkingFocusLost(fFramePosition, target);
				connect();
				ensureAnnotationModelInstalled();
				if (fCurrentTarget != null)
					fCurrentTarget.linkingFocusGained(pos, fCurrentTarget);
			}
		}
	}

	private void select() {
		ITextViewer viewer= fCurrentTarget.getViewer();
		if (viewer instanceof ITextViewerExtension5) {
			ITextViewerExtension5 extension5= (ITextViewerExtension5) viewer;
			extension5.exposeModelRange(new Region(fFramePosition.offset, fFramePosition.length));
		} else if (!viewer.overlapsWithVisibleRegion(fFramePosition.offset, fFramePosition.length)) {
			viewer.resetVisibleRegion();
		}
		viewer.revealRange(fFramePosition.offset, fFramePosition.length);
		viewer.setSelectedRange(fFramePosition.offset, fFramePosition.length);
	}

	private void redraw() {
		if (fCurrentTarget.fAnnotationModel != null)
			fCurrentTarget.fAnnotationModel.switchToPosition(fModel, fFramePosition);
	}

	private void connect() {
		Assert.isNotNull(fCurrentTarget);
		ITextViewer viewer= fCurrentTarget.getViewer();
		Assert.isNotNull(viewer);
		fCurrentTarget.fWidget= viewer.getTextWidget();
		if (fCurrentTarget.fWidget == null)
			leave(ILinkedModeListener.EXIT_ALL);

		if (fCurrentTarget.fKeyListener == null) {
			fCurrentTarget.fKeyListener= new KeyListener();
			((ITextViewerExtension) viewer).prependVerifyKeyListener(fCurrentTarget.fKeyListener);
		} else
			fCurrentTarget.fKeyListener.setEnabled(true);

		registerAutoEditVetoer(viewer);

		((IPostSelectionProvider) viewer).addPostSelectionChangedListener(fSelectionListener);

		createAnnotationModel();

		showSelection();

		fCurrentTarget.fShell= fCurrentTarget.fWidget.getShell();
		if (fCurrentTarget.fShell == null)
			leave(ILinkedModeListener.EXIT_ALL);
		fCurrentTarget.fShell.addShellListener(fCloser);

		fAssistant.install(viewer);

		viewer.addTextInputListener(fCloser);

		viewer.getDocument().addDocumentListener(fDocumentListener);

		IDocumentUndoManager undoManager= DocumentUndoManagerRegistry.getDocumentUndoManager(viewer.getDocument());
		if (undoManager != null) {
			undoManager.addDocumentUndoListener(fCloser);
		}
	}

	/**
	 * Reveals the selection on the current target's widget, if it is valid.
	 */
	private void showSelection() {
		final StyledText widget= fCurrentTarget.fWidget;
		if (widget == null || widget.isDisposed())
			return;

		// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=132263
		widget.getDisplay().asyncExec(() -> {
			if (!widget.isDisposed())
				try {
					widget.showSelection();
				} catch (IllegalArgumentException e) {
					/*
					 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=66914
					 * if the StyledText is in setRedraw(false) mode, its
					 * selection may not be up2date and calling showSelection
					 * will throw an IAE.
					 * We don't have means to find out whether the selection is valid
					 * or whether the widget is redrawing or not therefore we try
					 * and ignore an IAE.
					 */
				}
		});
	}

	/**
	 * Registers our auto edit vetoer with the viewer.
	 *
	 * @param viewer the viewer we want to veto ui-triggered changes within
	 *        linked positions
	 */
	private void registerAutoEditVetoer(ITextViewer viewer) {
		try {
			String[] contentTypes= getContentTypes(viewer.getDocument());
			if (viewer instanceof ITextViewerExtension2) {
				ITextViewerExtension2 vExtension= ((ITextViewerExtension2) viewer);
				for (String contentType : contentTypes) {
					vExtension.prependAutoEditStrategy(fAutoEditVetoer, contentType);
				}
			} else {
				Assert.isTrue(false);
			}

		} catch (BadPartitioningException e) {
			leave(ILinkedModeListener.EXIT_ALL);
		}
	}

	private void unregisterAutoEditVetoer(ITextViewer viewer) {
		try {
			String[] contentTypes= getContentTypes(viewer.getDocument());
			if (viewer instanceof ITextViewerExtension2) {
				ITextViewerExtension2 vExtension= ((ITextViewerExtension2) viewer);
				for (String contentType : contentTypes) {
					vExtension.removeAutoEditStrategy(fAutoEditVetoer, contentType);
				}
			} else {
				Assert.isTrue(false);
			}
		} catch (BadPartitioningException e) {
			leave(ILinkedModeListener.EXIT_ALL);
		}
	}

	/**
	 * Returns all possible content types of <code>document</code>.
	 *
	 * @param document the document
	 * @return all possible content types of <code>document</code>
	 * @throws BadPartitioningException if partitioning is invalid for this document
	 * @since 3.1
	 */
	private String[] getContentTypes(IDocument document) throws BadPartitioningException {
		if (document instanceof IDocumentExtension3) {
			IDocumentExtension3 ext= (IDocumentExtension3) document;
			String[] partitionings= ext.getPartitionings();
			Set<String> contentTypes= new HashSet<>(20);
			for (String partitioning : partitionings) {
				contentTypes.addAll(Arrays.asList(ext.getLegalContentTypes(partitioning)));
			}
			contentTypes.add(IDocument.DEFAULT_CONTENT_TYPE);
			return contentTypes.toArray(new String[contentTypes.size()]);
		}
		return document.getLegalContentTypes();
	}

	private void createAnnotationModel() {
		if (fCurrentTarget.fAnnotationModel == null) {
			LinkedPositionAnnotations lpa= new LinkedPositionAnnotations();
			if (fSimple) {
				lpa.markExitTarget(true);
				lpa.markFocus(false);
				lpa.markSlaves(false);
				lpa.markTargets(false);
			}
			lpa.setTargets(fIterator.getPositions());
			lpa.setExitTarget(fExitPosition);
			lpa.connect(fCurrentTarget.getViewer().getDocument());
			fCurrentTarget.fAnnotationModel= lpa;
		}
	}

	private String getUniqueKey() {
		return "linked.annotationmodelkey."+toString(); //$NON-NLS-1$
	}

	private void disconnect() {
		Assert.isNotNull(fCurrentTarget);
		ITextViewer viewer= fCurrentTarget.getViewer();
		Assert.isNotNull(viewer);

		viewer.getDocument().removeDocumentListener(fDocumentListener);

		fAssistant.uninstall();
		fAssistant.removeProposalListener(fProposalListener);

		fCurrentTarget.fWidget= null;

		Shell shell= fCurrentTarget.fShell;
		fCurrentTarget.fShell= null;

		if (shell != null && !shell.isDisposed())
			shell.removeShellListener(fCloser);

		// this one is asymmetric: we don't install the model in
		// connect, but leave it to its callers to ensure they
		// have the model installed if they need it
		uninstallAnnotationModel(fCurrentTarget);

		unregisterAutoEditVetoer(viewer);

		// don't remove the verify key listener to let it keep its position
		// in the listener queue
		if (fCurrentTarget.fKeyListener != null)
			fCurrentTarget.fKeyListener.setEnabled(false);

		((IPostSelectionProvider) viewer).removePostSelectionChangedListener(fSelectionListener);

		IDocumentUndoManager undoManager= DocumentUndoManagerRegistry.getDocumentUndoManager(viewer.getDocument());
		if (undoManager != null) {
			undoManager.removeDocumentUndoListener(fCloser);
		}

		redraw();
	}

	void leave(final int flags) {
		if (!fIsActive)
			return;
		fIsActive= false;

		endCompoundChangeIfNeeded();

		Display display= null;
		if (fCurrentTarget.fWidget != null && !fCurrentTarget.fWidget.isDisposed())
			display= fCurrentTarget.fWidget.getDisplay();

		if (fCurrentTarget.fAnnotationModel != null)
			fCurrentTarget.fAnnotationModel.removeAllAnnotations();
		disconnect();

		for (LinkedModeUITarget fTarget : fTargets) {
			LinkedModeUITarget target= fTarget;
			ITextViewer viewer= target.getViewer();
			if (target.fKeyListener != null) {
				((ITextViewerExtension) viewer).removeVerifyKeyListener(target.fKeyListener);
				target.fKeyListener= null;
			}

			viewer.removeTextInputListener(fCloser);
		}

		for (LinkedModeUITarget fTarget : fTargets) {
			if (fTarget.fAnnotationModel != null) {
				fTarget.fAnnotationModel.removeAllAnnotations();
				fTarget.fAnnotationModel.disconnect(fTarget.getViewer().getDocument());
				fTarget.fAnnotationModel = null;
			}
			uninstallAnnotationModel(fTarget);
		}


		if ((flags & ILinkedModeListener.UPDATE_CARET) != 0 && fExitPosition != null && fFramePosition != fExitPosition && !fExitPosition.isDeleted())
			switchPosition(fExitPosition, true, false);

		final List<IDocument> docs= new ArrayList<>();
		for (LinkedModeUITarget fTarget : fTargets) {
			IDocument doc= fTarget.getViewer().getDocument();
			if (doc != null)
				docs.add(doc);
		}

		fModel.stopForwarding(flags);

		Runnable runnable= () -> {
			if (fExitPosition != null)
				fExitPosition.getDocument().removePosition(fExitPosition);

			for (IDocument doc : docs) {
				doc.removePositionUpdater(fPositionUpdater);
				boolean uninstallCat= false;
				String[] cats= doc.getPositionCategories();
				for (String cat : cats) {
					if (getCategory().equals(cat)) {
						uninstallCat= true;
						break;
					}
				}
				if (uninstallCat)
					try {
						doc.removePositionCategory(getCategory());
					} catch (BadPositionCategoryException e) {
						// ignore
					}
			}
			fModel.exit(flags);
		};

		// remove positions (both exit positions AND linked positions in the
		// model) asynchronously to make sure that the annotation painter
		// gets correct document offsets.
		if (display != null)
			display.asyncExec(runnable);
		else
			runnable.run();
	}

	private void endCompoundChangeIfNeeded() {
		if (fHasOpenCompoundChange) {
			ITextViewerExtension extension= (ITextViewerExtension) fCurrentTarget.getViewer();
			IRewriteTarget target= extension.getRewriteTarget();
			target.endCompoundChange();
			fHasOpenCompoundChange= false;
		}
	}

	private void beginCompoundChangeIfNeeded() {
		if (!fHasOpenCompoundChange) {
			ITextViewerExtension extension= (ITextViewerExtension) fCurrentTarget.getViewer();
			IRewriteTarget target= extension.getRewriteTarget();
			target.beginCompoundChange();
			fHasOpenCompoundChange= true;
		}
	}

	/**
	 * Returns the currently selected region or <code>null</code>.
	 *
	 * @return the currently selected region or <code>null</code>
	 */
	public IRegion getSelectedRegion() {
		if (fFramePosition != null)
			return new Region(fFramePosition.getOffset(), fFramePosition.getLength());
		if (fExitPosition != null)
			return new Region(fExitPosition.getOffset(), fExitPosition.getLength());
		return null;
	}

	private String getCategory() {
		return toString();
	}

	/**
	 * Sets the context info property. If set to <code>true</code>, context
	 * info will be invoked on the current target's viewer whenever a position
	 * is switched.
	 *
	 * @param doContextInfo <code>true</code> if context information should be
	 *        displayed
	 */
	public void setDoContextInfo(boolean doContextInfo) {
		fDoContextInfo= doContextInfo;
	}

	/**
	 * Sets the focus callback which will get informed when the focus of the
	 * linked mode UI changes.
	 * <p>
	 * If there is a listener installed already, it will be replaced.
	 * </p>
	 *
	 * @param listener the new listener, never <code>null</code>.
	 */
	protected void setPositionListener(ILinkedModeUIFocusListener listener) {
		Assert.isNotNull(listener);
		fPositionListener= listener;
	}

	/**
	 * Sets the "simple" mode of the receiver. A linked mode UI in simple mode
	 * merely draws the exit position, but not the target, focus, and slave
	 * positions. Default is <code>false</code>. This method must be called
	 * before it is entered.
	 *
	 * @param simple <code>true</code> if the UI should be in simple mode.
	 */
	public void setSimpleMode(boolean simple) {
		fSimple= simple;
	}

	/**
	 * Enables the support for colored labels in the proposal popup.
	 * <p>Completion proposals can implement {@link ICompletionProposalExtension6}
	 * to provide colored proposal labels.</p>
	 *
	 * @param isEnabled if <code>true</code> the support for colored labels is enabled in the proposal popup
	 * @since 3.4
	 */
	public void enableColoredLabels(boolean isEnabled) {
		fAssistant.enableColoredLabels(isEnabled);
	}

}
