/*
 * Copyright (C) 2008-2012, Google Inc.
 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.eclipse.jgit.lib;

import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;

/**
 * Batch of reference updates to be applied to a repository.
 * <p>
 * The batch update is primarily useful in the transport code, where a client or
 * server is making changes to more than one reference at a time.
 */
public class BatchRefUpdate {
	private final RefDatabase refdb;

	/** Commands to apply during this batch. */
	private final List<ReceiveCommand> commands;

	/** Does the caller permit a forced update on a reference? */
	private boolean allowNonFastForwards;

	/** Identity to record action as within the reflog. */
	private PersonIdent refLogIdent;

	/** Message the caller wants included in the reflog. */
	private String refLogMessage;

	/** Should the result value be appended to {@link #refLogMessage}. */
	private boolean refLogIncludeResult;

	/**
	 * Initialize a new batch update.
	 *
	 * @param refdb
	 *            the reference database of the repository to be updated.
	 */
	protected BatchRefUpdate(RefDatabase refdb) {
		this.refdb = refdb;
		this.commands = new ArrayList<ReceiveCommand>();
	}

	/**
	 * @return true if the batch update will permit a non-fast-forward update to
	 *         an existing reference.
	 */
	public boolean isAllowNonFastForwards() {
		return allowNonFastForwards;
	}

	/**
	 * Set if this update wants to permit a forced update.
	 *
	 * @param allow
	 *            true if this update batch should ignore merge tests.
	 * @return {@code this}.
	 */
	public BatchRefUpdate setAllowNonFastForwards(boolean allow) {
		allowNonFastForwards = allow;
		return this;
	}

	/** @return identity of the user making the change in the reflog. */
	public PersonIdent getRefLogIdent() {
		return refLogIdent;
	}

	/**
	 * Set the identity of the user appearing in the reflog.
	 * <p>
	 * The timestamp portion of the identity is ignored. A new identity with the
	 * current timestamp will be created automatically when the update occurs
	 * and the log record is written.
	 *
	 * @param pi
	 *            identity of the user. If null the identity will be
	 *            automatically determined based on the repository
	 *            configuration.
	 * @return {@code this}.
	 */
	public BatchRefUpdate setRefLogIdent(final PersonIdent pi) {
		refLogIdent = pi;
		return this;
	}

	/**
	 * Get the message to include in the reflog.
	 *
	 * @return message the caller wants to include in the reflog; null if the
	 *         update should not be logged.
	 */
	public String getRefLogMessage() {
		return refLogMessage;
	}

	/** @return {@code true} if the ref log message should show the result. */
	public boolean isRefLogIncludingResult() {
		return refLogIncludeResult;
	}

	/**
	 * Set the message to include in the reflog.
	 *
	 * @param msg
	 *            the message to describe this change. It may be null if
	 *            appendStatus is null in order not to append to the reflog
	 * @param appendStatus
	 *            true if the status of the ref change (fast-forward or
	 *            forced-update) should be appended to the user supplied
	 *            message.
	 * @return {@code this}.
	 */
	public BatchRefUpdate setRefLogMessage(String msg, boolean appendStatus) {
		if (msg == null && !appendStatus)
			disableRefLog();
		else if (msg == null && appendStatus) {
			refLogMessage = ""; //$NON-NLS-1$
			refLogIncludeResult = true;
		} else {
			refLogMessage = msg;
			refLogIncludeResult = appendStatus;
		}
		return this;
	}

	/**
	 * Don't record this update in the ref's associated reflog.
	 *
	 * @return {@code this}.
	 */
	public BatchRefUpdate disableRefLog() {
		refLogMessage = null;
		refLogIncludeResult = false;
		return this;
	}

	/** @return true if log has been disabled by {@link #disableRefLog()}. */
	public boolean isRefLogDisabled() {
		return refLogMessage == null;
	}

	/** @return commands this update will process. */
	public List<ReceiveCommand> getCommands() {
		return Collections.unmodifiableList(commands);
	}

	/**
	 * Add a single command to this batch update.
	 *
	 * @param cmd
	 *            the command to add, must not be null.
	 * @return {@code this}.
	 */
	public BatchRefUpdate addCommand(ReceiveCommand cmd) {
		commands.add(cmd);
		return this;
	}

	/**
	 * Add commands to this batch update.
	 *
	 * @param cmd
	 *            the commands to add, must not be null.
	 * @return {@code this}.
	 */
	public BatchRefUpdate addCommand(ReceiveCommand... cmd) {
		return addCommand(Arrays.asList(cmd));
	}

	/**
	 * Add commands to this batch update.
	 *
	 * @param cmd
	 *            the commands to add, must not be null.
	 * @return {@code this}.
	 */
	public BatchRefUpdate addCommand(Collection<ReceiveCommand> cmd) {
		commands.addAll(cmd);
		return this;
	}

