package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.git.ref.RefOrder;
import com.google.common.collect.ImmutableList;
import org.apache.commons.lang3.StringUtils;

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

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

public enum GitRefPattern {

    /**
     * Ref pattern denoting local branches.
     */
    HEADS("refs", "heads") {

        @Nonnull
        @Override
        public String sort(@Nullable RefOrder order) {
            if (order == RefOrder.ALPHABETICAL) {
                //Paranoid: Apply alphabetical sort just in case git changes the default order at some point
                return defaultSort();
            }
            return "-committerdate"; //Sort by descending (leading -) committer date
        }
    },

    /**
     * Ref pattern denoting open pull requests.
     */
    PULL_REQUESTS("refs", "pull-requests"),
    /**
     * Ref pattern denoting remote branches (agnostic of remote name).
     */
    REMOTES("refs", "remotes"),
    /**
     * Ref pattern denoting tags.
     */
    TAGS("refs", "tags") {

        @Nonnull
        @Override
        public String sort(@Nullable RefOrder order) {
            if (order == RefOrder.ALPHABETICAL) {
                //Paranoid: Apply alphabetical sort just in case git changes the default order at some point
                return defaultSort();
            }

            //for-each-ref exposes 4 date fields:
            //1. authordate
            //2. committerdate
            //3. taggerdate
            //4. creatordate
            //authordate and committerdate come in two flavours, with or without a leading *
            //
            //For lightweight tags (the default tag created by git), the tag is a direct pointer to the tagged
            //object. As a result, its authordate and committerdate values are those of the underlying object,
            //and *authordate *committerdate have no values. Because they are direct pointers, they have no data
            //of their own; this in turn means they have no taggerdate.
            //
            //For signed and annotated tags, the tag is its own object. As a result, its authordate and
            //committerdate have no values, but *authordate and *committerdate return the values from
            //the tagged object. Because they are their own object, they have a taggerdate.
            //
            //creatordate is the only common field across all three tag patterns. So in order to get a reliable
            //modified ordering across all patterns, that's the field we use.
            return "-creatordate";
        }
    };

    private final String[] components;
    private final String path;

    GitRefPattern(String... components) {
        this.components = components;

        StringBuilder builder = new StringBuilder(50);
        for (String component : components) {
            builder.append(component).append("/");
        }
        path = builder.toString();
    }

    /**
     * Retrieves the components that make up this pattern's {@link #getPath() path}.
     *
     * @return the path components for this pattern
     */
    @Nonnull
    public String[] getComponents() {
        return components.clone();
    }

    /**
     * Retrieves the path under the {@code .git} directory containing all refs for this pattern.
     * <p>
     * Note: Paths returned from this method will <i>always</i> end with a trailing {@code /}.
     *
     * @return the ref path for this pattern
     */
    @Nonnull
    public String getPath() {
        return path;
    }

    /**
     * Retrieves a <i>non-force</i> refspec for this pattern. The refspec is structured so that all refs that match
     * this pattern on the remote will update refs of the same pattern locally. For example, for {@link #HEADS}, the
     * refspec will be {@code refs/heads/*:refs/heads/*}.
     *
     * @return a refspec for refs matching this pattern
     */
    @Nonnull
    public String getRefspec() {
        return getRefspec(false);
    }

    /**
     * Retrieves a refspec for this pattern. optionally including a leading {@code +} to allow forced updates. The
     * refspec is structured so that all refs that match this pattern on the remote will update refs of the same
     * pattern locally. For example, for {@link #HEADS}, the refspec will be {@code +refs/heads/*:refs/heads/*} if
     * {@code force} is {@code true}, and {@code refs/heads/*:refs/heads/*} otherwise.
     *
     * @param force true for a forced update
     * @return a refspec for refs matching this pattern, optionally allowing forced updates
     */
    @Nonnull
    public String getRefspec(boolean force) {
        String refspec = path + "*";

        StringBuilder builder = new StringBuilder();
        if (force) {
            builder.append("+");
        }
        builder.append(refspec)
                .append(":")
                .append(refspec);

        return builder.toString();
    }

    /**
     * Applies the git precedence ordering for the provided ref, constrained by this pattern, to produce an ordered list
     * of search candidates. That means that where the git precedence would search across branches and tags and remotes,
     * the returned list only searches within a given type, such as tags, but it contains paths under the {@code .git}
     * directory in the same order {@code git} would search them.
     *
     * @param ref the ref for which to build a search order
     * @return a list of possible locations for matching the provided ref, in order of their precedence in git
     * @throws IllegalArgumentException if the provided {@code ref} is empty or blank
     * @throws NullPointerException if the provided {@code ref} is {@code null}
     */
    @Nonnull
    public List<String> getSearchOrder(@Nonnull String ref) {
        String qualified = qualify(ref);

        ImmutableList.Builder<String> builder = ImmutableList.builder();
        builder.add(qualified);

        String prefixed = path + ref;
        if (!qualified.equals(prefixed)) {
            builder.add(prefixed);
        }

        return builder.build();
    }

