/*******************************************************************************
 * 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;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
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.widgets.Display;
import org.eclipse.swt.widgets.Shell;

import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.AbstractOperation;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IOperationHistoryListener;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.commands.operations.ObjectUndoContext;
import org.eclipse.core.commands.operations.OperationHistoryEvent;
import org.eclipse.core.commands.operations.OperationHistoryFactory;

import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;

import org.eclipse.jface.dialogs.MessageDialog;


/**
 * Standard implementation of {@link org.eclipse.jface.text.IUndoManager}.
 * <p>
 * It registers with the connected text viewer as text input listener and
 * document listener and logs all changes. It also monitors mouse and keyboard
 * activities in order to partition the stream of text changes into undo-able
 * edit commands.
 * </p>
 * <p>
 * Since 3.1 this undo manager is a facade to the global operation history.
 * </p>
 * <p>
 * The usage of {@link org.eclipse.core.runtime.IAdaptable} in the JFace
 * layer has been approved by Platform UI, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=87669#c9
 * </p>
 * <p>
 * This class is not intended to be subclassed.
 * </p>
 *
 * @see org.eclipse.jface.text.ITextViewer
 * @see org.eclipse.jface.text.ITextInputListener
 * @see org.eclipse.jface.text.IDocumentListener
 * @see org.eclipse.core.commands.operations.IUndoableOperation
 * @see org.eclipse.core.commands.operations.IOperationHistory
 * @see MouseListener
 * @see KeyListener
 * @deprecated As of 3.2, replaced by {@link TextViewerUndoManager}
 * @noextend This class is not intended to be subclassed by clients.
 */
@Deprecated
public class DefaultUndoManager implements IUndoManager, IUndoManagerExtension {

	/**
	 * Represents an undo-able edit command.
	 * <p>
	 * Since 3.1 this implements the interface for IUndoableOperation.
	 * </p>
	 */
	class TextCommand extends AbstractOperation {

		/** The start index of the replaced text. */
		protected int fStart= -1;
		/** The end index of the replaced text. */
		protected int fEnd= -1;
		/** The newly inserted text. */
		protected String fText;
		/** The replaced text. */
		protected String fPreservedText;

		/** The undo modification stamp. */
		protected long fUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
		/** The redo modification stamp. */
		protected long fRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;

		/**
		 * Creates a new text command.
		 *
		 * @param context the undo context for this command
		 * @since 3.1
		 */
		TextCommand(IUndoContext context) {
			super(JFaceTextMessages.getString("DefaultUndoManager.operationLabel")); //$NON-NLS-1$
			addContext(context);
		}

		/**
		 * Re-initializes this text command.
		 */
		protected void reinitialize() {
			fStart= fEnd= -1;
			fText= fPreservedText= null;
			fUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
			fRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
		}

		/**
		 * Sets the start and the end index of this command.
		 *
		 * @param start the start index
		 * @param end the end index
		 */
		protected void set(int start, int end) {
			fStart= start;
			fEnd= end;
			fText= null;
			fPreservedText= null;
		}

		@Override
		public void dispose() {
			reinitialize();
		}

		/**
		 * Undo the change described by this command.
		 *
		 * @since 2.0
		 */
		protected void undoTextChange() {
			try {
				IDocument document= fTextViewer.getDocument();
				if (document instanceof IDocumentExtension4)
					((IDocumentExtension4)document).replace(fStart, fText.length(), fPreservedText, fUndoModificationStamp);
				else
					document.replace(fStart, fText.length(), fPreservedText);
			} catch (BadLocationException x) {
			}
		}

