/*******************************************************************************
 * Copyright (c) 2005, 2015 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.ltk.core.refactoring;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.core.resources.IFile;

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;

import org.eclipse.text.edits.InsertEdit;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.TextEditCopier;
import org.eclipse.text.edits.TextEditGroup;
import org.eclipse.text.edits.TextEditProcessor;
import org.eclipse.text.edits.UndoEdit;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentEvent;
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.jface.text.IDocumentListener;
import org.eclipse.jface.text.IPositionUpdater;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;

import org.eclipse.ltk.internal.core.refactoring.BufferValidationState;
import org.eclipse.ltk.internal.core.refactoring.Changes;
import org.eclipse.ltk.internal.core.refactoring.ContentStamps;
import org.eclipse.ltk.internal.core.refactoring.Lock;
import org.eclipse.ltk.internal.core.refactoring.MultiStateUndoChange;
import org.eclipse.ltk.internal.core.refactoring.NonDeletingPositionUpdater;
import org.eclipse.ltk.internal.core.refactoring.RefactoringCorePlugin;

/**
 * A multi state text file change is a special change object that applies a sequence of {@link TextEdit
 * text edit trees} to a document. The multi state text file change manages the text edit trees.
 * <p>
 * A multi state text file change offers the ability to access the original content of
 * the document as well as creating a preview of the change. The edit
 * trees get copied when creating any kind of preview. Therefore no region
 * updating on the original edit trees takes place when requesting a preview
 * (for more information on region updating see class {@link TextEdit TextEdit}.
 * If region tracking is required for a preview it can be enabled via a call
 * to the method {@link #setKeepPreviewEdits(boolean) setKeepPreviewEdits}.
 * If enabled the multi state text file change keeps the copied edit trees executed for the
 * preview allowing clients to map an original edit to an executed edit. The
 * executed edit can then be used to determine its position in the preview.
 * </p>
 *
 * @since 3.2
 */
public class MultiStateTextFileChange extends TextEditBasedChange {

	private static final class ComposableBufferChange {

		private TextEdit fEdit;

		private List<TextEditBasedChangeGroup> fGroups;

		private TextEdit getEdit() {
			return fEdit;
		}

		private List<TextEditBasedChangeGroup> getGroups() {
			return fGroups;
		}

		private void setEdit(final TextEdit edit) {
			Assert.isNotNull(edit);

			fEdit= edit;
		}

		private void setGroups(final List<TextEditBasedChangeGroup> groups) {
			Assert.isNotNull(groups);

			fGroups= groups;
		}
	}

	private static final class ComposableBufferChangeGroup extends TextEditBasedChangeGroup {

		private final Set<TextEdit> fEdits= new HashSet<>();

		private ComposableBufferChangeGroup(final MultiStateTextFileChange change, final TextEditGroup group) {
			super(change, group);

			final TextEdit[] edits= group.getTextEdits();
			for (TextEdit edit : edits) {
				cacheEdit(edit);
			}
		}

		private void cacheEdit(final TextEdit edit) {
			fEdits.add(edit);

			final TextEdit[] edits= edit.getChildren();
			for (TextEdit e : edits) {
				cacheEdit(e);
			}
		}

		private boolean containsEdit(final TextEdit edit) {
			return fEdits.contains(edit);
		}

		private Set<TextEdit> getCachedEdits() {
			return fEdits;
		}
	}

	private static final class ComposableEditPosition extends Position {

		private String fText;

		private String getText() {
			return fText;
		}

		private void setText(final String text) {
			Assert.isNotNull(text);

			fText= text;
		}
	}

	private static final class ComposableUndoEdit {

		private ComposableBufferChangeGroup fGroup;

		private TextEdit fOriginal;

		private ReplaceEdit fUndo;

		private ComposableBufferChangeGroup getGroup() {
			return fGroup;
		}

		private TextEdit getOriginal() {
			return fOriginal;
		}

		private String getOriginalText() {
			if (fOriginal instanceof ReplaceEdit) {
				return ((ReplaceEdit) getOriginal()).getText();
			} else if (fOriginal instanceof InsertEdit) {
				return ((InsertEdit) getOriginal()).getText();
			}
			return ""; //$NON-NLS-1$
		}

