/*******************************************************************************
 * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
 * Copyright (C) 2011, Mathias Kinzler <mathias.kinzler@sap.com>
 * Copyright (C) 2012, 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.core.op;

import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.util.Collection;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.egit.core.Activator;
import org.eclipse.egit.core.EclipseGitProgressTransformer;
import org.eclipse.egit.core.internal.CoreText;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.osgi.util.NLS;

/**
 * Push operation: pushing from local repository to one or many remote ones.
 */
public class PushOperation {
	private static final int WORK_UNITS_PER_TRANSPORT = 10;

	private final Repository localDb;

	private final PushOperationSpecification specification;

	private final boolean dryRun;

	private final String remoteName;

	private final int timeout;

	private OutputStream out;

	private PushOperationResult operationResult;

	private CredentialsProvider credentialsProvider;

	/**
	 * Create push operation for provided specification.
	 *
	 * @param localDb
	 *            local repository.
	 * @param specification
	 *            specification of ref updates for remote repositories.
	 * @param dryRun
	 *            true if push operation should just check for possible result
	 *            and not really update remote refs, false otherwise - when push
	 *            should act normally.
	 * @param timeout
	 *            the timeout in seconds (0 for no timeout)
	 */
	public PushOperation(final Repository localDb,
			final PushOperationSpecification specification,
			final boolean dryRun, int timeout) {
		this(localDb, null, specification, dryRun, timeout);
	}

	/**
	 * Creates a push operation for a remote configuration.
	 *
	 * @param localDb
	 * @param remoteName
	 * @param dryRun
	 * @param timeout
	 */
	public PushOperation(final Repository localDb, final String remoteName,
			final boolean dryRun, int timeout) {
		this(localDb, remoteName, null, dryRun, timeout);
	}

	private PushOperation(final Repository localDb, final String remoteName,
			PushOperationSpecification specification, final boolean dryRun,
			int timeout) {
		this.localDb = localDb;
		this.specification = specification;
		this.dryRun = dryRun;
		this.remoteName = remoteName;
		this.timeout = timeout;
	}

	/**
	 * @param credentialsProvider
	 */
	public void setCredentialsProvider(CredentialsProvider credentialsProvider) {
		this.credentialsProvider = credentialsProvider;
	}

	/**
	 * @return the operation's credentials provider
	 */
	public CredentialsProvider getCredentialsProvider() {
		return credentialsProvider;
	}

	/**
	 * @return push operation result
	 */
	public PushOperationResult getOperationResult() {
		if (operationResult == null)
			throw new IllegalStateException(CoreText.OperationNotYetExecuted);
		return operationResult;
	}

	/**
	 * @return operation specification, as provided in constructor (may be
	 *         <code>null</code>)
	 */
	public PushOperationSpecification getSpecification() {
		return specification;
	}

