package com.atlassian.bitbucket.scm;

import com.atlassian.bitbucket.commit.*;
import com.atlassian.bitbucket.repository.Repository;
import com.google.common.collect.ImmutableSet;

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

import static java.util.Objects.requireNonNull;

public class CommitsCommandParameters extends AbstractCommandParameters {

    /**
     * @deprecated in 4.6 for removal in 5.0. Use {@link AbstractCommitsRequest#UNLIMITED_MESSAGE_LENGTH} instead.
     */
    @Deprecated
    @SuppressWarnings("unused")
    public static final int UNLIMITED_MESSAGE_LENGTH = AbstractCommitRequest.UNLIMITED_MESSAGE_LENGTH;

    private final boolean all;
    private final Set<String> excludes;
    private final boolean followRenames;
    private final Boolean ignoringMissing;
    private final Set<String> includes;
    private final int maxMessageLength;
    private final CommitListMergeFilter merges;
    private final Set<String> paths;
    private final Repository secondaryRepository;
    private final boolean traversing;
    private final boolean withMessages;

    private CommitsCommandParameters(Builder builder) {
        this.all = builder.all;
        //If a traversal was not requested, ignore any excludes
        this.excludes = builder.traversing ? builder.excludes.build() : Collections.emptySet();
        this.followRenames = builder.followRenames;
        this.ignoringMissing = builder.ignoringMissing;
        //If "all" was specified, ignore any explicit includes
        this.includes = all ? Collections.emptySet() : builder.includes.build();
        this.maxMessageLength = builder.maxMessageLength;
        this.merges = builder.merges;
        this.paths = builder.paths.build();
        this.secondaryRepository = builder.secondaryRepository;
        this.traversing = builder.traversing;
        this.withMessages = builder.withMessages && maxMessageLength != 0;

        if (followRenames) {
            if (paths.isEmpty()) {
                throw new IllegalStateException("A path must be supplied when attempting to follow renames");
            }
            if (paths.size() > 1) {
                throw new IllegalStateException("Following renames is only possible with a single path");
            }
        }
    }

    @Nonnull
    public Set<String> getExcludes() {
        return excludes;
    }

    @Nonnull
    public Set<String> getIncludes() {
        return includes;
    }

    /**
     * @return the maximum length to read for {@link Commit#getMessage() commit messages}; if set, messages
     *         longer than this will be truncated; -1 indicates unlimited message length
     * @since 4.5
     */
    public int getMaxMessageLength() {
        return maxMessageLength;
    }

    /**
     * Retrieves the strategy being used to filter merge commits.
     *
     * @return how merge commits will be filtered
     * @since 4.8
     * @see CommitListMergeFilter
     */
    @Nonnull
    public CommitListMergeFilter getMerges() {
        return merges;
    }

    @Nonnull
    public Set<String> getPaths() {
        return paths;
    }

    @Nonnull
    public Repository getSecondaryRepository() {
        return secondaryRepository;
    }

    public boolean hasExcludes() {
        return !excludes.isEmpty();
    }

    public boolean hasIncludes() {
        return !includes.isEmpty();
    }

    public boolean hasPaths() {
        return !paths.isEmpty();
    }

    public boolean hasSecondaryRepository() {
        return secondaryRepository != null;
    }

    public boolean isAll() {
        return all;
    }

    /**
     * Whether or not the commit history will attempt to follow renames. This is only applicable for individual file
     * {@link #getPaths() paths}
     *
     * @return {@code true} if renames are followed, {@code false} otherwise
     *
     * @since 4.5
     */
    public boolean isFollowingRenames() {
        return followRenames;
    }

    /**
     * @return whether or not to ignore missing commits, or {@code null} to let the application figure out whether to do so.
     *
     * @since 4.5
     * @deprecated in 4.5. This return type will change to {@code boolean} in 5.0.
     */
    @Nullable
    @Deprecated
    public Boolean isIgnoringMissing() {
        return ignoringMissing;
    }

    public boolean isTraversing() {
        return traversing;
    }

    /**
     * @return {@code true} if the comment messages should be retrieved. {@code false} otherwise.
     */
    public boolean isWithMessages() {
        return withMessages;
    }

