package com.atlassian.bitbucket.mesh.util;

import com.atlassian.bitbucket.mesh.git.hosting.GitPacketListener;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;

import static java.util.Objects.requireNonNull;

public class PackfileListener implements GitPacketListener {

    private final Predicate<byte[]> isInPackSection;
    private final Consumer<String> responseConsumer;
    private final BiConsumer<ByteBuffer, Boolean> stderrConsumer;
    private final OutputStream writer;

    private boolean inPackSection;

    public PackfileListener(@Nonnull OutputStream writer, @Nonnull Consumer<String> responseConsumer,
                            @Nonnull BiConsumer<ByteBuffer, Boolean> stderrConsumer) {
        this(writer, responseConsumer, stderrConsumer, PackfileListener::isPackSignature);
    }

    //Allow the caller to provide a predicate for determining when the pack section has started (for example
    //to handle upload archive responses, where a flush packet indicates that the pack is coming next, rather
    //than a pack signature)
    public PackfileListener(@Nonnull OutputStream writer, @Nonnull Consumer<String> responseConsumer,
                            @Nonnull BiConsumer<ByteBuffer, Boolean> stderrConsumer,
                            @Nonnull Predicate<byte[]> isInPackSection) {
        this.isInPackSection = requireNonNull(isInPackSection, "isInPack");
        this.responseConsumer = requireNonNull(responseConsumer, "responseConsumer");
        this.stderrConsumer = requireNonNull(stderrConsumer, "stderrConsumer");
        this.writer = requireNonNull(writer, "writer");
    }

    @Override
    public void onPacket(@Nonnull byte[] packet) {
        if (!inPackSection && isInPackSection.test(packet)) {
            inPackSection = true;
        }
        if (inPackSection && packet.length > 4) {
            //The first byte is the stream identifier, which should be 1 for stdout. Note that
            //this is not part of the pkt-line format; it's just how some commands (such as
            //git upload-pack and git archive) work
            if (packet[4] == (byte) 1) {
                try {
                    //Drop the packet length/stream ID and write the rest
                    writer.write(packet, 5, packet.length - 5);
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            } else {
                //A packet of stderr. Drop the packet length/stream ID and forward
                //it to the stderrConsumer
                stderrConsumer.accept(ByteBuffer.wrap(packet, 5, packet.length - 5), false);
            }
        } else {
            //Capture these parts of the response (anything prior to the flush packet) for validation
            responseConsumer.accept(new String(packet));
        }
    }

    private static boolean isPackSignature(@Nonnull byte[] packet) {
        //Pack files start with the PACK signature. We expect to see a pkt-line for stdout (1) that starts with PACK
        return packet.length > 8 &&
               packet[4] == 1 && packet[5] == 'P' && packet[6] == 'A' && packet[7] == 'C' && packet[8] == 'K';
    }
}