		private ReplaceEdit getUndo() {
			return fUndo;
		}

		private void setGroup(final ComposableBufferChangeGroup group) {
			Assert.isNotNull(group);

			fGroup= group;
		}

		private void setOriginal(final TextEdit edit) {
			fOriginal= edit;
		}

		private void setUndo(final ReplaceEdit undo) {
			Assert.isNotNull(undo);

			fUndo= undo;
		}
	}

	/** The position category for the resulting edit positions */
	private static final String COMPOSABLE_POSITION_CATEGORY= "ComposableEditPositionCategory_" + System.currentTimeMillis(); //$NON-NLS-1$

	/** The position category for the preview region range marker */
	private static final String MARKER_POSITION_CATEGORY= "MarkerPositionCategory_" + System.currentTimeMillis(); //$NON-NLS-1$

	/** The text file buffer */
	private ITextFileBuffer fBuffer;

	/** The last string obtained from a document */
	private String fCachedString;

	/** The internal change objects */
	private final ArrayList<ComposableBufferChange> fChanges= new ArrayList<>(4);

	/** The content stamp */
	private ContentStamp fContentStamp;

	/** The text edit copier */
	private TextEditCopier fCopier;

	/** The text file buffer reference count */
	private int fCount;

	/** The dirty flag */
	private boolean fDirty;

	/** The affected file */
	private IFile fFile;

	/** The save mode */
	private int fSaveMode= TextFileChange.KEEP_SAVE_STATE;

	/** The validation state */
	private BufferValidationState fValidationState;

	/**
	 * Creates a new composite text file change.
	 * <p>
	 * The default text type is <code>txt</code>.
	 * </p>
	 *
	 * @param name
	 *            the name of the composite text file change
	 * @param file
	 *            the text file to apply the change to
	 */
	public MultiStateTextFileChange(final String name, final IFile file) {
		super(name);

		Assert.isNotNull(file);
		fFile= file;

		setTextType("txt"); //$NON-NLS-1$
	}

	/**
	 * Acquires a document from the file buffer manager.
	 *
	 * @param monitor
	 *            the progress monitor to use
	 * @return the document
	 * @throws CoreException if the document could not successfully be acquired
	 */
	private IDocument acquireDocument(final IProgressMonitor monitor) throws CoreException {
		if (fCount > 0)
			return fBuffer.getDocument();

		final ITextFileBufferManager manager= FileBuffers.getTextFileBufferManager();
		final IPath path= fFile.getFullPath();

		manager.connect(path, LocationKind.IFILE, monitor);
		fCount++;

		fBuffer= manager.getTextFileBuffer(path, LocationKind.IFILE);

		final IDocument document= fBuffer.getDocument();
		fContentStamp= ContentStamps.get(fFile, document);

		return document;
	}

	/**
	 * Adds a new text change to this composite change.
	 * <p>
	 * The text change which is added is not changed in any way. Rather
	 * the contents of the text change are retrieved and stored internally
	 * in this composite text change.
	 * </p>
	 *
	 * @param change
	 *            the text change to add
	 */
	public final void addChange(final TextChange change) {
		Assert.isNotNull(change);

		final ComposableBufferChange result= new ComposableBufferChange();
		result.setEdit(change.getEdit());

		final TextEditBasedChangeGroup[] groups= change.getChangeGroups();
		final List<TextEditBasedChangeGroup> list= new ArrayList<>(groups.length);

		for (TextEditBasedChangeGroup g : groups) {
			final TextEditBasedChangeGroup group= new ComposableBufferChangeGroup(this, g.getTextEditGroup());
			list.add(group);
			addChangeGroup(group);
		}
		result.setGroups(list);
		fChanges.add(result);
	}