	/**
	 * Execute this batch update.
	 * <p>
	 * The default implementation of this method performs a sequential reference
	 * update over each reference.
	 *
	 * @param walk
	 *            a RevWalk to parse tags in case the storage system wants to
	 *            store them pre-peeled, a common performance optimization.
	 * @param monitor
	 *            progress monitor to receive update status on.
	 * @throws IOException
	 *             the database is unable to accept the update. Individual
	 *             command status must be tested to determine if there is a
	 *             partial failure, or a total failure.
	 */
	public void execute(RevWalk walk, ProgressMonitor monitor)
			throws IOException {
		monitor.beginTask(JGitText.get().updatingReferences, commands.size());
		List<ReceiveCommand> commands2 = new ArrayList<ReceiveCommand>(
				commands.size());
		List<String> namesToCheck = new ArrayList<String>(commands.size());
		// First delete refs. This may free the name space for some of the
		// updates.
		for (ReceiveCommand cmd : commands) {
			try {
				if (cmd.getResult() == NOT_ATTEMPTED) {
					cmd.updateType(walk);
					switch (cmd.getType()) {
					case CREATE:
						namesToCheck.add(cmd.getRefName());
						commands2.add(cmd);
						break;
					case UPDATE:
					case UPDATE_NONFASTFORWARD:
						commands2.add(cmd);
						break;
					case DELETE:
						RefUpdate rud = newUpdate(cmd);
						monitor.update(1);
						cmd.setResult(rud.delete(walk));
					}
				}
			} catch (IOException err) {
				cmd.setResult(
						REJECTED_OTHER_REASON,
						MessageFormat.format(JGitText.get().lockError,
								err.getMessage()));
			}
		}
		if (!commands2.isEmpty()) {
			// What part of the name space is already taken
			Collection<String> takenNames = new HashSet<String>(refdb.getRefs(
					RefDatabase.ALL).keySet());
			Collection<String> takenPrefixes = getTakenPrefixes(takenNames);

			// Now to the update that may require more room in the name space
			for (ReceiveCommand cmd : commands2) {
				try {
					if (cmd.getResult() == NOT_ATTEMPTED) {
						cmd.updateType(walk);
						RefUpdate ru = newUpdate(cmd);
						SWITCH: switch (cmd.getType()) {
						case DELETE:
							// Performed in the first phase
							break;
						case UPDATE:
						case UPDATE_NONFASTFORWARD:
							RefUpdate ruu = newUpdate(cmd);
							cmd.setResult(ruu.update(walk));
							break;
						case CREATE:
							for (String prefix : getPrefixes(cmd.getRefName())) {
								if (takenNames.contains(prefix)) {
									cmd.setResult(Result.LOCK_FAILURE);
									break SWITCH;
								}
							}
							if (takenPrefixes.contains(cmd.getRefName())) {
								cmd.setResult(Result.LOCK_FAILURE);
								break SWITCH;
							}
							ru.setCheckConflicting(false);
							addRefToPrefixes(takenPrefixes, cmd.getRefName());
							takenNames.add(cmd.getRefName());
							cmd.setResult(ru.update(walk));
						}
					}
				} catch (IOException err) {
					cmd.setResult(REJECTED_OTHER_REASON, MessageFormat.format(
							JGitText.get().lockError, err.getMessage()));
				} finally {
					monitor.update(1);
				}
			}
		}
		monitor.endTask();
	}

	private static Collection<String> getTakenPrefixes(
			final Collection<String> names) {
		Collection<String> ref = new HashSet<String>();
		for (String name : names)
			ref.addAll(getPrefixes(name));
		return ref;
	}

	private static void addRefToPrefixes(Collection<String> prefixes,
			String name) {
		for (String prefix : getPrefixes(name)) {
			prefixes.add(prefix);
		}
	}

	static Collection<String> getPrefixes(String s) {
		Collection<String> ret = new HashSet<String>();
		int p1 = s.indexOf('/');
		while (p1 > 0) {
			ret.add(s.substring(0, p1));
			p1 = s.indexOf('/', p1 + 1);
		}
		return ret;
	}

	/**
	 * Create a new RefUpdate copying the batch settings.
	 *
	 * @param cmd
	 *            specific command the update should be created to copy.
	 * @return a single reference update command.
	 * @throws IOException
	 *             the reference database cannot make a new update object for
	 *             the given reference.
	 */
	protected RefUpdate newUpdate(ReceiveCommand cmd) throws IOException {
		RefUpdate ru = refdb.newUpdate(cmd.getRefName(), false);
		if (isRefLogDisabled())
			ru.disableRefLog();
		else {
			ru.setRefLogIdent(refLogIdent);
			ru.setRefLogMessage(refLogMessage, refLogIncludeResult);
		}
		switch (cmd.getType()) {
		case DELETE:
			if (!ObjectId.zeroId().equals(cmd.getOldId()))
				ru.setExpectedOldObjectId(cmd.getOldId());
			ru.setForceUpdate(true);
			return ru;

		case CREATE:
		case UPDATE:
		case UPDATE_NONFASTFORWARD:
		default:
			ru.setForceUpdate(isAllowNonFastForwards());
			ru.setExpectedOldObjectId(cmd.getOldId());
			ru.setNewObjectId(cmd.getNewId());
			return ru;
		}
	}
}
