/*******************************************************************************
 * Copyright (c) 2000, 2007 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.team.internal.ui.synchronize;

import java.io.*;

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.*;
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 but may not subclass it.
 * 
 * @since 3.3
 */
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;

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

	/* (non-Javadoc)
	 * @see org.eclipse.compare.BufferedContent#setContent(byte[])
	 */
	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) {
					ByteArrayInputStream is = new ByteArrayInputStream(getContent());
					try {
						IFile file = (IFile) resource;
						if (file.exists())
							file.setContents(is, false, true, monitor);
						else
							file.create(is, false, monitor);
						fDirty = false;
					} finally {
						fireContentChanged();
						if (is != null)
							try {
								is.close();
							} catch (IOException ex) {
							}
					}
				}
				updateTimestamp();
			}
		}
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.compare.ResourceNode#getContents()
	 */
	public InputStream getContents() throws CoreException {
		if (exists)
			return super.getContents();
		return null;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
	 */
	public Object getAdapter(Class adapter) {
		if (adapter == ISharedDocumentAdapter.class) {
			if (isSharedDocumentsEnable())
				return 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() {
				public void handleDocumentConnected() {
					LocalResourceTypedElement.this.updateTimestamp();
					if (sharedDocumentListener != null)
						sharedDocumentListener.handleDocumentConnected();
				}
				public void handleDocumentFlushed() {
					LocalResourceTypedElement.this.fireContentChanged();
					if (sharedDocumentListener != null)
						sharedDocumentListener.handleDocumentFlushed();
				}
				public void handleDocumentDeleted() {
					LocalResourceTypedElement.this.update();
					if (sharedDocumentListener != null)
						sharedDocumentListener.handleDocumentDeleted();
				}
				public void handleDocumentSaved() {
					LocalResourceTypedElement.this.updateTimestamp();
					if (sharedDocumentListener != null)
						sharedDocumentListener.handleDocumentSaved();
				}
				public void handleDocumentDisconnected() {
					if (sharedDocumentListener != null)
						sharedDocumentListener.handleDocumentDisconnected();
				}
			});
		return sharedDocumentAdapter;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.compare.ResourceNode#isEditable()
	 */
	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();
	}

	/**
	 * ZSave 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;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.compare.ResourceNode#createStream()
	 */
	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;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.compare.ResourceNode#hashCode()
	 */
	public int hashCode() {
		return getResource().hashCode();
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.compare.ResourceNode#equals(java.lang.Object)
	 */
	public boolean equals(Object other) {
		if (other instanceof LocalResourceTypedElement) {
			LocalResourceTypedElement otherElement = (LocalResourceTypedElement) other;
			return otherElement.getResource().equals(getResource()) 
				&& exists == otherElement.exists;
		}
		return super.equals(other);
	}

	/**
	 * 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;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.compare.BufferedContent#fireContentChanged()
	 */
	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()}
	 */
	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;
	}

}
