/*******************************************************************************
 * Copyright (c) 2000, 2010 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 org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
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.MalformedTreeException;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.UndoEdit;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.link.LinkedModeModel;

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.RefactoringCorePlugin;

/**
 * A change to perform the reverse change of a {@link TextFileChange}.
 * <p>
 * This class is not intended to be instantiated by clients. It is
 * usually created by a <code>TextFileChange</code> object.
 * </p>
 * <p>
 * The class should be subclassed by clients also subclassing <code>
 * TextFileChange</code> to provide a proper undo change object.
 * </p>
 * @since 3.0
 */
public class UndoTextFileChange extends Change {

	private String fName;
	private UndoEdit fUndo;
	private IFile fFile;
	private ContentStamp fContentStampToRestore;
	private int fSaveMode;

	private boolean fDirty;
	private BufferValidationState fValidationState;

	/**
	 * Create a new undo text file change object.
	 *
	 * @param name the human readable name of the change
	 * @param file the file the change is working on
	 * @param stamp the content stamp to restore when the undo is executed
	 * @param undo the edit representing the undo modifications
	 * @param saveMode the save mode as specified by {@link TextFileChange}
	 *
	 * @see TextFileChange#KEEP_SAVE_STATE
	 * @see TextFileChange#FORCE_SAVE
	 * @see TextFileChange#LEAVE_DIRTY
	 */
	protected UndoTextFileChange(String name, IFile file, UndoEdit undo, ContentStamp stamp, int saveMode) {
		Assert.isNotNull(name);
		Assert.isNotNull(file);
		Assert.isNotNull(undo);
		fName= name;
		fFile= file;
		fUndo= undo;
		fContentStampToRestore= stamp;
		fSaveMode= saveMode;
	}

	/**
	 * Returns the change's save mode.
	 *
	 * @return the change's save mode
	 *
	 * @see TextFileChange#KEEP_SAVE_STATE
	 * @see TextFileChange#FORCE_SAVE
	 * @see TextFileChange#LEAVE_DIRTY
	 */
	public int getSaveMode() {
		return fSaveMode;
	}

	@Override
	public String getName() {
		return fName;
	}

	/**
	 * Hook to create an undo change for the given undo edit. This hook
	 * gets called while performing the change to construct the corresponding
	 * undo change object.
	 * <p>
	 * Subclasses may override it to create a different undo change.
	 * </p>
	 * @param edit the {@link UndoEdit undo edit} to create a undo change for
	 * @param stampToRestore the content stamp to restore when the undo
	 *  edit is executed.
	 *
	 * @return the undo change
	 *
	 * @throws CoreException if an undo change can't be created
	 */
	protected Change createUndoChange(UndoEdit edit, ContentStamp stampToRestore) throws CoreException {
		return new UndoTextFileChange(getName(), fFile, edit, stampToRestore, fSaveMode);
	}

	@Override
	public Object getModifiedElement() {
		return fFile;
	}

	@Override
	public Object[] getAffectedObjects() {
		Object modifiedElement= getModifiedElement();
		if (modifiedElement == null)
			return null;
		return new Object[] { modifiedElement };
	}

	@Override
	public void initializeValidationData(IProgressMonitor pm) {
		if (pm == null)
			pm= new NullProgressMonitor();
		pm.beginTask("", 1); //$NON-NLS-1$
		try {
			fValidationState= BufferValidationState.create(fFile);
		} finally {
			pm.done();
		}
	}

	@Override
	public RefactoringStatus isValid(IProgressMonitor pm) throws CoreException {
		if (pm == null)
			pm= new NullProgressMonitor();
		pm.beginTask("", 1); //$NON-NLS-1$
		try {
			if (fValidationState == null)
				throw new CoreException(new Status(IStatus.ERROR, RefactoringCorePlugin.getPluginId(), "UndoTextFileChange has not been initialialized")); //$NON-NLS-1$

			ITextFileBuffer buffer= FileBuffers.getTextFileBufferManager().getTextFileBuffer(fFile.getFullPath(), LocationKind.IFILE);
			fDirty= buffer != null && buffer.isDirty();
			return fValidationState.isValid(needsSaving(), true);
		} finally {
			pm.done();
		}
	}

