package com.limegroup.gnutella.util;

import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;

import org.limewire.util.ByteOrder;

import junit.framework.Test;

import com.limegroup.gnutella.connection.ReadBufferChannel;
import com.limegroup.gnutella.connection.WriteBufferChannel;

import com.limegroup.gnutella.io.ByteBufferCache;

/**
 * Tests some of the functionality of the Circular buffer.  
 *
 */
public class CircularByteBufferTest extends LimeTestCase {

    public CircularByteBufferTest(String name) {
        super(name);
    }

    public static Test suite() {
        return buildTestSuite(CircularByteBufferTest.class);
    }
    
    static StubCache cache;
    
    public void setUp() {
    	cache = new StubCache();
    }
    
    public void testBasic() throws Exception {
        byte [] in = new byte[]{(byte)1,(byte)2};
        CircularByteBuffer buf = new CircularByteBuffer(2,cache);
        assertEquals(2,buf.remainingIn());
        assertEquals(0,buf.remainingOut());
        
        buf.put(ByteBuffer.wrap(in));
        
        assertEquals(0,buf.remainingIn());
        assertEquals(2,buf.remainingOut());
        
        byte [] out = new byte[2];
        buf.get(out);
        
        assertEquals(2,buf.remainingIn());
        assertEquals(0,buf.remainingOut());

        assertEquals(in[0],out[0]);
        assertEquals(in[1],out[1]);
    }
    
    public void testOverflow() throws Exception {
        byte [] in = new byte[]{(byte)1,(byte)2};
        CircularByteBuffer buf = new CircularByteBuffer(1,cache);
        
        assertEquals(1,buf.remainingIn());
        assertEquals(0,buf.remainingOut());
        
        try {
            buf.put(ByteBuffer.wrap(in));
            fail(" should have overflown");
        } catch (BufferOverflowException expected) {}
        
        assertEquals(1,buf.remainingIn());
        assertEquals(0,buf.remainingOut());
    }
    
    public void testUnderflow() throws Exception {
        byte [] in = new byte[]{(byte)1,(byte)2};
        CircularByteBuffer buf = new CircularByteBuffer(2,cache);
        
        buf.put(ByteBuffer.wrap(in));
        
        assertEquals(0,buf.remainingIn());
        assertEquals(2,buf.remainingOut());
        
        byte [] out = new byte[3];
        
        try {
            buf.get(out);
            fail("should have underflown");
        } catch (BufferUnderflowException expected) {}
        
        assertEquals(0,buf.remainingIn());
        assertEquals(2,buf.remainingOut());
    }
    
    public void testWrapAround() throws Exception {
        byte [] in = new byte[30];
        for(int i = 0;i < 20;i++) in[i]=(byte)i;
        
        CircularByteBuffer buf = new CircularByteBuffer(5,cache);
        ReadableByteChannel source = new ReadBufferChannel(in);
        
        // R1 R2 R3 R4 R5
        assertEquals(5,buf.read(source));
        
        assertEquals(0,buf.remainingIn());
        assertEquals(5,buf.remainingOut());
        assertEquals(5, buf.size());
        
        // R1 R2 - - -
        buf.get();buf.get();
        assertEquals(2,buf.remainingIn());
        assertEquals(3,buf.remainingOut());
        assertEquals(2,buf.read(source));
        
        assertEquals(0,buf.remainingIn());
        assertEquals(5,buf.remainingOut());
        
        // W1 W2 W3 W4 W5
        WritableByteChannel sink = new WriteBufferChannel(20);
        assertEquals(5,buf.write(sink));
        assertEquals(5,buf.read(source));
        
        // - - W1 W2 W3
        buf.get();buf.get();
        assertEquals(3,buf.write(sink));
        assertEquals(5,buf.read(source));
        
        // W5 W1 W2 W3 W4
        buf.get();
        assertEquals(1,buf.read(source));
        assertEquals(5,buf.write(sink));
        
        // R5 R1 R2 R3 R4
        assertEquals(5,buf.read(source));
    }
    
    /**
     * tests getting of int with varying endinanness.
     */
    public static void testGetInt() throws Exception {
    	CircularByteBuffer buf = new CircularByteBuffer(4, cache);
    	
    	byte [] data = new byte[4];
    	ByteOrder.int2leb(50, data, 0);
    	ReadableByteChannel source = new ReadBufferChannel(data);
    	buf.read(source);
    	
    	// default order should be big endian
    	assertNotEquals(50,buf.getInt());
    	buf.order(java.nio.ByteOrder.LITTLE_ENDIAN);
    	source = new ReadBufferChannel(data);
    	buf.read(source);
    	assertEquals(50, buf.getInt());
    	
    	ByteOrder.int2beb(50, data, 0);
    	buf.order(java.nio.ByteOrder.BIG_ENDIAN);
    	source = new ReadBufferChannel(data);
    	buf.read(source);
    	assertEquals(50, buf.getInt());
    }
    
    /**
     * tests that the buffer uses the provided cache.
     */
    public static void testUsesCache() throws Exception {
    	// at first, the cache has not checked out any buffers.
    	assertNull(cache.checkedOut);
    	CircularByteBuffer buf = new CircularByteBuffer(4, cache);
    	byte [] data = new byte[4];
    	ByteOrder.int2leb(50, data, 0);
    	ReadableByteChannel source = new ReadBufferChannel(data);
    	buf.read(source);
    	
    	// after we read, something does get checked out.
    	assertNotNull(cache.checkedOut);
    	ByteBuffer used = cache.checkedOut;
    	
    	// read some more, but not all.  Buffer should be still checked out.
    	buf.get();buf.get();
    	assertEquals(2, buf.size());
    	assertSame(used, cache.checkedOut);
    	
    	// read everything
    	buf.get();buf.get();
    	assertEquals(0,buf.size());
    	assertNull(cache.checkedOut);
    }
    
    /**
     * tests the discarding of data.
     */
    public static void testDiscard() throws Exception {
    	CircularByteBuffer buf = new CircularByteBuffer(4, cache);
    	byte [] data = new byte[4];
    	ByteOrder.int2leb(50, data, 0);
    	ReadableByteChannel source = new ReadBufferChannel(data);
    	buf.read(source);
    	
    	assertEquals(4, buf.size());
    	buf.discard(3);
    	assertEquals(1, buf.size());
    	try {
    		buf.discard(2);
    		fail("bue expected");
    	} catch (BufferUnderflowException expected){}
    	
    	buf.discard(1);
    	assertEquals(0,buf.size());
    }
    
    private static class StubCache extends ByteBufferCache {

    	ByteBuffer checkedOut;
		public StubCache() {}

		@Override
		public void clearCache() {}

		@Override
		public ByteBuffer getDirect() {
			return null;
		}

		@Override
		public ByteBuffer getHeap() {
			fail(" illegal method invocation");
			return null;
		}

		@Override
		public ByteBuffer getHeap(int size) {
			assertNull(checkedOut);
			checkedOut = ByteBuffer.allocate(size);
			return checkedOut;
		}

		@Override
		public long getHeapCacheSize() {
			return 0;
		}

		@Override
		public void release(ByteBuffer buffer) {
			assertSame(checkedOut, buffer);
			checkedOut = null;
		}
    	
    }
}
