package it.unimi.dsi.fastutil.io;

import it.unimi.dsi.fastutil.io.FastBufferedInputStream;
import it.unimi.dsi.fastutil.io.FastBufferedInputStream.LineTerminator;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Random;

import org.junit.Test;

import static org.junit.Assert.*;

public class FastBufferedInputStreamTest {
	private final static boolean DEBUG = false;
	
	/** A byte array input stream that will return its data in small chunks,
	 * even it could actually return more data, and skips less bytes than it could.
	 */
	
	private static class BastardByteArrayInputStream extends ByteArrayInputStream {
		private final static long seed = System.currentTimeMillis();
		private final static Random r = new Random( seed );
		static {
			System.err.println( "Seed: " + seed );
		}

		public BastardByteArrayInputStream( byte[] array ) {
			super( array );
		}

		@Override
		public int read( byte[] buffer, int offset, int length ) {
			int k = r.nextInt( 2 ) + 1;
			return super.read( buffer, offset, length < k ? length : k );
		}
		
		public long skip( long n ) {
			int k = r.nextInt( 2 );
			return super.skip( n < k ? n : k );
		}

	}

	public void testReadline( int bufferSize ) throws IOException {
		FastBufferedInputStream stream;
		byte[] b;
		
		stream = new FastBufferedInputStream( new BastardByteArrayInputStream( new byte[] { 'A', 'B', 'C', '\r' } ), bufferSize );
		
		b = new byte[ 4 ];
		stream.readLine( b, 0, b.length, EnumSet.of( LineTerminator.CR ) );
		assertTrue( Arrays.toString( b ), Arrays.equals( b, new byte[] { 'A', 'B', 'C', 0 } ) );
		assertEquals( 4, stream.position() );
		assertEquals( -1, stream.readLine( b, 0, b.length, EnumSet.of( LineTerminator.CR ) ) );

		stream = new FastBufferedInputStream( new BastardByteArrayInputStream( new byte[] { 'A', 'B', 'C', '\r' } ), bufferSize );
		assertEquals( 4, stream.readLine( b, 0, b.length, EnumSet.of( LineTerminator.LF ) ) );
		assertEquals( 4, stream.position() );

		stream = new FastBufferedInputStream( new BastardByteArrayInputStream( new byte[] { 'A', 'B', 'C', '\r' } ), bufferSize );
		assertEquals( 4, stream.readLine( b, 0, b.length, EnumSet.of( LineTerminator.LF ) ) );
		assertEquals( 4, stream.position() );

		stream = new FastBufferedInputStream( new BastardByteArrayInputStream( new byte[] { 'A', 'B', 'C', '\r' } ), bufferSize );
		assertEquals( 4, stream.readLine( b, 0, b.length, EnumSet.of( LineTerminator.CR_LF ) ) );
		assertEquals( 4, stream.position() );

		stream = new FastBufferedInputStream( new BastardByteArrayInputStream( new byte[] { 'A', 'B', 'C', '\r' } ), bufferSize );
		assertEquals( 4, stream.readLine( b, 0, b.length, EnumSet.of( LineTerminator.CR_LF ) ) );
		assertTrue( Arrays.equals( b, new byte[] { 'A', 'B', 'C', '\r' } ) );
		assertEquals( 4, stream.position() );
		
		b = new byte[ 4 ];
		stream = new FastBufferedInputStream( new BastardByteArrayInputStream( new byte[] { 'A', 'B', 'C', '\r' } ), bufferSize );
		stream.readLine( b, 0, 2, EnumSet.of( LineTerminator.CR ) );
		assertTrue( Arrays.equals( b, new byte[] { 'A', 'B', 0, 0 } ) );
		assertEquals( 2, stream.position() );
		
		// Reads with only LF as terminator
		stream = new FastBufferedInputStream( new BastardByteArrayInputStream( new byte[] { 'A', 'B', 'C', '\r', '\n', 'D' } ), bufferSize );
		assertEquals( 4, stream.readLine( b, 0, 4, EnumSet.of( LineTerminator.LF ) ) );
		assertTrue( Arrays.equals( b, new byte[] { 'A', 'B', 'C', '\r' } ) );
		assertEquals( 4, stream.position() );
		assertEquals( 0, stream.readLine( b, 0, 4, EnumSet.of( LineTerminator.LF ) ) );
		assertEquals( 5, stream.position() );
		assertTrue( Arrays.equals( b, new byte[] { 'A', 'B', 'C', '\r' } ) );
		assertEquals( 1, stream.readLine( b, 2, 2, EnumSet.of( LineTerminator.LF ) ) );
		assertEquals( 6, stream.position() );
		assertTrue( Arrays.equals( b, new byte[] { 'A', 'B', 'D', '\r' } ) );

		// Reads with both LF and CR/LF as terminators
		b = new byte[ 4 ];
		stream = new FastBufferedInputStream( new BastardByteArrayInputStream( new byte[] { 'A', 'B', 'C', '\r', '\n', 'D' } ), bufferSize );
		assertEquals( 3, stream.readLine( b, 0, 4, EnumSet.of( LineTerminator.CR, LineTerminator.CR_LF ) ) );
		assertEquals( 5, stream.position() );
		assertTrue( Arrays.equals( b, new byte[] { 'A', 'B', 'C', 0 } ) );
		assertEquals( 1, stream.readLine( b, 2, 2, EnumSet.of( LineTerminator.CR, LineTerminator.CR_LF ) ) );
		assertEquals( 6, stream.position() );
		assertTrue( Arrays.equals( b, new byte[] { 'A', 'B', 'D', 0 } ) );

		// Reads with only CR as terminator
		b = new byte[ 4 ];
		stream = new FastBufferedInputStream( new BastardByteArrayInputStream( new byte[] { 'A', 'B', 'C', '\r', '\n', 'D' } ), bufferSize );
		assertEquals( 3, stream.readLine( b, 0, 4, EnumSet.of( LineTerminator.CR ) ) );
		assertEquals( 4, stream.position() );
		assertTrue( Arrays.equals( b, new byte[] { 'A', 'B', 'C', 0 } ) );
		assertEquals( 2, stream.readLine( b, 2, 2, EnumSet.of( LineTerminator.CR ) ) );
		assertEquals( 6, stream.position() );
		assertTrue( Arrays.equals( b, new byte[] { 'A', 'B', '\n', 'D' } ) );

		// Reads with only CR/LF as terminator
		stream = new FastBufferedInputStream( new BastardByteArrayInputStream( new byte[] { 'A', 'B', 'C', '\r', '\n', 'D' } ), bufferSize );
		b = new byte[ 4 ];
		assertEquals( 3, stream.readLine( b, 0, 4, EnumSet.of( LineTerminator.CR_LF ) ) );
		assertEquals( 5, stream.position() );
		assertTrue( Arrays.equals( b, new byte[] { 'A', 'B', 'C', 0 } ) );
		assertEquals( 1, stream.readLine( b, 0, 4, EnumSet.of( LineTerminator.CR_LF ) ) );
		assertEquals( 6, stream.position() );
		assertTrue( Arrays.equals( b, new byte[] { 'D', 'B', 'C', 0 } ) );
		assertEquals( -1, stream.readLine( b, 0, 4, EnumSet.of( LineTerminator.CR_LF ) ) );

		// Reads with both CR and CR/LF as terminator
		
		// CR at end-of-file
		stream = new FastBufferedInputStream( new BastardByteArrayInputStream( new byte[] { 'A', 'B', 'C', '\r' } ), bufferSize );
		b = new byte[ 4 ];
		assertEquals( 3, stream.readLine( b, 0, 4, EnumSet.of( LineTerminator.CR_LF, LineTerminator.CR ) ) );
		assertEquals( 4, stream.position() );
		assertTrue( Arrays.equals( b, new byte[] { 'A', 'B', 'C', 0 } ) );

	}

