/*******************************************************************************
 * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
 * Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br>
 * Copyright (C) 2013, Robin Stocker <robin@nibor.org>
 *
 * 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
 *******************************************************************************/
package org.eclipse.egit.ui.internal.revision;

import java.io.File;
import java.lang.reflect.InvocationTargetException;

import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.CompareEditorInput;
import org.eclipse.compare.ICompareContainer;
import org.eclipse.compare.IEditableContent;
import org.eclipse.compare.IResourceProvider;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.structuremergeviewer.DiffNode;
import org.eclipse.compare.structuremergeviewer.Differencer;
import org.eclipse.compare.structuremergeviewer.ICompareInput;
import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.compare.structuremergeviewer.IStructureComparator;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFileState;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.internal.storage.IndexFileRevision;
import org.eclipse.egit.core.internal.storage.OpenWorkspaceVersionEnabled;
import org.eclipse.egit.ui.internal.CommonUtils;
import org.eclipse.egit.ui.internal.CompareUtils;
import org.eclipse.egit.ui.internal.EgitUiEditorUtils;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.graphics.Image;
import org.eclipse.team.core.history.IFileRevision;
import org.eclipse.team.internal.ui.Utils;
import org.eclipse.team.internal.ui.synchronize.EditableSharedDocumentAdapter.ISharedDocumentAdapterListener;
import org.eclipse.team.internal.ui.synchronize.LocalResourceSaveableComparison;
import org.eclipse.team.internal.ui.synchronize.LocalResourceTypedElement;
import org.eclipse.team.ui.synchronize.SaveableCompareEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.ISaveablesLifecycleListener;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.Saveable;
import org.eclipse.ui.SaveablesLifecycleEvent;
import org.eclipse.ui.texteditor.ITextEditor;

/**
 * The input provider for the compare editor when working on resources
 * under Git control.
 */
@SuppressWarnings("restriction")
public class GitCompareFileRevisionEditorInput extends SaveableCompareEditorInput {

	private ITypedElement left;
	private ITypedElement right;
	private ITypedElement ancestor;

	/**
	 * Creates a new CompareFileRevisionEditorInput.
	 * @param left
	 * @param right
	 * @param page
	 */
	public GitCompareFileRevisionEditorInput(ITypedElement left, ITypedElement right, IWorkbenchPage page) {
		super(new CompareConfiguration(), page);
		this.left = left;
		this.right = right;
	}

	/**
	 * Creates a new CompareFileRevisionEditorInput.
	 * @param left
	 * @param right
	 * @param ancestor
	 * @param page
	 */
	public GitCompareFileRevisionEditorInput(ITypedElement left, ITypedElement right, ITypedElement ancestor, IWorkbenchPage page) {
		super(new CompareConfiguration(), page);
		this.left = left;
		this.right = right;
		this.ancestor = ancestor;
	}

	FileRevisionTypedElement getRightRevision() {
		if (right instanceof FileRevisionTypedElement) {
			return (FileRevisionTypedElement) right;
		}
		return null;
	}

	FileRevisionTypedElement getLeftRevision() {
		if (left instanceof FileRevisionTypedElement) {
			return (FileRevisionTypedElement) left;
		}
		return null;
	}

	FileRevisionTypedElement getAncestorRevision() {
		if (ancestor instanceof FileRevisionTypedElement)
			return (FileRevisionTypedElement) ancestor;
		return null;
	}

	private static void ensureContentsCached(FileRevisionTypedElement left, FileRevisionTypedElement right, FileRevisionTypedElement ancestor,
			IProgressMonitor monitor) {
		if (left != null) {
			try {
				left.cacheContents(monitor);
			} catch (CoreException e) {
				Activator.logError(e.getMessage(), e);
			}
		}
		if (right != null) {
			try {
				right.cacheContents(monitor);
			} catch (CoreException e) {
				Activator.logError(e.getMessage(), e);
			}
		}
		if (ancestor != null) {
			try {
				ancestor.cacheContents(monitor);
			} catch (CoreException e) {
				Activator.logError(e.getMessage(), e);
			}
		}
	}

	private boolean isLeftEditable(ICompareInput input) {
		Object tmpLeft = input.getLeft();
		return isEditable(tmpLeft);
	}

	private boolean isRightEditable(ICompareInput input) {
		Object tmpRight = input.getRight();
		return isEditable(tmpRight);
	}

	private boolean isEditable(Object object) {
		if (object instanceof IEditableContent) {
			return ((IEditableContent) object).isEditable();
		}
		return false;
	}

