package com.twelvemonkeys.io;

import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;

import static org.junit.Assert.*;

/**
 * SeekableInputStreamAbstractTestCase
 * <p/>
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableInputStreamAbstractTestCase.java#4 $
 */
public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbstractTestCase implements SeekableInterfaceTest {
    //// TODO: Figure out a better way of creating interface tests without duplicating code
    final SeekableAbstractTestCase seekableTestCase = new SeekableAbstractTestCase() {
        protected Seekable createSeekable() {
            return makeInputStream();
        }
    };

    @Override
    protected SeekableInputStream makeInputStream() {
        return (SeekableInputStream) super.makeInputStream();
    }

    @Override
    protected SeekableInputStream makeInputStream(final int pSize) {
        return (SeekableInputStream) super.makeInputStream(pSize);
    }

    protected SeekableInputStream makeInputStream(byte[] pBytes) {
        return makeInputStream(new ByteArrayInputStream(pBytes));
    }

    protected abstract SeekableInputStream makeInputStream(InputStream pStream);

    @Test
    @Override
    public void testResetAfterReset() throws Exception {
        InputStream input = makeInputStream(makeOrderedArray(25));

        if (!input.markSupported()) {
            return; // Not supported, skip test
        }

        assertTrue("Expected to read positive value", input.read() >= 0);

        int readlimit = 5;

        // Mark
        input.mark(readlimit);
        int read = input.read();
        assertTrue("Expected to read positive value", read >= 0);

        input.reset();
        assertEquals("Expected value read differs from actual", read, input.read());

        // Reset after read limit passed, may either throw exception, or reset to last good mark
        try {
            input.reset();
            assertEquals("Re-read of reset data should be first", 0, input.read());
        }
        catch (Exception e) {
            assertTrue("Wrong read-limit IOException message", e.getMessage().contains("mark"));
        }
    }

    @Test
    public void testSeekable() {
        seekableTestCase.testSeekable();
    }

    @Test
    public void testFlushBeyondCurrentPos() throws Exception {
        SeekableInputStream seekable = makeInputStream(20);

        int pos = 10;
        try {
            seekable.flushBefore(pos);
            fail("Flush beyond current position should throw IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
            // Ignore
        }
    }

    @Test
    public void testSeek() throws Exception {
        SeekableInputStream seekable = makeInputStream(55);
        int pos = 37;

        seekable.seek(pos);
        long streamPos = seekable.getStreamPosition();
        assertEquals("Stream positon should match seeked position", pos, streamPos);
    }

    @Test
    public void testSeekFlush() throws Exception {
        SeekableInputStream seekable = makeInputStream(133);
        int pos = 45;
        seekable.seek(pos);
        seekable.flushBefore(pos);
        long flushedPos = seekable.getFlushedPosition();
        assertEquals("Flushed positon should match position", pos, flushedPos);

        try {
            seekable.seek(pos - 1);
            fail("Read before flushed position succeeded");
        }
        catch (IndexOutOfBoundsException e) {
            // Ignore
        }
    }

    @Test
    public void testMarkFlushReset() throws Exception {
        SeekableInputStream seekable = makeInputStream(77);

        seekable.mark();

        int position = 55;
        seekable.seek(position);
        seekable.flushBefore(position);

        try {
            seekable.reset();
            fail("Reset before flushed position succeeded");
        }
        catch (IOException e) {
            // Ignore
        }

        assertEquals(position, seekable.getStreamPosition());
    }

    @Test
    public void testSeekSkipRead() throws Exception {
        SeekableInputStream seekable = makeInputStream(133);
        int pos = 45;
        for (int i = 0; i < 10; i++) {
            seekable.seek(pos);
            //noinspection ResultOfMethodCallIgnored
            seekable.skip(i);
            byte[] bytes = FileUtil.read(seekable);
            assertEquals(133, seekable.getStreamPosition());
            assertEquals(133 - 45- i, bytes.length);
        }
    }

    protected void testSeekSkip(SeekableInputStream pSeekable, String pStr) throws IOException {
        System.out.println();
        pSeekable.seek(pStr.length());
        FileUtil.read(pSeekable);
        for (int i = 0; i < 10; i++) {
            byte[] bytes = FileUtil.read(pSeekable);
            int len = bytes.length;
            if (len != 0) {
                System.err.println("Error in buffer length after full read...");
                System.err.println("len: " + len);
                System.err.println("bytes: \"" + new String(bytes) + "\"");
                break;
            }
        }

        System.out.println();

        for (int i = 0; i < 10; i++) {
            pSeekable.seek(0);
            int skip = i * 3;
            //noinspection ResultOfMethodCallIgnored
            pSeekable.skip(skip);
            String str = new String(FileUtil.read(pSeekable));
            System.out.println(str);
            if (str.length() != pStr.length() - skip) {
                throw new Error("Error in buffer length after skip");
            }
        }

        System.out.println();
        System.out.println("seek/skip ok!");
        System.out.println();
    }

    protected static void markReset(SeekableInputStream pSeekable) throws IOException {
        for (int i = 0; i < 10; i++) {
            pSeekable.mark();
            System.out.println(new String(FileUtil.read(pSeekable)));
            pSeekable.reset();
        }

        System.out.println();
        System.out.println("mark/reset ok!");
    }

    protected static void timeRead(SeekableInputStream pSeekable) throws IOException {
        for (int i = 0; i < 5000; i++) {
            pSeekable.mark();
            FileUtil.read(pSeekable);
            pSeekable.reset();
        }

        long start = System.currentTimeMillis();
        final int times = 200000;
        for (int i = 0; i < times; i++) {
            pSeekable.mark();
            FileUtil.read(pSeekable);
            pSeekable.reset();
        }
        long time = System.currentTimeMillis() - start;

        System.out.println("Time; " + time + "ms (" + (time / (float) times) + "ms/inv)");
    }

    /*

    // Test code below...
    protected final static String STR = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Fusce massa orci, adipiscing vel, dapibus et, vulputate tristique, tortor. Quisque sodales. Mauris varius turpis et pede. Nam ac dolor vel diam condimentum elementum. Pellentesque eget tellus. Praesent magna. Sed fringilla. Proin ullamcorper tincidunt ante. Fusce dapibus nibh nec dolor. Etiam erat. Nullam dignissim laoreet nibh. Maecenas scelerisque. Pellentesque in quam. Maecenas sollicitudin, magna nec imperdiet facilisis, metus quam tristique ipsum, vitae consequat massa purus eget leo. Nulla ipsum. Proin non purus eget tellus lobortis iaculis. In lorem justo, posuere id, vulputate at, adipiscing ut, nisl. Nunc dui erat, tincidunt ac, interdum quis, rutrum et, libero. Etiam lectus dui, viverra sit amet, elementum ut, malesuada sed, massa. Vestibulum mi nulla, sodales vel, vestibulum sed, congue blandit, velit.";

    protected static void flushSeek(SeekableInputStream pSeekable, String pStr) throws IOException {
        pSeekable.seek(0);
        pSeekable.mark();
        int pos = pStr.length() / 2;
        try {
            pSeekable.flushBefore(pos);
            System.err.println("Error in flush/seek");
        }
        catch (IndexOutOfBoundsException e) {
            // Ignore
        }
        pSeekable.seek(pos);
        long streamPos = pSeekable.getStreamPosition();
        if (streamPos != pos) {
            System.err.println("Streampos not equal seeked pos");
        }

        pSeekable.flushBefore(pos);
        long flushedPos = pSeekable.getFlushedPosition();
        if (flushedPos != pos) {
            System.err.println("flushedpos not equal set flushed pos");
        }

        for (int i = 0; i < 10; i++) {
            pSeekable.seek(pos);
            //noinspection ResultOfMethodCallIgnored
            pSeekable.skip(i);
            System.out.println(new String(FileUtil.read(pSeekable)));
        }

        try {
            pSeekable.seek(pos - 1);
            System.err.println("Error in flush/seek");
        }
        catch (IndexOutOfBoundsException e) {
            // Ignore
        }
        try {
            pSeekable.reset();
            System.err.println("Error in flush/seek");
        }
        catch (IOException e) {
            // Ignore
        }

        System.out.println();
        System.out.println("flush/seek ok!");
    }

    protected static void seekSkip(SeekableInputStream pSeekable, String pStr) throws IOException {
        System.out.println();
        pSeekable.seek(pStr.length());
        FileUtil.read(pSeekable);
        for (int i = 0; i < 10; i++) {
            byte[] bytes = FileUtil.read(pSeekable);
            int len = bytes.length;
            if (len != 0) {
                System.err.println("Error in buffer length after full read...");
                System.err.println("len: " + len);
                System.err.println("bytes: \"" + new String(bytes) + "\"");
                break;
            }
        }

        System.out.println();

        for (int i = 0; i < 10; i++) {
            pSeekable.seek(0);
            int skip = i * 3;
            //noinspection ResultOfMethodCallIgnored
            pSeekable.skip(skip);
            String str = new String(FileUtil.read(pSeekable));
            System.out.println(str);
            if (str.length() != pStr.length() - skip) {
                throw new Error("Error in buffer length after skip");
            }
        }

        System.out.println();
        System.out.println("seek/skip ok!");
        System.out.println();
    }

    protected static void markReset(SeekableInputStream pSeekable) throws IOException {
        for (int i = 0; i < 10; i++) {
            pSeekable.mark();
            System.out.println(new String(FileUtil.read(pSeekable)));
            pSeekable.reset();
        }

        System.out.println();
        System.out.println("mark/reset ok!");
    }

    protected static void timeRead(SeekableInputStream pSeekable) throws IOException {
        for (int i = 0; i < 5000; i++) {
            pSeekable.mark();
            FileUtil.read(pSeekable);
            pSeekable.reset();
        }

        long start = System.currentTimeMillis();
        final int times = 200000;
        for (int i = 0; i < times; i++) {
            pSeekable.mark();
            FileUtil.read(pSeekable);
            pSeekable.reset();
        }
        long time = System.currentTimeMillis() - start;

        System.out.println("Time; " + time + "ms (" + (time / (float) times) + "ms/inv)");
    }
     */

    @Test
    public void testReadResetReadDirectBufferBug() throws IOException {
        // Make sure we use the exact size of the buffer
        final int size = 1024;

        // Fill bytes
        byte[] bytes = new byte[size * 2];
        sRandom.nextBytes(bytes);

        // Create wrapper stream
        SeekableInputStream stream = makeInputStream(bytes);

        // Read to fill the buffer, then reset
        int val;

        val = stream.read();
        assertFalse("Unexepected EOF", val == -1);
        val = stream.read();
        assertFalse("Unexepected EOF", val == -1);
        val = stream.read();
        assertFalse("Unexepected EOF", val == -1);
        val = stream.read();
        assertFalse("Unexepected EOF", val == -1);

        stream.seek(0);

        // Read fully and compare
        byte[] result = new byte[size];

        readFully(stream, result);
        assertTrue(rangeEquals(bytes, 0, result, 0, size));

        readFully(stream, result);
        assertTrue(rangeEquals(bytes, size, result, 0, size));
    }

    @Test
    public void testReadAllByteValuesRegression() throws IOException {
        final int size = 128;

        // Fill bytes
        byte[] bytes = new byte[256];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) i;
        }

        // Create wrapper stream
        SeekableInputStream stream = makeInputStream(bytes);

        // Fill buffer
        byte[] buffer = new byte[size];
        while (stream.read(buffer) >= 0) {
        }

        stream.seek(0);
        for (int i = 0; i < bytes.length; i += 2) {
            assertEquals("Wrong stream position", i, stream.getStreamPosition());
            int count = stream.read(buffer, 0, 2);
            assertEquals(2, count);
            assertEquals(String.format("Wrong value read at pos %d", stream.getStreamPosition()), bytes[i], buffer[0]);
            assertEquals(String.format("Wrong value read at pos %d", stream.getStreamPosition()), bytes[i + 1], buffer[1]);
        }

        stream.seek(0);
        for (int i = 0; i < bytes.length; i++) {
            assertEquals("Wrong stream position", i, stream.getStreamPosition());
            int actual = stream.read();
            assertEquals(String.format("Wrong value read at pos %d", stream.getStreamPosition()), bytes[i] & 0xff, actual);
            assertEquals(String.format("Wrong value read at pos %d", stream.getStreamPosition()), bytes[i], (byte) actual);
        }

    }

    @Test
    public void testCloseUnderlyingStream() throws IOException {
        final boolean[] closed = new boolean[1];

        ByteArrayInputStream input = new ByteArrayInputStream(makeRandomArray(256)) {
            @Override
            public void close() throws IOException {
                closed[0] = true;
                super.close();
            }
        };

        SeekableInputStream stream = makeInputStream(input);

        try {
            FileUtil.read(stream); // Read until EOF

            assertEquals("EOF not reached (test case broken)", -1, stream.read());
            assertFalse("Underlying stream closed before close", closed[0]);
        }
        finally {
            stream.close();
        }

        assertTrue("Underlying stream not closed", closed[0]);

    }

    private void readFully(InputStream pStream, byte[] pResult) throws IOException {
        int pos = 0;
        while (pos < pResult.length) {
            int read = pStream.read(pResult,  pos, pResult.length - pos);
            if (read == -1) {
                throw new EOFException();
            }
            pos += read;
        }
    }

    /**
     * Test two arrays for range equality. That is, they contain the same elements for some specified range.
     *
     * @param pFirst one array to test for equality
     * @param pFirstOffset the offset into the first array to start testing for equality
     * @param pSecond the other array to test for equality
     * @param pSecondOffset the offset into the second array to start testing for equality
     * @param pLength the length of the range to check for equality
     *
     * @return {@code true} if both arrays are non-{@code null}
     * and have at least {@code offset + pLength} elements
     * and all elements in the range from the first array is equal to the elements from the second array,
     * or if {@code pFirst == pSecond} (including both arrays being {@code null})
     * and {@code pFirstOffset == pSecondOffset}.
     * Otherwise {@code false}.
     */
    static boolean rangeEquals(byte[] pFirst, int pFirstOffset, byte[] pSecond, int pSecondOffset, int pLength) {
        if (pFirst == pSecond && pFirstOffset == pSecondOffset) {
            return true;
        }

        if (pFirst == null || pSecond == null) {
            return false;
        }

        if (pFirst.length < pFirstOffset + pLength || pSecond.length < pSecondOffset + pLength) {
            return false;
        }

        for (int i = 0; i < pLength; i++) {
            if (pFirst[pFirstOffset + i] != pSecond[pSecondOffset + i]) {
                return false;
            }
        }

        return true;
    }
}