	@Override
	public Change perform(IProgressMonitor pm) throws CoreException {
		if (pm == null)
			pm= new NullProgressMonitor();
		ITextFileBufferManager manager= FileBuffers.getTextFileBufferManager();
		pm.beginTask("", 2); //$NON-NLS-1$
		ITextFileBuffer buffer= null;
		try {
			manager.connect(fFile.getFullPath(), LocationKind.IFILE, new SubProgressMonitor(pm, 1));
			buffer= manager.getTextFileBuffer(fFile.getFullPath(), LocationKind.IFILE);
			IDocument document= buffer.getDocument();
			ContentStamp currentStamp= ContentStamps.get(fFile, document);

			boolean[] setContentStampSuccess= { false };
			UndoEdit redo= performEdits(buffer, document, setContentStampSuccess);

			if (needsSaving()) {
				buffer.commit(pm, false);
				if (!setContentStampSuccess[0]) {
					// We weren't able to restore document stamp.
					// Since we save restore the file stamp instead
					ContentStamps.set(fFile, fContentStampToRestore);
				}
			}
			return createUndoChange(redo, currentStamp);
		} catch (BadLocationException e) {
			if (fValidationState == null || !fValidationState.wasDerived())
				throw Changes.asCoreException(e);
			else
				return new NullChange();
		} catch (MalformedTreeException e) {
			if (fValidationState == null || !fValidationState.wasDerived())
				throw Changes.asCoreException(e);
			else
				return new NullChange();
		} catch (CoreException e) {
			if (fValidationState == null || !fValidationState.wasDerived())
				throw e;
			else
				return new NullChange();
		} finally {
			if (buffer != null)
				manager.disconnect(fFile.getFullPath(), LocationKind.IFILE, new SubProgressMonitor(pm, 1));
		}
	}

	private UndoEdit performEdits(ITextFileBuffer buffer, final IDocument document, final boolean[] setContentStampSuccess) throws MalformedTreeException, BadLocationException, CoreException {
		if (! buffer.isSynchronizationContextRequested()) {
			return doPerformEdits(document, setContentStampSuccess);
		}

		ITextFileBufferManager fileBufferManager= FileBuffers.getTextFileBufferManager();

		/** The lock for waiting for computation in the UI thread to complete. */
		final Lock completionLock= new Lock();
		final UndoEdit[] result= new UndoEdit[1];
		final BadLocationException[] badLocationException= new BadLocationException[1];
		final MalformedTreeException[] malformedTreeException= new MalformedTreeException[1];
		final CoreException[] coreException= new CoreException[1];
		Runnable runnable= () -> {
			synchronized (completionLock) {
				try {
					result[0]= doPerformEdits(document, setContentStampSuccess);
				} catch (BadLocationException e) {
					badLocationException[0]= e;
				} catch (MalformedTreeException e) {
					malformedTreeException[0]= e;
				} catch (CoreException e) {
					coreException[0]= e;
				} finally {
					completionLock.fDone= true;
					completionLock.notifyAll();
				}
			}
		};

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

		if (badLocationException[0] != null) {
			throw badLocationException[0];
		} else if (malformedTreeException[0] != null) {
			throw malformedTreeException[0];
		} else if (coreException[0] != null) {
			throw coreException[0];
		}
		return result[0];
	}

	private UndoEdit doPerformEdits(IDocument document, boolean[] setContentStampSuccess) throws MalformedTreeException, BadLocationException, CoreException {
		// perform the changes
		LinkedModeModel.closeAllModels(document);
		UndoEdit redo= fUndo.apply(document, TextEdit.CREATE_UNDO);

		// try to restore the document content stamp
		setContentStampSuccess[0]= ContentStamps.set(document, fContentStampToRestore);
		return redo;
	}

	@Override
	public void dispose() {
		if (fValidationState != null) {
			fValidationState.dispose();
		}
	}

	private boolean needsSaving() {
		return (fSaveMode & TextFileChange.FORCE_SAVE) != 0 || !fDirty && (fSaveMode & TextFileChange.KEEP_SAVE_STATE) != 0;
	}
}