	/**
	 * @param actMonitor
	 *            may be <code>null</code> if progress monitoring is not desired
	 * @throws InvocationTargetException
	 *             not really used: failure is communicated via the result (see
	 *             {@link #getOperationResult()})
	 */
	public void run(IProgressMonitor actMonitor)
			throws InvocationTargetException {

		if (operationResult != null)
			throw new IllegalStateException(CoreText.OperationAlreadyExecuted);

		if (this.specification != null)
			for (URIish uri : this.specification.getURIs()) {
				for (RemoteRefUpdate update : this.specification
						.getRefUpdates(uri))
					if (update.getStatus() != Status.NOT_ATTEMPTED)
						throw new IllegalStateException(
								CoreText.RemoteRefUpdateCantBeReused);
			}
		IProgressMonitor monitor;
		if (actMonitor == null)
			monitor = new NullProgressMonitor();
		else
			monitor = actMonitor;

		final int totalWork;
		if (specification != null)
			totalWork = specification.getURIsNumber()
					* WORK_UNITS_PER_TRANSPORT;
		else
			totalWork = 1;
		if (dryRun)
			monitor.beginTask(CoreText.PushOperation_taskNameDryRun, totalWork);
		else
			monitor.beginTask(CoreText.PushOperation_taskNameNormalRun,
					totalWork);

		operationResult = new PushOperationResult();
		Git git = new Git(localDb);

		if (specification != null)
			for (final URIish uri : specification.getURIs()) {
				final SubProgressMonitor subMonitor = new SubProgressMonitor(
						monitor, WORK_UNITS_PER_TRANSPORT,
						SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK);

				try {
					if (monitor.isCanceled()) {
						operationResult.addOperationResult(uri,
								CoreText.PushOperation_resultCancelled);
						continue;
					}

					Collection<RemoteRefUpdate> refUpdates = specification.getRefUpdates(uri);
					final EclipseGitProgressTransformer gitSubMonitor = new EclipseGitProgressTransformer(
							subMonitor);

					try {
						Transport transport = Transport.open(localDb, uri);
						transport.setDryRun(dryRun);
						transport.setTimeout(timeout);
						if (credentialsProvider != null)
							transport.setCredentialsProvider(credentialsProvider);
						PushResult result = transport.push(gitSubMonitor, refUpdates, out);

						operationResult.addOperationResult(result.getURI(), result);
						specification.addURIRefUpdates(result.getURI(), result.getRemoteUpdates());
					} catch (JGitInternalException e) {
						String errorMessage = e.getCause() != null ? e
								.getCause().getMessage() : e.getMessage();
						String userMessage = NLS.bind(
										CoreText.PushOperation_InternalExceptionOccurredMessage,
										errorMessage);
						handleException(uri, e, userMessage);
					} catch (Exception e) {
						handleException(uri, e, e.getMessage());
					}

					monitor.worked(WORK_UNITS_PER_TRANSPORT);
				} finally {
					// Dirty trick to get things always working.
					subMonitor.beginTask("", WORK_UNITS_PER_TRANSPORT); //$NON-NLS-1$
					subMonitor.done();
					subMonitor.done();
				}
			}
		else {
			final EclipseGitProgressTransformer gitMonitor = new EclipseGitProgressTransformer(
					monitor);
			try {
				Iterable<PushResult> results = git.push().setRemote(
						remoteName).setDryRun(dryRun).setTimeout(timeout)
						.setProgressMonitor(gitMonitor).setCredentialsProvider(
credentialsProvider)
						.setOutputStream(out).call();
				for (PushResult result : results) {
					operationResult.addOperationResult(result.getURI(), result);
				}
			} catch (JGitInternalException e) {
				String errorMessage = e.getCause() != null ? e.getCause()
						.getMessage() : e.getMessage();
				String userMessage = NLS.bind(
						CoreText.PushOperation_InternalExceptionOccurredMessage,
						errorMessage);
				URIish uri = getPushURIForErrorHandling();
				handleException(uri, e, userMessage);
			} catch (Exception e) {
				URIish uri = getPushURIForErrorHandling();
				handleException(uri, e, e.getMessage());
			}
		}
		monitor.done();
	}

	private void handleException(final URIish uri, Exception e,
			String userMessage) {
		String uriString;
		if (uri != null) {
			operationResult.addOperationResult(uri, userMessage);
			uriString = uri.toString();
		} else
			uriString = "retrieving URI failed"; //$NON-NLS-1$

		String userMessageForUri = NLS.bind(
				CoreText.PushOperation_ExceptionOccurredDuringPushOnUriMessage,
				uriString, userMessage);
		Activator.logError(userMessageForUri, e);
	}

	private URIish getPushURIForErrorHandling() {
		RemoteConfig rc = null;
		try {
			rc = new RemoteConfig(localDb.getConfig(), remoteName);
			return rc.getPushURIs().isEmpty() ? rc.getURIs().get(0) : rc
					.getPushURIs().get(0);
		} catch (URISyntaxException e) {
			// should not happen
			Activator.logError("Reading RemoteConfig failed", e); //$NON-NLS-1$
			return null;
		}
	}

	/**
	 * Sets the output stream this operation will write sideband messages to.
	 *
	 * @param out
	 *            the outputstream to write to
	 * @since 3.0
	 */
	public void setOutputStream(OutputStream out) {
		this.out = out;
	}
}
