package htsjdk.samtools.cram.encoding.core;

import htsjdk.HtsjdkTest;
import htsjdk.samtools.cram.encoding.CRAMCodec;
import htsjdk.samtools.cram.io.BitOutputStream;
import htsjdk.samtools.cram.io.DefaultBitInputStream;
import htsjdk.samtools.cram.io.DefaultBitOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class SubexponentialIntegerCodecTest extends HtsjdkTest {

    private void testCodec(final int offset, final int k, final int[] values, final byte[] expected)
            throws IOException {
        byte[] writtenOut;
        try (final ByteArrayOutputStream os = new ByteArrayOutputStream();
                final BitOutputStream bos = new DefaultBitOutputStream(os)) {

            final CRAMCodec<Integer> writeCodec = new SubexponentialIntegerCodec(null, bos, offset, k);
            for (final int value : values) {
                writeCodec.write(value);
            }

            bos.flush();
            writtenOut = os.toByteArray();
            Assert.assertEquals(writtenOut, expected);
        }

        final int[] readBack = new int[values.length];
        try (final InputStream is = new ByteArrayInputStream(writtenOut);
                final DefaultBitInputStream dbis = new DefaultBitInputStream(is)) {

            final CRAMCodec<Integer> readCodec = new SubexponentialIntegerCodec(dbis, null, offset, k);
            for (int i = 0; i < values.length; i++) {
                readBack[i] = readCodec.read();
            }
        }

        Assert.assertEquals(readBack, values);
    }

    // test that the offsets result in a non-negative data series
    // and the codec behaves as we expect

    @DataProvider(name = "basicTest")
    public Object[][] basicTestData() {
        return new Object[][] {
            {
                -100,
                0,
                new int[] {100, 101, 102, 104, 107, 108},

                // with k = 0:
                // 0 -> b = 0, u = 0 -> 0
                // 1 -> b = 0, u = 1 -> 10
                // 2 -> b = 1, u = 2 -> 1100
                // 4 -> b = 2, u = 3 -> 111000
                // 7 -> b = 2, u = 3 -> 111011
                // 8 -> b = 3, u = 4 -> 11110000
                // 0101 1001 1100 0111 0111 1110 000 = 59 C7 7E 0
                new byte[] {0x59, (byte) 0xC7, 0x7E, 0}
            },
            {
                10015,
                2,
                new int[] {-10015, -10014, -10013, -10011, -10008, -10007},

                // with k = 2:
                // 0 -> b = 2, u = 0 -> 000
                // 1 -> b = 2, u = 0 -> 001
                // 2 -> b = 2, u = 0 -> 010
                // 4 -> b = 2, u = 1 -> 1000
                // 7 -> b = 2, u = 1 -> 1011
                // 8 -> b = 3, u = 2 -> 110000
                // 0000 0101 0100 0101 1110 000 = 05 45 E0
                new byte[] {0x05, 0x45, (byte) 0xE0}
            },
        };
    }

    @Test(dataProvider = "basicTest")
    public void basicTest(final int offset, final int k, final int[] values, final byte[] expected) throws IOException {
        testCodec(offset, k, values, expected);
    }

    // throw Exceptions when offsets + values are negative

    @DataProvider(name = "negativeTest")
    public Object[][] negativeTestData() {
        // tuples of bitsPerValue and offsets + values which are negative
        return new Object[][] {
            {0, -1},
            {-1, 0},
            {0, -1},
            {-1, 0},
            {-100, 99},
            {99, -100},
        };
    }

    @Test(dataProvider = "negativeTest", expectedExceptions = IllegalArgumentException.class)
    public void negativeTest(final int offset, final int value) throws IOException {
        final int k = 0;

        try (final ByteArrayOutputStream os = new ByteArrayOutputStream();
                final BitOutputStream bos = new DefaultBitOutputStream(os)) {

            final CRAMCodec<Integer> codec = new SubexponentialIntegerCodec(null, bos, offset, k);
            codec.write(value);
        }
    }
}