	// Copied from TextChange
	private TextEditProcessor createTextEditProcessor(ComposableBufferChange change, IDocument document, int flags, boolean preview) {
		List<TextEdit> excludes= new ArrayList<>(0);
		for (TextEditBasedChangeGroup group : change.getGroups()) {
			if (!group.isEnabled()) {
				excludes.addAll(Arrays.asList(group.getTextEdits()));
			}
		}

		if (preview) {
			fCopier= new TextEditCopier(change.getEdit());
			TextEdit copiedEdit= fCopier.perform();
			boolean keep= getKeepPreviewEdits();
			if (keep)
				flags= flags | TextEdit.UPDATE_REGIONS;
			LocalTextEditProcessor result= new LocalTextEditProcessor(document, copiedEdit, flags);
			result.setExcludes(mapEdits(excludes.toArray(new TextEdit[excludes.size()]), fCopier));
			if (!keep)
				fCopier= null;
			return result;
		} else {
			LocalTextEditProcessor result= new LocalTextEditProcessor(document, change.getEdit(), flags | TextEdit.UPDATE_REGIONS);
			result.setExcludes(excludes.toArray(new TextEdit[excludes.size()]));
			return result;
		}
	}

	/**
	 * Creates the corresponding text edit to the event.
	 *
	 * @param document
	 *            the document
	 * @param offset
	 *            the offset of the event
	 * @param length
	 *            the length of the event
	 * @param text
	 *            the text of the event
	 * @return the undo edit
	 */
	private ReplaceEdit createUndoEdit(final IDocument document, final int offset, final int length, final String text) {
		String currentText= null;
		try {
			currentText= document.get(offset, length);
		} catch (BadLocationException cannotHappen) {
			// Cannot happen
		}

		if (fCachedString != null && fCachedString.equals(currentText))
			currentText= fCachedString;
		else
			fCachedString= currentText;

		return new ReplaceEdit(offset, text != null ? text.length() : 0, currentText);
	}

	/*
	 * @see org.eclipse.ltk.core.refactoring.Change#dispose()
	 */
	@Override
	public final void dispose() {
		if (fValidationState != null) {
			fValidationState.dispose();
		}
	}

	/*
	 * @see org.eclipse.ltk.core.refactoring.TextEditBasedChange#getCurrentContent(org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public final String getCurrentContent(final IProgressMonitor monitor) throws CoreException {
		return getCurrentDocument(monitor).get();
	}

	/*
	 * @see org.eclipse.ltk.core.refactoring.TextEditBasedChange#getCurrentContent(org.eclipse.jface.text.IRegion,boolean,int,org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public final String getCurrentContent(final IRegion region, final boolean expand, final int surround, final IProgressMonitor monitor) throws CoreException {
		Assert.isNotNull(region);
		Assert.isTrue(surround >= 0);
		final IDocument document= getCurrentDocument(monitor);
		Assert.isTrue(document.getLength() >= region.getOffset() + region.getLength());
		return getContent(document, region, expand, surround);
	}

	/**
	 * Returns a document representing the current state of the buffer,
	 * prior to the application of the change.
	 * <p>
	 * The returned document should not be modified.
	 * </p>
	 *
	 * @param monitor
	 *            the progress monitor to use, or <code>null</code>
	 * @return the current document, or the empty document
	 * @throws CoreException
	 *             if no document could be acquired
	 */
	public final IDocument getCurrentDocument(IProgressMonitor monitor) throws CoreException {
		if (monitor == null)
			monitor= new NullProgressMonitor();
		IDocument result= null;
		monitor.beginTask("", 2); //$NON-NLS-1$
		try {
			result= acquireDocument(new SubProgressMonitor(monitor, 1));
		} finally {
			if (result != null)
				releaseDocument(result, new SubProgressMonitor(monitor, 1));
		}
		monitor.done();
		if (result == null)
			result= new Document();
		return result;
	}

	/*
	 * @see org.eclipse.ltk.core.refactoring.Change#getModifiedElement()
	 */
	@Override
	public final Object getModifiedElement() {
		return fFile;
	}

