/*******************************************************************************
 * 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.jdt.internal.ui.javaeditor;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.CoreException;

import org.eclipse.core.resources.IResource;

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

import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ISynchronizable;
import org.eclipse.jface.text.source.IAnnotationModel;

import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IFileEditorInput;

import org.eclipse.ui.editors.text.FileDocumentProvider;

import org.eclipse.jdt.core.ElementChangedEvent;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.IElementChangedListener;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaElementDelta;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IOrdinaryClassFile;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;

import org.eclipse.jdt.ui.text.IJavaPartitions;
import org.eclipse.jdt.ui.text.JavaTextTools;

import org.eclipse.jdt.internal.ui.IResourceLocator;
import org.eclipse.jdt.internal.ui.JavaPlugin;


/**
 * A document provider for class files. Class files can be either inside
 */
public class ClassFileDocumentProvider extends FileDocumentProvider {

	/**
	 * An input change listener to request the editor to reread the input.
	 */
	public interface InputChangeListener {
		void inputChanged(IClassFileEditorInput input);
	}

	/**
	 * Synchronizes the document with external resource changes.
	 */
	protected class ClassFileSynchronizer implements IElementChangedListener {

		protected IClassFileEditorInput fInput;
		protected IPackageFragmentRoot fPackageFragmentRoot;

		/**
		 * Default constructor.
		 *
		 * @param input the class file editor input
		 */
		public ClassFileSynchronizer(IClassFileEditorInput input) {

			fInput= input;

			IJavaElement parent= fInput.getClassFile().getParent();
			while (parent != null && !(parent instanceof IPackageFragmentRoot)) {
				parent= parent.getParent();
			}
			fPackageFragmentRoot= (IPackageFragmentRoot) parent;
		}

		/**
		 * Installs the synchronizer.
		 */
		public void install() {
			JavaCore.addElementChangedListener(this);
		}

		/**
		 * Uninstalls the synchronizer.
		 */
		public void uninstall() {
			JavaCore.removeElementChangedListener(this);
		}

		/*
		 * @see IElementChangedListener#elementChanged
		 */
		@Override
		public void elementChanged(ElementChangedEvent e) {
			check(fPackageFragmentRoot, e.getDelta());
		}

		/**
		 * Recursively check whether the class file has been deleted.
		 *
		 * @param input the package fragment root
		 * @param delta the Java element delta
		 * @return <code>true</code> if delta processing can be stopped
		 */
		protected boolean check(IPackageFragmentRoot input, IJavaElementDelta delta) {
			IJavaElement element= delta.getElement();

			if ((delta.getKind() & IJavaElementDelta.REMOVED) != 0 || (delta.getFlags() & IJavaElementDelta.F_CLOSED) != 0) {
				// http://dev.eclipse.org/bugs/show_bug.cgi?id=19023
				if (element.equals(input.getJavaProject()) || element.equals(input)) {
					handleDeleted(fInput);
					return true;
				}
			}

			if (((delta.getFlags() & IJavaElementDelta.F_ARCHIVE_CONTENT_CHANGED) != 0) && input.equals(element)) {
				handleDeleted(fInput);
				return true;
			}

			if (((delta.getFlags() & IJavaElementDelta.F_REMOVED_FROM_CLASSPATH) != 0) && input.equals(element)) {
				handleDeleted(fInput);
				return true;
			}

			for (IJavaElementDelta subdelta : delta.getAffectedChildren()) {
				if (check(input, subdelta)) {
					return true;
				}
			}

			if ((delta.getFlags() & IJavaElementDelta.F_SOURCEDETACHED) != 0 ||
				(delta.getFlags() & IJavaElementDelta.F_SOURCEATTACHED) != 0)
			{
				IClassFile file= fInput != null ? fInput.getClassFile() : null;
				IJavaProject project= input != null ? input.getJavaProject() : null;

				boolean isOnClasspath= false;
				if (file != null && project != null)
					isOnClasspath= project.isOnClasspath(file);

				if (isOnClasspath) {
					fireInputChanged(fInput);
					return false;
				} else {
					handleDeleted(fInput);
					return true;
				}
			}

			return false;
		}
	}

