/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 */
package org.apache.mina.util.byteaccess;

import static org.easymock.EasyMock.createStrictControl;
import static org.junit.Assert.assertEquals;

import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.util.byteaccess.ByteArray.Cursor;
import org.apache.mina.util.byteaccess.CompositeByteArray.CursorListener;
import org.apache.mina.util.byteaccess.CompositeByteArrayRelativeWriter.ChunkedExpander;
import org.apache.mina.util.byteaccess.CompositeByteArrayRelativeWriter.Flusher;
import org.easymock.IMocksControl;
import org.junit.Test;

/**
 * Tests classes in the <code>byteaccess</code> package.
 * 
 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
 */
public class ByteAccessTest {

    private List<String> operations = new ArrayList<>();

    private void resetOperations() {
        operations.clear();
    }

    private void assertOperationCountEquals(int expectedCount) {
        assertEquals("Operations: " + operations, expectedCount, operations.size());
    }

    private void addOperation(String description) {
        operations.add(description);
    }

    @Test
    public void testBufferByteArray() throws Exception {
        ByteArray ba = getByteArrayFactory().create(1000);
        testAbsoluteReaderAndWriter(0, 1000, ba, ba);
        testAbsoluteReaderAndWriter(0, 1000, ba, ba);
        Cursor readCursor = ba.cursor();
        Cursor writeCursor = ba.cursor();
        testRelativeReaderAndWriter(1000, readCursor, writeCursor);
    }

    @Test
    public void testCompositeAddAndRemove() throws Exception {
        CompositeByteArray cba = new CompositeByteArray();
        assertEquals(0, cba.first());
        assertEquals(0, cba.last());
        cba.addFirst(getByteArrayFactory().create(100));
        assertEquals(-100, cba.first());
        assertEquals(0, cba.last());
        cba.addFirst(getByteArrayFactory().create(100));
        assertEquals(-200, cba.first());
        assertEquals(0, cba.last());
        cba.addLast(getByteArrayFactory().create(100));
        assertEquals(-200, cba.first());
        assertEquals(100, cba.last());
        cba.removeFirst();
        assertEquals(-100, cba.first());
        assertEquals(100, cba.last());
        cba.addLast(getByteArrayFactory().create(100));
        assertEquals(-100, cba.first());
        assertEquals(200, cba.last());
        cba.removeLast();
        assertEquals(-100, cba.first());
        assertEquals(100, cba.last());
        cba.removeFirst();
        assertEquals(0, cba.first());
        assertEquals(100, cba.last());
        cba.removeFirst();
        assertEquals(100, cba.first());
        assertEquals(100, cba.last());
        cba.addLast(getByteArrayFactory().create(100));
        assertEquals(100, cba.first());
        assertEquals(200, cba.last());
    }

    private BufferByteArray wrapString(String string) {
        byte[] bytes = string.getBytes();
        IoBuffer bb = IoBuffer.wrap(bytes);
        BufferByteArray ba = new BufferByteArray(bb) {

            @Override
            public void free() {
                addOperation(this + ".free()");
                // Nothing to do.
            }

        };
        return ba;
    }

    private String toString(ByteArray ba) {
        IoBuffer bb = IoBuffer.allocate(ba.length());
        ba.get(0, bb);
        byte[] bytes = bb.array();
        String string = new String(bytes);
        return string;
    }

    @Test
    public void testCompositeStringJoin() throws Exception {
        ByteArray ba1 = wrapString("Hello");
        ByteArray ba2 = wrapString("MINA");
        ByteArray ba3 = wrapString("World");

        CompositeByteArray cba = new CompositeByteArray();
        cba.addLast(ba1);
        cba.addLast(ba2);
        cba.addLast(ba3);

        assertEquals("HelloMINAWorld", toString(cba));
    }

    @Test
    public void testCompositeCursor() throws Exception {
        IMocksControl mc = createStrictControl();

        ByteArray ba1 = getByteArrayFactory().create(10);
        ByteArray ba2 = getByteArrayFactory().create(10);
        ByteArray ba3 = getByteArrayFactory().create(10);

        CompositeByteArray cba = new CompositeByteArray();
        cba.addLast(ba1);
        cba.addLast(ba2);
        cba.addLast(ba3);

        CursorListener cl = mc.createMock(CursorListener.class);

        mc.reset();
        mc.replay();
        Cursor cursor = cba.cursor(cl);
        mc.verify();

        mc.reset();
        cl.enteredFirstComponent(0, ba1);
        mc.replay();
        cursor.get();
        mc.verify();

        mc.reset();
        mc.replay();
        cursor.setIndex(10);
        mc.verify();

        mc.reset();
        cl.enteredNextComponent(10, ba2);
        mc.replay();
        cursor.put((byte) 55);
        mc.verify();

        mc.reset();
        mc.replay();
        cursor.setIndex(9);
        mc.verify();

        mc.reset();
        cl.enteredPreviousComponent(0, ba1);
        cl.enteredNextComponent(10, ba2);
        mc.replay();
        cursor.putInt(66);
        mc.verify();

        mc.reset();
        cl.enteredNextComponent(20, ba3);
        mc.replay();
        cursor.setIndex(29);
        cursor.get();
        mc.verify();

        cba.removeLast(); // Force cursor to relocate itself.

        mc.reset();
        cl.enteredLastComponent(10, ba2);
        mc.replay();
        cursor.setIndex(15);
        cursor.get();
        mc.verify();

        mc.reset();
        cl.enteredPreviousComponent(0, ba1);
        mc.replay();
        cursor.setIndex(0);
        cursor.get();
        mc.verify();
    }

