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

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

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

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

public abstract class AbstractGitDiffCoreBuilder<B extends GitDiffCoreBuilder<B>>
        extends AbstractGitProcessBuilder<B>
        implements GitDiffCoreBuilder<B> {

    protected final Set<String> paths;

    protected String ancestor;
    protected String context;
    protected String rev;

    private GitDiffColor color;
    private String dstPrefix;
    private boolean fullIndex;
    private GitDiffRenames renames;
    private String srcPrefix;
    private Integer threshold;
    private GitDiffWhitespace whitespace;

    public AbstractGitDiffCoreBuilder(GitProcessBuilder builder) {
        super(builder);

        color = GitDiffColor.DEFAULT;
        paths = new LinkedHashSet<>();
        renames = GitDiffRenames.OFF;
        whitespace = GitDiffWhitespace.SHOW;
    }

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

        return self();
    }

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

        color(GitDiffColor.fromRpc(value.getColor()))
                .dstPrefix(value.hasDstPrefix() ? value.getDstPrefix() : null)
                .srcPrefix(value.hasSrcPrefix() ? value.getSrcPrefix() : null)
                .whitespace(GitDiffWhitespace.fromRpc(value.getWhitespace()));
        if (value.hasContextLines()) {
            contextLines(value.getContextLines());
        }

        GitDiffRenames renames = GitDiffRenames.fromRpc(value.getRenames());
        if (renames.supportsThreshold() && value.hasRenameThreshold()) {
            renames(renames, value.getRenameThreshold());
        } else {
            renames(renames);
        }

        return self();
    }

    @Nonnull
    @Override
    public B clearPaths() {
        paths.clear();

        return self();
    }

    @Nonnull
    @Override
    public B color(@Nonnull GitDiffColor value) {
        color = requireNonNull(value, "color");

        return self();
    }

    @Nonnull
    @Override
    public B contextLines(int value) {
        checkArgument(value >= 0, "Context value must be a non-negative integer");
        context = Integer.toString(value);

        return self();
    }

    @Nonnull
    @Override
    public B defaultContextLines() {
        context = null;

        return self();
    }

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

        return self();
    }

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

        return self();
    }

    @Nonnull
    @Override
    public B path(String value) {
        addIfNotBlank(paths, value);

        return self();
    }

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

        return self();
    }

    @Nonnull
    @Override
    public B paths(Iterable<String> values) {
        if (values != null) {
            addIfNotBlank(paths, values);
        }

        return self();
    }

    @Nonnull
    @Override
    public B renames(@Nonnull GitDiffRenames value) {
        return renames(value, null);
    }

    @Nonnull
    @Override
    public B renames(@Nonnull GitDiffRenames renames, Integer threshold) {
        this.renames = requireNonNull(renames, "renames");
        this.threshold = renames.supportsThreshold() ? checkThreshold(threshold) : null;

        return self();
    }

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

        return self();
    }

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

        return self();
    }

    @Nonnull
    @Override
    public B whitespace(@Nonnull GitDiffWhitespace value) {
        whitespace = requireNonNull(value, "whitespace");

        return self();
    }

    @Override
    protected void applyArguments() {
        //This can't use a simple "isFlagged()" approach because FIND_COPIES_HARDER requires two flags
        if (renames == GitDiffRenames.FIND_COPIES_HARDER) {
            builder.argument(applyThreshold("-C")); //Detect copies
            builder.argument("--find-copies-harder");
        } else if (renames == GitDiffRenames.FIND_COPIES) {
            builder.argument(applyThreshold("-C")); //Detect copies
        } else if (renames == GitDiffRenames.FIND_RENAMES) {
            //Warning: Using -C implies -M. Using both does whichever happens second. So -C -M actually
            //         disables copy detection and only performs rename detection.
            builder.argument(applyThreshold("-M")); //Detect renames
        } else if (renames == GitDiffRenames.OFF) {
            builder.argument("--no-renames");
        }

        if (color.isFlagged()) {
            builder.argument(color.getFlag());
        }
        //Use more distinctive prefixes than a/ and b/
        if (StringUtils.isNotBlank(dstPrefix)) {
            builder.argument("--dst-prefix=" + dstPrefix);
        }
        if (fullIndex) {
            builder.argument("--full-index");
        }
        if (StringUtils.isNotBlank(srcPrefix)) {
            builder.argument("--src-prefix=" + srcPrefix);
        }
        if (whitespace.isFlagged()) {
            builder.argument(whitespace.getFlag());
        }
    }

    protected void applyPaths() {
        builder.argument("--");
        paths.forEach(builder::rawArgument);
    }

    private String applyThreshold(String base) {
        return threshold == null ? base : base + threshold + '%';
    }

    private static Integer checkThreshold(Integer threshold) {
        if (threshold == null) {
            return null;
        }

        // The -M or -C threshold can be specified with or without a trailing '%', and this changes the meaning.
        // With a trailing '%' the threshold is a percentage, between 0 and 100 inclusive.
        int value = threshold; //Just unbox once
        checkArgument(value >= 0 && value <= 100, "The threshold must be a positive integer between 0 and 100");

        return threshold;
    }
}
