/*******************************************************************************
 * Copyright (c) 2001, 2009 IBM Corporation and others.
 * All rights reserved. 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
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     Jesper Steen M�ller - initial IDocumentExtension4 support - #102822
 *     
 *******************************************************************************/
package org.eclipse.wst.sse.core.internal.undo;

import java.util.EventObject;

import org.eclipse.emf.common.command.BasicCommandStack;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CommandStack;
import org.eclipse.emf.common.command.CommandStackListener;
import org.eclipse.emf.common.command.CompoundCommand;
import org.eclipse.jface.text.DocumentRewriteSession;
import org.eclipse.jface.text.DocumentRewriteSessionType;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.SSECoreMessages;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.events.IStructuredDocumentListener;
import org.eclipse.wst.sse.core.internal.provisional.events.NewDocumentEvent;
import org.eclipse.wst.sse.core.internal.provisional.events.NoChangeEvent;
import org.eclipse.wst.sse.core.internal.provisional.events.RegionChangedEvent;
import org.eclipse.wst.sse.core.internal.provisional.events.RegionsReplacedEvent;
import org.eclipse.wst.sse.core.internal.provisional.events.StructuredDocumentEvent;
import org.eclipse.wst.sse.core.internal.provisional.events.StructuredDocumentRegionsReplacedEvent;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.util.Assert;
import org.eclipse.wst.sse.core.internal.util.Utilities;

public class StructuredTextUndoManager implements IStructuredTextUndoManager {

	class InternalCommandStackListener implements CommandStackListener {
		public void commandStackChanged(EventObject event) {
			resetInternalCommands();
		}
	}

	class InternalStructuredDocumentListener implements IStructuredDocumentListener {

		public void newModel(NewDocumentEvent structuredDocumentEvent) {
			// Do nothing. Do not push the new model's structuredDocument
			// changes
			// onto the undo command stack, or else the user may be able to
			// undo
			// an existing file to an empty file.
		}

		public void noChange(NoChangeEvent structuredDocumentEvent) {
			// Since "no change", do nothing.
		}

		public void nodesReplaced(StructuredDocumentRegionsReplacedEvent structuredDocumentEvent) {
			processStructuredDocumentEvent(structuredDocumentEvent);
		}

		private void processStructuredDocumentEvent(String textDeleted, String textInserted, int textStart, int textEnd) {
			if (fTextCommand != null && textStart == fTextCommand.getTextEnd()) {
				// append to the text command
				fTextCommand.setTextDeleted(fTextCommand.getTextDeleted().concat(textDeleted));
				fTextCommand.setTextInserted(fTextCommand.getTextInserted().concat(textInserted));
				fTextCommand.setTextEnd(textEnd);
			}
			else if (fTextCommand != null && textStart == fTextCommand.getTextStart() - 1 && textEnd <= fTextCommand.getTextEnd() - 1 && textDeleted.length() == 1 && textInserted.length() == 0 && fTextCommand.getTextDeleted().length() > 0) {
				// backspace pressed
				// erase a character in the file
				fTextCommand.setTextDeleted(textDeleted.concat(fTextCommand.getTextDeleted()));
				fTextCommand.setTextStart(textStart);
			}
			else {
				createNewTextCommand(textDeleted, textInserted, textStart, textEnd);
			}

			// save cursor position
			fCursorPosition = textEnd;
		}

		private void processStructuredDocumentEvent(StructuredDocumentEvent structuredDocumentEvent) {
			// Note: fListening tells us if we should listen to the
			// StructuredDocumentEvent.
			// fListening is set to false right before the undo/redo process
			// and
			// then set to true again
			// right after the undo/redo process to block out and ignore all
			// StructuredDocumentEvents generated
			// by the undo/redo process.

			// Process StructuredDocumentEvent if fListening is true.
			//
			// We are executing a command from the command stack if the
			// requester
			// is a command (for example, undo/redo).
			// We should not process the flat model event when we are
			// executing a
			// command from the command stack.
			if (fUndoManagementEnabled && !(structuredDocumentEvent.getOriginalRequester() instanceof Command)) {
				// check requester if not recording
				if (!fRecording)
					checkRequester(structuredDocumentEvent.getOriginalRequester());

				// process the structuredDocumentEvent
				String textDeleted = structuredDocumentEvent.getDeletedText();
				String textInserted = structuredDocumentEvent.getText();
				int textStart = structuredDocumentEvent.getOffset();
				int textEnd = textStart + textInserted.length();
				processStructuredDocumentEvent(textDeleted, textInserted, textStart, textEnd);
			}
		}

