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

import com.atlassian.bitbucket.commit.CommitCallback;
import com.atlassian.bitbucket.compare.CompareRef;
import com.atlassian.bitbucket.compare.CompareRequest;
import com.atlassian.bitbucket.content.ChangeCallback;
import com.atlassian.bitbucket.content.ChangeContext;
import com.atlassian.bitbucket.content.DiffContentCallback;
import com.atlassian.bitbucket.content.DiffContext;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.io.SingleLineOutputHandler;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.scm.compare.CompareChangeCommandParameters;
import com.atlassian.bitbucket.scm.compare.CompareDiffCommandParameters;
import com.atlassian.bitbucket.scm.git.GitUtils;
import com.atlassian.bitbucket.scm.git.command.GitCommand;
import com.atlassian.bitbucket.scm.git.command.GitCommandBuilderFactory;
import com.atlassian.bitbucket.scm.git.command.GitCompareCommandFactory;
import com.atlassian.bitbucket.scm.git.command.diff.GitDiffBuilder;
import com.atlassian.bitbucket.scm.git.command.merge.GitNoCommonAncestorException;
import com.atlassian.bitbucket.scm.git.command.revlist.GitRevListBuilder;
import com.atlassian.stash.internal.scm.git.command.diff.CallbackDiffTreeOutputHandler;
import com.atlassian.stash.internal.scm.git.command.diff.DiffOutputHandler;
import com.atlassian.stash.internal.scm.git.command.diff.DiffTreeOutputHandler;
import com.atlassian.stash.internal.scm.git.command.merge.MergeBaseExitHandler;
import com.atlassian.stash.internal.scm.git.command.merge.MergeBaseOutputHandler;
import com.atlassian.stash.internal.scm.git.command.revlist.CallbackRevListOutputHandler;
import com.atlassian.stash.internal.scm.git.command.revlist.RevListOutputHandler;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;
import java.util.Set;

