package com.atlassian.stash.internal.hook;

import com.google.common.annotations.VisibleForTesting;

import java.io.*;
import java.nio.charset.StandardCharsets;

import static com.google.common.base.Preconditions.checkState;

/**
 * An abstraction of the Stash <em>SCM hook input protocol</em>.
 * <p>
 * Incoming data over the socket is expected as a series of chunks in the following format:
 * <code>[ASCII char][unsigned short][UTF8 string]</code>
 * and ends with a terminating <code>E</code> UTF8 char. The meaning of each part of the chunk is:
 * <dl>
 *     <dt>ASCII char</dt>
 *     <dd>The <em>channel</em> of the data. Can be either the process id (X), the hook type (T), an argument (A), or standard input (I)</dd>
 *     <dt>unsigned short</dt>
 *     <dd>The length of the UTF8 string to be sent.</dd>
 *     <dt>UTF8 string</dt>
 *     <dd>The data</dd>
 * </dl>
 */
public class SocketTransferInput implements Closeable {

    private final DataInputStream in;

    public SocketTransferInput(InputStream in) {
        this.in = new DataInputStream(in);
    }

    @VisibleForTesting
    char readAsciiChar() throws IOException {
        byte b = (byte) in.read();
        if (b == -1) {
            throw new EOFException();
        }
        int c = b & 0xff;
        checkState(c <= 127, "Only ASCII characters are supported");
        return (char) c;
    }

    public Chunk readChunk() throws IOException {
        ChunkType type = ChunkType.forChar(readAsciiChar());
        return new Chunk(type, type == ChunkType.END ? null : readString());
    }

    public String readRequestId() throws IOException {
        Chunk chunk = readChunk();
        checkState(chunk.getType() == ChunkType.REQUEST_ID);
        return chunk.getValue();
    }

    public String readHookType() throws IOException {
        Chunk chunk = readChunk();
        checkState(chunk.getType() == ChunkType.HOOK_TYPE);
        return chunk.getValue();
    }

    @Override
    public void close() throws IOException {
        in.close();
    }

    private String readString() throws IOException {
        // Note that we cannot use readUTF here because it reads "modified UTF-8", which is different from UTF-8
        int len = in.readUnsignedShort();
        byte[] buffer = new byte[len];
        in.readFully(buffer);
        return new String(buffer, StandardCharsets.UTF_8);
    }

    public static class Chunk {

        private final ChunkType type;
        private final String value;

        public Chunk(ChunkType type, String value) {
            this.type = type;
            this.value = value;
        }

        public ChunkType getType() {
            return type;
        }

        public String getValue() {
            return value;
        }
    }

    public enum ChunkType {
        REQUEST_ID('X'),
        HOOK_TYPE('T'),
        ARG('A'),
        STDIN('I'),
        END('E');

        private final char character;

        ChunkType(char character) {
            this.character = character;
        }

        public char getCharacter() {
            return character;

        }

        public static ChunkType forChar(char c) {
            for (ChunkType type : values()) {
                if (type.getCharacter() == c) {
                    return type;
                }
            }
            throw new IllegalArgumentException(String.format("Unknown channel %s", c));
        }
    }
}
