/*******************************************************************************
 * Copyright (c) 2000, 2008 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.internal.core.refactoring;

import java.util.LinkedList;

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.TextEdit;
import org.eclipse.text.edits.UndoEdit;

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

import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.ContentStamp;
import org.eclipse.ltk.core.refactoring.NullChange;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextFileChange;

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

	private ContentStamp fContentStampToRestore;

	private boolean fDirty;

	private IFile fFile;

	private String fName;

	private int fSaveMode;

	private UndoEdit[] fUndos;

	private BufferValidationState fValidationState;

	/**
	 * Create a new multi state undo 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 undos
	 *            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
	 */
	public MultiStateUndoChange(String name, IFile file, UndoEdit[] undos, ContentStamp stamp, int saveMode) {
		Assert.isNotNull(name);
		Assert.isNotNull(file);
		Assert.isNotNull(undos);
		fName= name;
		fFile= file;
		fUndos= undos;
		fContentStampToRestore= stamp;
		fSaveMode= saveMode;
	}

	/**
	 * 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 edits
	 *            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[] edits, ContentStamp stampToRestore) throws CoreException {
		return new MultiStateUndoChange(getName(), fFile, edits, stampToRestore, fSaveMode);
	}

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

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

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

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

	/**
	 * 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 final int getSaveMode() {
		return fSaveMode;
	}

	@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(), "MultiStateUndoChange 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();
		}
	}

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

	@Override
	public Change perform(IProgressMonitor pm) throws CoreException {
		if (fValidationState == null || fValidationState.isValid(needsSaving(), false).hasFatalError())
			return new NullChange();
		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);
			// perform the changes
			LinkedList<UndoEdit> list= new LinkedList<>();
			for (UndoEdit edit : fUndos) {
				UndoEdit redo= edit.apply(document, TextEdit.CREATE_UNDO);
				list.addFirst(redo);
			}

			// try to restore the document content stamp
			boolean success= ContentStamps.set(document, fContentStampToRestore);
			if (needsSaving()) {
				buffer.commit(pm, false);
				if (!success) {
					// We weren't able to restore document stamp.
					// Since we save restore the file stamp instead
					ContentStamps.set(fFile, fContentStampToRestore);
				}
			}
			return createUndoChange(list.toArray(new UndoEdit[list.size()]), currentStamp);
		} catch (BadLocationException e) {
			throw Changes.asCoreException(e);
		} finally {
			if (buffer != null)
				manager.disconnect(fFile.getFullPath(), LocationKind.IFILE, new SubProgressMonitor(pm, 1));
		}
	}
}