	/**
	 * Correcting the visibility of <code>FileSynchronizer</code>.
	 */
	protected class _FileSynchronizer extends FileSynchronizer {
		public _FileSynchronizer(IFileEditorInput fileEditorInput) {
			super(fileEditorInput);
		}
	}

	/**
	 * Bundle of all required informations.
	 */
	protected class ClassFileInfo extends FileInfo {

		ClassFileSynchronizer fClassFileSynchronizer= null;

		ClassFileInfo(IDocument document, IAnnotationModel model, _FileSynchronizer fileSynchronizer) {
			super(document, model, fileSynchronizer);
		}

		ClassFileInfo(IDocument document, IAnnotationModel model, ClassFileSynchronizer classFileSynchronizer) {
			super(document, model, null);
			fClassFileSynchronizer= classFileSynchronizer;
		}
	}

	/** Input change listeners. */
	private List<InputChangeListener> fInputListeners= new ArrayList<>();

	/**
	 * Creates a new document provider.
	 */
	public ClassFileDocumentProvider() {
		super();
	}

	/*
	 * @see StorageDocumentProvider#setDocumentContent(IDocument, IEditorInput)
	 */
	@Override
	protected boolean setDocumentContent(IDocument document, IEditorInput editorInput, String encoding) throws CoreException {
		if (editorInput instanceof IClassFileEditorInput) {
			IClassFile classFile= ((IClassFileEditorInput) editorInput).getClassFile();
			String source= classFile.getSource();
			if (source == null)
				source= ""; //$NON-NLS-1$
			document.set(source);
			return true;
		}
		return super.setDocumentContent(document, editorInput, encoding);
	}

	/**
	 * Creates an annotation model derived from the given class file editor input.
	 *
	 * @param classFileEditorInput the editor input from which to query the annotations
	 * @return the created annotation model
	 * @exception CoreException if the editor input could not be accessed
	 */
	protected IAnnotationModel createClassFileAnnotationModel(IClassFileEditorInput classFileEditorInput) throws CoreException {
		IResource resource= null;
		IClassFile classFile= classFileEditorInput.getClassFile();

		IResourceLocator locator= classFile.getAdapter(IResourceLocator.class);
		if (locator != null)
			resource= locator.getContainingResource(classFile);

		if (resource != null) {
			ClassFileMarkerAnnotationModel model= new ClassFileMarkerAnnotationModel(resource);
			model.setClassFile(classFile);
			return model;
		}

		return null;
	}

	/*
	 * @see org.eclipse.ui.editors.text.StorageDocumentProvider#createEmptyDocument()
	 * @since 3.1
	 */
	@Override
	protected IDocument createEmptyDocument() {
		IDocument document= FileBuffers.getTextFileBufferManager().createEmptyDocument(null, LocationKind.IFILE);
		if (document instanceof ISynchronizable)
			((ISynchronizable)document).setLockObject(new Object());
		return document;
	}

	/*
	 * @see AbstractDocumentProvider#createDocument(Object)
	 */
	@Override
	protected IDocument createDocument(Object element) throws CoreException {
		IDocument document= super.createDocument(element);
		if (document != null) {
			JavaTextTools tools= JavaPlugin.getDefault().getJavaTextTools();
			tools.setupJavaDocumentPartitioner(document, IJavaPartitions.JAVA_PARTITIONING);
		}
		return document;
	}

	/*
	 * @see AbstractDocumentProvider#createElementInfo(Object)
	 */
	@Override
	protected ElementInfo createElementInfo(Object element) throws CoreException {

		if (element instanceof IClassFileEditorInput) {

			IClassFileEditorInput input = (IClassFileEditorInput) element;
			ExternalClassFileEditorInput external= null;
			if (input instanceof ExternalClassFileEditorInput)
				external= (ExternalClassFileEditorInput) input;

			if (external != null) {
				try {
					refreshFile(external.getFile());
				} catch (CoreException x) {
					handleCoreException(x, JavaEditorMessages.ClassFileDocumentProvider_error_createElementInfo);
				}
			}

			IDocument d= createDocument(input);
			IAnnotationModel m= createClassFileAnnotationModel(input);

			if (external != null) {
				_FileSynchronizer f= new _FileSynchronizer(external);
				f.install();
				ClassFileInfo info= new ClassFileInfo(d, m, f);
				info.fModificationStamp= computeModificationStamp(external.getFile());
				info.fEncoding= getPersistedEncoding(element);
				return info;
			} else if (input instanceof InternalClassFileEditorInput) {
				ClassFileSynchronizer s= new ClassFileSynchronizer(input);
				s.install();
				ClassFileInfo info= new ClassFileInfo(d, m, s);
				info.fEncoding= getPersistedEncoding(element);
				return info;
			}
		}

		return null;
	}