    @Test
    public void testCompositeByteArray() throws Exception {
        CompositeByteArray ba = new CompositeByteArray();
        for (int i = 0; i < 1000; i += 100) {
            ba.addLast(getByteArrayFactory().create(100));
        }
        resetOperations();
        testAbsoluteReaderAndWriter(0, 1000, ba, ba);
        testAbsoluteReaderAndWriter(0, 1000, ba, ba);
        assertOperationCountEquals(0);
        Cursor readCursor = ba.cursor();
        Cursor writeCursor = ba.cursor();
        testRelativeReaderAndWriter(1000, readCursor, writeCursor);
        assertOperationCountEquals(0);
    }

    @Test
    public void testCompositeByteArrayRelativeReaderAndWriter() throws Exception {
        CompositeByteArray cba = new CompositeByteArray();
        CompositeByteArrayRelativeReader cbarr = new CompositeByteArrayRelativeReader(cba, true);
        CompositeByteArrayRelativeWriter cbarw = new CompositeByteArrayRelativeWriter(cba, getExpander(100),
                getFlusher(), false);
        resetOperations();
        testRelativeReaderAndWriter(10, cbarr, cbarw);
        assertOperationCountEquals(2);
        resetOperations();
        testRelativeReaderAndWriter(100, cbarr, cbarw);
        assertOperationCountEquals(3);
        resetOperations();
        testRelativeReaderAndWriter(1000, cbarr, cbarw);
        assertOperationCountEquals(30);
        resetOperations();
        testRelativeReaderAndWriter(10000, cbarr, cbarw);
        assertOperationCountEquals(300);
        resetOperations();
        testRelativeReaderAndWriter(90, cbarr, cbarw);
        assertOperationCountEquals(0); // Last free doesn't occur, since cursor only moves lazily.
    }

    @Test
    public void testCompositeByteArrayRelativeReaderAndWriterWithFlush() throws Exception {
        CompositeByteArray cba = new CompositeByteArray();
        CompositeByteArrayRelativeReader cbarr = new CompositeByteArrayRelativeReader(cba, true);
        CompositeByteArrayRelativeWriter cbarw = new CompositeByteArrayRelativeWriter(cba, getExpander(100),
                getFlusher(), true);
        resetOperations();
        testRelativeReaderAndWriter(10, cbarr, cbarw);
        assertOperationCountEquals(2);
        resetOperations();
        testRelativeReaderAndWriter(100, cbarr, cbarw);
        assertOperationCountEquals(4);
        resetOperations();
        testRelativeReaderAndWriter(1000, cbarr, cbarw);
        assertOperationCountEquals(40);
        resetOperations();
        testRelativeReaderAndWriter(10000, cbarr, cbarw);
        assertOperationCountEquals(400);
        resetOperations();
        testRelativeReaderAndWriter(90, cbarr, cbarw);
        assertOperationCountEquals(0); // Last free doesn't occur, since cursor only moves lazily.
    }