	@Test
	public void testReadLine() throws IOException {
		testReadline( 1 );
		testReadline( 2 );
		testReadline( 3 );
		testReadline( 4 );
		testReadline( 5 );
		testReadline( 6 );
		testReadline( 7 );
		testReadline( 100 );
	}

	public void testSkip( int bufferSize ) throws IOException {
		FastBufferedInputStream stream;
		
		stream = new FastBufferedInputStream( new BastardByteArrayInputStream( new byte[] { 'A', 'B', 'C', '\r', '\n', 'D' } ), bufferSize );
		assertEquals( 2, stream.skip( 2 ) );
		assertEquals( 2, stream.position() );
		assertEquals( 1, stream.skip( 1 ) );
		assertEquals( 3, stream.position() );
		assertEquals( 3, stream.skip( 4 ) );
		assertEquals( 6, stream.position() );
		assertEquals( 0, stream.skip( 1 ) );
		assertEquals( 6, stream.position() );
	}

	@Test
	public void testSkip() throws IOException {
		testSkip( 1 );
		testSkip( 2 );
		testSkip( 3 );
		testSkip( 4 );
		testSkip( 5 );
		testSkip( 6 );
		testSkip( 7 );
		testSkip( 100 );
	}
	
	@Test
	public void testPosition() throws IOException {
		File temp = File.createTempFile( this.getClass().getSimpleName(), ".tmp" );
		temp.deleteOnExit();
		FileOutputStream fos = new FileOutputStream( temp );
		fos.write( new byte[] { 0, 1, 2, 3, 4 } );
		fos.close();
		
		FastBufferedInputStream stream = new FastBufferedInputStream( new FileInputStream( temp ), 2 );
		byte[] b = new byte[ 2 ];
		stream.read( b );
		stream.flush();
		stream.position( 0 );
		assertEquals( 0, stream.read() );
		stream.close();
		
		stream = new FastBufferedInputStream( new FileInputStream( temp ) );
		b = new byte[ 1 ];
		stream.read( b );
		stream.flush();
		stream.position( 0 );
		assertEquals( 0, stream.read() );
		stream.close();

		stream = new FastBufferedInputStream( new FileInputStream( temp ) );
		b = new byte[ 5 ];
		stream.read( b );
		stream.flush();
		assertEquals( -1, stream.read() );
		stream.position( 5 );
		assertEquals( -1, stream.read() );
		stream.position( 0 );
		assertEquals( 0, stream.read() );
		stream.position( 1 );
		assertEquals( 1, stream.read() );
		stream.position( 3 );
		assertEquals( 3, stream.read() );
		stream.position( 1 );
		assertEquals( 1, stream.read() );
		stream.position( 0 );
		assertEquals( 0, stream.read() );
		stream.close();
	}
	