public class DefaultGitCompareCommandFactory
        extends AbstractGitCompareCommandFactory
        implements GitCompareCommandFactory {

    private static final String NULL_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";

    private final I18nService i18nService;

    public DefaultGitCompareCommandFactory(GitCommandBuilderFactory builderFactory, GitRepositoryConfig repositoryConfig,
                                           I18nService i18nService, InternalGitScmConfig config) {
        super(builderFactory, repositoryConfig, config);
        this.i18nService = i18nService;
    }

    @Nonnull
    @Override
    public GitCommand<Void> changes(@Nonnull CompareRequest compareRequest,
                                    @Nonnull CompareChangeCommandParameters parameters,
                                    @Nonnull ChangeCallback callback) {

        String mergeBase = resolveMergeBase(compareRequest);
        ChangeContext context = toContext(compareRequest, parameters, mergeBase);

        // by definition the merge base will be reachable from both the source and target repository
        CompareRef fromRef = compareRequest.getFromRef();
        Repository repository = fromRef.getRepository();

        DiffTreeOutputHandler<Void> outputHandler = new CallbackDiffTreeOutputHandler(callback, context);
        return diffTree(repository, mergeBase, fromRef.getId(), parameters.getPaths())
                .format(outputHandler.getFormat())
                .build(outputHandler);
    }

    @Nonnull
    @Override
    public GitCommand<Void> commits(@Nonnull CompareRequest compareRequest, @Nonnull CommitCallback callback) {
        CompareRef fromRef = compareRequest.getFromRef();
        CompareRef toRef = compareRequest.getToRef();
        Repository fromRepository = fromRef.getRepository();
        Repository toRepository = toRef.getRepository();

        RevListOutputHandler<Void> outputHandler = new CallbackRevListOutputHandler(toRepository, callback);
        GitRevListBuilder builder = revList(fromRepository, fromRef.getId(), toRepository, toRef.getId());
        return builder.format(outputHandler.getFormat()).build(outputHandler);
    }

    @Nonnull
    @Override
    public GitCommand<Void> diff(@Nonnull CompareRequest compareRequest,
                                 @Nonnull CompareDiffCommandParameters parameters,
                                 @Nonnull DiffContentCallback callback) {
        // computing the merge base is only to generate a valid DiffContext (with the correct 'fromHash' value);
        // the diff command itself supports the A...B notation to compute the diff from the merge base
        String mergeBase = resolveMergeBase(compareRequest);
        DiffContext context = toContext(compareRequest, parameters, mergeBase);
        Repository repository = compareRequest.getFromRef().getRepository();

        GitDiffBuilder builder = diff(repository, mergeBase, compareRequest.getFromRef().getId(), parameters);
        return repositoryConfig.getDiffStrategy(repository).apply(builder)
                .build(new DiffOutputHandler(callback, context, false));
    }

    /**
     * Resolves the merge base (aka the closest common ancestor) from two commits.
     */
    @Nonnull
    private String resolveMergeBase(CompareRequest compareRequest) {
        CompareRef fromRef = compareRequest.getFromRef();
        CompareRef toRef = compareRequest.getToRef();
        String fromId = fromRef.getId();
        String toId = toRef.getId();

        // if the target ref is in a different repository and not a full SHA, resolve it to make it non-ambiguous
        boolean isCrossRepository = !Objects.equal(fromRef.getRepository().getId(), toRef.getRepository().getId());
        if (isCrossRepository && !GitUtils.isHash(toId)) {
            toId = resolveRef(toRef);
        }

        // check if the two ids are the same
        // note that the logic is intentionally kept simple:
        // - it assumes that most callers will consistently provide both ids with the same format (i.e. both ids
        //   as SHAs of the same length, both ids as qualified refs or both ids as unqualified refs)
        // - as a result, if both ids are commit ids, it does not check whether they start with the same prefix;
        // - if both ids as ref names on a single-repository request, it does not qualify one of the refs if the
        //   other is unqualified;
        // - and, for the remaining cases, it does not resolve the ids to check whether they point to the same
        //   commit (to avoid extra Git process calls)
        boolean isSameId = Objects.equal(fromId, toId);
        if (isSameId && (!isCrossRepository || GitUtils.isHash(fromId))) {
            return fromId;
        }

        try {
            Set<String> mergeBases = getBuilderFactory(fromRef.getRepository(), toRef.getRepository())
                    .mergeBase()
                        .between(fromId, toId)
                        .exitHandler(new MergeBaseExitHandler(i18nService, null, ImmutableSet.of(fromId, toId)))
                    .build(new MergeBaseOutputHandler())
                    .call();
            return mergeBases == null ? NULL_TREE : Iterables.getFirst(mergeBases, NULL_TREE);
        } catch (GitNoCommonAncestorException e) {
            // the two refs have no common ancestor, so instead diff the from ref against the null tree;
            // http://thread.gmane.org/gmane.comp.version-control.git/146468/
            // http://thread.gmane.org/gmane.comp.version-control.git/146468/focus=146478
            // this works both with the diff-tree and diff commands
            return NULL_TREE;
        }
    }

    private String resolveRef(CompareRef toRef) {
        return StringUtils.chomp(getBuilderFactory(toRef.getRepository())
                .command("rev-parse")
                .argument("--verify")
                .argument("-q")
                .argument(toRef.getId())
                .build(new SingleLineOutputHandler())
                .call());
    }

    private ChangeContext toContext(CompareRequest compareRequest, CompareChangeCommandParameters parameters, String mergeBase) {
        return new ChangeContext.Builder(mergeBase, compareRequest.getFromRef().getId())
                .maxChanges(parameters.getMaxChanges())
                .build();
    }

    private DiffContext toContext(CompareRequest compareRequest, CompareDiffCommandParameters parameters, String mergeBase) {
        return parameters.toContext(compareRequest, mergeBase);
    }

}