		public void regionChanged(RegionChangedEvent structuredDocumentEvent) {
			processStructuredDocumentEvent(structuredDocumentEvent);
		}

		public void regionsReplaced(RegionsReplacedEvent structuredDocumentEvent) {
			processStructuredDocumentEvent(structuredDocumentEvent);
		}

	}

	private static final String TEXT_CHANGE_TEXT = SSECoreMessages.Text_Change_UI_; //$NON-NLS-1$
	private CommandStack fCommandStack = null;
	private StructuredTextCompoundCommandImpl fCompoundCommand = null;
	private String fCompoundCommandDescription = null;
	private String fCompoundCommandLabel = null;
	int fCursorPosition = 0;
	// private IStructuredModel fStructuredModel = null;
	private IDocument fDocument;
	private InternalCommandStackListener fInternalCommandStackListener;
	// private Map fTextViewerToListenerMap = new HashMap();
	private IStructuredDocumentListener fInternalStructuredDocumentListener;
	private IDocumentSelectionMediator[] fMediators = null;
	private boolean fRecording = false;
	private int fRecordingCount = 0;
	private Object fRequester;
	StructuredTextCommandImpl fTextCommand = null;
	private int fUndoCursorPosition = -1;
	boolean fUndoManagementEnabled = true;
	private int fUndoSelectionLength = 0;

	public StructuredTextUndoManager() {
		this(new BasicCommandStack());
	}

	public StructuredTextUndoManager(CommandStack commandStack) {
		setCommandStack(commandStack);
	}

	private void addDocumentSelectionMediator(IDocumentSelectionMediator mediator) {
		if (!Utilities.contains(fMediators, mediator)) {
			int oldSize = 0;

			if (fMediators != null) {
				// normally won't be null, but we need to be sure, for first
				// time through
				oldSize = fMediators.length;
			}

			int newSize = oldSize + 1;
			IDocumentSelectionMediator[] newMediators = new IDocumentSelectionMediator[newSize];
			if (fMediators != null) {
				System.arraycopy(fMediators, 0, newMediators, 0, oldSize);
			}

			// add the new undo mediator to last position
			newMediators[newSize - 1] = mediator;

			// now switch new for old
			fMediators = newMediators;
		}
		else {
			removeDocumentSelectionMediator(mediator);
			addDocumentSelectionMediator(mediator);
		}
	}

	public void beginRecording(Object requester) {
		beginRecording(requester, null, null);
	}

	public void beginRecording(Object requester, int cursorPosition, int selectionLength) {
		beginRecording(requester, null, null);

		fUndoCursorPosition = cursorPosition;
		fUndoSelectionLength = selectionLength;
	}

	public void beginRecording(Object requester, String label) {
		beginRecording(requester, label, null);
	}

	public void beginRecording(Object requester, String label, int cursorPosition, int selectionLength) {
		beginRecording(requester, label, null);

		fUndoCursorPosition = cursorPosition;
		fUndoSelectionLength = selectionLength;
	}

	public void beginRecording(Object requester, String label, String description) {
		// save the requester
		fRequester = requester;

		// update label and desc only on the first level when recording is
		// nested
		if (fRecordingCount == 0) {
			fCompoundCommandLabel = label;
			if (fCompoundCommandLabel == null)
				fCompoundCommandLabel = TEXT_CHANGE_TEXT;

			fCompoundCommandDescription = description;
			if (fCompoundCommandDescription == null)
				fCompoundCommandDescription = TEXT_CHANGE_TEXT;

			// clear commands
			fTextCommand = null;
			fCompoundCommand = null;
		}

		// update counter and flag
		fRecordingCount++;
		fRecording = true;

		// no undo cursor position and undo selection length specified
		// reset undo cursor position and undo selection length
		fUndoCursorPosition = -1;
		fUndoSelectionLength = 0;
	}

	public void beginRecording(Object requester, String label, String description, int cursorPosition, int selectionLength) {
		beginRecording(requester, label, description);

		fUndoCursorPosition = cursorPosition;
		fUndoSelectionLength = selectionLength;
	}

