/*******************************************************************************
 * Copyright (c) 2000, 2017 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.team.internal.ui.synchronize;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.eclipse.compare.ISharedDocumentAdapter;
import org.eclipse.compare.ResourceNode;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.team.core.history.IFileHistoryProvider;
import org.eclipse.team.core.history.IFileRevision;
import org.eclipse.team.internal.ui.Utils;
import org.eclipse.ui.IEditorInput;

/**
 * A buffered resource node with the following characteristics:
 * <ul>
 * <li>Supports the use of file buffers (see {@link ISharedDocumentAdapter}).
 * <li>Does not support file systems hierarchies (i.e. should not be used to
 * represent a folder).
 * <li>Does not allow editing when the file does not exist (see
 * {@link #isEditable()}).
 * <li>Tracks whether the file has been changed on disk since it was loaded
 * through the element (see {@link #isSynchronized()}).
 * <li>Any buffered contents must either be saved or discarded when the element
 * is no longer needed (see {@link #commit(IProgressMonitor)},
 * {@link #saveDocument(boolean, IProgressMonitor)} and {@link #discardBuffer()}
 * ).
 * </ul>
 * <p>
 * This class may be instantiated.
 * </p>
 *
 * @since 3.3
 * @noextend This class is not intended to be subclassed by clients.
 */
public class LocalResourceTypedElement extends ResourceNode implements IAdaptable {

	private boolean fDirty = false;
	private EditableSharedDocumentAdapter sharedDocumentAdapter;
	private long timestamp;
	private boolean exists;
	private boolean useSharedDocument = true;
	private EditableSharedDocumentAdapter.ISharedDocumentAdapterListener sharedDocumentListener;
	private String author;

	/**
	 * Creates an element for the given resource.
	 * @param resource the resource
	 */
	public LocalResourceTypedElement(IResource resource) {
		super(resource);
		exists = resource.exists();
	}

	@Override
	public void setContent(byte[] contents) {
		fDirty = true;
		super.setContent(contents);
	}

	/**
	 * Commits buffered contents to the underlying resource. Note that if the
	 * element has a shared document, the commit will not succeed since the
	 * contents will be buffered in the shared document and will not be pushed
	 * to this element using {@link #setContent(byte[])}. Clients should check
	 * whether the element {@link #isConnected()} and, if it is, they should call
	 * {@link #saveDocument(boolean, IProgressMonitor)} to save the buffered contents to
	 * the underlying resource.
	 * @param monitor a progress monitor
	 * @throws CoreException
	 */
	public void commit(IProgressMonitor monitor) throws CoreException {
		if (isDirty()) {
			if (isConnected()) {
				saveDocument(true, monitor);
			} else {
				IResource resource = getResource();
				if (resource instanceof IFile) {
					try (ByteArrayInputStream is = new ByteArrayInputStream(getContent())) {
						IFile file = (IFile) resource;
						if (file.exists())
							file.setContents(is, false, true, monitor);
						else
							file.create(is, false, monitor);
						fDirty = false;
					} catch (IOException closeException) {
						// ignored
					} finally {
						fireContentChanged();
					}
				}
				updateTimestamp();
			}
		}
	}