    @Test
    public void testCompositeRemoveTo() throws Exception {
        CompositeByteArray cba = new CompositeByteArray();
        {
            // Remove nothing.
            resetOperations();
            ByteArray removed = cba.removeTo(0);
            assertEquals(0, removed.first());
            assertEquals(0, removed.last());
            assertEquals(0, cba.first());
            assertEquals(0, cba.last());
            removed.free();
            assertOperationCountEquals(0);
        }
        cba.addLast(getByteArrayFactory().create(100));
        {
            // Remove nothing.
            resetOperations();
            ByteArray removed = cba.removeTo(0);
            assertEquals(0, removed.first());
            assertEquals(0, removed.last());
            assertEquals(0, cba.first());
            assertEquals(100, cba.last());
            removed.free();
            assertOperationCountEquals(0);
        }
        {
            // Remove entire component.
            resetOperations();
            ByteArray removed = cba.removeTo(100);
            assertEquals(0, removed.first());
            assertEquals(100, removed.last());
            assertEquals(100, cba.first());
            assertEquals(100, cba.last());
            removed.free();
            assertOperationCountEquals(1);
        }
        {
            // Remove nothing.
            resetOperations();
            ByteArray removed = cba.removeTo(100);
            assertEquals(0, removed.first());
            assertEquals(0, removed.last());
            assertEquals(100, cba.first());
            assertEquals(100, cba.last());
            removed.free();
            assertOperationCountEquals(0);
        }
        cba.addLast(getByteArrayFactory().create(100));
        {
            // Remove nothing.
            resetOperations();
            ByteArray removed = cba.removeTo(100);
            assertEquals(0, removed.first());
            assertEquals(0, removed.last());
            assertEquals(100, cba.first());
            assertEquals(200, cba.last());
            removed.free();
            assertOperationCountEquals(0);
        }
        {
            // Remove half a component.
            resetOperations();
            ByteArray removed = cba.removeTo(150);
            assertEquals(0, removed.first());
            assertEquals(50, removed.last());
            assertEquals(150, cba.first());
            assertEquals(200, cba.last());
            removed.free();
            assertOperationCountEquals(0); // Doesn't free until component finished.
        }
        {
            // Remove nothing.
            resetOperations();
            ByteArray removed = cba.removeTo(150);
            assertEquals(0, removed.first());
            assertEquals(0, removed.last());
            assertEquals(150, cba.first());
            assertEquals(200, cba.last());
            removed.free();
            assertOperationCountEquals(0);
        }
        {
            // Remove other half.
            resetOperations();
            ByteArray removed = cba.removeTo(200);
            assertEquals(0, removed.first());
            assertEquals(50, removed.last());
            assertEquals(200, cba.first());
            assertEquals(200, cba.last());
            removed.free();
            assertOperationCountEquals(1); // Frees ByteArray behind both buffers.
        }
    }

    @Test
    public void testCompositeByteArraySlicing() {
        CompositeByteArray cba = new CompositeByteArray();
        cba.addLast(getByteArrayFactory().create(10));
        cba.addLast(getByteArrayFactory().create(10));
        cba.addLast(getByteArrayFactory().create(10));
        testByteArraySlicing(cba, 0, 30);
        testByteArraySlicing(cba, 5, 10);
        testByteArraySlicing(cba, 10, 20);
        testByteArraySlicing(cba, 1, 28);
        testByteArraySlicing(cba, 19, 2);
    }

    @Test
    public void testBufferByteArraySlicing() {
        ByteArray bba = getByteArrayFactory().create(30);
        testByteArraySlicing(bba, 0, 30);
        testByteArraySlicing(bba, 5, 10);
        testByteArraySlicing(bba, 10, 20);
        testByteArraySlicing(bba, 1, 28);
        testByteArraySlicing(bba, 19, 2);

    }

    private void testByteArraySlicing(ByteArray ba, int start, int length) {
        ByteArray slice = ba.slice(start, length);
        for (int i = 0; i < length; i++) {
            byte b1 = (byte) (i % 67);
            byte b2 = (byte) (i % 36);
            int sourceIndex = i + start;
            int sliceIndex = i;
            ba.put(sourceIndex, b1);
            assertEquals(b1, ba.get(sourceIndex));
            assertEquals(b1, slice.get(sliceIndex));
            slice.put(sliceIndex, b2);
            assertEquals(b2, ba.get(sourceIndex));
            assertEquals(b2, slice.get(sliceIndex));
        }
    }

    private ChunkedExpander getExpander(final int chunkSize) {
        return new ChunkedExpander(getByteArrayFactory(), chunkSize) {
            @Override
            public void expand(CompositeByteArray cba, int minSize) {
                addOperation("ChunkedExpander(" + chunkSize + ").expand(" + cba + "," + minSize + ")");
                super.expand(cba, minSize);
            }
        };
    }

    private Flusher getFlusher() {
        return new CompositeByteArrayRelativeWriter.Flusher() {

            public void flush(ByteArray ba) {
                addOperation("Flusher().flush(" + ba + ")");
                ba.free();
            }

        };
    }

    private SimpleByteArrayFactory getByteArrayFactory() {
        return new SimpleByteArrayFactory() {
            @Override
            public ByteArray create(final int size) {
                if (size < 0) {
                    throw new IllegalArgumentException("Buffer size must not be negative:" + size);
                }
                IoBuffer bb = IoBuffer.allocate(size);
                ByteArray ba = new BufferByteArray(bb) {

                    @Override
                    public void free() {
                        addOperation(this + ".free()");
                        // Nothing to do.
                    }

                };
                addOperation("SimpleByteArrayFactory().create(" + size + ") = " + ba);
                return ba;
            }
        };
    }