	private IResource getResource() {
		if (left instanceof IResourceProvider) {
			IResourceProvider resourceProvider = (IResourceProvider) left;
			return resourceProvider.getResource();
		}
		return null;
	}

	private ICompareInput createCompareInput() {
		return compare(left, right, ancestor);
	}

	private DiffNode compare(ITypedElement actLeft, ITypedElement actRight, ITypedElement actAncestor) {
		if (actLeft.getType().equals(ITypedElement.FOLDER_TYPE)) {
			//			return new MyDiffContainer(null, left,right);
			DiffNode diffNode = new DiffNode(null, Differencer.CHANGE,
					actAncestor, actLeft, actRight);
			ITypedElement[] lc = (ITypedElement[])((IStructureComparator)actLeft).getChildren();
			ITypedElement[] rc = (ITypedElement[])((IStructureComparator)actRight).getChildren();
			ITypedElement[] ac = null;
			if (actAncestor != null)
				ac = (ITypedElement[]) ((IStructureComparator) actAncestor)
						.getChildren();
			int li=0;
			int ri=0;
			while (li<lc.length && ri<rc.length) {
				ITypedElement ln = lc[li];
				ITypedElement rn = rc[ri];
				ITypedElement an = null;
				if (ac != null)
					an = ac[ri];
				int compareTo = ln.getName().compareTo(rn.getName());
				// TODO: Git ordering!
				if (compareTo == 0) {
					if (!ln.equals(rn))
						diffNode.add(compare(ln,rn, an));
					++li;
					++ri;
				} else if (compareTo < 0) {
					DiffNode childDiffNode = new DiffNode(Differencer.ADDITION, an, ln, null);
					diffNode.add(childDiffNode);
					if (ln.getType().equals(ITypedElement.FOLDER_TYPE)) {
						ITypedElement[] children = (ITypedElement[])((IStructureComparator)ln).getChildren();
						if(children != null && children.length > 0) {
							for (ITypedElement child : children) {
								childDiffNode.add(addDirectoryFiles(child, Differencer.ADDITION));
							}
						}
					}
					++li;
				} else {
					DiffNode childDiffNode = new DiffNode(Differencer.DELETION, an, null, rn);
					diffNode.add(childDiffNode);
					if (rn.getType().equals(ITypedElement.FOLDER_TYPE)) {
						ITypedElement[] children = (ITypedElement[])((IStructureComparator)rn).getChildren();
						if(children != null && children.length > 0) {
							for (ITypedElement child : children) {
								childDiffNode.add(addDirectoryFiles(child, Differencer.DELETION));
							}
						}
					}
					++ri;
				}
			}
			while (li<lc.length) {
				ITypedElement ln = lc[li];
				ITypedElement an = null;
				if (ac != null)
					an= ac[li];
				DiffNode childDiffNode = new DiffNode(Differencer.ADDITION, an, ln, null);
				diffNode.add(childDiffNode);
				if (ln.getType().equals(ITypedElement.FOLDER_TYPE)) {
					ITypedElement[] children = (ITypedElement[])((IStructureComparator)ln).getChildren();
					if(children != null && children.length > 0) {
						for (ITypedElement child : children) {
							childDiffNode.add(addDirectoryFiles(child, Differencer.ADDITION));
						}
					}
				}
				++li;
			}
			while (ri<rc.length) {
				ITypedElement rn = rc[ri];
				ITypedElement an = null;
				if (ac != null)
					an = ac[ri];
				DiffNode childDiffNode = new DiffNode(Differencer.DELETION, an, null, rn);
				diffNode.add(childDiffNode);
				if (rn.getType().equals(ITypedElement.FOLDER_TYPE)) {
					ITypedElement[] children = (ITypedElement[])((IStructureComparator)rn).getChildren();
					if(children != null && children.length > 0) {
						for (ITypedElement child : children) {
							childDiffNode.add(addDirectoryFiles(child, Differencer.DELETION));
						}
					}
				}
				++ri;
			}
			return diffNode;
		} else {
			if (actAncestor != null)
				return new DiffNode(Differencer.CONFLICTING, actAncestor, actLeft, actRight);
			else
				return new DiffNode(actLeft, actRight);
		}
	}

	private DiffNode addDirectoryFiles(ITypedElement elem, int diffType) {
		ITypedElement l = null;
		ITypedElement r = null;
		if (diffType == Differencer.DELETION) {
			r = elem;
		} else {
			l = elem;
		}

		if (elem.getType().equals(ITypedElement.FOLDER_TYPE)) {
			DiffNode diffNode = null;
			diffNode = new DiffNode(null,Differencer.CHANGE,null,l,r);
			ITypedElement[] children = (ITypedElement[])((IStructureComparator)elem).getChildren();
			for (ITypedElement child : children) {
				diffNode.add(addDirectoryFiles(child, diffType));
			}
			return diffNode;
		} else {
			return new DiffNode(diffType, null, l, r);
		}
	}