    /**
     * Retrieves a flag that may be provided to the {@code git rev-list --sort=} option to sort refs in the specified
     * {@code order}.
     *
     * @param order the order to apply to refs
     * @return a {@code git rev-list --sort=} flag for producing the specified order
     */
    public String sort(@Nullable RefOrder order) {
        return defaultSort();
    }

    /**
     * Qualifies the provided {@code ref} according this pattern. The returned value is suitable for use as a ref's ID.
     * <p>
     * Note: The implementation of this method <i>attempts</i> to detect when a fully- or partially-qualified ref is
     * passed and further qualify the ref as appropriate. However, this may result in incorrect output when qualifying
     * a ref that <i>looks</i> fully- or partially-qualified but actually <i>isn't</i>. For example, qualifying a ref
     * like {@code "refs/heads/master"} will produce the same string, from the {@link #HEADS} pattern, which is wrong
     * if the branch's <i>display name</i> was actually {@code "refs/heads/master"}, meaning the qualified ref is
     * actually {@code "refs/heads/refs/heads/master"}.
     *
     * @param ref the ref to qualify, which may be fully- or partially-qualified
     * @return a fully qualified ref according to this pattern
     * @throws IllegalArgumentException if the provided {@code ref} is empty or blank
     * @throws NullPointerException if the provided {@code ref} is {@code null}
     */
    @Nonnull
    public String qualify(@Nonnull String ref) {
        checkArgument(StringUtils.isNotBlank(requireNonNull(ref, "ref")), "No ref was provided to qualify");

        //Given a pattern of "refs/heads/", we have 3 cases to consider for the provided ref:
        //1. "master"
        //2. "heads/master"
        //3. "refs/heads/master"
        //All of these should result in the same qualified ref, "refs/heads/master". Note that we intentionally choose
        //not to handle the case where we receive something like "refs/tags/v1.0.0" or "tags/v1.0.0". We're not trying
        //to prevent misuse that deliberate.
        if (ref.indexOf('/') > 0) {
            //Search for overlap between the path and the provided ref
            int index = 0;
            do {
                if (ref.startsWith(path.substring(index))) {
                    return path.substring(0, index) + ref;
                }

                index = path.indexOf('/', index) + 1;
            } while (index < path.length());

            //If we make it here there's no overlap, meaning we have a ref like "somethingOtherThanHeads/something".
            //That means the path should be refs/heads/<ref>, so we fall out of the if to the default logic.
        }

        //If there are no slashes in the ref, that's the simplest case. Direct prepend and done. We're optimised for
        //this case because it's going to be overwhelmingly the most common.
        return path + ref;
    }

    /**
     * Unqualifies the provided {@code ref} according to this pattern. The returned value is suitable for use as a
     * ref's display ID.
     * <p>
     * This process is the logical negation of {@link #qualify(String) qualifying a ref}. It removes the path for
     * this pattern from the provided {@code ref} if it is present. If the provided ref is partially-qualified,
     * the partial qualification is removed.
     * <p>
     * Examples, using the {@link #HEADS} pattern:
     * <ul>
     *     <li>{@code "refs/heads/master"} -&gt; {@code "master"}</li>
     *     <li>{@code "heads/master"} -&gt; {@code "master"}</li>
     *     <li>{@code "master"} -&gt; {@code "master"}</li>
     *     <li>{@code "refs/tags/some-tag"} -&gt; {@code "refs/tags/some-tag"}</li>
     * </ul>
     * Note that, in the second example, the partial qualifier "heads" was removed. Also note that in the last example,
     * which is a qualified {@link #TAGS tag}, the provided {@code ref} is returned unchanged.
     * <p>
     * As with {@link #qualify(String) qualifying a ref}, unqualifying a ref may produce incorrect results when provided
     * a ref that <i>looks</i> fully- or partially-qualified but actually <i>isn't</i>. For example, if the head in the
     * second example above was actually {@code "heads/master"} (meaning its fully-qualified name would be
     * {@code "refs/heads/heads/master"}), the unqualified name {@code "master"} would be incorrect.
     *
     * @param ref the ref to unqualify, which may be fully- or partially-qualified
     * @return an unqualified ref according to this pattern
     * @throws IllegalArgumentException if the provided {@code ref} is empty or blank
     * @throws NullPointerException if the provided {@code ref} is {@code null}
     */
    @Nonnull
    public String unqualify(@Nonnull String ref) {
        checkArgument(StringUtils.isNotBlank(requireNonNull(ref, "ref")), "No ref was provided to unqualify");

        if (ref.indexOf('/') > 0) {
            int index = 0;
            do {
                if (ref.startsWith(path.substring(index))) {
                    return ref.substring(path.length() - index);
                }

                index = path.indexOf('/', index) + 1;
            } while (index < path.length());
        }

        //If the ref contains no slashes, there's clearly nothing to unqualify
        return ref;
    }

    /**
     * Retrieves the default sort flag, to be applied to refs when no more-specific sort is desired (or makes sense).
     *
     * @return the default sort
     */
    protected String defaultSort() {
        return "refname";
    }
}