    private void testRelativeReaderAndWriter(int length, IoRelativeReader reader, IoRelativeWriter writer) {
        for (int i = 0; i < length; i++) {
            byte b = (byte) (i % 67);
            writer.put(b);
            assertEquals(b, reader.get());
        }
    }

    private void testAbsoluteReaderAndWriter(int start, int length, IoAbsoluteReader reader, IoAbsoluteWriter writer) {
        for (int i = start; i < length; i++) {
            byte b = (byte) (i % 67);
            writer.put(i, b);
            assertEquals(b, reader.get(i));
        }
    }

    @Test
    public void testByteArrayPrimitiveAccess() {
        ByteArray bbaBig = getByteArrayFactory().create(1000);
        bbaBig.order(ByteOrder.BIG_ENDIAN);
        testPrimitiveAccess(bbaBig.cursor(), bbaBig.cursor());

        ByteArray bbaLittle = getByteArrayFactory().create(1000);
        bbaLittle.order(ByteOrder.LITTLE_ENDIAN);
        testPrimitiveAccess(bbaLittle.cursor(), bbaLittle.cursor());
    }

    @Test
    public void testByteArrayBufferAccess() {
        ByteArray ba = getByteArrayFactory().create(1);
        ba.put(0, (byte) 99);
        IoBuffer bb = IoBuffer.allocate(2);

        bb.clear();
        Cursor cursor = ba.cursor();
        assertEquals(0, cursor.getIndex());
        assertEquals(1, cursor.getRemaining());
        assertEquals(0, bb.position());
        assertEquals(2, bb.remaining());
        cursor.get(bb);
        assertEquals(1, cursor.getIndex());
        assertEquals(0, cursor.getRemaining());
        assertEquals(1, bb.position());
        assertEquals(1, bb.remaining());
    }

    @Test
    public void testCompositeByteArrayPrimitiveAccess() {
        CompositeByteArray cbaBig = new CompositeByteArray();
        cbaBig.order(ByteOrder.BIG_ENDIAN);
        for (int i = 0; i < 1000; i++) {
            ByteArray component = getByteArrayFactory().create(1);
            component.order(ByteOrder.BIG_ENDIAN);
            cbaBig.addLast(component);
        }
        testPrimitiveAccess(cbaBig.cursor(), cbaBig.cursor());

        CompositeByteArray cbaLittle = new CompositeByteArray();
        cbaLittle.order(ByteOrder.LITTLE_ENDIAN);
        for (int i = 0; i < 1000; i++) {
            ByteArray component = getByteArrayFactory().create(1);
            component.order(ByteOrder.LITTLE_ENDIAN);
            cbaLittle.addLast(component);
        }
        testPrimitiveAccess(cbaLittle.cursor(), cbaLittle.cursor());
    }

    @Test
    public void testCompositeByteArrayWrapperPrimitiveAccess() {
        CompositeByteArray cbaBig = new CompositeByteArray();
        cbaBig.order(ByteOrder.BIG_ENDIAN);
        for (int i = 0; i < 1000; i++) {
            ByteArray component = getByteArrayFactory().create(1);
            component.order(ByteOrder.BIG_ENDIAN);
            cbaBig.addLast(component);
        }
        testPrimitiveAccess(new CompositeByteArrayRelativeWriter(cbaBig, getExpander(10), getFlusher(), false),
                new CompositeByteArrayRelativeReader(cbaBig, true));

        CompositeByteArray cbaLittle = new CompositeByteArray();
        cbaLittle.order(ByteOrder.LITTLE_ENDIAN);
        for (int i = 0; i < 1000; i++) {
            ByteArray component = getByteArrayFactory().create(1);
            component.order(ByteOrder.LITTLE_ENDIAN);
            cbaLittle.addLast(component);
        }
        testPrimitiveAccess(new CompositeByteArrayRelativeWriter(cbaLittle, getExpander(10), getFlusher(), false),
                new CompositeByteArrayRelativeReader(cbaLittle, true));
    }

    private void testPrimitiveAccess(IoRelativeWriter write, IoRelativeReader read) {
        byte b = (byte) 0x12;
        write.put(b);
        assertEquals(b, read.get());

        short s = (short) 0x12;
        write.putShort(s);
        assertEquals(s, read.getShort());

        int i = 0x12345678;
        write.putInt(i);
        assertEquals(i, read.getInt());

        long l = 0x1234567890123456L;
        write.putLong(l);
        assertEquals(l, read.getLong());

        float f = Float.intBitsToFloat(i);
        write.putFloat(f);
        assertEquals(f, read.getFloat(), 0);

        double d = Double.longBitsToDouble(l);
        write.putDouble(d);
        assertEquals(d, read.getDouble(), 0);

        char c = (char) 0x1234;
        write.putChar(c);
        assertEquals(c, read.getChar());
    }

}