	private void initLabels(ICompareInput input) {
		CompareConfiguration cc = getCompareConfiguration();
		if (getLeftRevision() != null) {
			String leftLabel = getFileRevisionLabel(getLeftRevision());
			cc.setLeftLabel(leftLabel);
		} else if (getResource() != null) {
			String label = NLS.bind(UIText.GitCompareFileRevisionEditorInput_LocalLabel, new Object[]{ input.getLeft().getName() });
			cc.setLeftLabel(label);
		} else {
			cc.setLeftLabel(left.getName());
		}
		if (getRightRevision() != null) {
			String rightLabel = getFileRevisionLabel(getRightRevision());
			cc.setRightLabel(rightLabel);
		} else {
			cc.setRightLabel(right.getName());
		}
		if (getAncestorRevision() != null) {
			String ancestorLabel = getFileRevisionLabel(getAncestorRevision());
			cc.setAncestorLabel(ancestorLabel);
		}

	}

	private String getFileRevisionLabel(FileRevisionTypedElement element) {
		Object fileObject = element.getFileRevision();
		if (fileObject instanceof LocalFileRevision){
			return NLS.bind(UIText.GitCompareFileRevisionEditorInput_LocalHistoryLabel, new Object[]{element.getName(), element.getTimestamp()});
		} else if (fileObject instanceof IndexFileRevision) {
			if (isEditable(element))
				return NLS.bind(
						UIText.GitCompareFileRevisionEditorInput_IndexEditableLabel,
						element.getName());
			else
				return NLS.bind(
						UIText.GitCompareFileRevisionEditorInput_IndexLabel,
						element.getName());
		} else {
			return NLS.bind(UIText.GitCompareFileRevisionEditorInput_RevisionLabel, new Object[]{element.getName(),
					CompareUtils.truncatedRevision(element.getContentIdentifier()), element.getAuthor()});
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.compare.CompareEditorInput#getToolTipText()
	 */
	public String getToolTipText() {
		Object[] titleObject = new Object[3];
		titleObject[0] = getLongName(left);
		titleObject[1] = CompareUtils.truncatedRevision(getContentIdentifier(getLeftRevision()));
		titleObject[2] = CompareUtils.truncatedRevision(getContentIdentifier(getRightRevision()));
		return NLS.bind(UIText.GitCompareFileRevisionEditorInput_CompareTooltip, titleObject);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.compare.CompareEditorInput#getTitle()
	 */
	public String getTitle() {
		Object[] titleObject = new Object[3];
		titleObject[0] = getShortName(left);
		titleObject[1] = CompareUtils.truncatedRevision(getContentIdentifier(getLeftRevision()));
		titleObject[2] = CompareUtils.truncatedRevision(getContentIdentifier(getRightRevision()));
		return NLS.bind(UIText.GitCompareFileRevisionEditorInput_CompareTooltip, titleObject);
	}

	/* (non-Javadoc)
	 * @see org.eclipse.compare.CompareEditorInput#getAdapter(java.lang.Class)
	 */
	public Object getAdapter(Class adapter) {
		if (adapter == IFile.class || adapter == IResource.class) {
			return getResource();
		}
		return super.getAdapter(adapter);
	}

	private String getShortName(ITypedElement element) {
		if (element instanceof FileRevisionTypedElement){
			FileRevisionTypedElement fileRevisionElement = (FileRevisionTypedElement) element;
			return fileRevisionElement.getName();
		} else if (element instanceof LocalResourceTypedElement){
			LocalResourceTypedElement typedContent = (LocalResourceTypedElement) element;
			return typedContent.getResource().getName();
		}
		return element.getName();
	}

	private String getLongName(ITypedElement element) {
		if (element instanceof FileRevisionTypedElement){
			FileRevisionTypedElement fileRevisionElement = (FileRevisionTypedElement) element;
			return fileRevisionElement.getPath();
		}
		else if (element instanceof LocalResourceTypedElement){
			LocalResourceTypedElement typedContent = (LocalResourceTypedElement) element;
			return typedContent.getResource().getFullPath().toString();
		}
		return element.getName();
	}

	private String getContentIdentifier(ITypedElement element){
		if (element instanceof FileRevisionTypedElement){
			FileRevisionTypedElement fileRevisionElement = (FileRevisionTypedElement) element;
			Object fileObject = fileRevisionElement.getFileRevision();
			if (fileObject instanceof LocalFileRevision){
				try {
					IStorage storage = ((LocalFileRevision) fileObject).getStorage(new NullProgressMonitor());
					if (CompareUtils.getAdapter(storage, IFileState.class) != null){
						//local revision
						return UIText.GitCompareFileRevisionEditorInput_LocalRevision;
					} else if (CompareUtils.getAdapter(storage, IFile.class) != null) {
						//current revision
						return UIText.GitCompareFileRevisionEditorInput_CurrentRevision;
					}
				} catch (CoreException e) {
					Activator
							.logError(
									UIText.GitCompareFileRevisionEditorInput_contentIdentifier,
									e);
				}
			} else {
				return fileRevisionElement.getContentIdentifier();
			}
		}
		return UIText.GitCompareFileRevisionEditorInput_CurrentTitle;
	}

	@Override
	protected void fireInputChange() {
		// have the diff node notify its listeners of a change
		((NotifiableDiffNode) getCompareResult()).fireChange();
	}

	@Override
	protected ICompareInput prepareCompareInput(IProgressMonitor monitor)
			throws InvocationTargetException, InterruptedException {
		ICompareInput input = createCompareInput();
		getCompareConfiguration().setLeftEditable(isLeftEditable(input));
		getCompareConfiguration().setRightEditable(isRightEditable(input));
		ensureContentsCached(getLeftRevision(), getRightRevision(), getAncestorRevision(), monitor);
		initLabels(input);
		setTitle(NLS.bind(UIText.GitCompareFileRevisionEditorInput_CompareInputTitle, new String[] { input.getName() }));

		// The compare editor (Structure Compare) will show the diff filenames
		// with their project relative path. So, no need to also show directory entries.
		DiffNode flatDiffNode = new NotifiableDiffNode(null,
				ancestor != null ? Differencer.CONFLICTING : Differencer.CHANGE, ancestor, left, right);
		flatDiffView(flatDiffNode, (DiffNode) input);

		return flatDiffNode;
	}

	@Override
	protected Saveable createSaveable() {
		// copied from
		// org.eclipse.team.ui.synchronize.SaveableCompareEditorInput.createSaveable()
		Object compareResult = getCompareResult();
		Assert.isNotNull(compareResult,
				"This method cannot be called until after prepareInput is called"); //$NON-NLS-1$
		return new InternalResourceSaveableComparison(
				(ICompareInput) compareResult, this);
	}

	private void flatDiffView(DiffNode rootNode, DiffNode currentNode) {
		if(currentNode != null) {
			IDiffElement[] dElems = currentNode.getChildren();
			if(dElems != null) {
				for(IDiffElement dElem : dElems) {
					DiffNode dNode = (DiffNode) dElem;
					if(dNode.getChildren() != null && dNode.getChildren().length > 0) {
						flatDiffView(rootNode, dNode);
					} else {
						rootNode.add(dNode);
					}
				}
			}
		}
	}

	@Override
	public void registerContextMenu(MenuManager menu,
			final ISelectionProvider selectionProvider) {
		super.registerContextMenu(menu, selectionProvider);
		registerOpenWorkspaceVersion(menu, selectionProvider);
	}

	private void registerOpenWorkspaceVersion(MenuManager menu,
			final ISelectionProvider selectionProvider) {
		FileRevisionTypedElement leftRevision = getLeftRevision();
		if (leftRevision != null) {
			IFileRevision fileRevision = leftRevision.getFileRevision();
			if (fileRevision instanceof OpenWorkspaceVersionEnabled) {
				OpenWorkspaceVersionEnabled workspaceVersion = (OpenWorkspaceVersionEnabled) fileRevision;
				final File workspaceFile = new File(workspaceVersion
						.getRepository().getWorkTree(),
						workspaceVersion.getGitPath());
				if (workspaceFile.exists())
					menu.addMenuListener(new IMenuListener() {
						public void menuAboutToShow(IMenuManager manager) {
							Action action = new OpenWorkspaceVersionAction(
									UIText.CommitFileDiffViewer_OpenWorkingTreeVersionInEditorMenuLabel,
									selectionProvider, workspaceFile);
							manager.insertAfter("file", action); //$NON-NLS-1$
						}
					});
			}
		}
	}

	private class OpenWorkspaceVersionAction extends Action {

		private final ISelectionProvider selectionProvider;

		private final File workspaceFile;

		private OpenWorkspaceVersionAction(String text,
				ISelectionProvider selectionProvider, File workspaceFile) {
			super(text);
			this.selectionProvider = selectionProvider;
			this.workspaceFile = workspaceFile;
		}

		@Override
		public void run() {
			int selectedLine = 0;
			if (selectionProvider.getSelection() instanceof ITextSelection) {
				ITextSelection selection = (ITextSelection) selectionProvider
						.getSelection();
				selectedLine = selection.getStartLine();
			}

			IWorkbenchWindow window = PlatformUI.getWorkbench()
					.getActiveWorkbenchWindow();
			IWorkbenchPage page = window.getActivePage();
			IEditorPart editor = EgitUiEditorUtils.openEditor(workspaceFile,
					page);
			selectLine(editor, selectedLine);
		}

		private void selectLine(IEditorPart editorPart, int selectedLine) {
			if (editorPart instanceof ITextEditor) {
				ITextEditor editor = (ITextEditor) editorPart;
				IDocument document = editor.getDocumentProvider().getDocument(
						editor.getEditorInput());
				if (document != null)
					try {
						IRegion line = document
								.getLineInformation(selectedLine);
						editor.selectAndReveal(line.getOffset(), 0);
					} catch (BadLocationException e) {
						// line seems not to exist in
						// workspace version
					}
			}
		}
	}

	/**
	 * ITypedElement without content. May be used to indicate that a file is not
	 * available.
	 */
	public static class EmptyTypedElement implements ITypedElement{

		private String name;

		/**
		 * @param name the name used for display
		 */
		public EmptyTypedElement(String name) {
			this.name = name;
		}

		public Image getImage() {
			return null;
		}

		public String getName() {
			return name;
		}

		public String getType() {
			return ITypedElement.UNKNOWN_TYPE;
		}

	}

	// copy of
	// org.eclipse.team.ui.synchronize.SaveableCompareEditorInput.InternalResourceSaveableComparison
	// we need to copy this private class to prevent NPE in
	// org.eclipse.team.internal.ui.synchronize.LocalResourceSaveableComparison.propertyChange(PropertyChangeEvent)
	// when moving and saving partial changes when comparing version from git
	// index with HEAD
	private class InternalResourceSaveableComparison extends
			LocalResourceSaveableComparison implements
			ISharedDocumentAdapterListener {
		private LocalResourceTypedElement lrte;

		private boolean connected = false;

		public InternalResourceSaveableComparison(ICompareInput input,
				CompareEditorInput editorInput) {
			super(input, editorInput, left);
			ITypedElement element = left;
			if (element instanceof LocalResourceTypedElement) {
				lrte = (LocalResourceTypedElement) element;
				if (lrte.isConnected()) {
					registerSaveable(true);
				} else {
					lrte.setSharedDocumentListener(this);
				}
			}
		}

		protected void fireInputChange() {
			GitCompareFileRevisionEditorInput.this.fireInputChange();
		}

		public void dispose() {
			super.dispose();
			if (lrte != null)
				lrte.setSharedDocumentListener(null);
		}

		public void handleDocumentConnected() {
			if (connected)
				return;
			connected = true;
			registerSaveable(false);
			if (lrte != null)
				lrte.setSharedDocumentListener(null);
		}

		private void registerSaveable(boolean init) {
			ICompareContainer container = getContainer();
			IWorkbenchPart part = container.getWorkbenchPart();
			if (part != null) {
				ISaveablesLifecycleListener lifecycleListener = getSaveablesLifecycleListener(part);
				// Remove this saveable from the lifecycle listener
				if (!init)
					lifecycleListener
							.handleLifecycleEvent(new SaveablesLifecycleEvent(
									part, SaveablesLifecycleEvent.POST_CLOSE,
									new Saveable[] { this }, false));
				// Now fix the hashing so it uses the connected document
				initializeHashing();
				// Finally, add this saveable back to the listener
				lifecycleListener
						.handleLifecycleEvent(new SaveablesLifecycleEvent(part,
								SaveablesLifecycleEvent.POST_OPEN,
								new Saveable[] { this }, false));
			}
		}

		private ISaveablesLifecycleListener getSaveablesLifecycleListener(
				IWorkbenchPart part) {
			ISaveablesLifecycleListener listener = (ISaveablesLifecycleListener) Utils
					.getAdapter(part, ISaveablesLifecycleListener.class);
			if (listener == null)
				listener = CommonUtils.getService(part.getSite(), ISaveablesLifecycleListener.class);
			return listener;
		}

		public void handleDocumentDeleted() {
			// Ignore
		}

		public void handleDocumentDisconnected() {
			// Ignore
		}

		public void handleDocumentFlushed() {
			// Ignore
		}

		public void handleDocumentSaved() {
			// Ignore
		}
	}

}
