package com.atlassian.bitbucket.commit;

import com.atlassian.bitbucket.idx.CommitIndex;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.util.BuilderSupport;
import com.google.common.collect.ImmutableSet;

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

import static java.util.Objects.requireNonNull;

/**
 * A base class for requests to retrieve a series of commits.
 *
 * @since 4.6
 */
public abstract class AbstractCommitsRequest {

    /**
     * A symbolic constant for {@link #getMaxMessageLength() max message length} which means the full commit
     * message should be read regardless of length.
     */
    public static final int UNLIMITED_MESSAGE_LENGTH = -1;

    private final boolean followRenames;
    private final Boolean ignoringMissing;
    private final int maxMessageLength;
    private final CommitListMergeFilter merges;
    private final Set<String> paths;
    private final Set<String> propertyKeys;
    private final Repository repository;

    protected AbstractCommitsRequest(AbstractCommitsRequestBuilder<?> builder) {
        followRenames = builder.followRenames;
        ignoringMissing = builder.ignoringMissing;
        maxMessageLength = builder.maxMessageLength;
        merges = builder.merges;
        paths = builder.paths.build();
        propertyKeys = builder.propertyKeys.build();
        repository = builder.repository;

        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");
            }
        }
    }

    /**
     * @return the max message length to be processed for any of the commits, which may be
     *         {@link #UNLIMITED_MESSAGE_LENGTH} to include the full commit message regardless of length
     */
    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;
    }

    /**
     * Retrieves paths, which may identify directories or files within the repository, which are used to limit
     * returned commits. When multiple paths are provided, commits which modify <i>any</i> of the specified paths
     * may be returned.
     *
     * @return a set containing zero or more paths
     */
    @Nonnull
    public Set<String> getPaths() {
        return paths;
    }

    /**
     * Retrieves the keys of {@link CommitIndex index} properties to load for returned commits.
     *
     * @return a set containing zero or more commit attributes to load
     * @see CommitIndex#getProperties(String, Iterable)
     * @see CommitIndex#getProperties(Iterable, Iterable)
     */
    @Nonnull
    public Set<String> getPropertyKeys() {
        return propertyKeys;
    }

    /**
     * @return the repository the commits should be loaded from
     */
    @Nonnull
    public Repository getRepository() {
        return repository;
    }

    /**
     * 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
     */
    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.
     * @deprecated in 4.6. This return type will change to {@code boolean} in 5.0.
     */
    @Nullable
    @Deprecated
    public Boolean isIgnoringMissing() {
        return ignoringMissing;
    }

    public abstract static class AbstractCommitsRequestBuilder<B extends AbstractCommitsRequestBuilder<B>>
            extends BuilderSupport {

        private final ImmutableSet.Builder<String> paths;
        private final ImmutableSet.Builder<String> propertyKeys;
        private final Repository repository;

        private boolean followRenames;
        private Boolean ignoringMissing;
        private int maxMessageLength;
        private CommitListMergeFilter merges;

        protected AbstractCommitsRequestBuilder(@Nonnull Repository repository) {
            this.repository = requireNonNull(repository, "repository");

            maxMessageLength = UNLIMITED_MESSAGE_LENGTH;
            merges = CommitListMergeFilter.INCLUDE;
            paths = ImmutableSet.builder();
            propertyKeys = ImmutableSet.builder();
        }

        @SuppressWarnings("deprecation")
        protected AbstractCommitsRequestBuilder(@Nonnull AbstractCommitsRequest request) {
            this(requireNonNull(request, "request").getRepository());

            followRenames(request.isFollowingRenames())
                    .ignoreMissing(request.isIgnoringMissing())
                    .merges(request.getMerges())
                    .paths(request.getPaths())
                    .propertyKeys(request.getPropertyKeys());
        }

        @Nonnull
        public B followRenames(boolean value) {
            followRenames = value;

            return self();
        }

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

            return self();
        }

        @Nonnull
        public B maxMessageLength(int value) {
            maxMessageLength = value;

            return self();
        }

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

            return self();
        }

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

            return self();
        }

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

            return self();
        }

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

            return self();
        }

        @Nonnull
        public B propertyKey(@Nullable String value) {
            addIf(NOT_BLANK, propertyKeys, value);

            return self();
        }

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

            return self();
        }

        @Nonnull
        public B propertyKeys(@Nullable String value, @Nullable String... values) {
            addIf(NOT_BLANK, propertyKeys, value, values);

            return self();
        }

        protected abstract B self();
    }
}
