/*******************************************************************************
 * Copyright (c) 2000, 2013 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 org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;

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

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

import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.jface.text.IDocumentListener;

import org.eclipse.ltk.core.refactoring.RefactoringStatus;

public abstract class BufferValidationState {

	protected final IFile fFile;
	protected final boolean fExisted;
	protected final boolean fDerived;
	protected final boolean fWasDirty;
	protected final String fEncoding;

	protected static class ModificationStamp {
		private int fKind;
		private long fValue;
		public static final int FILE= 1;
		public static final int DOCUMENT= 2;

		public static ModificationStamp createFile(long value) {
			return new ModificationStamp(FILE, value);
		}
		public static ModificationStamp createDocument(long value) {
			return new ModificationStamp(DOCUMENT, value);
		}
		private ModificationStamp(int kind, long value) {
			fKind= kind;
			fValue= value;
		}
		public boolean isFileStamp() {
			return fKind == FILE;
		}
		public boolean isDocumentStamp() {
			return fKind == DOCUMENT;
		}
		public int getKind() {
			return fKind;
		}
		public long getValue() {
			return fValue;
		}
	}

	public static BufferValidationState create(IFile file) {
		ITextFileBuffer buffer= getBuffer(file);
		if (buffer == null) {
			return new ModificationStampValidationState(file);
		} else {
			IDocument document= buffer.getDocument();
			if (document instanceof IDocumentExtension4 || !buffer.isDirty()) {
				return new ModificationStampValidationState(file);
			} else {
				return new NoStampValidationState(file);
			}
		}
	}

	public boolean wasDirty() {
		return fWasDirty;
	}

	public boolean wasDerived() {
		return fDerived;
	}

	public RefactoringStatus isValid(boolean needsSaving) throws CoreException {
		return isValid(needsSaving, false);
	}

	public RefactoringStatus isValid(boolean needsSaving, boolean resilientForDerived) throws CoreException {
		if (resilientForDerived && fDerived) {
			return new RefactoringStatus();
		}
		if (!fExisted) {
			if (fFile.exists())
				return RefactoringStatus.createFatalErrorStatus(Messages.format(
					RefactoringCoreMessages.TextChanges_error_existing,
					BasicElementLabels.getPathLabel(fFile.getFullPath(), false)));
		} else {
			if (!fFile.exists())
				return RefactoringStatus.createFatalErrorStatus(Messages.format(
					RefactoringCoreMessages.TextChanges_error_not_existing,
					BasicElementLabels.getPathLabel(fFile.getFullPath(), false)));
		}
		if (needsSaving) {
			if (fFile.isReadOnly()) {
				return RefactoringStatus.createFatalErrorStatus(Messages.format(
					RefactoringCoreMessages.TextChanges_error_read_only,
					BasicElementLabels.getPathLabel(fFile.getFullPath(), false)));
			} else if (!fFile.isSynchronized(IResource.DEPTH_ZERO)) {
				return RefactoringStatus.createFatalErrorStatus(Messages.format(
					RefactoringCoreMessages.TextChanges_error_outOfSync,
					BasicElementLabels.getPathLabel(fFile.getFullPath(), false)));
			}
		}
		if (fEncoding == null) {
			return RefactoringStatus.createFatalErrorStatus(Messages.format(
				RefactoringCoreMessages.BufferValidationState_no_character_encoding,
				BasicElementLabels.getPathLabel(fFile.getFullPath(), false)));
		} else if (!fEncoding.equals(fFile.getCharset(true))) {
			return RefactoringStatus.createFatalErrorStatus(Messages.format(
				RefactoringCoreMessages.BufferValidationState_character_encoding_changed,
				BasicElementLabels.getPathLabel(fFile.getFullPath(), false)));
		}
		return new RefactoringStatus();
	}

	public void dispose() {
	}


	protected BufferValidationState(IFile file) {
		fFile= file;
		fExisted= file.exists();
		fDerived= file.isDerived();
		fWasDirty= isDirty(fFile);
		String encoding;
		try {
			encoding= file.getCharset(true);
		} catch (CoreException e) {
			encoding= null;
		}
		fEncoding= encoding;
	}

	protected IDocument getDocument() {
		ITextFileBuffer buffer= getBuffer(fFile);
		if (buffer == null)
			return null;
		return buffer.getDocument();

	}

	protected static boolean isDirty(IFile file) {
		ITextFileBuffer buffer= getBuffer(file);
		if (buffer == null)
			return false;
		return buffer.isDirty();
	}

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

	protected ModificationStamp getModificationStamp() {
		ITextFileBuffer buffer= getBuffer(fFile);
		if (buffer == null) {
			return ModificationStamp.createFile(fFile.getModificationStamp());
		} else {
			IDocument document= buffer.getDocument();
			if (document instanceof IDocumentExtension4) {
				return ModificationStamp.createDocument(((IDocumentExtension4)document).getModificationStamp());
			} else {
				return ModificationStamp.createFile(fFile.getModificationStamp());
			}
		}
	}
}

/**
 * Buffer validation state for dirty files whose document does not support
 * modification stamps.
 */
