package com.atlassian.bitbucket.mesh.git.hosting;

import com.google.common.base.Charsets;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

import static java.util.Objects.requireNonNull;

/**
 * Utility class that can be used for classifying git packets
 */
public class GitPackets {

    public static final String FLUSH_PACKET = "0000";

    private static final byte[] DELIM = "0001".getBytes(StandardCharsets.UTF_8);
    private static final byte[] DONE = "0008done".getBytes(StandardCharsets.UTF_8);
    private static final byte[] DONE_LINE = "0009done\n".getBytes(StandardCharsets.UTF_8);
    private static final byte[] FLUSH = FLUSH_PACKET.getBytes(StandardCharsets.UTF_8);

    private GitPackets() {
        // utility class; should not be instantiated
    }

    @Nonnull
    public static byte[] getDelim() {
        return DELIM.clone();
    }

    @Nonnull
    public static byte[] getFlush() {
        return FLUSH.clone();
    }

    public static boolean isDelim(byte[] packet) {
        return Arrays.equals(DELIM, packet);
    }

    public static boolean isDone(byte[] packet) {
        return packet != null && Arrays.equals(DONE, packet) || Arrays.equals(DONE_LINE, packet);
    }

    public static boolean isFlush(byte[] packet) {
        return packet != null && Arrays.equals(FLUSH, packet);
    }

    /**
     * Reads a git packet from an {@link InputStream}. A packet is defined as 4 bytes containing the packet size, followed
     * by the rest of the packet. For instance 0009done\n is 1 packet. 0009 means that the packet is 9 bytes (including
     * the packet size marker).
     *
     * @param inputStream the inputstream to read from
     * @return a byte[] representing the packet, or {@code null} if the inputstream is depleted.
     * @throws IOException when the underlying inputstream throws an IOException, or when the data in the inputstream
     *         does not match the expected format
     */
    @Nullable
    public static byte[] readPacket(@Nonnull InputStream inputStream) throws IOException {
        byte[] packetSizeBuffer = new byte[4];
        int read = readIntoBuffer(requireNonNull(inputStream, "inputStream"), packetSizeBuffer, 0, 4);
        if (read == -1) {
            // nothing to read
            return null;
        }
        checkBytesRead(4, read);

        int packetSize;
        String packetSizeString = new String(packetSizeBuffer, Charsets.UTF_8);
        try {
            packetSize = Integer.parseInt(packetSizeString, 16);
        } catch (NumberFormatException e) {
            throw new IOException("This does not appear to be a git packet stream; packet size was: " + packetSizeString);
        }

        if (packetSize <= 4) {
            // the FLUSH (0000) and DELIM (0001) packets are special marker packets and should be returned without
            // reading mores bytes
            return packetSizeBuffer;
        }

        byte[] packet = new byte[packetSize];
        System.arraycopy(packetSizeBuffer, 0, packet, 0, 4);
        read = readIntoBuffer(inputStream, packet, 4, packetSize - 4);
        checkBytesRead(packetSize - 4, read);

        return packet;
    }

    @Nonnull
    public static String toLine(byte[] packet) {
        if (packet == null || packet.length < 4) {
            throw new IllegalArgumentException("Git packets need to be at least 4 bytes long");
        }
        return new String(packet, 4, packet.length - 4, StandardCharsets.UTF_8);
    }

    private static void checkBytesRead(int expectedBytesRead, int actualBytesRead) throws IOException {
        if (expectedBytesRead != actualBytesRead) {
            throw new IOException("Premature EOF detected. Expected " + expectedBytesRead + " bytes, but only " +
                    actualBytesRead + " were available");
        }
    }

    /**
     * Reads from the provided {@code inputStream} to fill the {@code buffer} from {@code startIndex} with (@code length)
     * bytes. This method will continue reading from {@code inputStream} until {@code buffer} is full, or until
     * {@code inputStream} is depleted.
     *
     * @param inputStream the inputstream to read from
     * @param buffer the buffer to read into
     * @param startIndex the buffer offset
     * @param length the number of bytes to read
     * @return -1 if no bytes are read, otherwise the number of bytes read. If any value other than {@code length} is
     *         returned, {@code inputStream} is depleted.
     * @throws IOException if {@link InputStream#read() input stream read} fails
     */
    private static int readIntoBuffer(InputStream inputStream, byte[] buffer, int startIndex, int length) throws IOException {
        int offset = startIndex;
        int endIndex = offset + length;
        while (offset < endIndex) {
            int read = inputStream.read(buffer, offset, endIndex - offset);
            if (read == -1) {
                break;
            }
            offset += read;
        }
        return (offset == startIndex) ? -1 : (offset - startIndex);
    }
}
