package com.atlassian.stash.internal.scm.git.command;

import com.atlassian.bitbucket.scm.Command;
import com.atlassian.bitbucket.scm.CommandFailedException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.stash.internal.scm.git.command.clone.WindowsForkCommandExitHandler;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.git.GitAgent;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.command.GitScmCommandBuilder;
import com.atlassian.bitbucket.scm.git.GitScmConfig;
import com.atlassian.bitbucket.scm.git.command.clone.GitCloneBuilder;
import org.apache.commons.lang3.SystemUtils;

import java.io.File;
import java.util.concurrent.ExecutorService;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * A base class for constructing {@link Command commands} which create forks of existing
 * repositories.
 * <p>
 * Derived classes must implement {@link #createRepository(GitScmCommandBuilder)}. This base class provides a
 * {@link #fork(GitScmCommandBuilder, boolean) fork} method which can be used to simplify that implementation.
 * <p>
 * In addition to providing a helper method for creating forks, this base class also handles removing the "origin"
 * remote from newly-created forks prior to {@link #configureRepository() configuring} them.
 *
 * @since 2.4
 */
public abstract class AbstractForkCommand extends AbstractCreateCommand {

    protected final Repository parent;

    public AbstractForkCommand(ExecutorService executorService, GitCommandBuilderFactory builderFactory,
                               GitScmConfig config, I18nService i18nService,
                               Repository repository, Repository parent) {
        super(executorService, builderFactory, config, i18nService, repository);

        this.parent = checkNotNull(parent, "parent");
    }

    @Override
    protected void configureRepository() {
        //After the clone is created, remove its "origin" remote, to eliminate the need to keep it up-to-date. This
        //is done here, as opposed to in createRepository(), so that AbstractCreateCommand will handle deleting the
        //partially-created fork automatically if an exception is thrown
        builder().remote()
                .rm(GitAgent.ORIGIN)
                .build()
                .call();

        //Apply initial configuration to the new fork
        super.configureRepository();
    }

    /**
     * Clones the {@link #parent parent} to create the new fork {@link #repository repository}. The fork is always
     * created as a bare clone, but the caller can choose whether to create a {@code shared} clone or not.
     * <p>
     * When {@code shared} is {@code true}, an alternate will be used to allow the new fork to access its parent
     * repository's {@code objects/} directory. Shared fork creation takes almost no time, since no objects need
     * to be copied into the new repository, but the new fork is dependent on its parent's continued existence
     * since it doesn't have a complete object database on its own. This technique works the same on both Windows
     * and Linux, and for all supported versions of git.
     * <p>
     * When {@code shared} is {@code false}, objects will be copied directly into the new fork. This may make fork
     * creation take longer. How the objects are copied depends on the operating system and git version. On Linux,
     * git will create hard links to the parent repository's packs, allowing them to be accessed directly. As the
     * repositories diverge, those links will be broken over time. On Windows, what happens is more difficult to
     * predict. It appears some versions of git use hard links on Windows as well (NTFS supports them) while other
     * versions use bulk I/O methods to copy the files instead. Either way, there is no dependency between the new
     * fork and its parent.
     *
     * @param builder used to create the {@link GitScmCommandBuilder#clone() clone} command
     * @param shared  {@code true} to use alternates and create a shared clone; otherwise, {@code false} to create
     *                a "simple" clone with hard links or copied objects
     * @return {@code true} if the initial command succeeded; otherwise, {@code false} if the fork had to be retried
     */
    protected boolean fork(GitScmCommandBuilder builder, boolean shared) {
        File repositoryDir = config.getRepositoryDir(repository);
        File originDir = config.getRepositoryDir(parent);
        GitCloneBuilder cloneBuilder = builder.clone()
                .bare()
                .directory(repositoryDir)
                .origin(originDir)
                .shared(shared);

        WindowsForkCommandExitHandler exitHandler = null;
        if (SystemUtils.IS_OS_WINDOWS) {
            //Don't use a shared clone on retry, even if the initial fork command was for a shared clone. Alternates
            //won't be rewritten correctly on home moves, if the paths are Cygwin-style.
            exitHandler = new WindowsForkCommandExitHandler(cloneBuilder, i18nService, repositoryDir, originDir, false);
        }
        log.trace("Forking {} to {}", parent.getName(), repository.getName());
        cloneBuilder.build().call();

        return exitHandler == null || !exitHandler.isCygwin();
    }

    @Override
    protected CommandFailedException newCommandFailedException(Throwable cause) {
        throw new CommandFailedException(i18nService.createKeyedMessage("bitbucket.git.fork.failed",
                parent.getProject().getKey(), parent.getSlug(), repository.getProject().getKey(), repository.getSlug()));
    }
}
