package htsjdk.samtools.cram.io;

import htsjdk.samtools.util.RuntimeEOFException;
import htsjdk.samtools.util.RuntimeIOException;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Methods to read and write LTF8 as per CRAM specs.
 *
 * LTF8 encodes longs as 1 to 9 bytes depending on the highest set bit.  See {@link ITF8} for more details.
 */
public class LTF8 {

    static final int MAX_BYTES = 9;
    static final int MAX_BITS = 8 * MAX_BYTES;

    /**
     * Reads an unsigned long value from the input stream. The sign bit should be interpreted just as other bits in the value.
     *
     * @param inputStream input stream to be read from
     * @return value encoded in the stream as LTF8
     */
    public static long readUnsignedLTF8(final InputStream inputStream) {
        try {
            final int b1 = inputStream.read();
            if (b1 == -1)
                throw new RuntimeEOFException();

            if ((b1 & 128) == 0)
                return b1;

            if ((b1 & 64) == 0)
                return ((b1 & 127) << 8) | inputStream.read();

            if ((b1 & 32) == 0) {
                final int b2 = inputStream.read();
                final int b3 = inputStream.read();
                return ((b1 & 63) << 16) | b2 << 8 | b3;
            }

            if ((b1 & 16) == 0) {
                long result = ((long) (b1 & 31) << 24);
                result |= inputStream.read() << 16;
                result |= inputStream.read() << 8;
                result |= inputStream.read();
                return result;
            }

            if ((b1 & 8) == 0) {
                long value = ((long) (b1 & 15) << 32);
                value |= ((0xFF & ((long) inputStream.read())) << 24);
                value |= (inputStream.read() << 16);
                value |= (inputStream.read() << 8);
                value |= inputStream.read();
                return value;
            }

            if ((b1 & 4) == 0) {
                long result = ((long) (b1 & 7) << 40);
                result |= (0xFF & ((long) inputStream.read())) << 32;
                result |= (0xFF & ((long) inputStream.read())) << 24;
                result |= inputStream.read() << 16;
                result |= inputStream.read() << 8;
                result |= inputStream.read();
                return result;
            }

            if ((b1 & 2) == 0) {
                long result = ((long) (b1 & 3) << 48);
                result |= (0xFF & ((long) inputStream.read())) << 40;
                result |= (0xFF & ((long) inputStream.read())) << 32;
                result |= (0xFF & ((long) inputStream.read())) << 24;
                result |= inputStream.read() << 16;
                result |= inputStream.read() << 8;
                result |= inputStream.read();
                return result;
            }

            if ((b1 & 1) == 0) {
                long result = (0xFF & ((long) inputStream.read())) << 48;
                result |= (0xFF & ((long) inputStream.read())) << 40;
                result |= (0xFF & ((long) inputStream.read())) << 32;
                result |= (0xFF & ((long) inputStream.read())) << 24;
                result |= inputStream.read() << 16;
                result |= inputStream.read() << 8;
                result |= inputStream.read();
                return result;
            }

            long result = (0xFF & ((long) inputStream.read())) << 56;
            result |= (0xFF & ((long) inputStream.read())) << 48;
            result |= (0xFF & ((long) inputStream.read())) << 40;
            result |= (0xFF & ((long) inputStream.read())) << 32;
            result |= (0xFF & ((long) inputStream.read())) << 24;
            result |= inputStream.read() << 16;
            result |= inputStream.read() << 8;
            result |= inputStream.read();
            return result;
        } catch (IOException e) {
            throw new RuntimeIOException(e);
        }
    }

    /**
     * Writes an unsigned long value to the output stream. The sign bit is interpreted just as other bits in the value.
     *
     * @param value the value to be written
     * @param outputStream    the output stream to write to
     * @return the number of bits written
     */
    public static int writeUnsignedLTF8(final long value, final OutputStream outputStream) {
        try {
            if ((value >>> 7) == 0) {
                // no control bits
                outputStream.write((int) value);
                return 8;
            }

            if ((value >>> 14) == 0) {
                // one control bit
                outputStream.write((int) ((value >> 8) | 0x80));
                outputStream.write((int) (value & 0xFF));
                return 16;
            }

            if ((value >>> 21) == 0) {
                // two control bits
                outputStream.write((int) ((value >> 16) | 0xC0));
                outputStream.write((int) ((value >> 8) & 0xFF));
                outputStream.write((int) (value & 0xFF));
                return 24;
            }

            if ((value >>> 28) == 0) {
                // three control bits
                outputStream.write((int) ((value >> 24) | 0xE0));
                outputStream.write((int) ((value >> 16) & 0xFF));
                outputStream.write((int) ((value >> 8) & 0xFF));
                outputStream.write((int) (value & 0xFF));
                return 32;
            }

            if ((value >>> 35) == 0) {
                // four control bits
                outputStream.write((int) ((value >> 32) | 0xF0));
                outputStream.write((int) ((value >> 24) & 0xFF));
                outputStream.write((int) ((value >> 16) & 0xFF));
                outputStream.write((int) ((value >> 8) & 0xFF));
                outputStream.write((int) (value & 0xFF));
                return 40;
            }

            if ((value >>> 42) == 0) {
                // five control bits
                outputStream.write((int) ((value >> 40) | 0xF8));
                outputStream.write((int) ((value >> 32) & 0xFF));
                outputStream.write((int) ((value >> 24) & 0xFF));
                outputStream.write((int) ((value >> 16) & 0xFF));
                outputStream.write((int) ((value >> 8) & 0xFF));
                outputStream.write((int) (value & 0xFF));
                return 48;
            }

            if ((value >>> 49) == 0) {
                // six control bits
                outputStream.write((int) ((value >> 48) | 0xFC));
                outputStream.write((int) ((value >> 40) & 0xFF));
                outputStream.write((int) ((value >> 32) & 0xFF));
                outputStream.write((int) ((value >> 24) & 0xFF));
                outputStream.write((int) ((value >> 16) & 0xFF));
                outputStream.write((int) ((value >> 8) & 0xFF));
                outputStream.write((int) (value & 0xFF));
                return 56;
            }

            if ((value >>> 56) == 0) {
                // seven control bits
                outputStream.write(0xFE);
                outputStream.write((int) ((value >> 48) & 0xFF));
                outputStream.write((int) ((value >> 40) & 0xFF));
                outputStream.write((int) ((value >> 32) & 0xFF));
                outputStream.write((int) ((value >> 24) & 0xFF));
                outputStream.write((int) ((value >> 16) & 0xFF));
                outputStream.write((int) ((value >> 8) & 0xFF));
                outputStream.write((int) (value & 0xFF));
                return 64;
            }

            // eight control bits
            outputStream.write((0xFF));
            outputStream.write((int) ((value >> 56) & 0xFF));
            outputStream.write((int) ((value >> 48) & 0xFF));
            outputStream.write((int) ((value >> 40) & 0xFF));
            outputStream.write((int) ((value >> 32) & 0xFF));
            outputStream.write((int) ((value >> 28) & 0xFF));
            outputStream.write((int) ((value >> 16) & 0xFF));
            outputStream.write((int) ((value >> 8) & 0xFF));
            outputStream.write((int) (value & 0xFF));
            return 72;
        } catch (IOException e) {
            throw new RuntimeIOException(e);
        }
    }
}