		@Override
		public boolean canUndo() {

			if (isConnected() && isValid()) {
				IDocument doc= fTextViewer.getDocument();
				if (doc instanceof IDocumentExtension4) {
					long docStamp= ((IDocumentExtension4)doc).getModificationStamp();

					// Normal case: an undo is valid if its redo will restore document
					// to its current modification stamp
					boolean canUndo= docStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP ||
						docStamp == getRedoModificationStamp();

					/* Special case to check if the answer is false.
					 * If the last document change was empty, then the document's
					 * modification stamp was incremented but nothing was committed.
					 * The operation being queried has an older stamp.  In this case only,
					 * the comparison is different.  A sequence of document changes that
					 * include an empty change is handled correctly when a valid commit
					 * follows the empty change, but when #canUndo() is queried just after
					 * an empty change, we must special case the check.  The check is very
					 * specific to prevent false positives.
					 * see https://bugs.eclipse.org/bugs/show_bug.cgi?id=98245
					 */
					if (!canUndo &&
							this == fHistory.getUndoOperation(fUndoContext)  &&  // this is the latest operation
							this != fCurrent && // there is a more current operation not on the stack
							!fCurrent.isValid() &&  // the current operation is not a valid document modification
							fCurrent.fUndoModificationStamp != // the invalid current operation has a document stamp
								IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) {
						canUndo= fCurrent.fRedoModificationStamp == docStamp;
					}
					/*
					 * When the composite is the current command, it may hold the timestamp
					 * of a no-op change.  We check this here rather than in an override of
					 * canUndo() in CompoundTextCommand simply to keep all the special case checks
					 * in one place.
					 */
					if (!canUndo &&
							this == fHistory.getUndoOperation(fUndoContext)  &&  // this is the latest operation
							this instanceof CompoundTextCommand &&
							this == fCurrent && // this is the current operation
							this.fStart == -1 &&  // the current operation text is not valid
							fCurrent.fRedoModificationStamp != IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) {  // but it has a redo stamp
						canUndo= fCurrent.fRedoModificationStamp == docStamp;
					}

				}
				// if there is no timestamp to check, simply return true per the 3.0.1 behavior
				return true;
			}
			return false;
		}

		@Override
		public boolean canRedo() {
			if (isConnected() && isValid()) {
				IDocument doc= fTextViewer.getDocument();
				if (doc instanceof IDocumentExtension4) {
					long docStamp= ((IDocumentExtension4)doc).getModificationStamp();
					return docStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP ||
						docStamp == getUndoModificationStamp();
				}
				// if there is no timestamp to check, simply return true per the 3.0.1 behavior
				return true;
			}
			return false;
		}

		@Override
		public boolean canExecute() {
			return isConnected();
		}

		@Override
		public IStatus execute(IProgressMonitor monitor, IAdaptable uiInfo) {
			// Text commands execute as they are typed, so executing one has no effect.
			return Status.OK_STATUS;
		}

		/*
		 * Undo the change described by this command. Also selects and
		 * reveals the change.
		 */

		/**
		 * Undo the change described by this command. Also selects and
		 * reveals the change.
		 *
		 * @param monitor	the progress monitor to use if necessary
		 * @param uiInfo	an adaptable that can provide UI info if needed
		 * @return the status
		 */
		@Override
		public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) {
			if (isValid()) {
				undoTextChange();
				selectAndReveal(fStart, fPreservedText == null ? 0 : fPreservedText.length());
				resetProcessChangeSate();
				return Status.OK_STATUS;
			}
			return IOperationHistory.OPERATION_INVALID_STATUS;
		}

		/**
		 * Re-applies the change described by this command.
		 *
		 * @since 2.0
		 */
		protected void redoTextChange() {
			try {
				IDocument document= fTextViewer.getDocument();
				if (document instanceof IDocumentExtension4)
					((IDocumentExtension4)document).replace(fStart, fEnd - fStart, fText, fRedoModificationStamp);
				else
					fTextViewer.getDocument().replace(fStart, fEnd - fStart, fText);
			} catch (BadLocationException x) {
			}
		}