	void checkRequester(Object requester) {
		if (fRequester != null && !fRequester.equals(requester)) {
			// Force restart of recording so the last compound command is
			// closed.
			//
			// However, we should not force restart of recording when the
			// request came from StructuredDocumentToTextAdapter or
			// XMLModelImpl
			// because cut/paste requests and character inserts to the
			// textViewer are from StructuredDocumentToTextAdapter,
			// and requests to delete a node in the XMLTableTreeViewer are
			// from XMLModelImpl (which implements IStructuredModel).

			if (!(requester instanceof IStructuredModel || requester instanceof IStructuredDocument)) {
				resetInternalCommands();
			}
		}
	}



	public void connect(IDocumentSelectionMediator mediator) {
		Assert.isNotNull(mediator);
		if (fDocument == null) {
			// add this undo manager as structured document listener
			fDocument = mediator.getDocument();
			// future_TODO: eventually we want to refactor or allow either
			// type of document, but for now, we'll do instanceof check, and
			// fail
			// if not right type
			if (fDocument instanceof IStructuredDocument) {
				((IStructuredDocument) fDocument).addDocumentChangedListener(getInternalStructuredDocumentListener());
			}
			else {
				throw new IllegalArgumentException("only meditator with structured documents currently handled"); //$NON-NLS-1$
			}
		}
		else {
			// if we've already had our document set, we'll just do this fail
			// fast integrity check
			if (!fDocument.equals(mediator.getDocument()))
				throw new IllegalStateException("Connection to undo manager failed. Document for document selection mediator inconistent with undo manager."); //$NON-NLS-1$
		}

		addDocumentSelectionMediator(mediator);
	}

	void createNewTextCommand(String textDeleted, String textInserted, int textStart, int textEnd) {
		StructuredTextCommandImpl textCommand = new StructuredTextCommandImpl(fDocument);
		textCommand.setLabel(TEXT_CHANGE_TEXT);
		textCommand.setDescription(TEXT_CHANGE_TEXT);
		textCommand.setTextStart(textStart);
		textCommand.setTextEnd(textEnd);
		textCommand.setTextDeleted(textDeleted);
		textCommand.setTextInserted(textInserted);

		if (fRecording) {
			if (fCompoundCommand == null) {
				StructuredTextCompoundCommandImpl compoundCommand = new StructuredTextCompoundCommandImpl();
				compoundCommand.setUndoCursorPosition(fUndoCursorPosition);
				compoundCommand.setUndoSelectionLength(fUndoSelectionLength);

				compoundCommand.setLabel(fCompoundCommandLabel);
				compoundCommand.setDescription(fCompoundCommandDescription);
				compoundCommand.append(textCommand);

				fCompoundCommand = compoundCommand;
			}
			else {
				fCompoundCommand.append(textCommand);
			}
		}
		else {
			fCommandStack.execute(textCommand);
		}

		fTextCommand = textCommand;
	}

	/**
	 * Disable undo management.
	 */
	public void disableUndoManagement() {
		fUndoManagementEnabled = false;
	}

	public void disconnect(IDocumentSelectionMediator mediator) {
		removeDocumentSelectionMediator(mediator);

		if (fMediators != null && fMediators.length == 0 && fDocument != null) {
			// remove this undo manager as structured document listener
			// future_TODO: eventually we want to refactor or allow either
			// type of document, but for now, we'll do instanceof check, and
			// fail
			// if not right type
			if (fDocument instanceof IStructuredDocument) {
				((IStructuredDocument) fDocument).removeDocumentChangedListener(getInternalStructuredDocumentListener());
			}
			else {
				throw new IllegalArgumentException("only meditator with structured documents currently handled"); //$NON-NLS-1$
			}
			// if no longer listening to document, then dont even track it
			// anymore
			// (this allows connect to reconnect to document again)
			fDocument = null;
		}
	}

	public void enableUndoManagement() {
		fUndoManagementEnabled = true;
	}

	public void endRecording(Object requester) {
		int cursorPosition = (fTextCommand != null) ? fTextCommand.getTextEnd() : -1;
		int selectionLength = 0;

		endRecording(requester, cursorPosition, selectionLength);
	}

