package com.atlassian.bitbucket.mesh.git.builder.revlist;

import com.atlassian.bitbucket.mesh.git.builder.AbstractGitProcessBuilder;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilder;
import com.atlassian.bitbucket.mesh.rpc.v1.RpcRevListOptions;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.Nonnull;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import static com.atlassian.bitbucket.mesh.rpc.util.ByteStringUtils.toStringsLazily;
import static java.util.Objects.requireNonNull;

public abstract class AbstractGitRevCoreBuilder<B extends GitRevCoreBuilder<B>>
        extends AbstractGitProcessBuilder<B>
        implements GitRevCoreBuilder<B> {

    protected final List<String> revs;

    protected boolean all;
    protected boolean boundary;
    protected boolean branches;
    protected boolean ignoreMissing;
    protected int skip;
    protected boolean stdin;
    protected boolean tags;

    private final Set<String> files;

    private String format;
    private LeftOrRight leftOrRight;
    private boolean leftRight;
    private int limit;
    private GitRevListOrder order;
    private GitRevListMerges merges;
    private boolean parents;
    private Instant since;
    private GitRevListWalk walk;

    protected AbstractGitRevCoreBuilder(GitProcessBuilder builder) {
        super(builder);

        files = new LinkedHashSet<>();
        leftOrRight = LeftOrRight.DEFAULT;
        order = GitRevListOrder.DEFAULT;
        merges = GitRevListMerges.DEFAULT;
        revs = new ArrayList<>();
        walk = GitRevListWalk.DEFAULT;
    }

    @Nonnull
    @Override
    public B all(boolean value) {
        all = value;

        return self();
    }

    @Nonnull
    @Override
    public B applyOptions(@Nonnull RpcRevListOptions value) {
        requireNonNull(value, "options");

        all(value.getAll())
                .branches(value.getBranches())
                .files(toStringsLazily(value.getPathsList()))
                .format(StringUtils.trimToNull(value.getFormat()))
                .ignoreMissing(value.getIgnoreMissing())
                .limit(value.getLimit())
                .merges(GitRevListMerges.fromRpc(value.getMerges()))
                .order(GitRevListOrder.fromRpc(value.getOrder()))
                .parents(value.getParents())
                .revs(toStringsLazily(value.getRevsList()))
                .skip(value.getSkip())
                .tags(value.getTags())
                .walk(GitRevListWalk.fromRpc(value.getWalk()));
        if (value.getLimit() > 0) {
            limit(value.getLimit());
        }
        if (value.hasSince()) {
            since(Instant.ofEpochSecond(value.getSince()));
        }

        return self();
    }

    @Nonnull
    @Override
    public B boundary(boolean value) {
        boundary = value;

        return self();
    }

    @Nonnull
    @Override
    public B branches(boolean value) {
        branches = value;

        return self();
    }

    @Nonnull
    @Override
    public B clearFiles() {
        files.clear();

        return self();
    }

    @Nonnull
    @Override
    public B clearRevs() {
        revs.clear();

        return self();
    }

    @Nonnull
    @Override
    public B file(String value) {
        addIfNotBlank(files, value);

        return self();
    }

    @Nonnull
    @Override
    public B files(Iterable<String> values) {
        addIfNotBlank(files, values);

        return self();
    }

    @Nonnull
    @Override
    public B files(String value, String... values) {
        addIfNotBlank(files, value, values);

        return self();
    }

    @Nonnull
    @Override
    public B format(String value) {
        format = value;

        return self();
    }

    @Nonnull
    @Override
    public B ignoreMissing(boolean value) {
        ignoreMissing = value;

        return self();
    }

    @Nonnull
    @Override
    public B leftOnly(boolean value) {
        leftOrRight = value ? LeftOrRight.LEFT_ONLY : LeftOrRight.DEFAULT;

        return self();
    }

    @Nonnull
    @Override
    public B leftRight(boolean value) {
        leftRight = value;

        return self();
    }

    @Nonnull
    @Override
    public B limit(int value) {
        limit = value;

        return self();
    }

    @Nonnull
    @Override
    public B merges(@Nonnull GitRevListMerges value) {
        merges = requireNonNull(value, "merges");

        return self();
    }

    @Nonnull
    @Override
    public B order(@Nonnull GitRevListOrder value) {
        order = requireNonNull(value, "order");

        return self();
    }

    @Nonnull
    @Override
    public B parents(boolean value) {
        parents = value;

        return self();
    }

    @Nonnull
    @Override
    public B rev(String value) {
        addIfNotBlank(revs, value);

        return self();
    }

    @Nonnull
    @Override
    public B revs(Iterable<String> values) {
        addIfNotBlank(revs, values);

        return self();
    }

    @Nonnull
    @Override
    public B revs(String value, String... values) {
        addIfNotBlank(revs, value, values);

        return self();
    }

    @Nonnull
    @Override
    public B rightOnly(boolean value) {
        leftOrRight = value ? LeftOrRight.RIGHT_ONLY : LeftOrRight.DEFAULT;

        return self();
    }

    @Nonnull
    @Override
    public B since(Instant value) {
        since = value;

        return self();
    }

    @Nonnull
    @Override
    public B skip(int value) {
        skip = value;

        return self();
    }

    @Nonnull
    @Override
    public B stdin(boolean value) {
        stdin = value;

        return self();
    }

    @Nonnull
    @Override
    public B tags(boolean value) {
        tags = value;

        return self();
    }

    @Nonnull
    @Override
    public B walk(@Nonnull GitRevListWalk value) {
        walk = requireNonNull(value, "walk");

        return self();
    }

    @Override
    protected void applyArguments() {
        applyFlags();
        applyRevs();
        applyFiles();
    }

    protected void applyFiles() {
        //Anecdotal testing seems to indicate always passing the hyphens is safe. To simplify the handling for the
        //commit ID and file, we always include them. Even if we pass neither commit ID nor path, git handles
        //the presence of the double-hyphen appropriately.
        builder.argument("--");
        files.forEach(builder::rawArgument);
    }

    protected void applyFlags() {
        if (all) {
            builder.argument("--all");
        }
        if (boundary) {
            builder.argument("--boundary");
        }
        if (branches) {
            builder.argument("--branches");
        }
        if (StringUtils.isNotBlank(format)) {
            builder.argument("--format=" + format);
        }
        if (ignoreMissing) {
            builder.argument("--ignore-missing");
        }
        if (leftRight) {
            builder.argument("--left-right");
        }
        if (limit > 0) {
            builder.argument("-" + limit);
        }
        if (leftOrRight.isFlagged()) {
            builder.argument(leftOrRight.getFlag());
        }
        if (order.isFlagged()) {
            builder.argument(order.getFlag());
        }
        if (merges.isFlagged()) {
            builder.argument(merges.getFlag());
        }
        if (parents) {
            builder.argument("--parents");
        }
        if (since != null) {
            builder.argument("--since=" + since.getEpochSecond());
        }
        if (skip > 0) {
            builder.argument("--skip=" + skip);
        }
        if (stdin) {
            builder.argument("--stdin");
        }
        if (tags) {
            builder.argument("--tags");
        }
        if (walk.isFlagged()) {
            builder.argument(walk.getFlag());
        }
    }

    protected abstract void applyRevs();

    private enum LeftOrRight {

        DEFAULT,
        LEFT_ONLY("--left-only"),
        RIGHT_ONLY("--right-only");

        private final String flag;

        LeftOrRight() {
            flag = null;
        }

        LeftOrRight(String flag) {
            this.flag = flag;
        }

        public String getFlag() {
            return flag;
        }

        public boolean isFlagged() {
            return flag != null;
        }
    }
}
