package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.config.ProcessConfig;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderFactory;
import com.atlassian.bitbucket.mesh.git.builder.GitProcessBuilderSupport;
import com.atlassian.bitbucket.mesh.git.builder.archive.GitArchiveFormat;
import com.atlassian.bitbucket.mesh.git.builder.blame.GitBlame;
import com.atlassian.bitbucket.mesh.git.builder.blame.GitBlameBuilder;
import com.atlassian.bitbucket.mesh.git.builder.diff.GitDiffBuilder;
import com.atlassian.bitbucket.mesh.git.builder.diff.GitDiffOutput;
import com.atlassian.bitbucket.mesh.git.builder.log.GitLogBuilder;
import com.atlassian.bitbucket.mesh.git.builder.revlist.FragmentRevListStdioHandler;
import com.atlassian.bitbucket.mesh.git.exception.InvalidArgumentException;
import com.atlassian.bitbucket.mesh.rpc.v1.*;
import com.google.protobuf.ByteString;
import io.grpc.stub.StreamObserver;

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

import static com.atlassian.bitbucket.mesh.rpc.util.ByteStringUtils.*;
import static com.atlassian.bitbucket.mesh.rpc.v1.RpcBlameType.PORCELAIN;

public class GrpcPorcelainService extends PorcelainServiceGrpc.PorcelainServiceImplBase {

    private final GitProcessBuilderFactory builderFactory;
    private final ProcessConfig processConfig;

    public GrpcPorcelainService(GitProcessBuilderFactory builderFactory, ProcessConfig processConfig) {
        this.builderFactory = builderFactory;
        this.processConfig = processConfig;
    }

    @Override
    public void archive(RpcArchiveRequest request, StreamObserver<RpcStreamFragment> responseObserver) {
        builderFactory.builder(request).archive()
                .format(GitArchiveFormat.fromRpc(request.getFormat()))
                .paths(toStringsLazily(request.getPathsList()))
                .prefix(emptyToNull(request.getPrefix()))
                .rev(request.getRev().toStringUtf8())
                .build(new FragmentProcessStdioHandler<>(responseObserver, RpcStreamFragmentFactory.INSTANCE))
                .start();
    }

    @Override
    public void blame(RpcBlameRequest request, StreamObserver<RpcStreamFragment> responseObserver) {
        GitBlame blame = builderFactory.builder(request).blame();

        GitBlameBuilder builder = request.getType() == PORCELAIN ? blame.porcelain() : blame.incremental();
        builder.file(request.getPath().toStringUtf8())
                .rev(request.getRev().toStringUtf8());

        RpcBlameRange range = request.getRange();
        if (range.getEndOrOffsetCase() == RpcBlameRange.EndOrOffsetCase.OFFSET) {
            builder.range(range.getStart(), range.getOffset(), true);
        } else {
            builder.range(range.getStart(), range.getEnd(), false);
        }

        builder.build(new FragmentProcessStdioHandler<>(responseObserver, RpcStreamFragmentFactory.INSTANCE))
                .start();
    }

    @Override
    public void diff(RpcDiffRequest request, StreamObserver<RpcStreamFragment> responseObserver) {
        GitDiffBuilder builder = builderFactory.builder(request).diff()
                .ancestor(emptyToNull(request.getAncestor()))
                .paths(toStringsLazily(request.getPathsList()))
                .rev(request.getRev().toStringUtf8());
        //Apply diff options, if any
        if (request.hasDiffOptions()) {
            builder.applyOptions(request.getDiffOptions());
        }

        builder.build(new FragmentProcessStdioHandler<>(responseObserver, RpcStreamFragmentFactory.INSTANCE))
                .start();
    }

    @Override
    public void formatPatch(RpcFormatPatchRequest request, StreamObserver<RpcStreamFragment> responseObserver) {
        builderFactory.builder(request).formatPatch()
                .allAncestors(request.getAllAncestors())
                .ancestor(emptyToNull(request.getAncestor()))
                .rev(request.getRev().toStringUtf8())
                .build(new FragmentProcessStdioHandler<>(responseObserver, RpcStreamFragmentFactory.INSTANCE))
                .start();
    }

    @Override
    public void log(RpcLogRequest request, StreamObserver<RpcStreamFragment> responseObserver) {
        GitLogBuilder builder = builderFactory.builder(request).log()
                .follow(request.getFollow())
                .output(GitDiffOutput.fromRpc(request.getDiffOutput()));

        if (request.hasLogOptions()) {
            RpcRevListOptions options = request.getLogOptions();
            builder.applyOptions(options);

            List<ByteString> rpcRevs = options.getRevsList();
            if (!rpcRevs.isEmpty() && computeTotalLength(options) > processConfig.getCommandLineSize()) {
                List<String> revs = toStrings(rpcRevs);

                //If the paths+revs are too long to fit on the command line, drop the revs from the arguments
                //and pass them using --stdin instead
                builder.clearRevs()
                        .stdin(true)
                        //Always pass at least one rev on the command line. This works around a bug in git log
                        //where, when --ignore-missing is used with --stdin, if all the revs are missing, log
                        //incorrectly defaults to including HEAD
                        .rev(revs.get(0))
                        .build(new FragmentRevListStdioHandler(responseObserver, revs))
                        .start();
                return;
            }
        }

        builder.build(new FragmentProcessStdioHandler<>(responseObserver, RpcStreamFragmentFactory.INSTANCE))
                .start();
    }

    @Override
    public StreamObserver<RpcLogStdinFragment> logStdin(StreamObserver<RpcStreamFragment> responseObserver) {
        return new BidirectionalFragmentRequestObserver<>(responseObserver,
                new AbstractStreamFragmentHandler<RpcLogStdinFragment>() {

                    @Nonnull
                    @Override
                    public GitProcessBuilderSupport<?> createBuilder(@Nonnull RpcLogStdinFragment fragment) {
                        requireCase(fragment.getRequestOneofCase(), RpcLogStdinFragment.RequestOneofCase.REQUEST);

                        RpcLogRequest request = fragment.getRequest();

                        GitLogBuilder builder = builderFactory.builder(request).log()
                                .follow(request.getFollow())
                                .output(GitDiffOutput.fromRpc(request.getDiffOutput()));

                        if (request.hasLogOptions()) {
                            builder.applyOptions(request.getLogOptions());
                        }

                        return builder.stdin(true);
                    }

                    @Nonnull
                    @Override
                    public RpcStreamChunk onFragment(@Nonnull RpcLogStdinFragment fragment) {
                        requireCase(fragment.getRequestOneofCase(), RpcLogStdinFragment.RequestOneofCase.STDIN);

                        return fragment.getStdin();
                    }
                });
    }

    static int computeTotalLength(RpcRevListOptions options) {
        return computeTotalLength(options.getPathsList()) + computeTotalLength(options.getRevsList());
    }

    private static int computeTotalLength(List<ByteString> arguments) {
        if (arguments.isEmpty()) {
            return 0;
        }

        return arguments.size() + arguments.stream().mapToInt(ByteString::size).sum();
    }

    private static <T> void requireCase(T actual, T expected) {
        if (actual != expected) {
            throw new InvalidArgumentException("Received [" + actual + "] where [" + expected + "] was expected");
        }
    }
}