	public void endRecording(Object requester, int cursorPosition, int selectionLength) {
		// Recording could be stopped by forceEndOfPendingCommand(). Make sure
		// we are still recording before proceeding, or else fRecordingCount
		// may not be balanced.
		if (fRecording) {
			if (fCompoundCommand != null) {
				fCompoundCommand.setRedoCursorPosition(cursorPosition);
				fCompoundCommand.setRedoSelectionLength(selectionLength);
			}

			// end recording is a logical stopping point for text command,
			// even when fRecordingCount > 0 (in nested beginRecording)
			fTextCommand = null;

			// update counter and flag
			if (fRecordingCount > 0)
				fRecordingCount--;
			if (fRecordingCount == 0) {
				
				// Finally execute the commands accumulated in the compound command.
				
				if (fCompoundCommand != null) {
					fCommandStack.execute(fCompoundCommand);
				}
				
				fRecording = false;

				// reset compound command only when fRecordingCount ==
				// 0
				fCompoundCommand = null;
				fCompoundCommandLabel = null;
				fCompoundCommandDescription = null;

				// Also reset fRequester
				fRequester = null;
			}
		}
	}

	/**
	 * Utility method to find model given document
	 */
	private IStructuredModel findStructuredModel(IDocument document) {
		IModelManager modelManager = StructuredModelManager.getModelManager();
		IStructuredModel structuredModel = modelManager.getExistingModelForRead(document);
		return structuredModel;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.wst.sse.core.undo.IStructuredTextUndoManager#forceEndOfPendingCommand(java.lang.Object,
	 *      int, int)
	 */
	public void forceEndOfPendingCommand(Object requester, int currentPosition, int length) {
		if (fRecording)
			endRecording(requester, currentPosition, length);
		else
			resetInternalCommands();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.wst.sse.core.undo.IStructuredTextUndoManager#getCommandStack()
	 */
	public CommandStack getCommandStack() {
		return fCommandStack;
	}

	/**
	 * @return
	 */
	private CommandStackListener getInternalCommandStackListener() {
		if (fInternalCommandStackListener == null) {
			fInternalCommandStackListener = new InternalCommandStackListener();
		}
		return fInternalCommandStackListener;
	}

	/**
	 * @return
	 */
	private IStructuredDocumentListener getInternalStructuredDocumentListener() {
		if (fInternalStructuredDocumentListener == null) {
			fInternalStructuredDocumentListener = new InternalStructuredDocumentListener();
		}
		return fInternalStructuredDocumentListener;
	}

	public Command getRedoCommand() {
		return fCommandStack.getRedoCommand();
	}

	public Command getUndoCommand() {
		return fCommandStack.getUndoCommand();
	}

	public void redo() {
		redo(null);
	}

	public void redo(IDocumentSelectionMediator requester) {
		IStructuredModel model = findStructuredModel(fDocument);

		if (redoable()) {
			IDocumentExtension4 docExt4 = null;
			DocumentRewriteSession rewriteSession = null;
			try {
				if (model != null)
					model.aboutToChangeModel();

				Command redoCommand = getRedoCommand();
				if (redoCommand instanceof CompoundCommand &&
						model.getStructuredDocument() instanceof IDocumentExtension4) {
					docExt4 = (IDocumentExtension4)model.getStructuredDocument();
				}
				rewriteSession = (docExt4 == null) ? null :
					docExt4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED);

				// make sure to redo before setting document selection
				fCommandStack.redo();

				// set document selection
				setRedoDocumentSelection(requester, redoCommand);
			}
			finally {
				if (docExt4 != null && rewriteSession != null)
					docExt4.stopRewriteSession(rewriteSession);
				if (model != null) {
					model.changedModel();
					model.releaseFromRead();
				}
			}
		}
	}

	public boolean redoable() {
		return fCommandStack.canRedo();
	}

	private void removeDocumentSelectionMediator(IDocumentSelectionMediator mediator) {
		if (fMediators != null && mediator != null) {
			// if its not in the array, we'll ignore the request
			if (Utilities.contains(fMediators, mediator)) {
				int oldSize = fMediators.length;
				int newSize = oldSize - 1;
				IDocumentSelectionMediator[] newMediators = new IDocumentSelectionMediator[newSize];
				int index = 0;
				for (int i = 0; i < oldSize; i++) {
					if (fMediators[i] == mediator) { // ignore
					}
					else {
						// copy old to new if its not the one we are removing
						newMediators[index++] = fMediators[i];
					}
				}
				// now that we have a new array, let's switch it for the old
				// one
				fMediators = newMediators;
			}
		}
	}

