/******************************************************************************
 *  Copyright (c) 2011, 2014 GitHub Inc 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:
 *    Kevin Sawicki (GitHub Inc.) - initial API and implementation
 *    Marc-Andre Laperle (Ericsson) - Set the input to null when not visible
 *****************************************************************************/
package org.eclipse.egit.ui.internal.blame;

import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;

import org.eclipse.core.resources.IStorage;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.egit.core.internal.job.JobUtil;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.JobFamilies;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.UIUtils;
import org.eclipse.egit.ui.internal.CompareUtils;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.blame.BlameOperation.BlameHistoryPageInput;
import org.eclipse.egit.ui.internal.blame.BlameRevision.Diff;
import org.eclipse.egit.ui.internal.commit.CommitEditor;
import org.eclipse.egit.ui.internal.commit.DiffStyleRangeFormatter;
import org.eclipse.egit.ui.internal.commit.DiffViewer;
import org.eclipse.egit.ui.internal.commit.RepositoryCommit;
import org.eclipse.egit.ui.internal.history.HistoryPageInput;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.AbstractInformationControl;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IInformationControlExtension2;
import org.eclipse.jface.text.source.IVerticalRulerInfo;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.EditList;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Link;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.team.core.history.IFileRevision;
import org.eclipse.team.ui.history.IHistoryView;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;

/**
 * Annotation information control
 */