	/*
	 * @see org.eclipse.ltk.core.refactoring.TextEditBasedChange#getPreviewContent(org.eclipse.ltk.core.refactoring.TextEditBasedChangeGroup[],org.eclipse.jface.text.IRegion,boolean,int,org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public final String getPreviewContent(final TextEditBasedChangeGroup[] groups, final IRegion region, final boolean expand, final int surround, final IProgressMonitor monitor) throws CoreException {

		final Set<TextEditBasedChangeGroup> cachedGroups= new HashSet<>(Arrays.asList(groups));
		final IDocument document= new Document(getCurrentDocument(monitor).get());

		// Marks the region in the document to be previewed
		final Position range= new Position(region.getOffset(), region.getLength());
		try {

			final TextEditBasedChangeGroup[] changedGroups= getChangeGroups();

			LinkedList<LinkedList<ComposableUndoEdit>> compositeUndo= new LinkedList<>();
			for (ComposableBufferChange change : fChanges) {

				try {
					// Have to use a copy
					fCopier= new TextEditCopier(change.getEdit());
					TextEdit copy= fCopier.perform();

					// Create a mapping from the copied edits to its originals
					final Map<TextEdit, TextEdit> originalMap= new HashMap<>();
					for (TextEditBasedChangeGroup textEditBasedChangeGroup : change.getGroups()) {

						final ComposableBufferChangeGroup group= (ComposableBufferChangeGroup) textEditBasedChangeGroup;
						for (TextEdit originalEdit : group.getCachedEdits()) {

							final TextEdit copiedEdit= fCopier.getCopy(originalEdit);

							if (copiedEdit != null)
								originalMap.put(copiedEdit, originalEdit);
							else
								RefactoringCorePlugin.logErrorMessage("Could not find a copy for the indexed text edit " + originalEdit.toString()); //$NON-NLS-1$
						}
					}

					final ComposableBufferChangeGroup[] currentGroup= { null};
					final TextEdit[] currentEdit= { null};

					// Text edit processor which sets the change group and
					// current edit when processing
					final TextEditProcessor processor= new TextEditProcessor(document, copy, TextEdit.NONE) {

						@Override
						protected final boolean considerEdit(final TextEdit edit) {

							final TextEdit originalEdit= originalMap.get(edit);
							if (originalEdit != null) {

								currentEdit[0]= originalEdit;

								boolean found= false;
								for (int offset= 0; offset < changedGroups.length && !found; offset++) {

									final ComposableBufferChangeGroup group= (ComposableBufferChangeGroup) changedGroups[offset];
									if (group.containsEdit(originalEdit)) {

										currentGroup[0]= group;
										found= true;
									}
								}
								if (!found)
									currentGroup[0]= null;

							} else if (!(edit instanceof MultiTextEdit)) {
								RefactoringCorePlugin.logErrorMessage("Could not find the original of the copied text edit " + edit.toString()); //$NON-NLS-1$
							}
							return true;
						}
					};

					final LinkedList<ComposableUndoEdit> eventUndos= new LinkedList<>();

					// Listener to record the undos on the document (offsets
					// relative to the document event)
					final IDocumentListener listener= new IDocumentListener() {

						@Override
						public final void documentAboutToBeChanged(final DocumentEvent event) {
							final ComposableUndoEdit edit= new ComposableUndoEdit();

							edit.setGroup(currentGroup[0]);
							edit.setOriginal(currentEdit[0]);
							edit.setUndo(createUndoEdit(document, event.getOffset(), event.getLength(), event.getText()));

							eventUndos.addFirst(edit);
						}

						@Override
						public final void documentChanged(final DocumentEvent event) {
							// Do nothing
						}
					};

					try {
						// Record undos in LIFO order
						document.addDocumentListener(listener);
						processor.performEdits();
					} finally {
						document.removeDocumentListener(listener);
					}

					compositeUndo.addFirst(eventUndos);

				} finally {
					fCopier= null;
				}
			}

			final IPositionUpdater positionUpdater= event -> {

				final int eventOffset= event.getOffset();
				final int eventLength= event.getLength();
				final int eventOldEndOffset= eventOffset + eventLength;
				final String eventText= event.getText();
				final int eventNewLength= eventText == null ? 0 : eventText.length();
				final int eventNewEndOffset= eventOffset + eventNewLength;
				final int deltaLength= eventNewLength - eventLength;

				try {

					final Position[] positions= event.getDocument().getPositions(COMPOSABLE_POSITION_CATEGORY);
					for (Position position : positions) {
						if (position.isDeleted())
							continue;

						final int offset= position.getOffset();
						final int length= position.getLength();
						final int end= offset + length;

						if (offset > eventOldEndOffset) {
							// position comes way after change - shift
							position.setOffset(offset + deltaLength);
						} else if ((end < eventOffset) // Position comes way before change - leave
								// alone
								|| (offset == eventOffset)) { // Leave alone, since the edits are overlapping
						} else if (offset <= eventOffset && end >= eventOldEndOffset) {
							// event completely internal to the position
							// -
							// adjust length
							position.setLength(length + deltaLength);
						} else if (offset < eventOffset) {
							// event extends over end of position - include
							// the
							// replacement text into the position
							position.setLength(eventNewEndOffset - offset);
						} else if (end > eventOldEndOffset) {
							// event extends from before position into it -
							// adjust
							// offset and length, including the replacement
							// text into
							// the position
							position.setOffset(eventOffset);
							int deleted= eventOldEndOffset - offset;
							position.setLength(length - deleted + eventNewLength);
						} else {
							// event comprises the position - keep it at the
							// same
							// position, but always inside the replacement
							// text
							int newOffset= Math.min(offset, eventNewEndOffset);
							int newEndOffset= Math.min(end, eventNewEndOffset);
							position.setOffset(newOffset);
							position.setLength(newEndOffset - newOffset);
						}
					}
				} catch (BadPositionCategoryException exception) {
					// ignore and return
				}
			};

			try {

				document.addPositionCategory(COMPOSABLE_POSITION_CATEGORY);
				document.addPositionUpdater(positionUpdater);

				// Apply undos in LIFO order to get to the original document
				// Track the undos of edits which are in change groups to be
				// previewed and insert
				// Undo edits for them (corresponding to the linearized net
				// effect on the original document)
				final LinkedList<ComposableEditPosition> undoQueue= new LinkedList<>();
				for (LinkedList<ComposableUndoEdit> outer : compositeUndo) {
					for (ComposableUndoEdit edit : outer) {

						final ReplaceEdit undo= edit.getUndo();

						final int offset= undo.getOffset();
						final int length= undo.getLength();
						final String text= undo.getText();

						ComposableEditPosition position= new ComposableEditPosition();
						if (cachedGroups.contains(edit.getGroup())) {

							if (text == null || text.isEmpty()) {
								position.offset= offset;
								if (length != 0) {
									// Undo is a delete, create final insert
									// edit
									position.length= 0;
									position.setText(edit.getOriginalText());
								} else
									RefactoringCorePlugin.logErrorMessage("Dubious undo edit found: " + undo.toString()); //$NON-NLS-1$

							} else if (length == 0) {
								position.offset= offset;
								// Undo is an insert, create final delete
								// edit
								position.setText(""); //$NON-NLS-1$
								position.length= text.length();
							} else {
								// Undo is a replace, create final replace edit
								position.offset= offset;
								position.length= length;
								position.setText(edit.getOriginalText());
							}

							document.addPosition(COMPOSABLE_POSITION_CATEGORY, position);
						}

						position= new ComposableEditPosition();
						position.offset= undo.getOffset();
						position.length= undo.getLength();
						position.setText(undo.getText());

						undoQueue.add(position);
					}

					for (final Iterator<ComposableEditPosition> iterator= undoQueue.iterator(); iterator.hasNext();) {
						final ComposableEditPosition position= iterator.next();

						document.replace(position.offset, position.length, position.getText());
						iterator.remove();
					}
				}

				// Use a simple non deleting position updater for the range
				final IPositionUpdater markerUpdater= new NonDeletingPositionUpdater(MARKER_POSITION_CATEGORY);

				try {

					final Position[] positions= document.getPositions(COMPOSABLE_POSITION_CATEGORY);
					document.addPositionCategory(MARKER_POSITION_CATEGORY);
					document.addPositionUpdater(markerUpdater);
					document.addPosition(MARKER_POSITION_CATEGORY, range);

					for (Position p : positions) {
						final ComposableEditPosition position= (ComposableEditPosition) p;
						document.replace(position.offset, position.length, position.getText() != null ? position.getText() : ""); //$NON-NLS-1$
					}

				} catch (BadPositionCategoryException exception) {
					RefactoringCorePlugin.log(exception);
				} finally {
					document.removePositionUpdater(markerUpdater);
					try {
						document.removePosition(MARKER_POSITION_CATEGORY, range);
						document.removePositionCategory(MARKER_POSITION_CATEGORY);
					} catch (BadPositionCategoryException exception) {
						// Cannot happen
					}
				}
			} catch (BadPositionCategoryException exception) {
				RefactoringCorePlugin.log(exception);
			} finally {
				document.removePositionUpdater(positionUpdater);
				try {
					document.removePositionCategory(COMPOSABLE_POSITION_CATEGORY);
				} catch (BadPositionCategoryException exception) {
					RefactoringCorePlugin.log(exception);
				}
			}

			return getContent(document, new Region(range.offset, range.length), expand, surround);

		} catch (MalformedTreeException | BadLocationException exception) {
			RefactoringCorePlugin.log(exception);
		}
		return getPreviewDocument(monitor).get();
	}

	/*
	 * @see org.eclipse.ltk.core.refactoring.TextEditBasedChange#getPreviewContent(org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public final String getPreviewContent(final IProgressMonitor monitor) throws CoreException {
		return getPreviewDocument(monitor).get();
	}

	/**
	 * Returns a document representing the preview of the refactored buffer,
	 * after the application of the change object.
	 *
	 * @param monitor
	 *            the progress monitor to use, or <code>null</code>
	 * @return the preview document, or an empty document
	 * @throws CoreException
	 *             if no document could be acquired
	 */
	public final IDocument getPreviewDocument(IProgressMonitor monitor) throws CoreException {
		if (monitor == null)
			monitor= new NullProgressMonitor();

		IDocument result= null;
		IDocument document= null;

		try {

			document= acquireDocument(new SubProgressMonitor(monitor, 1));
			if (document != null) {
				result= new Document(document.get());

				performChanges(result, null, true);
			}

		} catch (BadLocationException exception) {
			throw Changes.asCoreException(exception);
		} finally {
			if (document != null) {
				releaseDocument(document, new SubProgressMonitor(monitor, 1));
			}
			monitor.done();
		}
		if (result == null)
			result= new Document();
		return result;
	}