		/**
		 * Re-applies the change described by this command that previously been
		 * rolled back. Also selects and reveals the change.
		 *
		 * @param monitor	the progress monitor to use if necessary
		 * @param uiInfo	an adaptable that can provide UI info if needed
		 * @return the status
		 */
		@Override
		public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) {
			if (isValid()) {
				redoTextChange();
				resetProcessChangeSate();
				selectAndReveal(fStart, fText == null ? 0 : fText.length());
				return Status.OK_STATUS;
			}
			return IOperationHistory.OPERATION_INVALID_STATUS;
		}

		/**
		 * Update the command in response to a commit.
		 *
		 * @since 3.1
		 */

		protected void updateCommand() {
			fText= fTextBuffer.toString();
			fTextBuffer.setLength(0);
			fPreservedText= fPreservedTextBuffer.toString();
			fPreservedTextBuffer.setLength(0);
		}

		/**
		 * Creates a new uncommitted text command depending on whether
		 * a compound change is currently being executed.
		 *
		 * @return a new, uncommitted text command or a compound text command
		 */
		protected TextCommand createCurrent() {
			return fFoldingIntoCompoundChange ? new CompoundTextCommand(fUndoContext) : new TextCommand(fUndoContext);
		}

		/**
		 * Commits the current change into this command.
		 */
		protected void commit() {
			if (fStart < 0) {
				if (fFoldingIntoCompoundChange) {
					fCurrent= createCurrent();
				} else {
					reinitialize();
				}
			} else {
				updateCommand();
				fCurrent= createCurrent();
			}
			resetProcessChangeSate();
		}

		/**
		 * Updates the text from the buffers without resetting
		 * the buffers or adding anything to the stack.
		 *
		 * @since 3.1
		 */
		protected void pretendCommit() {
			if (fStart > -1) {
				fText= fTextBuffer.toString();
				fPreservedText= fPreservedTextBuffer.toString();
			}
		}

		/**
		 * Attempt a commit of this command and answer true if a new
		 * fCurrent was created as a result of the commit.
		 *
		 * @return true if the command was committed and created a
		 * new fCurrent, false if not.
		 * @since 3.1
		 */
		protected boolean attemptCommit() {
			pretendCommit();
			if (isValid()) {
				DefaultUndoManager.this.commit();
				return true;
			}
			return false;
		}

		/**
		 * Checks whether this text command is valid for undo or redo.
		 *
		 * @return <code>true</code> if the command is valid for undo or redo
		 * @since 3.1
		 */
		protected boolean isValid() {
			return fStart > -1 &&
				fEnd > -1 &&
				fText != null;
		}

		@Override
		public String toString() {
			String delimiter= ", "; //$NON-NLS-1$
			StringBuilder text= new StringBuilder(super.toString());
			text.append("\n"); //$NON-NLS-1$
			text.append(this.getClass().getName());
			text.append(" undo modification stamp: "); //$NON-NLS-1$
			text.append(fUndoModificationStamp);
			text.append(" redo modification stamp: "); //$NON-NLS-1$
			text.append(fRedoModificationStamp);
			text.append(" start: "); //$NON-NLS-1$
			text.append(fStart);
			text.append(delimiter);
			text.append("end: "); //$NON-NLS-1$
			text.append(fEnd);
			text.append(delimiter);
			text.append("text: '"); //$NON-NLS-1$
			text.append(fText);
			text.append('\'');
			text.append(delimiter);
			text.append("preservedText: '"); //$NON-NLS-1$
			text.append(fPreservedText);
			text.append('\'');
			return text.toString();
		}

		/**
		 * Return the undo modification stamp
		 *
		 * @return the undo modification stamp for this command
		 * @since 3.1
		 */
		protected long getUndoModificationStamp() {
			return fUndoModificationStamp;
		}

		/**
		 * Return the redo modification stamp
		 *
		 * @return the redo modification stamp for this command
		 * @since 3.1
		 */
		protected long getRedoModificationStamp() {
			return fRedoModificationStamp;
		}
	}

	/**
	 * Represents an undo-able edit command consisting of several
	 * individual edit commands.
	 */
	class CompoundTextCommand extends TextCommand {

		/** The list of individual commands */
		private List<TextCommand> fCommands= new ArrayList<>();

		/**
		 * Creates a new compound text command.
		 *
		 * @param context the undo context for this command
		 * @since 3.1
		 */
		CompoundTextCommand(IUndoContext context) {
			super(context);
		}

		/**
		 * Adds a new individual command to this compound command.
		 *
		 * @param command the command to be added
		 */
		protected void add(TextCommand command) {
			fCommands.add(command);
		}

		@Override
		public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) {
			resetProcessChangeSate();

			int size= fCommands.size();
			if (size > 0) {

				TextCommand c;

				for (int i= size -1; i > 0;  --i) {
					c= fCommands.get(i);
					c.undoTextChange();
				}

				c= fCommands.get(0);
				c.undo(monitor, uiInfo);
			}

			return Status.OK_STATUS;
		}

		@Override
		public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) {
			resetProcessChangeSate();

			int size= fCommands.size();
			if (size > 0) {

				TextCommand c;

				for (int i= 0; i < size -1;  ++i) {
					c= fCommands.get(i);
					c.redoTextChange();
				}

				c= fCommands.get(size -1);
				c.redo(monitor, uiInfo);
			}
			return Status.OK_STATUS;
		}

		/*
		 * @see TextCommand#updateCommand

		 */

		@Override
		protected void updateCommand() {
			// first gather the data from the buffers
			super.updateCommand();

			// the result of the command update is stored as a child command
			TextCommand c= new TextCommand(fUndoContext);
			c.fStart= fStart;
			c.fEnd= fEnd;
			c.fText= fText;
			c.fPreservedText= fPreservedText;
			c.fUndoModificationStamp= fUndoModificationStamp;
			c.fRedoModificationStamp= fRedoModificationStamp;
			add(c);

			// clear out all indexes now that the child is added
			reinitialize();
		}

		/*
		 * @see TextCommand#createCurrent
		 */
		@Override
		protected TextCommand createCurrent() {

			if (!fFoldingIntoCompoundChange)
				return new TextCommand(fUndoContext);

			reinitialize();
			return this;
		}

		@Override
		protected void commit() {
			// if there is pending data, update the command
			if (fStart > -1)
				updateCommand();
			fCurrent= createCurrent();
			resetProcessChangeSate();
		}

		/**
		 * Checks whether the command is valid for undo or redo.
		 *
		 * @return true if the command is valid.
		 * @since 3.1
		 */
		@Override
		protected boolean isValid() {
			if (isConnected())
				return (fStart > -1 || !fCommands.isEmpty());
			return false;
		}

		/**
		 * Returns the undo modification stamp.
		 *
		 * @return the undo modification stamp
		 * @since 3.1
		 */
		@Override
		protected long getUndoModificationStamp() {
			if (fStart > -1)
				return super.getUndoModificationStamp();
			else if (!fCommands.isEmpty())
				return fCommands.get(0).getUndoModificationStamp();

			return fUndoModificationStamp;
		}

		/**
		 * Returns the redo modification stamp.
		 *
		 * @return the redo modification stamp
		 * @since 3.1
		 */
		@Override
		protected long getRedoModificationStamp() {
			if (fStart > -1)
				return super.getRedoModificationStamp();
			else if (!fCommands.isEmpty())
				return fCommands.get(fCommands.size()-1).getRedoModificationStamp();

			return fRedoModificationStamp;
		}
	}

	/**
	 * Internal listener to mouse and key events.
	 */
	class KeyAndMouseListener implements MouseListener, KeyListener {

		/*
		 * @see MouseListener#mouseDoubleClick
		 */
		@Override
		public void mouseDoubleClick(MouseEvent e) {
		}

		/*
		 * If the right mouse button is pressed, the current editing command is closed
		 * @see MouseListener#mouseDown
		 */
		@Override
		public void mouseDown(MouseEvent e) {
			if (e.button == 1)
				commit();
		}

		/*
		 * @see MouseListener#mouseUp
		 */
		@Override
		public void mouseUp(MouseEvent e) {
		}

		/*
		 * @see KeyListener#keyPressed
		 */
		@Override
		public void keyReleased(KeyEvent e) {
		}

		/*
		 * On cursor keys, the current editing command is closed
		 * @see KeyListener#keyPressed
		 */
		@Override
		public void keyPressed(KeyEvent e) {
			switch (e.keyCode) {
				case SWT.ARROW_UP:
				case SWT.ARROW_DOWN:
				case SWT.ARROW_LEFT:
				case SWT.ARROW_RIGHT:
					commit();
					break;
			}
		}
	}

	/**
	 * Internal listener to document changes.
	 */
	class DocumentListener implements IDocumentListener {

		private String fReplacedText;

		@Override
		public void documentAboutToBeChanged(DocumentEvent event) {
			try {
				fReplacedText= event.getDocument().get(event.getOffset(), event.getLength());
				fPreservedUndoModificationStamp= event.getModificationStamp();
			} catch (BadLocationException x) {
				fReplacedText= null;
			}
		}

		@Override
		public void documentChanged(DocumentEvent event) {
			fPreservedRedoModificationStamp= event.getModificationStamp();

			// record the current valid state for the top operation in case it remains the
			// top operation but changes state.
			IUndoableOperation op= fHistory.getUndoOperation(fUndoContext);
			boolean wasValid= false;
			if (op != null)
				wasValid= op.canUndo();
			// Process the change, providing the before and after timestamps
			processChange(event.getOffset(), event.getOffset() + event.getLength(), event.getText(), fReplacedText, fPreservedUndoModificationStamp, fPreservedRedoModificationStamp);

			// now update fCurrent with the latest buffers from the document change.
			fCurrent.pretendCommit();

			if (op == fCurrent) {
				// if the document change did not cause a new fCurrent to be created, then we should
				// notify the history that the current operation changed if its validity has changed.
				if (wasValid != fCurrent.isValid())
					fHistory.operationChanged(op);
			}
			else {
				// if the change created a new fCurrent that we did not yet add to the
				// stack, do so if it's valid and we are not in the middle of a compound change.
				if (fCurrent != fLastAddedCommand && fCurrent.isValid()) {
					addToCommandStack(fCurrent);
				}
			}
		}
	}

	/**
	 * Internal text input listener.
	 */
	class TextInputListener implements ITextInputListener {

		@Override
		public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
			if (oldInput != null && fDocumentListener != null) {
				oldInput.removeDocumentListener(fDocumentListener);
				commit();
			}
		}

		@Override
		public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
			if (newInput != null) {
				if (fDocumentListener == null)
					fDocumentListener= new DocumentListener();
				newInput.addDocumentListener(fDocumentListener);
			}
		}

	}

	/*
	 * @see IOperationHistoryListener
	 * @since 3.1
	 */
	class HistoryListener implements IOperationHistoryListener {
		private IUndoableOperation fOperation;

		@Override
		public void historyNotification(final OperationHistoryEvent event) {
			final int type= event.getEventType();
			switch (type) {
			case OperationHistoryEvent.ABOUT_TO_UNDO:
			case OperationHistoryEvent.ABOUT_TO_REDO:
				// if this is one of our operations
				if (event.getOperation().hasContext(fUndoContext)) {
						fTextViewer.getTextWidget().getDisplay().syncExec(() -> {
							// if we are undoing/redoing a command we generated, then ignore
							// the document changes associated with this undo or redo.
							if (event.getOperation() instanceof TextCommand) {
								if (fTextViewer instanceof TextViewer)
									((TextViewer) fTextViewer).ignoreAutoEditStrategies(true);
								listenToTextChanges(false);

								// in the undo case only, make sure compounds are closed
								if (type == OperationHistoryEvent.ABOUT_TO_UNDO) {
									if (fFoldingIntoCompoundChange) {
										endCompoundChange();
								}
							}
							} else {
								// the undo or redo has our context, but it is not one of
								// our commands.  We will listen to the changes, but will
								// reset the state that tracks the undo/redo history.
								commit();
								fLastAddedCommand= null;
						}
						});
					fOperation= event.getOperation();
				}
				break;
			case OperationHistoryEvent.UNDONE:
			case OperationHistoryEvent.REDONE:
			case OperationHistoryEvent.OPERATION_NOT_OK:
				if (event.getOperation() == fOperation) {
						fTextViewer.getTextWidget().getDisplay().syncExec(() -> {
							listenToTextChanges(true);
							fOperation= null;
							if (fTextViewer instanceof TextViewer)
								((TextViewer) fTextViewer).ignoreAutoEditStrategies(false);
						});
				}
				break;
			}
		}

	}

	/** Text buffer to collect text which is inserted into the viewer */
	private StringBuilder fTextBuffer;
	/** Text buffer to collect viewer content which has been replaced */
	private StringBuilder fPreservedTextBuffer;
	/** The document modification stamp for undo. */
	protected long fPreservedUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
	/** The document modification stamp for redo. */
	protected long fPreservedRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
	/** The internal key and mouse event listener */
	private KeyAndMouseListener fKeyAndMouseListener;
	/** The internal document listener */
	private DocumentListener fDocumentListener;
	/** The internal text input listener */
	private TextInputListener fTextInputListener;


	/** Indicates inserting state */
	private boolean fInserting= false;
	/** Indicates overwriting state */
	private boolean fOverwriting= false;
	/** Indicates whether the current change belongs to a compound change */
	private boolean fFoldingIntoCompoundChange= false;

	/** The text viewer the undo manager is connected to */
	private ITextViewer fTextViewer;

	/** Supported undo level */
	private int fUndoLevel;
	/** The currently constructed edit command */
	private TextCommand fCurrent;
	/** The last delete edit command */
	private TextCommand fPreviousDelete;

	/**
	 * The undo context.
	 * @since 3.1
	 */
	private IOperationHistory fHistory;
	/**
	 * The operation history.
	 * @since 3.1
	 */
	private IUndoContext fUndoContext;
	/**
	 * The operation history listener used for managing undo and redo before
	 * and after the individual commands are performed.
	 * @since 3.1
	 */
	private IOperationHistoryListener fHistoryListener= new HistoryListener();

	/**
	 * The command last added to the operation history.  This must be tracked
	 * internally instead of asking the history, since outside parties may be placing
	 * items on our undo/redo history.
	 */
	private TextCommand fLastAddedCommand= null;

	/**
	 * Creates a new undo manager who remembers the specified number of edit commands.
	 *
	 * @param undoLevel the length of this manager's history
	 */
	public DefaultUndoManager(int undoLevel) {
		fHistory= OperationHistoryFactory.getOperationHistory();
		setMaximalUndoLevel(undoLevel);
	}

	/**
	 * Returns whether this undo manager is connected to a text viewer.
	 *
	 * @return <code>true</code> if connected, <code>false</code> otherwise
	 * @since 3.1
	 */
	private boolean isConnected() {
		return fTextViewer != null;
	}

	/*
	 * @see IUndoManager#beginCompoundChange
	 */
	@Override
	public void beginCompoundChange() {
		if (isConnected()) {
			fFoldingIntoCompoundChange= true;
			commit();
		}
	}


	/*
	 * @see IUndoManager#endCompoundChange
	 */
	@Override
	public void endCompoundChange() {
		if (isConnected()) {
			fFoldingIntoCompoundChange= false;
			commit();
		}
	}

	/**
	 * Registers all necessary listeners with the text viewer.
	 */
	private void addListeners() {
		StyledText text= fTextViewer.getTextWidget();
		if (text != null) {
			fKeyAndMouseListener= new KeyAndMouseListener();
			text.addMouseListener(fKeyAndMouseListener);
			text.addKeyListener(fKeyAndMouseListener);
			fTextInputListener= new TextInputListener();
			fTextViewer.addTextInputListener(fTextInputListener);
			fHistory.addOperationHistoryListener(fHistoryListener);
			listenToTextChanges(true);
		}
	}

	/**
	 * Unregister all previously installed listeners from the text viewer.
	 */
	private void removeListeners() {
		StyledText text= fTextViewer.getTextWidget();
		if (text != null) {
			if (fKeyAndMouseListener != null) {
				text.removeMouseListener(fKeyAndMouseListener);
				text.removeKeyListener(fKeyAndMouseListener);
				fKeyAndMouseListener= null;
			}
			if (fTextInputListener != null) {
				fTextViewer.removeTextInputListener(fTextInputListener);
				fTextInputListener= null;
			}
			listenToTextChanges(false);
			fHistory.removeOperationHistoryListener(fHistoryListener);
		}
	}

	/**
	 * Adds the given command to the operation history if it is not part of
	 * a compound change.
	 *
	 * @param command the command to be added
	 * @since 3.1
	 */
	private void addToCommandStack(TextCommand command){
		if (!fFoldingIntoCompoundChange || command instanceof CompoundTextCommand) {
			fHistory.add(command);
			fLastAddedCommand= command;
		}
	}

	/**
	 * Disposes the command stack.
	 *
	 * @since 3.1
	 */
	private void disposeCommandStack() {
		fHistory.dispose(fUndoContext, true, true, true);
	}

	/**
	 * Initializes the command stack.
	 *
	 * @since 3.1
	 */
	private void initializeCommandStack() {
		if (fHistory != null && fUndoContext != null)
			fHistory.dispose(fUndoContext, true, true, false);

	}

	/**
	 * Switches the state of whether there is a text listener or not.
	 *
	 * @param listen the state which should be established
	 */
	private void listenToTextChanges(boolean listen) {
		if (listen) {
			if (fDocumentListener == null && fTextViewer.getDocument() != null) {
				fDocumentListener= new DocumentListener();
				fTextViewer.getDocument().addDocumentListener(fDocumentListener);
			}
		} else if (!listen) {
			if (fDocumentListener != null && fTextViewer.getDocument() != null) {
				fTextViewer.getDocument().removeDocumentListener(fDocumentListener);
				fDocumentListener= null;
			}
		}
	}

	/**
	 * Closes the current editing command and opens a new one.
	 */
	private void commit() {
		// if fCurrent has never been placed on the command stack, do so now.
		// this can happen when there are multiple programmatically commits in a single
		// document change.
		if (fLastAddedCommand != fCurrent) {
			fCurrent.pretendCommit();
			if (fCurrent.isValid())
				addToCommandStack(fCurrent);
		}
		fCurrent.commit();
	}

	/**
	 * Reset processChange state.
	 *
	 * @since 3.2
	 */
	private void resetProcessChangeSate() {
		fInserting= false;
		fOverwriting= false;
		fPreviousDelete.reinitialize();
	}

	/**
	 * Checks whether the given text starts with a line delimiter and
	 * subsequently contains a white space only.
	 *
	 * @param text the text to check
	 * @return <code>true</code> if the text is a line delimiter followed by whitespace, <code>false</code> otherwise
	 */
	private boolean isWhitespaceText(String text) {

		if (text == null || text.isEmpty())
			return false;

		String[] delimiters= fTextViewer.getDocument().getLegalLineDelimiters();
		int index= TextUtilities.startsWith(delimiters, text);
		if (index > -1) {
			char c;
			int length= text.length();
			for (int i= delimiters[index].length(); i < length; i++) {
				c= text.charAt(i);
				if (c != ' ' && c != '\t')
					return false;
			}
			return true;
		}

		return false;
	}

	private void processChange(int modelStart, int modelEnd, String insertedText, String replacedText, long beforeChangeModificationStamp, long afterChangeModificationStamp) {

		if (insertedText == null)
			insertedText= ""; //$NON-NLS-1$

		if (replacedText == null)
			replacedText= ""; //$NON-NLS-1$

		int length= insertedText.length();
		int diff= modelEnd - modelStart;

		if (fCurrent.fUndoModificationStamp == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP)
			fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;

		// normalize
		if (diff < 0) {
			int tmp= modelEnd;
			modelEnd= modelStart;
			modelStart= tmp;
		}

		if (modelStart == modelEnd) {
			// text will be inserted
			if ((length == 1) || isWhitespaceText(insertedText)) {
				// by typing or whitespace
				if (!fInserting || (modelStart != fCurrent.fStart + fTextBuffer.length())) {
					fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
					if (fCurrent.attemptCommit())
						fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;

					fInserting= true;
				}
				if (fCurrent.fStart < 0)
					fCurrent.fStart= fCurrent.fEnd= modelStart;
				if (length > 0)
					fTextBuffer.append(insertedText);
			} else if (length >= 0) {
				// by pasting or model manipulation
				fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
				if (fCurrent.attemptCommit())
					fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;

				fCurrent.fStart= fCurrent.fEnd= modelStart;
				fTextBuffer.append(insertedText);
				fCurrent.fRedoModificationStamp= afterChangeModificationStamp;
				if (fCurrent.attemptCommit())
					fCurrent.fUndoModificationStamp= afterChangeModificationStamp;

			}
		} else {
			if (length == 0) {
				// text will be deleted by backspace or DEL key or empty clipboard
				length= replacedText.length();
				String[] delimiters= fTextViewer.getDocument().getLegalLineDelimiters();

				if ((length == 1) || TextUtilities.equals(delimiters, replacedText) > -1) {

					// whereby selection is empty

					if (fPreviousDelete.fStart == modelStart && fPreviousDelete.fEnd == modelEnd) {
						// repeated DEL

							// correct wrong settings of fCurrent
						if (fCurrent.fStart == modelEnd && fCurrent.fEnd == modelStart) {
							fCurrent.fStart= modelStart;
							fCurrent.fEnd= modelEnd;
						}
							// append to buffer && extend command range
						fPreservedTextBuffer.append(replacedText);
						++fCurrent.fEnd;

					} else if (fPreviousDelete.fStart == modelEnd) {
						// repeated backspace

							// insert in buffer and extend command range
						fPreservedTextBuffer.insert(0, replacedText);
						fCurrent.fStart= modelStart;

					} else {
						// either DEL or backspace for the first time

						fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
						if (fCurrent.attemptCommit())
							fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;

						// as we can not decide whether it was DEL or backspace we initialize for backspace
						fPreservedTextBuffer.append(replacedText);
						fCurrent.fStart= modelStart;
						fCurrent.fEnd= modelEnd;
					}

					fPreviousDelete.set(modelStart, modelEnd);

				} else if (length > 0) {
					// whereby selection is not empty
					fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
					if (fCurrent.attemptCommit())
						fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;

					fCurrent.fStart= modelStart;
					fCurrent.fEnd= modelEnd;
					fPreservedTextBuffer.append(replacedText);
				}
			} else {
				// text will be replaced

				if (length == 1) {
					length= replacedText.length();
					String[] delimiters= fTextViewer.getDocument().getLegalLineDelimiters();

					if ((length == 1) || TextUtilities.equals(delimiters, replacedText) > -1) {
						// because of overwrite mode or model manipulation
						if (!fOverwriting || (modelStart != fCurrent.fStart +  fTextBuffer.length())) {
							fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
							if (fCurrent.attemptCommit())
								fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;

							fOverwriting= true;
						}

						if (fCurrent.fStart < 0)
							fCurrent.fStart= modelStart;

						fCurrent.fEnd= modelEnd;
						fTextBuffer.append(insertedText);
						fPreservedTextBuffer.append(replacedText);
						fCurrent.fRedoModificationStamp= afterChangeModificationStamp;
						return;
					}
				}
				// because of typing or pasting whereby selection is not empty
				fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
				if (fCurrent.attemptCommit())
					fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;

				fCurrent.fStart= modelStart;
				fCurrent.fEnd= modelEnd;
				fTextBuffer.append(insertedText);
				fPreservedTextBuffer.append(replacedText);
			}
		}
		// in all cases, the redo modification stamp is updated on the open command
		fCurrent.fRedoModificationStamp= afterChangeModificationStamp;
	}

	/**
	 * Shows the given exception in an error dialog.
	 *
	 * @param title the dialog title
	 * @param ex the exception
	 * @since 3.1
	 */
	private void openErrorDialog(final String title, final Exception ex) {
		Shell shell= null;
		if (isConnected()) {
			StyledText st= fTextViewer.getTextWidget();
			if (st != null && !st.isDisposed())
				shell= st.getShell();
		}
		if (Display.getCurrent() != null)
			MessageDialog.openError(shell, title, ex.getLocalizedMessage());
		else {
			Display display;
			final Shell finalShell= shell;
			if (finalShell != null)
				display= finalShell.getDisplay();
			else
				display= Display.getDefault();
			display.syncExec(() -> MessageDialog.openError(finalShell, title, ex.getLocalizedMessage()));
		}
	}

	@Override
	public void setMaximalUndoLevel(int undoLevel) {
		fUndoLevel= Math.max(0, undoLevel);
		if (isConnected()) {
			fHistory.setLimit(fUndoContext, fUndoLevel);
		}
	}

	@Override
	public void connect(ITextViewer textViewer) {
		if (!isConnected() && textViewer != null) {
			fTextViewer= textViewer;
			fTextBuffer= new StringBuilder();
			fPreservedTextBuffer= new StringBuilder();
			if (fUndoContext == null)
				fUndoContext= new ObjectUndoContext(this);

			fHistory.setLimit(fUndoContext, fUndoLevel);

			initializeCommandStack();

			// open up the current command
			fCurrent= new TextCommand(fUndoContext);

			fPreviousDelete= new TextCommand(fUndoContext);
			addListeners();
		}
	}

	@Override
	public void disconnect() {
		if (isConnected()) {

			removeListeners();

			fCurrent= null;
			fTextViewer= null;
			disposeCommandStack();
			fTextBuffer= null;
			fPreservedTextBuffer= null;
			fUndoContext= null;
		}
	}

	@Override
	public void reset() {
		if (isConnected()) {
			initializeCommandStack();
			fCurrent= new TextCommand(fUndoContext);
			fFoldingIntoCompoundChange= false;
			fInserting= false;
			fOverwriting= false;
			fTextBuffer.setLength(0);
			fPreservedTextBuffer.setLength(0);
			fPreservedUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
			fPreservedRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
		}
	}

	@Override
	public boolean redoable() {
		return fHistory.canRedo(fUndoContext);
	}

	@Override
	public boolean undoable() {
		return fHistory.canUndo(fUndoContext);
	}

	@Override
	public void redo() {
		if (isConnected() && redoable()) {
			try {
				fHistory.redo(fUndoContext, null, null);
			} catch (ExecutionException ex) {
				openErrorDialog(JFaceTextMessages.getString("DefaultUndoManager.error.redoFailed.title"), ex); //$NON-NLS-1$
			}
		}
	}

	@Override
	public void undo() {
		if (isConnected() && undoable()) {
			try {
				fHistory.undo(fUndoContext, null, null);
			} catch (ExecutionException ex) {
				openErrorDialog(JFaceTextMessages.getString("DefaultUndoManager.error.undoFailed.title"), ex); //$NON-NLS-1$
			}
		}
	}

	/**
	 * Selects and reveals the specified range.
	 *
	 * @param offset the offset of the range
	 * @param length the length of the range
	 * @since 3.0
	 */
	protected void selectAndReveal(int offset, int length) {
		if (fTextViewer instanceof ITextViewerExtension5) {
			ITextViewerExtension5 extension= (ITextViewerExtension5) fTextViewer;
			extension.exposeModelRange(new Region(offset, length));
		} else if (!fTextViewer.overlapsWithVisibleRegion(offset, length))
			fTextViewer.resetVisibleRegion();

		fTextViewer.setSelectedRange(offset, length);
		fTextViewer.revealRange(offset, length);
	}

	@Override
	public IUndoContext getUndoContext() {
		return fUndoContext;
	}

}