    /**
     * Assembles a {@link CommitContext} from these parameters.
     *
     * @return a {@link CommitContext} describing these parameters
     */
    @Nonnull
    public CommitContext toContext() {
        return new CommitContext.Builder().build();
    }

    public static class Builder {

        private final ImmutableSet.Builder<String> excludes;
        private final ImmutableSet.Builder<String> includes;
        private final ImmutableSet.Builder<String> paths;

        private boolean all;
        private boolean followRenames;
        private Boolean ignoringMissing;
        private int maxMessageLength;
        private CommitListMergeFilter merges;
        private Repository secondaryRepository;
        private boolean traversing;
        private boolean withMessages;

        public Builder() {
            excludes = ImmutableSet.builder();
            includes = ImmutableSet.builder();
            maxMessageLength = AbstractCommitRequest.UNLIMITED_MESSAGE_LENGTH;
            merges = CommitListMergeFilter.INCLUDE;
            paths = ImmutableSet.builder();
            traversing = true;
            withMessages = true;
        }

        @SuppressWarnings("deprecation")
        public Builder(@Nonnull CommitsBetweenRequest request) {
            this();

            exclude(requireNonNull(request, "request").getExcludes())
                    .followRenames(request.isFollowingRenames())
                    .ignoreMissing(request.isIgnoringMissing())
                    .include(request.getIncludes())
                    .maxMessageLength(request.getMaxMessageLength())
                    .merges(request.getMerges())
                    .paths(request.getPaths())
                    .secondaryRepository(request.getSecondaryRepository());
        }

        @SuppressWarnings("deprecation")
        public Builder(@Nonnull CommitsRequest request) {
            this();

            followRenames(requireNonNull(request, "request").isFollowingRenames())
                    .ignoreMissing(request.isIgnoringMissing())
                    .include(request.getCommitId())
                    .maxMessageLength(request.getMaxMessageLength())
                    .merges(request.getMerges())
                    .paths(request.getPaths());
        }

        @Nonnull
        public Builder all(boolean value) {
            all = value;

            return this;
        }

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

        @Nonnull
        public Builder exclude(@Nullable Iterable<String> values) {
            addIf(NOT_BLANK, excludes, values);

            return this;
        }

        @Nonnull
        public Builder exclude(@Nullable String value, @Nullable String... values) {
            addIf(NOT_BLANK, excludes, value, values);

            return this;
        }

        /**
         * @since 4.5
         */
        @Nonnull
        public Builder followRenames(boolean value) {
            followRenames = value;

            return this;
        }

        /**
         * @since 4.5
         * @deprecated in 4.5. This parameter type will change to {@code boolean} in 5.0.
         */
        @Nonnull
        @Deprecated
        public Builder ignoreMissing(@Nullable Boolean value) {
            ignoringMissing = value;

            return this;
        }

        @Nonnull
        public Builder include(@Nullable Iterable<String> values) {
            addIf(NOT_BLANK, includes, values);

            return this;
        }

        @Nonnull
        public Builder include(@Nullable String value, @Nullable String... values) {
            addIf(NOT_BLANK, includes, value, values);

            return this;
        }

        /**
         * @since 4.5
         */
        @Nonnull
        public Builder maxMessageLength(int value) {
            maxMessageLength = value;

            return this;
        }

        /**
         * @since 4.8
         */
        @Nonnull
        public Builder merges(@Nonnull CommitListMergeFilter value) {
            merges = requireNonNull(value, "merges");

            return this;
        }

        @Nonnull
        public Builder path(@Nullable String value) {
            addIf(NOT_BLANK, paths, value);

            return this;
        }

        @Nonnull
        public Builder paths(@Nullable Iterable<String> values) {
            addIf(NOT_BLANK, paths, values);

            return this;
        }

        @Nonnull
        public Builder paths(@Nullable String value, @Nullable String... values) {
            addIf(NOT_BLANK, paths, value, values);

            return this;
        }

        @Nonnull
        public Builder secondaryRepository(@Nullable Repository value) {
            secondaryRepository = value;

            return this;
        }

        @Nonnull
        public Builder traverse(boolean value) {
            traversing = value;

            return this;
        }

        /**
         * @param value if commit messages should be included in the command output.
         * @return the updated builder.
         */
        @Nonnull
        public Builder withMessages(boolean value) {
            withMessages = value;

            return this;
        }
    }
}