	/**
	 * Returns the save mode of this change.
	 *
	 * @return the save mode
	 */
	public final int getSaveMode() {
		return fSaveMode;
	}

	/*
	 * @see org.eclipse.ltk.core.refactoring.Change#initializeValidationData(org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public final void initializeValidationData(IProgressMonitor monitor) {
		if (monitor == null)
			monitor= new NullProgressMonitor();
		monitor.beginTask("", 1); //$NON-NLS-1$
		try {
			fValidationState= BufferValidationState.create(fFile);
		} finally {
			monitor.worked(1);
		}
	}

	/*
	 * @see org.eclipse.ltk.core.refactoring.Change#isValid(org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public final RefactoringStatus isValid(IProgressMonitor monitor) throws CoreException, OperationCanceledException {
		if (monitor == null)
			monitor= new NullProgressMonitor();
		monitor.beginTask("", 1); //$NON-NLS-1$
		try {
			if (fValidationState == null)
				throw new CoreException(new Status(IStatus.ERROR, RefactoringCorePlugin.getPluginId(), "MultiStateTextFileChange has not been initialialized")); //$NON-NLS-1$


			final ITextFileBuffer buffer= FileBuffers.getTextFileBufferManager().getTextFileBuffer(fFile.getFullPath(), LocationKind.IFILE);
			fDirty= buffer != null && buffer.isDirty();

			final RefactoringStatus status= fValidationState.isValid(needsSaving());
			if (needsSaving()) {
				status.merge(Changes.validateModifiesFiles(new IFile[] { fFile }));
			} else {
				// we are reading the file. So it should be at least in sync
				status.merge(Changes.checkInSync(new IFile[] { fFile }));
			}

			return status;
		} finally {
			monitor.done();
		}
	}

	/**
	 * Does the change need saving?
	 *
	 * @return <code>true</code> if it needs saving, <code>false</code>
	 *         otherwise
	 */
	public final boolean needsSaving() {
		return (fSaveMode & TextFileChange.FORCE_SAVE) != 0 || !fDirty && (fSaveMode & TextFileChange.KEEP_SAVE_STATE) != 0;
	}

