package com.atlassian.bitbucket.mesh.git.porcelain;

import com.atlassian.bitbucket.mesh.git.AbstractParameters;
import com.atlassian.bitbucket.mesh.git.GitRefPattern;
import com.atlassian.bitbucket.mesh.git.file.EditFileParameters;
import com.atlassian.bitbucket.mesh.git.merge.MergeParameters;
import com.atlassian.bitbucket.mesh.git.rebase.RebaseParameters;
import com.atlassian.bitbucket.mesh.git.ref.Branch;
import com.atlassian.bitbucket.mesh.repository.Repository;
import com.google.common.collect.ImmutableSet;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Set;

import static java.util.Objects.requireNonNull;
import static java.util.Optional.ofNullable;

/**
 * A request to construct a new work tree, which can be used to run {@code git} commands that require one, such as
 * {@code git merge} or {@code git rebase}.
 * <p>
 * If a {@link #getCommit commit} is not specified, it is assumed that the work tree is being created for an
 * <i>empty</i> repository, to create a root commit. The resulting work tree has no commit checked out and has no
 * unstaged changes.
 * <p>
 * If a {@link #getCommit commit} is specified, by default the work tree is created by using {@code git reset} to
 * read in that commit's tree, but, for performance reasons, no files are checked out. This results in a work tree
 * that lists every file in the repository as deleted when running {@code git status}. {@code git merge} can still
 * be used in such a work tree, however, and will produce the correct output.
 * <p>
 * Some commands, such as {@code git cherry-pick} and {@code git rebase} <i>cannot</i> be used in a work tree with
 * staged or unstaged changes. For those commands, a {@link #isPristine pristine} work tree may be requested. The
 * processing required to prepare a pristine work tree is more complex, and imposes higher overhead, so they should
 * only be requested when a command requiring one will be used.
 * <p>
 * If a {@link #getBranch branch} is specified, the {@code HEAD} for the work tree will be set to that branch. If
 * a {@link #getCommit commit} is also specified, the branch will be set to that commit. Otherwise, while {@code HEAD}
 * will be set, the referenced branch will not exist. In Git parlance, such a branch is referred to as "unborn", and
 * is a normal state for a newly created repository. The first commit will "birth" the branch. If a branch is not
 * specified, it will default to {@code refs/heads/master}, the standard Git default branch.
 * <p>
 * While each work tree is created for a specific {@link #getRepository repository}, work trees <i>do not</i> have
 * an {@code origin} remote (or any other remotes).
 */
public class WorkTreeParameters extends AbstractParameters {

    private final Set<Repository> alternates;
    private final String branch;
    private final String commit;
    private final boolean pristine;
    private final Repository repository;
    private final Path targetDir;

    private WorkTreeParameters(Builder builder) {
        super(builder);

        alternates = builder.alternates.build();
        branch = builder.branch;
        commit = builder.commit;
        pristine = builder.pristine;
        repository = builder.repository;
        targetDir = builder.targetDir;
    }

    /**
     * @return a set of zero or more repositories to configure in {@code objects/info/alternates}, to make their
     *         objects available in the new work tree
     */
    @Nonnull
    public Set<Repository> getAlternates() {
        return alternates;
    }

    /**
     * @return the branch to be set as the {@code HEAD} of the work tree, which defaults to {@code refs/heads/master}
     *         if {@code empty()}
     */
    @Nonnull
    public Optional<String> getBranch() {
        return ofNullable(branch);
    }

    /**
     * @return the commit to set as the tip of the {@code HEAD} {@link #getBranch branch} (or {@code refs/heads/master}
     *         if a branch wasn't specified), or {@code empty()} to request a work tree for creating an initial commit
     */
    @Nonnull
    public Optional<String> getCommit() {
        return ofNullable(commit);
    }

    /**
     * @return the repository for which the work tree is being created, which will <i>always</i> be made available as
     *         an {@link #getAlternates alternate}
     */
    @Nonnull
    public Repository getRepository() {
        return repository;
    }

    /**
     * @return {@code true} if a pristine work tree is required; otherwise, {@code false} if the work tree can
     *         have unstaged changes
     */
    public boolean isPristine() {
        return pristine;
    }

    /**
     * @return the directory where the work tree should be created
     */
    @Nonnull
    public Optional<Path> getTargetDir() {
        return ofNullable(targetDir);
    }

    public static class Builder extends AbstractBuilder<Builder, WorkTreeParameters> {

        private final ImmutableSet.Builder<Repository> alternates = ImmutableSet.builder();
        private final String commit;
        private final Repository repository;

        private String branch;
        private boolean pristine;
        private Path targetDir;

        public Builder(@Nonnull EditFileParameters parameters) {
            super(parameters);

            repository = parameters.getRepository();

            Branch requestBranch = parameters.getBranch();
            if (requestBranch == null) {
                branch = GitRefPattern.HEADS.unqualify(parameters.getTargetBranch());
                commit = null;
            } else {
                branch = requestBranch.getId();
                commit = requestBranch.getLatestCommit();
            }
        }

        public Builder(@Nonnull MergeParameters parameters) {
            super(parameters);

            branch = parameters.getTo().getId();
            commit = parameters.getTo().getLatestCommit();
            repository = parameters.getRepository();

            if (parameters.isCrossRepository()) { // is cross repository
                alternates.add(parameters.getFromRepository());
            }
        }

        public Builder(@Nonnull RebaseParameters parameters) {
            super(parameters);

            branch = parameters.getBranch().getId();
            commit = parameters.getUpstream(); //Rebase will want to switch to "upstream" anyway, so check it out directly
            pristine = true;
            repository = parameters.getRepository();

            if (parameters.isCrossRepository()) {
                alternates.add(parameters.getUpstreamRepository());
            }
        }

        public Builder(@Nonnull Repository repository, @Nullable String commit) {
            this.repository = requireNonNull(repository, "repository");
            this.commit = commit;
        }

        public Builder(@Nonnull WorkTreeParameters parameters) {
            super(parameters);

            alternates.addAll(parameters.getAlternates());
            branch = parameters.getBranch().orElse(null);
            commit = parameters.getCommit().orElse(null);
            pristine = parameters.isPristine();
            repository = parameters.getRepository();
        }

        @Nonnull
        public Builder alternate(@Nonnull Repository value) {
            alternates.add(value);

            return self();
        }

        @Nonnull
        public Builder alternates(@Nonnull Iterable<Repository> values) {
            alternates.addAll(values);

            return self();
        }

        @Nonnull
        public Builder alternates(@Nonnull Repository value, @Nonnull Repository... values) {
            alternates.add(value).add(values);

            return self();
        }

        @Nonnull
        public Builder branch(@Nullable String value) {
            branch = value;

            return self();
        }

        @Nonnull
        @Override
        public WorkTreeParameters build() {
            return new WorkTreeParameters(this);
        }

        /**
         * @param value {@code true} to request a pristine working copy; otherwise, {@code false} to allow unstaged
         *              changes
         * @return {@code this}
         */
        @Nonnull
        public Builder pristine(boolean value) {
            pristine = value;

            return self();
        }

        /**
         * @param value directory for the work tree to be created in, or null to use the default
         * @return {@code this}
         */
        @Nonnull
        public Builder targetDir(@Nullable Path value) {
            targetDir = value;

            return self();
        }

        @Nonnull
        @Override
        protected Builder self() {
            return this;
        }
    }
}