class NoStampValidationState extends BufferValidationState {

	private IDocumentListener fDocumentListener;
	private FileBufferListener fFileBufferListener;
	private boolean fChanged;
	private long fContentStamp= IResource.NULL_STAMP;

	class DocumentChangedListener implements IDocumentListener {
		@Override
		public void documentAboutToBeChanged(DocumentEvent event) {
		}
		@Override
		public void documentChanged(DocumentEvent event) {
			NoStampValidationState.this.documentChanged();
		}
	}

	class FileBufferListener implements IFileBufferListener {
		@Override
		public void bufferCreated(IFileBuffer buffer) {
			// begin https://bugs.eclipse.org/bugs/show_bug.cgi?id=67821
			if (buffer.getLocation().equals(fFile.getFullPath()) && buffer instanceof ITextFileBuffer) {
				ITextFileBuffer textBuffer= (ITextFileBuffer)buffer;
				if (fDocumentListener == null)
					fDocumentListener= new DocumentChangedListener();
				textBuffer.getDocument().addDocumentListener(fDocumentListener);
			}
			// end fix https://bugs.eclipse.org/bugs/show_bug.cgi?id=67821
		}
		@Override
		public void bufferDisposed(IFileBuffer buffer) {
			// begin fix https://bugs.eclipse.org/bugs/show_bug.cgi?id=67821
			if (fDocumentListener != null && buffer.getLocation().equals(fFile.getFullPath())) {
				if (buffer instanceof ITextFileBuffer) {
					ITextFileBuffer textBuffer= (ITextFileBuffer)buffer;
					textBuffer.getDocument().removeDocumentListener(fDocumentListener);
					fDocumentListener= null;
				}
				fContentStamp= fFile.getModificationStamp();
			}
			// end fix https://bugs.eclipse.org/bugs/show_bug.cgi?id=67821
		}
		@Override
		public void bufferContentAboutToBeReplaced(IFileBuffer buffer) {
		}
		@Override
		public void bufferContentReplaced(IFileBuffer buffer) {
		}
		@Override
		public void stateChanging(IFileBuffer buffer) {
		}
		@Override
		public void dirtyStateChanged(IFileBuffer buffer, boolean isDirty) {
		}
		@Override
		public void stateValidationChanged(IFileBuffer buffer, boolean isStateValidated) {
		}
		@Override
		public void underlyingFileMoved(IFileBuffer buffer, IPath path) {
		}
		@Override
		public void underlyingFileDeleted(IFileBuffer buffer) {
		}
		@Override
		public void stateChangeFailed(IFileBuffer buffer) {
		}
	}

	public NoStampValidationState(IFile file) {
		super(file);
		fContentStamp= file.getModificationStamp();
		fFileBufferListener= new FileBufferListener();
		FileBuffers.getTextFileBufferManager().addFileBufferListener(fFileBufferListener);
		fDocumentListener= new DocumentChangedListener();
		getDocument().addDocumentListener(fDocumentListener);
	}

	@Override
	public RefactoringStatus isValid(boolean needsSaving, boolean resilientForDerived) throws CoreException {
		RefactoringStatus result= super.isValid(needsSaving, resilientForDerived);
		if (result.hasFatalError())
			return result;
		// If we have initialized the content stamp with the 'null' stamp then we can't compare it with
		// the current stamp since a change executed later could have set a concrete stamp for the
		// current content
		// if (fChanged || (fContentStamp != IResource.NULL_STAMP && fContentStamp != fFile.getModificationStamp())
		if (fChanged || fContentStamp != fFile.getModificationStamp()) {
			result.addFatalError(Messages.format(
				RefactoringCoreMessages.TextChanges_error_content_changed,
				BasicElementLabels.getPathLabel(fFile.getFullPath(), false)
				));
		}
		return result;
	}