public class BlameInformationControl extends AbstractInformationControl
		implements IInformationControlExtension2 {

	private IInformationControlCreator creator;

	private IVerticalRulerInfo rulerInfo;

	private BlameInformationControl hoverInformationControl;

	private BlameRevision revision;

	private ScrolledComposite scrolls;

	private Composite displayArea;

	private Label commitLabel;

	private Label authorLabel;

	private Label committerLabel;

	private StyledText messageText;

	/**
	 * 0-based line number
	 */
	private int revisionRulerLineNumber;

	private Composite diffComposite;

	private Link showAnnotationsLink;

	private SelectionAdapter showAnnotationsLinkSelectionAdapter;

	/**
	 * Create the information control for showing details on hover.
	 *
	 * @param parentShell
	 * @param creator
	 * @param rulerInfo
	 */
	public BlameInformationControl(Shell parentShell,
			IInformationControlCreator creator, IVerticalRulerInfo rulerInfo) {
		super(parentShell, false);
		this.creator = creator;
		this.rulerInfo = rulerInfo;
		create();
	}

	/**
	 * Create the enriched information control that is shown when moving the
	 * mouse over the control that was shown on hover (making it sticky).
	 *
	 * @param parentShell
	 * @param hoverInformationControl
	 *            the control that was used on hover (used for getting the
	 *            correct line that was hovered)
	 */
	public BlameInformationControl(Shell parentShell,
			BlameInformationControl hoverInformationControl) {
		// Make resizable and have a bottom bar for moving around
		super(parentShell, new ToolBarManager());
		this.hoverInformationControl = hoverInformationControl;
		create();
	}

	public IInformationControlCreator getInformationPresenterControlCreator() {
		return this.creator;
	}

	public boolean hasContents() {
		return true;
	}

	protected void createContent(Composite parent) {
		scrolls = new ScrolledComposite(parent, SWT.V_SCROLL | SWT.H_SCROLL);
		scrolls.setExpandHorizontal(true);
		scrolls.setExpandVertical(true);
		displayArea = new Composite(scrolls, SWT.NONE);
		scrolls.setContent(displayArea);
		displayArea.setForeground(parent.getForeground());
		displayArea.setBackground(parent.getBackground());
		displayArea.setBackgroundMode(SWT.INHERIT_FORCE);
		GridLayoutFactory.swtDefaults().equalWidth(true).applyTo(displayArea);

		Composite commitHeader = new Composite(displayArea, SWT.NONE);
		commitHeader.setLayout(GridLayoutFactory.fillDefaults().numColumns(3)
				.create());
		GridDataFactory.fillDefaults().grab(true, false).applyTo(commitHeader);

		commitLabel = new Label(commitHeader, SWT.READ_ONLY);
		commitLabel.setFont(JFaceResources.getBannerFont());

		Link openCommitLink = new Link(commitHeader, SWT.NONE);
		openCommitLink.setText(UIText.BlameInformationControl_OpenCommitLink);
		openCommitLink.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				openCommit();
			}
		});

		Link showInHistoryLink = new Link(commitHeader, SWT.NONE);
		showInHistoryLink.setText(UIText.BlameInformationControl_ShowInHistoryLink);
		showInHistoryLink.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				showCommitInHistory();
			}
		});

		authorLabel = new Label(displayArea, SWT.NONE);
		authorLabel.setForeground(parent.getForeground());
		authorLabel.setBackground(parent.getBackground());
		authorLabel.setFont(UIUtils.getItalicFont(JFaceResources.DEFAULT_FONT));
		GridDataFactory.fillDefaults().grab(true, false).applyTo(authorLabel);

		committerLabel = new Label(displayArea, SWT.NONE);
		committerLabel.setForeground(parent.getForeground());
		committerLabel.setBackground(parent.getBackground());
		committerLabel.setFont(UIUtils
				.getItalicFont(JFaceResources.DEFAULT_FONT));
		GridDataFactory.fillDefaults().grab(true, false)
				.applyTo(committerLabel);

		Label separator = new Label(displayArea, SWT.HORIZONTAL | SWT.SEPARATOR);
		GridDataFactory.fillDefaults().grab(true, false).applyTo(separator);

		messageText = new StyledText(displayArea, SWT.NONE);
		messageText.setForeground(parent.getForeground());
		messageText.setBackground(parent.getBackground());
		messageText.setEditable(false);
		messageText.setFont(UIUtils
				.getFont(UIPreferences.THEME_CommitMessageFont));
		GridDataFactory.fillDefaults().grab(true, true).applyTo(messageText);
	}

	public Point computeSizeHint() {
		Point computed = getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT, true);

		Point constraints = getSizeConstraints();
		if (constraints == null)
			return computed;

		Point constrainedSize = getShell().computeSize(constraints.x,
				SWT.DEFAULT, true);
		int width = Math.min(computed.x, constrainedSize.x);
		int height = Math.max(computed.y, constrainedSize.y);
		return new Point(width, height);
	}

	private void setControlVisible(Control control, boolean visible) {
		control.setVisible(visible);
		((GridData) control.getLayoutData()).exclude = !visible;
	}

	public void setInput(Object input) {
		if (input == null) {
			// Make sure we don't hold a reference to this when nothing is
			// shown, it can be big
			this.revision = null;
			if (showAnnotationsLink != null) {
				// This listener can also hold a reference because of a final
				// parameter, see createDiffLinkAndText(final RevCommit
				// parent...)
				if (!showAnnotationsLink.isDisposed())
					showAnnotationsLink
							.removeSelectionListener(showAnnotationsLinkSelectionAdapter);
				showAnnotationsLink = null;
				showAnnotationsLinkSelectionAdapter = null;
			}
			return;
		}

		this.revision = (BlameRevision) input;

		// Remember line number that was hovered over when the input was set.
		// Used for showing the diff hunk of this line instead of the full diff.
		if (rulerInfo != null)
			revisionRulerLineNumber = rulerInfo
					.getLineOfLastMouseButtonActivity();

		RevCommit commit = this.revision.getCommit();

		String linkText = MessageFormat.format(
				UIText.BlameInformationControl_Commit, commit.abbreviate(7)
						.name());
		commitLabel.setText(linkText);

		PersonIdent author = commit.getAuthorIdent();
		if (author != null) {
			setControlVisible(authorLabel, true);
			authorLabel.setText(MessageFormat.format(
					UIText.BlameInformationControl_Author, author.getName(),
					author.getEmailAddress(), author.getWhen()));
		} else
			setControlVisible(authorLabel, false);

		PersonIdent committer = commit.getCommitterIdent();
		setControlVisible(authorLabel, author != null);
		if (committer != null && !committer.equals(author)) {
			setControlVisible(committerLabel, true);
			committerLabel.setText(MessageFormat.format(
					UIText.BlameInformationControl_Committer,
					committer.getName(), committer.getEmailAddress(),
					committer.getWhen()));
		} else
			setControlVisible(committerLabel, false);

		messageText.setText(commit.getFullMessage());

		createDiffs();

		displayArea.layout();
		scrolls.setMinSize(displayArea.computeSize(SWT.DEFAULT, SWT.DEFAULT));
	}

	private void createDiffs() {
		if (diffComposite != null)
			diffComposite.dispose();

		RevCommit commit = revision.getCommit();
		if (commit.getParentCount() == 0)
			return;

		createDiffComposite();

		for (int i = 0; i < commit.getParentCount(); i++) {
			RevCommit parent = commit.getParent(i);
			createDiff(parent);
		}
	}

	private void createDiffComposite() {
		diffComposite = new Composite(displayArea, SWT.NONE);
		diffComposite.setLayoutData(GridDataFactory.fillDefaults()
				.grab(true, true).create());
		diffComposite.setLayout(GridLayoutFactory.fillDefaults().create());
	}

	private void createDiff(RevCommit parent) {
		Diff diff = revision.getDiffToParent(parent);
		if (diff != null) {
			try {
				createDiffLinkAndText(parent, diff);
			} catch (IOException e) {
				String msg = "Error creating diff in blame information control for commit " //$NON-NLS-1$
						+ parent.toObjectId();
				Activator.logError(msg, e);
			}
		}
	}

	private void createDiffLinkAndText(final RevCommit parent, final Diff diff)
			throws IOException {
		String parentId = parent.toObjectId().abbreviate(7).name();
		String parentMessage = parent.getShortMessage();

		EditList interestingDiff = getInterestingDiff(diff.getEditList());

		final Integer parentLine;
		if (!interestingDiff.isEmpty())
			parentLine = Integer.valueOf(interestingDiff.get(0).getBeginA());
		else
			parentLine = null;

		Composite header = new Composite(diffComposite, SWT.NONE);
		header.setLayout(GridLayoutFactory.fillDefaults().numColumns(2)
				.create());

		Label diffHeaderLabel = new Label(header, SWT.NONE);
		diffHeaderLabel.setText(NLS.bind(
				UIText.BlameInformationControl_DiffHeaderLabel, parentId,
				parentMessage));

		showAnnotationsLink = new Link(header, SWT.NONE);
		showAnnotationsLink
				.setText(UIText.BlameInformationControl_ShowAnnotationsLink);
		showAnnotationsLinkSelectionAdapter = new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				blameParent(parent, diff, parentLine);
			}
		};
		showAnnotationsLink
				.addSelectionListener(showAnnotationsLinkSelectionAdapter);

		DiffViewer diffText = new DiffViewer(diffComposite, null, SWT.NONE,
				false);
		diffText.setEditable(false);
		diffText.getControl().setLayoutData(
				GridDataFactory.fillDefaults().grab(true, true).create());

		IDocument document = new Document();
		DiffStyleRangeFormatter diffFormatter = new DiffStyleRangeFormatter(
				document);
		diffFormatter.setContext(1);
		diffFormatter.setRepository(revision.getRepository());
		diffFormatter.format(interestingDiff, diff.getOldText(),
				diff.getNewText());

		diffText.setDocument(document);
		diffText.setFormatter(diffFormatter);
	}

	private EditList getInterestingDiff(EditList fullDiff) {
		int hoverLineNumber = getHoverLineNumber();
		Integer sourceLine = revision.getSourceLine(hoverLineNumber);

		if (sourceLine == null)
			// Fall back to whole diff
			return fullDiff;

		int line = sourceLine.intValue();
		EditList interestingDiff = new EditList(1);
		for (Edit edit : fullDiff) {
			if (line >= edit.getBeginB() && line <= edit.getEndB())
				interestingDiff.add(edit);
		}
		return interestingDiff;
	}

	/**
	 * @return 0-based line number of hover
	 */
	private int getHoverLineNumber() {
		// If this is the enriched control, we have to use the line number of
		// the original hover control, otherwise the line number may have
		// changed if the mouse moved over another area.
		if (hoverInformationControl != null)
			return hoverInformationControl.getHoverLineNumber();
		else
			return revisionRulerLineNumber;
	}

	private void openCommit() {
		try {
			getShell().dispose();
			CommitEditor.open(new RepositoryCommit(revision.getRepository(),
					revision.getCommit()));
		} catch (PartInitException pie) {
			Activator.logError(pie.getLocalizedMessage(), pie);
		}
	}

	private void showCommitInHistory() {
		getShell().dispose();
		IHistoryView part;
		try {
			part = (IHistoryView) PlatformUI.getWorkbench()
					.getActiveWorkbenchWindow().getActivePage()
					.showView(IHistoryView.VIEW_ID);
		} catch (PartInitException e) {
			Activator.logError(e.getLocalizedMessage(), e);
			return;
		}

		if (part == null)
			return;

		Repository repository = revision.getRepository();
		if (!repository.isBare()) {
			String sourcePath = revision.getSourcePath();
			File file = new File(repository.getWorkTree(), sourcePath);
			BlameHistoryPageInput input = new BlameHistoryPageInput(repository,
					revision.getCommit(), file);
			part.showHistoryFor(input);
		} else {
			HistoryPageInput input = new BlameHistoryPageInput(repository,
					revision.getCommit());
			part.showHistoryFor(input);
		}
	}

	private void blameParent(RevCommit parent, Diff diff, Integer sourceLine) {
		try {
			String path = diff.getOldPath();
			IFileRevision rev = CompareUtils.getFileRevision(path, parent,
					revision.getRepository(), null);
			int line = sourceLine == null ? -1 : sourceLine.intValue();
			IStorage storage = rev.getStorage(new NullProgressMonitor());
			IWorkbenchPage page = PlatformUI.getWorkbench()
					.getActiveWorkbenchWindow().getActivePage();
			BlameOperation operation = new BlameOperation(
					revision.getRepository(), storage, path, parent,
					getShell(), page, line);
			JobUtil.scheduleUserJob(operation, UIText.ShowBlameHandler_JobName,
					JobFamilies.BLAME);
		} catch (IOException e) {
			Activator.logError(UIText.ShowBlameHandler_errorMessage, e);
		} catch (CoreException e) {
			Activator.logError(UIText.ShowBlameHandler_errorMessage, e);
		}
	}

	public void setVisible(boolean visible) {
		super.setVisible(visible);
		if (!visible)
			setInput(null);
	}
}