	/*
	 * @see org.eclipse.ltk.core.refactoring.Change#perform(org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public final Change perform(final IProgressMonitor monitor) throws CoreException {
		monitor.beginTask("", 3); //$NON-NLS-1$

		IDocument document= null;

		try {
			document= acquireDocument(new SubProgressMonitor(monitor, 1));

			final LinkedList<UndoEdit> undoList= new LinkedList<>();
			performChanges(document, undoList, false);

			if (needsSaving())
				fBuffer.commit(new SubProgressMonitor(monitor, 1), false);

			return new MultiStateUndoChange(getName(), fFile, undoList.toArray(new UndoEdit[undoList.size()]), fContentStamp, fSaveMode);

		} catch (BadLocationException exception) {
			throw Changes.asCoreException(exception);
		} finally {
			if (document != null) {
				releaseDocument(document, new SubProgressMonitor(monitor, 1));
			}
			monitor.done();
		}
	}

	/**
	 * Performs the changes on the specified document.
	 *
	 * @param document
	 *            the document to perform the changes on
	 * @param undoList
	 *            the undo list, or <code>null</code> to discard the undos
	 * @param preview
	 *            <code>true</code> if the changes are performed for preview,
	 *            <code>false</code> otherwise
	 * @throws BadLocationException
	 *             if the edit tree could not be applied
	 */
	private void performChanges(final IDocument document, final LinkedList<UndoEdit> undoList, final boolean preview) throws BadLocationException {
		if (! fBuffer.isSynchronizationContextRequested()) {
			performChangesInSynchronizationContext(document, undoList, preview);
			return;
		}

		ITextFileBufferManager fileBufferManager= FileBuffers.getTextFileBufferManager();

		/** The lock for waiting for computation in the UI thread to complete. */
		final Lock completionLock= new Lock();
		final BadLocationException[] exception= new BadLocationException[1];
		Runnable runnable= () -> {
			synchronized (completionLock) {
				try {
					performChangesInSynchronizationContext(document, undoList, preview);
				} catch (BadLocationException e) {
					exception[0]= e;
				} finally {
					completionLock.fDone= true;
					completionLock.notifyAll();
				}
			}
		};

		synchronized (completionLock) {
			fileBufferManager.execute(runnable);
			while (! completionLock.fDone) {
				try {
					completionLock.wait(500);
				} catch (InterruptedException x) {
				}
			}
		}

		if (exception[0] != null) {
			throw exception[0];
		}
	}