	void resetInternalCommands() {
		// Either the requester of the structured document change event is
		// changed, or the command stack is changed. Need to reset internal
		// commands so we won't continue to append changes.
		fCompoundCommand = null;
		fTextCommand = null;

		// Also reset fRequester
		fRequester = null;
	}

	public void setCommandStack(CommandStack commandStack) {
		if (fCommandStack != null)
			fCommandStack.removeCommandStackListener(getInternalCommandStackListener());

		fCommandStack = commandStack;

		if (fCommandStack != null)
			fCommandStack.addCommandStackListener(getInternalCommandStackListener());
	}

	private void setRedoDocumentSelection(IDocumentSelectionMediator requester, Command command) {
		int cursorPosition = -1;
		int selectionLength = 0;

		if (command instanceof CommandCursorPosition) {
			CommandCursorPosition commandCursorPosition = (CommandCursorPosition) command;
			cursorPosition = commandCursorPosition.getRedoCursorPosition();
			selectionLength = commandCursorPosition.getRedoSelectionLength();
		}
		else if (command instanceof StructuredTextCommand) {
			StructuredTextCommand structuredTextCommand = (StructuredTextCommand) command;
			cursorPosition = structuredTextCommand.getTextStart();
			selectionLength = structuredTextCommand.getTextInserted().length();
		}

		if (cursorPosition > -1 && fMediators != null && fMediators.length > 0) {
			for (int i = 0; i < fMediators.length; i++) {
				IDocument document = fMediators[i].getDocument();
				fMediators[i].undoOperationSelectionChanged(new UndoDocumentEvent(requester, document, cursorPosition, selectionLength));
			}
		}
	}

	private void setUndoDocumentSelection(IDocumentSelectionMediator requester, Command command) {
		int cursorPosition = -1;
		int selectionLength = 0;

		if (command instanceof CommandCursorPosition) {
			CommandCursorPosition commandCursorPosition = (CommandCursorPosition) command;
			cursorPosition = commandCursorPosition.getUndoCursorPosition();
			selectionLength = commandCursorPosition.getUndoSelectionLength();
		}
		else if (command instanceof StructuredTextCommand) {
			StructuredTextCommand structuredTextCommand = (StructuredTextCommand) command;
			cursorPosition = structuredTextCommand.getTextStart();
			selectionLength = structuredTextCommand.getTextDeleted().length();
		}

		if (cursorPosition > -1 && fMediators != null && fMediators.length > 0) {
			for (int i = 0; i < fMediators.length; i++) {
				IDocument document = fMediators[i].getDocument();
				fMediators[i].undoOperationSelectionChanged(new UndoDocumentEvent(requester, document, cursorPosition, selectionLength));
			}
		}
	}

	public void undo() {
		undo(null);
	}

	public void undo(IDocumentSelectionMediator requester) {
		// Force an endRecording before undo.
		//
		// For example, recording was turned on on the Design Page of
		// PageDesigner.
		// Then undo is invoked on the Source Page. Recording should be
		// stopped before we undo.
		// Note that redo should not be available when we switch to the Source
		// Page.
		// Therefore, this force ending of recording is not needed in redo.
		if (fRecording)
			endRecording(this);

		if (undoable()) {
			IStructuredModel model = findStructuredModel(fDocument);
			IDocumentExtension4 docExt4 = null;
			DocumentRewriteSession rewriteSession = null;

			try {
				if (model != null)
					model.aboutToChangeModel();

				Command undoCommand = getUndoCommand();
				if (undoCommand instanceof CompoundCommand &&
						model.getStructuredDocument() instanceof IDocumentExtension4) {
					docExt4 = (IDocumentExtension4)model.getStructuredDocument();
				}
				rewriteSession = (docExt4 == null) ? null :
					docExt4.startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED);
				
				// make sure to undo before setting document selection
				fCommandStack.undo();

				// set document selection
				setUndoDocumentSelection(requester, undoCommand);
			}
			finally {
				if (docExt4 != null && rewriteSession != null)
					docExt4.stopRewriteSession(rewriteSession);
				if (model != null) {
					model.changedModel();
					model.releaseFromRead();
				}
			}
		}
	}

	public boolean undoable() {
		return fCommandStack.canUndo();
	}
}
