package com.rabbitmq.client.test;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.impl.Frame;
import com.rabbitmq.client.impl.nio.ByteBufferInputStream;
import com.rabbitmq.client.impl.nio.ByteBufferOutputStream;
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.*;

import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

/**
 *
 */
public class FrameTest {

    @Test public void readFrames() throws IOException {
        Random random = new Random();
        int nbOfFrames = 100;
        AccumulatorReadableByteChannel channel = new AccumulatorReadableByteChannel();

        for(int i = 0; i < nbOfFrames; i++) {
            byte[] payload = new byte[random.nextInt(2000) + 1];
            Frame frame = new Frame(AMQP.FRAME_METHOD, 1, payload);
            channel.add(frame);
        }

        ByteBuffer buffer = ByteBuffer.allocate(8192);

        DataInputStream inputStream = new DataInputStream(
            new ByteBufferInputStream(channel, buffer)
        );

        int nbReadFrames = 0;
        channel.read(buffer);
        buffer.flip();
        while(buffer.hasRemaining()) {
            Frame.readFrom(inputStream);
            nbReadFrames++;
            if(!buffer.hasRemaining()) {
                buffer.clear();
                channel.read(buffer);
                buffer.flip();
            }

        }
        assertThat(nbReadFrames, equalTo(nbOfFrames));
    }

    @Test public void readLargeFrame() throws IOException {
        AccumulatorReadableByteChannel channel = new AccumulatorReadableByteChannel();

        int [] framesSize = new int [] {100, 75, 20000, 150};
        for (int frameSize : framesSize) {
            Frame frame = new Frame(AMQP.FRAME_METHOD, 1, new byte[frameSize]);
            channel.add(frame);
        }

        ByteBuffer buffer = ByteBuffer.allocate(8192);

        DataInputStream inputStream = new DataInputStream(
            new ByteBufferInputStream(channel, buffer)
        );

        int nbReadFrames = 0;
        channel.read(buffer);
        buffer.flip();
        while(buffer.hasRemaining()) {
            Frame.readFrom(inputStream);
            nbReadFrames++;
            if(!buffer.hasRemaining()) {
                buffer.clear();
                channel.read(buffer);
                buffer.flip();
            }

        }
        assertThat(nbReadFrames, equalTo(framesSize.length));
    }

    @Test
    public void writeFrames() throws IOException {
        List<Frame> frames = new ArrayList<Frame>();
        Random random = new Random();
        int totalFrameSize = 0;
        for(int i = 0; i < 100; i++) {
            byte[] payload = new byte[random.nextInt(2000) + 1];
            Frame frame = new Frame(AMQP.FRAME_METHOD, 1, payload);
            frames.add(frame);
            totalFrameSize += frame.size();
        }

        AccumulatorWritableByteChannel channel = new AccumulatorWritableByteChannel();
        ByteBuffer buffer = ByteBuffer.allocate(8192);

        for (Frame frame : frames) {
            frame.writeTo(new DataOutputStream(new ByteBufferOutputStream(channel, buffer)));
        }
        drain(channel, buffer);
        checkWrittenChunks(totalFrameSize, channel);
    }

    @Test public void writeLargeFrame() throws IOException {
        List<Frame> frames = new ArrayList<Frame>();
        int totalFrameSize = 0;
        int [] framesSize = new int [] {100, 75, 20000, 150};
        for (int frameSize : framesSize) {
            Frame frame = new Frame(AMQP.FRAME_METHOD, 1, new byte[frameSize]);
            frames.add(frame);
            totalFrameSize += frame.size();
        }

        AccumulatorWritableByteChannel channel = new AccumulatorWritableByteChannel();
        ByteBuffer buffer = ByteBuffer.allocate(8192);

        for (Frame frame : frames) {
            frame.writeTo(new DataOutputStream(new ByteBufferOutputStream(channel, buffer)));
        }
        drain(channel, buffer);
        checkWrittenChunks(totalFrameSize, channel);
    }

    private void checkWrittenChunks(int totalFrameSize, AccumulatorWritableByteChannel channel) {
        int totalWritten  = 0;
        for (byte[] chunk : channel.chunks) {
            totalWritten += chunk.length;
        }
        assertThat(totalWritten, equalTo(totalFrameSize));
    }

    private static class AccumulatorWritableByteChannel implements WritableByteChannel {

        List<byte[]> chunks = new ArrayList<byte[]>();

        Random random = new Random();

        @Override
        public int write(ByteBuffer src) throws IOException {
            int remaining = src.remaining();
            if(remaining > 0) {
                int toRead = random.nextInt(remaining) + 1;
                byte [] chunk = new byte[toRead];
                src.get(chunk);
                chunks.add(chunk);
                return toRead;
            } else {
                return remaining;
            }

        }

        @Override
        public boolean isOpen() {
            return false;
        }

        @Override
        public void close() throws IOException {

        }
    }

    private static class AccumulatorReadableByteChannel implements ReadableByteChannel {

        private List<Byte> bytesOfFrames = new LinkedList<Byte>();

        @Override
        public int read(ByteBuffer dst) throws IOException {
            int remaining = dst.remaining();
            int read = 0;
            if(remaining > 0) {
                Iterator<Byte> iterator = bytesOfFrames.iterator();
                while(iterator.hasNext() && read < remaining) {
                    dst.put(iterator.next());
                    iterator.remove();
                    read++;
                }
            }
            return read;
        }

        @Override
        public boolean isOpen() {
            return false;
        }

        @Override
        public void close() throws IOException {

        }

        void add(Frame frame) throws IOException {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(frame.size());
            DataOutputStream outputStream = new DataOutputStream(byteArrayOutputStream);
            frame.writeTo(outputStream);
            outputStream.flush();
            for (byte b : byteArrayOutputStream.toByteArray()) {
                bytesOfFrames.add(b);
            }
        }
    }

    public static void drain(WritableByteChannel channel, ByteBuffer buffer) throws IOException {
        buffer.flip();
        while(buffer.hasRemaining() && channel.write(buffer) != -1);
        buffer.clear();
    }

}