	private void performChangesInSynchronizationContext(final IDocument document, final LinkedList<UndoEdit> undoList, final boolean preview) throws BadLocationException {
		DocumentRewriteSession session= null;
		try {
			if (document instanceof IDocumentExtension4)
				session= ((IDocumentExtension4) document).startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED);

			for (ComposableBufferChange change : fChanges) {
				final UndoEdit edit= createTextEditProcessor(change, document, undoList != null ? TextEdit.CREATE_UNDO : TextEdit.NONE, preview).performEdits();
				if (undoList != null)
					undoList.addFirst(edit);
			}

		} finally {
			if (session != null)
				((IDocumentExtension4) document).stopRewriteSession(session);
		}
	}

	/**
	 * Releases the document.
	 *
	 * @param document
	 *            the document to release
	 * @param monitor
	 *            the progress monitor
	 * @throws CoreException if the document could not successfully be released
	 */
	private void releaseDocument(final IDocument document, final IProgressMonitor monitor) throws CoreException {
		Assert.isTrue(fCount > 0);

		if (fCount == 1)
			FileBuffers.getTextFileBufferManager().disconnect(fFile.getFullPath(), LocationKind.IFILE, monitor);

		fCount--;
	}

	/*
	 * @see org.eclipse.ltk.core.refactoring.TextEditBasedChange#setKeepPreviewEdits(boolean)
	 */
	@Override
	public final void setKeepPreviewEdits(final boolean keep) {
		super.setKeepPreviewEdits(keep);

		if (!keep)
			fCopier= null;
	}

	/**
	 * Sets the save mode.
	 *
	 * @param mode
	 *            The mode to set
	 */
	public final void setSaveMode(final int mode) {
		fSaveMode= mode;
	}
}
