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

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubProgressMonitor;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;

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.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension4;

import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.internal.core.refactoring.BasicElementLabels;
import org.eclipse.ltk.internal.core.refactoring.Messages;
import org.eclipse.ltk.internal.core.refactoring.RefactoringCoreMessages;

/**
 * Abstract change for resource based changes. The change controls the resource time stamp
 * and read only state of the resource and makes sure it is not changed before executing the change.
 *
 * @since 3.4
 */
public abstract class ResourceChange extends Change {

	/**
	 * The default validation method. It tests the modified element for existence and makes sure it has not been modified
	 * since the change has been created.
	 */
	public static final int VALIDATE_DEFAULT= 0;

	/**
	 * The 'not read only' validation method performs the default validations (see {@link #VALIDATE_DEFAULT}) and additionally ensures that the element
	 * is not read only.
	 */
	public static final int VALIDATE_NOT_READ_ONLY= 1 << 0;

	/**
	 * The 'not dirty' validation method performs the default validations (see {@link #VALIDATE_DEFAULT}) and additionally ensures that the element
	 * does not contain unsaved modifications.
	 */
	public static final int VALIDATE_NOT_DIRTY= 1 << 1;

	/**
	 * The 'save if dirty' validation method performs the default validations (see {@link #VALIDATE_DEFAULT}) and will
	 * save all unsaved modifications to the resource.
	 */
	public static final int SAVE_IF_DIRTY= 1 << 2;

	private long fModificationStamp;
	private boolean fReadOnly;
	private int fValidationMethod;

	/**
	 * Creates the resource change. The modification state will be
	 */
	public ResourceChange() {
		fModificationStamp= IResource.NULL_STAMP;
		fReadOnly= false;
		fValidationMethod= VALIDATE_DEFAULT;
	}

	/**
	 * Returns the resource of this change.
	 *
	 * @return the resource of this change
	 */
	protected abstract IResource getModifiedResource();

	@Override
	public void initializeValidationData(IProgressMonitor pm) {
		IResource resource= getModifiedResource();
		if (resource != null) {
			fModificationStamp= getModificationStamp(resource);
			fReadOnly= Resources.isReadOnly(resource);
		}
	}

	/**
	 * Sets the validation methods used when the current resource is validated in {@link #isValid(IProgressMonitor)}.
	 * <p>
	 * By default the validation method is {@link #VALIDATE_DEFAULT}. Change implementors can add {@link #VALIDATE_NOT_DIRTY},
	 *  {@link #VALIDATE_NOT_READ_ONLY} or {@link #SAVE_IF_DIRTY}.
	 * </p>
	 *
	 * @param validationMethod the validation method used in {@link #isValid(IProgressMonitor)}.
	 * Supported validation methods currently are:
	 * <ul><li>{@link #VALIDATE_DEFAULT}</li>
	 * <li>{@link #VALIDATE_NOT_DIRTY}</li>
	 * <li>{@link #VALIDATE_NOT_READ_ONLY}</li>
	 * <li>{@link #SAVE_IF_DIRTY}</li>
	 * </ul>
	 * or combinations of these variables.
	 */
	public void setValidationMethod(int validationMethod) {
		fValidationMethod= validationMethod;
	}

	/**
	 * This implementation of {@link Change#isValid(IProgressMonitor)} tests the modified resource using the validation method
	 * specified by {@link #setValidationMethod(int)}.
	 */
	@Override
	public RefactoringStatus isValid(IProgressMonitor pm) throws CoreException, OperationCanceledException {
		pm.beginTask("", 2); //$NON-NLS-1$
		try {
			RefactoringStatus result= new RefactoringStatus();
			IResource resource= getModifiedResource();
			checkExistence(result, resource);
			if (result.hasFatalError())
				return result;
			if (fValidationMethod == VALIDATE_DEFAULT)
				return result;

			ValidationState state= new ValidationState(resource);
			state.checkModificationStamp(result, fModificationStamp);
			if (result.hasFatalError())
				return result;
			state.checkSameReadOnly(result, fReadOnly);
			if (result.hasFatalError())
				return result;
			if ((fValidationMethod & VALIDATE_NOT_READ_ONLY) != 0) {
				state.checkReadOnly(result);
				if (result.hasFatalError())
					return result;
			}
			if ((fValidationMethod & SAVE_IF_DIRTY) != 0) {
				state.saveIfDirty(result, fModificationStamp, new SubProgressMonitor(pm, 1));
			}
			if ((fValidationMethod & VALIDATE_NOT_DIRTY) != 0) {
				state.checkDirty(result);
			}
			return result;
		} finally {
			pm.done();
		}
	}

	/**
	 * Utility method to validate a resource to be modified.
	 *
	 * @param result the status where the result will be added to
	 * @param resource the resource to validate
	 * @param validationMethod the validation method used in {@link #isValid(IProgressMonitor)}.
	 * Supported validation methods currently are:
	 * <ul><li>{@link #VALIDATE_DEFAULT}</li>
	 * <li>{@link #VALIDATE_NOT_DIRTY}</li>
	 * <li>{@link #VALIDATE_NOT_READ_ONLY}</li>
	 * <li>{@link #SAVE_IF_DIRTY}</li>
	 * </ul>
	 * or combinations of these methods.
	 */
	protected static void checkIfModifiable(RefactoringStatus result, IResource resource, int validationMethod) {
		checkExistence(result, resource);
		if (result.hasFatalError())
			return;
		if (validationMethod == VALIDATE_DEFAULT)
			return;
		ValidationState state= new ValidationState(resource);
		if ((validationMethod & VALIDATE_NOT_READ_ONLY) != 0) {
			state.checkReadOnly(result);
			if (result.hasFatalError())
				return;
		}
		if ((validationMethod & VALIDATE_NOT_DIRTY) != 0) {
			state.checkDirty(result);
		}
	}