	@Override
	public InputStream getContents() throws CoreException {
		if (exists)
			return super.getContents();
		return null;
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getAdapter(Class<T> adapter) {
		if (adapter == ISharedDocumentAdapter.class) {
			if (isSharedDocumentsEnable())
				return (T) getSharedDocumentAdapter();
			else
				return null;
		}
		return Platform.getAdapterManager().getAdapter(this, adapter);
	}

	/*
	 * Returned the shared document adapter for this element. If one does not exist
	 * yet, it will be created.
	 */
	private synchronized ISharedDocumentAdapter getSharedDocumentAdapter() {
		if (sharedDocumentAdapter == null)
			sharedDocumentAdapter = new EditableSharedDocumentAdapter(new EditableSharedDocumentAdapter.ISharedDocumentAdapterListener() {
				@Override
				public void handleDocumentConnected() {
					LocalResourceTypedElement.this.updateTimestamp();
					if (sharedDocumentListener != null)
						sharedDocumentListener.handleDocumentConnected();
				}
				@Override
				public void handleDocumentFlushed() {
					LocalResourceTypedElement.this.fireContentChanged();
					if (sharedDocumentListener != null)
						sharedDocumentListener.handleDocumentFlushed();
				}
				@Override
				public void handleDocumentDeleted() {
					LocalResourceTypedElement.this.update();
					if (sharedDocumentListener != null)
						sharedDocumentListener.handleDocumentDeleted();
				}
				@Override
				public void handleDocumentSaved() {
					LocalResourceTypedElement.this.updateTimestamp();
					if (sharedDocumentListener != null)
						sharedDocumentListener.handleDocumentSaved();
				}
				@Override
				public void handleDocumentDisconnected() {
					if (sharedDocumentListener != null)
						sharedDocumentListener.handleDocumentDisconnected();
				}
			});
		return sharedDocumentAdapter;
	}

	@Override
	public boolean isEditable() {
		// Do not allow non-existent files to be edited
		IResource resource = getResource();
		return resource.getType() == IResource.FILE && exists;
	}

	/**
	 * Return whether the element is connected to a shared document.
	 * When connected, the element can be saved using {@link #saveDocument(boolean, IProgressMonitor)}.
	 * Otherwise, {@link #commit(IProgressMonitor)} should be used to save the buffered contents.
	 * @return whether the element is connected to a shared document
	 */
	public boolean isConnected() {
		return sharedDocumentAdapter != null
				&& sharedDocumentAdapter.isConnected();
	}

	/**
	 * Save the shared document for this element. The save can only be performed
	 * if the element is connected to a shared document. If the element is not
	 * connected, <code>false</code> is returned.
	 * @param overwrite indicates whether overwrite should be performed
	 * 			while saving the given element if necessary
	 * @param monitor a progress monitor
	 * @return whether the save succeeded or not
	 * @throws CoreException
	 */
	public boolean saveDocument(boolean overwrite, IProgressMonitor monitor) throws CoreException {
		if (isConnected()) {
			IEditorInput input = sharedDocumentAdapter.getDocumentKey(this);
			sharedDocumentAdapter.saveDocument(input, overwrite, monitor);
			updateTimestamp();
			return true;
		}
		return false;
	}

	@Override
	protected InputStream createStream() throws CoreException {
		InputStream inputStream = super.createStream();
		updateTimestamp();
		return inputStream;
	}

	/**
	 * Update the cached timestamp of the resource.
	 */
	void updateTimestamp() {
		if (getResource().exists())
			timestamp = getResource().getLocalTimeStamp();
		else
			exists = false;
	}

	/**
	 * Return the cached timestamp of the resource.
	 * @return the cached timestamp of the resource
	 */
	private long getTimestamp() {
		return timestamp;
	}

	@Override
	public int hashCode() {
		return getResource().hashCode();
	}

	/*
	 * Returns <code>true</code> if the other object is of type
	 * <code>LocalResourceTypedElement</code> and their corresponding resources
	 * are identical. The content is not considered.
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj == this)
			return true;
		if (obj instanceof LocalResourceTypedElement) {
			LocalResourceTypedElement otherElement = (LocalResourceTypedElement) obj;
			return otherElement.getResource().equals(getResource())
					&& exists == otherElement.exists;
		}
		return false;
	}

	/**
	 * Method called to update the state of this element when the compare input that
	 * contains this element is issuing a change event. It is not necessarily the
	 * case that the {@link #isSynchronized()} will return <code>true</code> after this
	 * call.
	 */
	public void update() {
		exists = getResource().exists();
	}

	/**
	 * Return whether the contents provided by this typed element are in-sync with what is on
	 * disk. This method will return <code>false</code> if the file has been changed
	 * externally since the last time the contents were obtained or saved through this
	 * element.
	 * @return whether the contents provided by this typed element are in-sync with what is on
	 * disk
	 */
	public boolean isSynchronized() {
		long current = getResource().getLocalTimeStamp();
		return current == getTimestamp();
	}

	/**
	 * Return whether the resource of this element existed at the last time the typed
	 * element was updated.
	 * @return whether the resource of this element existed at the last time the typed
	 * element was updated
	 */
	public boolean exists() {
		return exists;
	}

	@Override
	protected void fireContentChanged() {
		super.fireContentChanged();
	}

	/**
	 * Discard of any buffered contents. This must be called
	 * when the local element is no longer needed but is dirty since a
	 * the element will connect to a shared document when a merge viewer
	 * flushes its contents to the element and it must be disconnected or the
	 * buffer will remain.
	 * #see {@link #isDirty()}
	 */
	@Override
	public void discardBuffer() {
		if (sharedDocumentAdapter != null)
			sharedDocumentAdapter.releaseBuffer();
		super.discardBuffer();
	}

	/**
	 * Return whether this element can use a shared document.
	 * @return whether this element can use a shared document
	 */
	public boolean isSharedDocumentsEnable() {
		return useSharedDocument && getResource().getType() == IResource.FILE && exists;
	}

	/**
	 * Set whether this element can use shared documents. The enablement
	 * will only apply to files (i.e. shared documents never apply to folders).
	 * @param enablement whether this element can use shared documents
	 */
	public void enableSharedDocument(boolean enablement) {
		this.useSharedDocument = enablement;
	}

	/**
	 * Return whether this element is dirty. The element is
	 * dirty if a merge viewer has flushed it's contents
	 * to the element and the contents have not been saved.
	 * @return whether this element is dirty
	 * @see #commit(IProgressMonitor)
	 * @see #saveDocument(boolean, IProgressMonitor)
	 * @see #discardBuffer()
	 */
	public boolean isDirty() {
		return fDirty || (sharedDocumentAdapter != null && sharedDocumentAdapter.hasBufferedContents());
	}

	public void setSharedDocumentListener(
			EditableSharedDocumentAdapter.ISharedDocumentAdapterListener sharedDocumentListener) {
		this.sharedDocumentListener = sharedDocumentListener;
	}

	/**
	 * Returns the author of the workspace file revision if any.
	 *
	 * @return the author or <code>null</code> if the author has not been fetched or is not
	 *         available
	 * @since 3.7
	 * @see #fetchAuthor(IProgressMonitor)
	 */
	public String getAuthor() {
		return author;
	}

	/**
	 * Fetches the author from the repository.
	 *
	 * @param monitor the progress monitor
	 * @throws CoreException if fetching the revision properties fails
	 * @since 3.7
	 */
	public void fetchAuthor(IProgressMonitor monitor) throws CoreException {
		author = null;

		IFileHistoryProvider fileHistoryProvider= Utils.getHistoryProvider(getResource());
		if (fileHistoryProvider == null)
			return;

		IFileRevision revision= fileHistoryProvider.getWorkspaceFileRevision(getResource());
		if (revision == null)
			return;

		author = revision.getAuthor();

		if (author == null && revision.isPropertyMissing()) {
			IFileRevision other = revision.withAllProperties(monitor);
			author = other.getAuthor();
		}
	}

	/**
	 * Sets the author.
	 *
	 * @param author the author
	 * @since 3.7
	 */
	public void setAuthor(String author) {
		this.author= author;
	}

}