	@Override
	public void dispose() {
		if (fFileBufferListener != null) {
			FileBuffers.getTextFileBufferManager().removeFileBufferListener(fFileBufferListener);
			// fix https://bugs.eclipse.org/bugs/show_bug.cgi?id=67821
			fFileBufferListener= null;
		}
		if (fDocumentListener != null) {
			getDocument().removeDocumentListener(fDocumentListener);
			// fix https://bugs.eclipse.org/bugs/show_bug.cgi?id=67821
			fDocumentListener= null;
		}
	}

	private void documentChanged() {
		fChanged= true;
		getDocument().removeDocumentListener(fDocumentListener);
		FileBuffers.getTextFileBufferManager().removeFileBufferListener(fFileBufferListener);
		fFileBufferListener= null;
		fDocumentListener= null;
	}
}

/**
 * Buffer validation state based on modification stamp.
 */
class ModificationStampValidationState extends BufferValidationState {

	private ModificationStamp fModificationStamp;

	public ModificationStampValidationState(IFile file) {
		super(file);
		fModificationStamp= getModificationStamp();
	}

	@Override
	public RefactoringStatus isValid(boolean needsSaving, boolean resilientForDerived) throws CoreException {
		RefactoringStatus result= super.isValid(needsSaving, resilientForDerived);
		if (result.hasFatalError())
			return result;
		ModificationStamp currentStamp= getModificationStamp();
		// we don't need to check the kind here since the document stamp
		// and file stamp are in sync for documents implementing
		// IDocumentExtension4. If both are file stamps the file must
		// not be dirty
		if (fModificationStamp.getValue() != currentStamp.getValue()
			// we know here that the stamp value are equal. However, if
			// the stamp is a null stamp then the king must be equal as well.
			|| (fModificationStamp.isFileStamp()
				&& fModificationStamp.getValue() == IResource.NULL_STAMP
				&& !currentStamp.isFileStamp())
			|| (fModificationStamp.isDocumentStamp()
				&& fModificationStamp.getValue() == IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP
				&& !currentStamp.isDocumentStamp())
			|| (fModificationStamp.isFileStamp()
				&& currentStamp.isFileStamp() && isDirty(fFile))) {
			result.addFatalError(Messages.format(
				RefactoringCoreMessages.TextChanges_error_content_changed,
				BasicElementLabels.getPathLabel(fFile.getFullPath(), false)
				));

		}
		return result;
	}
}

/*
class SavedBufferValidationState extends BufferValidationState {
	private long fModificationStamp;

	public SavedBufferValidationState(IFile file) {
		super(file);
		fModificationStamp= file.getModificationStamp();
	}

	public RefactoringStatus isValid(boolean needsSaving) {
		RefactoringStatus result= super.isValid(needsSaving);
		if (result.hasFatalError())
			return result;
		ModificationStamp currentStamp= getModificationStamp();
		if (fModificationStamp != currentStamp.value) {
			result.addFatalError(Messages.format(
				RefactoringCoreMessages.TextChanges_error_content_changed, //$NON-NLS-1$
				fFile.getFullPath().toString()
				));
		} else if (fFile.isReadOnly()) {
			result.addFatalError(Messages.format(
				RefactoringCoreMessages.TextChanges_error_read_only, //$NON-NLS-1$
				fFile.getFullPath().toString()
				));
		} else if (!fFile.isSynchronized(IResource.DEPTH_ZERO)) {
			result.addFatalError(Messages.format(
				RefactoringCoreMessages.TextChanges_error_outOfSync, //$NON-NLS-1$
				fFile.getFullPath().toString()
				));
		} else if (isDirty(fFile) && currentStamp.isFileStamp()){
			result.addFatalError(Messages.format(
				RefactoringCoreMessages.TextChanges_error_unsaved_changes, //$NON-NLS-1$
				fFile.getFullPath().toString()
				));
		}
		return result;
	}

}
*/