	private static void checkExistence(RefactoringStatus status, IResource element) {
		if (element == null) {
			status.addFatalError(RefactoringCoreMessages.ResourceChange_error_no_input);
		} else if (!element.exists()) {
			status.addFatalError(Messages.format(RefactoringCoreMessages.ResourceChange_error_does_not_exist, BasicElementLabels.getPathLabel(element.getFullPath(), false)));
		}
	}

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


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

	private long getModificationStamp(IResource resource) {
		if (!(resource instanceof IFile))
			return resource.getModificationStamp();
		IFile file= (IFile)resource;
		ITextFileBuffer buffer= getBuffer(file);
		if (buffer == null) {
			return file.getModificationStamp();
		} else {
			IDocument document= buffer.getDocument();
			if (document instanceof IDocumentExtension4) {
				return ((IDocumentExtension4)document).getModificationStamp();
			} else {
				return file.getModificationStamp();
			}
		}
	}

	private static ITextFileBuffer getBuffer(IFile file) {
		ITextFileBufferManager manager= FileBuffers.getTextFileBufferManager();
		return manager.getTextFileBuffer(file.getFullPath(), LocationKind.IFILE);
	}

	private static class ValidationState {
		private IResource fResource;
		private int fKind;
		private boolean fDirty;
		private boolean fReadOnly;
		private long fModificationStamp;
		private ITextFileBuffer fTextFileBuffer;
		public static final int RESOURCE= 1;
		public static final int DOCUMENT= 2;

		public ValidationState(IResource resource) {
			fResource= resource;
			if (resource instanceof IFile) {
				initializeFile((IFile) resource);
			} else {
				initializeResource(resource);
			}
		}

		public void saveIfDirty(RefactoringStatus status, long stampToMatch, IProgressMonitor pm) throws CoreException {
			if (fDirty) {
				if (fKind == DOCUMENT && fTextFileBuffer != null && stampToMatch == fModificationStamp) {
					fTextFileBuffer.commit(pm, false);
				} else {
					status.addFatalError(Messages.format(RefactoringCoreMessages.ResourceChange_error_unsaved, BasicElementLabels.getPathLabel(fResource.getFullPath(), false)));
				}
			}
		}

		public void checkDirty(RefactoringStatus status) {
			if (fDirty) {
				status.addFatalError(Messages.format(RefactoringCoreMessages.ResourceChange_error_unsaved, BasicElementLabels.getPathLabel(fResource.getFullPath(), false)));
			}
		}

		public void checkReadOnly(RefactoringStatus status) {
			if (fReadOnly) {
				status.addFatalError(Messages.format(RefactoringCoreMessages.ResourceChange_error_read_only, BasicElementLabels.getPathLabel(fResource.getFullPath(), false)));
			}
		}

		public void checkSameReadOnly(RefactoringStatus status, boolean valueToMatch) {
			if (fReadOnly != valueToMatch) {
				status.addFatalError(Messages.format(RefactoringCoreMessages.ResourceChange_error_read_only_state_changed, BasicElementLabels.getPathLabel(fResource.getFullPath(), false)));
			}
		}

		public void checkModificationStamp(RefactoringStatus status, long stampToMatch) {
			if (fKind == DOCUMENT) {
				if (stampToMatch != IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP && fModificationStamp != stampToMatch) {
					status.addFatalError(Messages.format(RefactoringCoreMessages.ResourceChange_error_has_been_modified, BasicElementLabels.getPathLabel(fResource.getFullPath(), false)));
				}
			} else {
				if (stampToMatch != IResource.NULL_STAMP && fModificationStamp != stampToMatch) {
					status.addFatalError(Messages.format(RefactoringCoreMessages.ResourceChange_error_has_been_modified, BasicElementLabels.getPathLabel(fResource.getFullPath(), false)));
				}
			}
		}

		private void initializeFile(IFile file) {
			fTextFileBuffer= getBuffer(file);
			if (fTextFileBuffer == null) {
				initializeResource(file);
			} else {
				IDocument document= fTextFileBuffer.getDocument();
				fDirty= fTextFileBuffer.isDirty();
				fReadOnly= Resources.isReadOnly(file);
				if (document instanceof IDocumentExtension4) {
					fKind= DOCUMENT;
					fModificationStamp= ((IDocumentExtension4) document).getModificationStamp();
				} else {
					fKind= RESOURCE;
					fModificationStamp= file.getModificationStamp();
				}
			}

		}

		private void initializeResource(IResource resource) {
			fKind= RESOURCE;
			fDirty= false;
			fReadOnly= Resources.isReadOnly(resource);
			fModificationStamp= resource.getModificationStamp();
		}
	}
}