	@Test
	public void testRead() throws IOException {
		// Reads with length larger than buffer size
		
		// No head, no stream
		InputStream stream = new FastBufferedInputStream( new ByteArrayInputStream( new byte[] {} ), 1 );
		byte[] b = new byte[ 4 ];
		
		assertEquals( -1, stream.read( b, 0, 2 ) );
		
		// Some head, no stream
		stream = new FastBufferedInputStream( new ByteArrayInputStream( new byte[] { 'A', 'B' } ), 2 );
		b = new byte[ 4 ];
		
		assertEquals( 1, stream.read( b, 0, 1 ) );
		assertEquals( 1, stream.read( b, 0, 3 ) );
		
		// Some head, some stream
		stream = new FastBufferedInputStream( new ByteArrayInputStream( new byte[] { 'A', 'B', 'C', 'D' } ), 2 );
		b = new byte[ 4 ];
		
		assertEquals( 1, stream.read( b, 0, 1 ) );
		assertEquals( 3, stream.read( b, 0, 3 ) );

		// No head, some stream
		stream = new FastBufferedInputStream( new ByteArrayInputStream( new byte[] { 'A', 'B', 'C', 'D' } ), 2 );
		b = new byte[ 4 ];
		
		assertEquals( 3, stream.read( b, 0, 3 ) );
		
		// Reads with length smaller than or equal to buffer size
		
		// No head, no stream
		stream = new FastBufferedInputStream( new ByteArrayInputStream( new byte[] {} ), 4 );
		b = new byte[ 4 ];
		
		assertEquals( -1, stream.read( b, 0, 2 ) );

	}
	
	public void testRandom( int bufferSize ) throws IOException {
		File temp = File.createTempFile( this.getClass().getSimpleName(), "tmp" );
		temp.deleteOnExit();
		
		// Create temp random file
		FileOutputStream out = new FileOutputStream( temp );
		Random random = new Random();
		int length = 100000 + random.nextInt( 10000 );
		for( int i = 0; i < length; i++ ) out.write( random.nextInt() );
		out.close();

		FastBufferedInputStream bis = new FastBufferedInputStream( new FileInputStream( temp ), bufferSize );
		FileInputStream test = new FileInputStream( temp );
		FileChannel fc = test.getChannel();
		int a1, a2, off, len, pos;
		byte b1[] = new byte[ 32768 ];
		byte b2[] = new byte[ 32768 ];

		while( true ) {

			switch( random.nextInt( 6 ) ) {

			case 0:
				if ( DEBUG ) System.err.println("read()");
				a1 = bis.read();
				a2 = test.read();
				assertEquals( a1, a2 );
				if ( a1 == -1 ) return;
				break;

			case 1:
				off = random.nextInt( b1.length );
				len = random.nextInt( b1.length - off + 1 );
				a1 = bis.read( b1, off, len );
				a2 = test.read( b2, off, len );
				if ( DEBUG ) System.err.println("read(b, " + off + ", " + len + ")");

				assertEquals( a1, a2 );

				for( int i = off; i < off + len; i++ ) assertEquals( "Position " + i, b1[ i ], b2[ i ] );
				break;

			case 2:
				if ( DEBUG ) System.err.println("available()");
				assertEquals( bis.available(), test.available() );
				break;

			case 3:
				if ( DEBUG ) System.err.println("position()" );
				pos = (int)bis.position();
				assertEquals( (int)fc.position(), pos );
				break;

			case 4:
				pos = random.nextInt( length );
				bis.position( pos );
				if ( DEBUG ) System.err.println("position(" + pos + ")" );
				(test = new FileInputStream( temp )).skip( pos );
				fc = test.getChannel();
				break;

			case 5:
				pos = random.nextInt( (int)(length - bis.position() + 1) );
				a1 = (int)bis.skip( pos );
				a2 = (int)test.skip( pos );
				if ( DEBUG ) System.err.println("skip(" + pos + ")" );
				assertEquals( a1, a2 );
				break;
			}
		}

	}

	@Test
	public void testRandom() throws IOException {
		testRandom( 1 );
		testRandom( 2 );
		testRandom( 3 );
		testRandom( 100 );
		testRandom( 2048 );
	}
}

