package picard.illumina.parser.readers;

import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import picard.PicardException;

import java.io.File;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.Iterator;

public class MMapBackedIteratorFactoryTest {
    public static File TestDataDir = new File("testdata/picard/illumina/readerTests");
    public static File BinFile     = new File(TestDataDir, "binary_passing.bin");

    public static final int FileLength = 51;
    //remember that the MMappedBinaryFileReader assumes little endianness
    public byte[] fileAsBytes(final int start, final int end){
         final int [] bInts = {
                         0x31, 0x22, 0x41, 0x01,    0x45, 0x6E, 0x64, 0x4F,
                         0x66, 0x48, 0x65, 0x61,    0x64, 0x65, 0x72, 0x42,
                         0x6F, 0x64, 0x79, 0x50,    0x61, 0x72, 0x74, 0x6F,
                         0x66, 0x54, 0x68, 0x65,    0x46, 0x69, 0x6C, 0x65,
                         0x37, 0x37, 0x0A, 0x45,    0x6E, 0x64, 0x43, 0x6F,
                         0x6D, 0x6D, 0x75, 0x6E,    0x69, 0x63, 0x61, 0x74,
                         0x69, 0x6F, 0x6E
        };

        final int total = end - start + 1;
        final byte [] bytes = new byte[total];
        for(int i = 0; i < total; i++) {
            bytes[i] = (byte)bInts[start + i];
        }
        return bytes;
    }

    //Note these integers are different from the ones in fileAsBytes->bInts (because we're talking about 4 bytes at a time not one)
    public ByteBuffer headerAsByteBuffer(final int headerSize) {
        final byte [] bytes = fileAsBytes(0, headerSize-1);
        final ByteBuffer bb = ByteBuffer.allocate(bytes.length);
        bb.put(bytes);
        bb.position(0);
        bb.order(ByteOrder.LITTLE_ENDIAN);
        return bb;
    }

    public ByteBuffer fileAsByteBuffer(final int headerSize) {
        final byte [] bytes = fileAsBytes(headerSize, FileLength-1);
        final ByteBuffer bb = ByteBuffer.allocate(bytes.length);
        bb.put(bytes);
        bb.position(0);
        bb.order(ByteOrder.LITTLE_ENDIAN);
        return bb;
    }

    abstract class FileTestDef<T> {
        public final int headerSize;
        public final BinaryFileIterator bbIter;
        public final int numElements;

        public FileTestDef(final int headerSize, final BinaryFileIterator<T> bbIter) {
            this.headerSize  = headerSize;
            this.bbIter = bbIter;
            this.numElements = fileAsBytes(headerSize, FileLength-1).length / bbIter.getElementSize();
        }

        public void test() {
            final ByteBuffer testBuffer = fileAsByteBuffer(headerSize);


            if(headerSize > 0) {
                final ByteBuffer headerBuffer = headerAsByteBuffer(headerSize);
                testHeaderBytes(headerBuffer, bbIter.getHeaderBytes());
            }

            bbIter.assertTotalElementsEqual(numElements);
            final Iterator<T> testIter = getTestIterator(testBuffer);

            while(hasNext(testIter, bbIter)) {
                Assert.assertEquals(testIter.next(), bbIter.next());
            }
        }

        public abstract Iterator<T> getTestIterator(final ByteBuffer byteBuffer);

        public boolean hasNext(final Iterator<T> testIter, final Iterator<T> fileIter) {
            if(testIter.hasNext() && fileIter.hasNext())
                return true;

            if(testIter.hasNext()) {
                throw new PicardException("Test data (testIter) has more iterations while fileIter does not!");
            }

            if(fileIter.hasNext()) {
                throw new PicardException("File data (fileIter) has more iterations while testIter does not!");
            }

            return false;
        }
    }

    abstract class NoHeaderTestIter<T> implements Iterator<T> {
        public final Buffer buf;

        public NoHeaderTestIter(final Buffer buf) {
            this.buf = buf;
        }

        public boolean hasNext() {
            return buf.hasRemaining();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    @DataProvider(name = "validTestDefs")
    public Object[][] validTestDefs() {
        return new Object[][] {
            {
                new FileTestDef<Integer>(15, MMapBackedIteratorFactory.getIntegerIterator(15, BinFile)) {
                    @Override
                    public Iterator<Integer> getTestIterator(final ByteBuffer byteBuffer) {
                        final IntBuffer ib = byteBuffer.asIntBuffer();
                        return new NoHeaderTestIter<Integer>(ib) {
                            @Override
                            public Integer next() {
                                return ib.get();
                            }
                        };
                    }
                }
            },
            {
                new FileTestDef<Byte>(2, MMapBackedIteratorFactory.getByteIterator(2, BinFile)) {
                    @Override
                    public Iterator<Byte> getTestIterator(final ByteBuffer byteBuffer) {
                        return new NoHeaderTestIter<Byte>(byteBuffer) {
                            @Override
                            public Byte next() {
                                return byteBuffer.get();
                            }
                        };
                    }
                }
            },
            {
                new FileTestDef<Float>(19, MMapBackedIteratorFactory.getFloatIterator(19, BinFile)) {
                    @Override
                    public Iterator<Float> getTestIterator(final ByteBuffer byteBuffer) {
                        return new NoHeaderTestIter<Float>(byteBuffer) {
                            @Override
                            public Float next() {
                                return byteBuffer.getFloat();
                            }
                        };
                    }
                }
            }
        };
    }

    @Test(dataProvider="validTestDefs")
    public void testValidConfigurations(final FileTestDef ftd) {
        ftd.test();
    }

    @Test
    public void onlyHeaderTest() {
        final BinaryFileIterator<Integer> bbIter = MMapBackedIteratorFactory.getIntegerIterator((int)BinFile.length(), BinFile);
        Assert.assertEquals(bbIter.getHeaderBytes(), headerAsByteBuffer((int)BinFile.length()));
        Assert.assertFalse(bbIter.hasNext());
    }

    @Test(expectedExceptions = PicardException.class)
    public void tooLargeHeaderTest() {
        final BinaryFileIterator<Integer> bbIter = MMapBackedIteratorFactory.getIntegerIterator(FileLength + 10, BinFile);
        bbIter.getHeaderBytes();
    }

    @Test(expectedExceptions = PicardException.class)
    public void negativeHeaderTest() {
        final BinaryFileIterator<Integer> bbIter = MMapBackedIteratorFactory.getIntegerIterator(-1, BinFile);
        bbIter.getHeaderBytes();
    }

    @DataProvider(name="invalidFileSizes")
    public Object [][] invalidFileSizes() {
        return new Object[][]{
            {1, 12}, //should result in two left over bytes
            {3, 11}, //should result in one left over elements
        };
    }

    @Test(expectedExceptions = PicardException.class, dataProvider = "invalidFileSizes")
    public void invalidFileSizeTests(final int headerSize, final int expectedElements) {
        final BinaryFileIterator<Integer> bbIter = MMapBackedIteratorFactory.getIntegerIterator(headerSize, BinFile);
        bbIter.assertTotalElementsEqual(expectedElements);
    }

    public void testHeaderBytes(final ByteBuffer bb1, final ByteBuffer bb2) {
        Assert.assertTrue(bb1.equals(bb2), "Header bytes are not equal! " + bb1.toString() + "  !=  " + bb2.toString());
    }
}