	/*
	 * @see FileDocumentProvider#disposeElementInfo(Object, ElementInfo)
	 */
	@Override
	protected void disposeElementInfo(Object element, ElementInfo info) {
		ClassFileInfo classFileInfo= (ClassFileInfo) info;
		if (classFileInfo.fClassFileSynchronizer != null) {
			classFileInfo.fClassFileSynchronizer.uninstall();
			classFileInfo.fClassFileSynchronizer= null;
		}

		super.disposeElementInfo(element, info);
	}

	/*
	 * @see org.eclipse.ui.texteditor.IDocumentProviderExtension3#isSynchronized(java.lang.Object)
	 * @since 3.0
	 */
	@Override
	public boolean isSynchronized(Object element) {
		Object elementInfo= getElementInfo(element);
		if (elementInfo instanceof ClassFileInfo) {
			IClassFileEditorInput input= (IClassFileEditorInput)element;
			IResource resource;
			try {
				resource= input.getClassFile().getUnderlyingResource();
			} catch (JavaModelException e) {
				return true;
			}
			return resource == null || resource.isSynchronized(IResource.DEPTH_ZERO);
		}
		return false;
	}

	/**
	 * Handles the deletion of the element underlying the given class file editor input.
	 * @param input the editor input
	 */
	protected void handleDeleted(IClassFileEditorInput input) {
		if (input == null) {
			fireElementDeleted(null);
			return;
		}

		if (input.exists())
			return;

		IClassFile cf= input.getClassFile();
		try {
			/*
			 * Let's try to find the class file - maybe the JAR changed
			 */
			if (cf instanceof IOrdinaryClassFile) {
				IType type= ((IOrdinaryClassFile) cf).getType();
				IJavaProject project= cf.getJavaProject();
				if (project != null) {
					type= project.findType(type.getFullyQualifiedName());
					if (type != null) {
						IEditorInput editorInput= EditorUtility.getEditorInput(type.getParent());
						if (editorInput instanceof IClassFileEditorInput) {
							fireInputChanged((IClassFileEditorInput)editorInput);
							return;
						}
					}
				}
			}
		} catch (JavaModelException x) {
			// Don't log and fall through: element deleted
		}

		fireElementDeleted(input);

	}

	/*
	 * @see org.eclipse.ui.editors.text.FileDocumentProvider#handleElementContentChanged(org.eclipse.ui.IFileEditorInput)
	 * @since 3.9
	 */
	@Override
	protected void handleElementContentChanged(IFileEditorInput fileEditorInput) {
		super.handleElementContentChanged(fileEditorInput);
		if (fileEditorInput instanceof ExternalClassFileEditorInput) {
			fireInputChanged((IClassFileEditorInput) fileEditorInput);
		}
	}

	/**
	 * Fires input changes to input change listeners.
	 *
	 * @param input the class file editor input
	 */
	protected void fireInputChanged(IClassFileEditorInput input) {
		List<InputChangeListener> list= new ArrayList<>(fInputListeners);
		for (InputChangeListener inputChangeListener : list)
			inputChangeListener.inputChanged(input);
	}

	/**
	 * Adds an input change listener.
	 *
	 * @param listener the input change listener
	 */
	public void addInputChangeListener(InputChangeListener listener) {
		fInputListeners.add(listener);
	}

	/**
	 * Removes an input change listener.
	 *
	 * @param listener the input change listener
	 */
	public void removeInputChangeListener(InputChangeListener listener) {
		fInputListeners.remove(listener);
	}

}
