/**
 * 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.cassandra.utils;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

import org.junit.Assert;
import org.junit.Test;

import org.apache.cassandra.io.util.DataOutputBuffer;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.fail;

public class ByteBufferUtilTest
{
    private static final String s = "cassandra";

    private ByteBuffer fromStringWithPosition(String s, int pos, boolean direct)
    {
        int l = s.length();
        ByteBuffer bb;
        if (direct)
        {
            bb = ByteBuffer.allocateDirect(l + pos);
        }
        else
        {
            ByteBuffer tmp = ByteBuffer.allocate(l + pos + 3);
            tmp.position(3);
            bb = tmp.slice(); // make bb have a non null arrayOffset
        }
        bb.position(pos);
        bb.mark();
        bb.put(s.getBytes());
        bb.reset();
        assert bb.position() == pos;
        return bb;
    }

    @Test
    public void testString() throws Exception
    {
        assert s.equals(ByteBufferUtil.string(ByteBufferUtil.bytes(s)));

        int pos = 10;
        ByteBuffer bb = fromStringWithPosition(s, 10, false);
        assert s.equals(ByteBufferUtil.string(bb, 10, s.length()));

        bb = fromStringWithPosition(s, 10, true);
        assert s.equals(ByteBufferUtil.string(bb, 10, s.length()));
    }

    @Test
    public void testGetArray()
    {
        byte[] t = s.getBytes();

        ByteBuffer bb = ByteBufferUtil.bytes(s);
        assertArrayEquals(t, ByteBufferUtil.getArray(bb));

        bb = fromStringWithPosition(s, 10, false);
        assertArrayEquals(t, ByteBufferUtil.getArray(bb));

        bb = fromStringWithPosition(s, 10, true);
        assertArrayEquals(t, ByteBufferUtil.getArray(bb));
    }

    @Test
    public void testLastIndexOf()
    {
        ByteBuffer bb = ByteBufferUtil.bytes(s);
        checkLastIndexOf(bb);

        bb = fromStringWithPosition(s, 10, false);
        checkLastIndexOf(bb);

        bb = fromStringWithPosition(s, 10, true);
        checkLastIndexOf(bb);
    }

    private void checkLastIndexOf(ByteBuffer bb)
    {
        assert bb.position() + 8 == ByteBufferUtil.lastIndexOf(bb, (byte) 'a', bb.position() + 8);
        assert bb.position() + 4 == ByteBufferUtil.lastIndexOf(bb, (byte) 'a', bb.position() + 7);
        assert bb.position() + 3 == ByteBufferUtil.lastIndexOf(bb, (byte) 's', bb.position() + 8);
        assert -1 == ByteBufferUtil.lastIndexOf(bb, (byte) 'o', bb.position() + 8);
        assert -1 == ByteBufferUtil.lastIndexOf(bb, (byte) 'd', bb.position() + 5);
    }

    @Test
    public void testClone()
    {
        ByteBuffer bb = ByteBufferUtil.bytes(s);
        ByteBuffer clone1 = ByteBufferUtil.clone(bb);
        assert bb != clone1;
        assert bb.equals(clone1);
        assert bb.array() != clone1.array();

        bb = fromStringWithPosition(s, 10, false);
        ByteBuffer clone2 = ByteBufferUtil.clone(bb);
        assert bb != clone2;
        assert bb.equals(clone2);
        assert clone1.equals(clone2);
        assert bb.array() != clone2.array();

        bb = fromStringWithPosition(s, 10, true);
        ByteBuffer clone3 = ByteBufferUtil.clone(bb);
        assert bb != clone3;
        assert bb.equals(clone3);
        assert clone1.equals(clone3);
    }

    @Test
    public void testArrayCopy()
    {
        ByteBuffer bb = ByteBufferUtil.bytes(s);
        checkArrayCopy(bb);

        bb = fromStringWithPosition(s, 10, false);
        checkArrayCopy(bb);

        bb = fromStringWithPosition(s, 10, true);
        checkArrayCopy(bb);
    }

    private void checkArrayCopy(ByteBuffer bb)
    {

        byte[] bytes = new byte[s.length()];
        ByteBufferUtil.copyBytes(bb, bb.position(), bytes, 0, s.length());
        assertArrayEquals(s.getBytes(), bytes);

        bytes = new byte[5];
        ByteBufferUtil.copyBytes(bb, bb.position() + 3, bytes, 1, 4);
        assertArrayEquals(Arrays.copyOfRange(s.getBytes(), 3, 7), Arrays.copyOfRange(bytes, 1, 5));
    }

    @Test
    public void testReadWrite() throws IOException
    {
        ByteBuffer bb = ByteBufferUtil.bytes(s);
        checkReadWrite(bb);

        bb = fromStringWithPosition(s, 10, false);
        checkReadWrite(bb);

        bb = fromStringWithPosition(s, 10, true);
        checkReadWrite(bb);
    }

    private void checkReadWrite(ByteBuffer bb) throws IOException
    {
        DataOutputBuffer out = new DataOutputBuffer();
        ByteBufferUtil.writeWithLength(bb, out);
        ByteBufferUtil.writeWithShortLength(bb, out);

        DataInputStream in = new DataInputStream(new ByteArrayInputStream(out.toByteArray()));
        assert bb.equals(ByteBufferUtil.readWithLength(in));
        assert bb.equals(ByteBufferUtil.readWithShortLength(in));
    }

    @Test
    public void testInputStream() throws IOException
    {
        ByteBuffer bb = ByteBuffer.allocate(13);
        bb.putInt(255);
        bb.put((byte) -3);
        bb.putLong(42L);
        bb.clear();

        DataInputStream in = new DataInputStream(ByteBufferUtil.inputStream(bb));
        assert in.readInt() == 255;
        assert in.readByte() == (byte)-3;
        assert in.readLong() == 42L;
    }

    @Test
    public void testIntBytesConversions()
    {
        // positive, negative, 1 and 2 byte cases, including a few edges that would foul things up unless you're careful
        // about masking away sign extension.
        int[] ints = new int[]
        {
            -20, -127, -128, 0, 1, 127, 128, 65534, 65535, -65534, -65535
        };

        for (int i : ints) {
            ByteBuffer ba = ByteBufferUtil.bytes(i);
            int actual = ByteBufferUtil.toInt(ba);
            assertEquals(i, actual);
        }
    }

    @Test(expected=CharacterCodingException.class)
    public void testDecode() throws IOException
    {
        ByteBuffer bytes = ByteBuffer.wrap(new byte[]{(byte)0xff, (byte)0xfe});
        ByteBufferUtil.string(bytes);
    }

    @Test
    public void testHexBytesConversion()
    {
        for (int i = Byte.MIN_VALUE; i <= Byte.MAX_VALUE; i++)
        {
            ByteBuffer bb = ByteBuffer.allocate(1);
            bb.put((byte) i);
            bb.clear();
            String s = ByteBufferUtil.bytesToHex(bb);
            ByteBuffer bb2 = ByteBufferUtil.hexToBytes(s);
            assertEquals(bb, bb2);
        }
        // check that non-zero buffer positions work,
        // i.e. that conversion accounts for the buffer offset and limit correctly
        ByteBuffer bb = ByteBuffer.allocate(4);
        for (int i = 0; i < 4; i++)
        {
            bb.put((byte) i);
        }
        // use a chunk out of the middle of the buffer
        bb.position(1);
        bb.limit(3);
        assertEquals(2, bb.remaining());
        String s = ByteBufferUtil.bytesToHex(bb);
        ByteBuffer bb2 = ByteBufferUtil.hexToBytes(s);
        assertEquals(bb, bb2);
        assertEquals("0102", s);
    }

    @Test
    public void testStartsAndEndsWith()
    {
        byte[] bytes = new byte[512];
        ThreadLocalRandom random = ThreadLocalRandom.current();

        random.nextBytes(bytes);

        ByteBuffer a = ByteBuffer.wrap(bytes);
        ByteBuffer b = a.duplicate();

        // let's take random slices of a and match
        for (int i = 0; i < 512; i++)
        {
            // prefix from the original offset
            b.position(0).limit(a.remaining() - random.nextInt(0, a.remaining() - 1));
            Assert.assertTrue(ByteBufferUtil.startsWith(a, b));
            Assert.assertTrue(ByteBufferUtil.startsWith(a, b.slice()));

            // prefix from random position inside of array
            int pos = random.nextInt(1, a.remaining() - 5);
            a.position(pos);
            b.limit(bytes.length - 1).position(pos);

            Assert.assertTrue(ByteBufferUtil.startsWith(a, b));

            a.position(0);

            // endsWith at random position
            b.limit(a.remaining()).position(random.nextInt(0, a.remaining() - 1));
            Assert.assertTrue(ByteBufferUtil.endsWith(a, b));
            Assert.assertTrue(ByteBufferUtil.endsWith(a, b.slice()));

        }

        a.limit(bytes.length - 1).position(0);
        b.limit(bytes.length - 1).position(1);

        assertFalse(ByteBufferUtil.startsWith(a, b));
        assertFalse(ByteBufferUtil.startsWith(a, b.slice()));

        Assert.assertTrue(ByteBufferUtil.endsWith(a, b));
        Assert.assertTrue(ByteBufferUtil.endsWith(a, b.slice()));


        a.position(5);

        assertFalse(ByteBufferUtil.startsWith(a, b));
        assertFalse(ByteBufferUtil.endsWith(a, b));
    }

    @Test
    public void testWriteZeroes()
    {
        byte[] initial = new byte[1024];
        Arrays.fill(initial, (byte) 1);
        for (ByteBuffer b : new ByteBuffer[] { ByteBuffer.allocate(1024), ByteBuffer.allocateDirect(1024) })
        {
            for (int i = 0; i <= 32; ++i)
                for (int j = 1024; j >= 1024 - 32; --j)
                {
                    b.clear();
                    b.put(initial);
                    b.flip();
                    b.position(i);
                    ByteBufferUtil.writeZeroes(b, j-i);
                    assertEquals(j, b.position());
                    int ii = 0;
                    for (; ii < i; ++ii)
                        assertEquals(initial[ii], b.get(ii));
                    for (; ii < j; ++ii)
                        assertEquals(0, b.get(ii));
                    for (; ii < 1024; ++ii)
                        assertEquals(initial[ii], b.get(ii));

                    b.clear();
                    b.put(initial);
                    b.limit(j).position(i);
                    ByteBuffer slice = b.slice();
                    ByteBufferUtil.writeZeroes(slice, slice.capacity());
                    assertFalse(slice.hasRemaining());
                    b.clear();  // reset position and limit for check
                    ii = 0;
                    for (; ii < i; ++ii)
                        assertEquals(initial[ii], b.get(ii));
                    for (; ii < j; ++ii)
                        assertEquals(0, b.get(ii));
                    for (; ii < 1024; ++ii)
                        assertEquals(initial[ii], b.get(ii));

                    slice.clear();
                    try
                    {
                        ByteBufferUtil.writeZeroes(slice, slice.capacity() + 1);
                        fail("Line above should throw.");
                    }
                    catch (BufferOverflowException | IndexOutOfBoundsException e)
                    {
                        // correct path
                    }
                }
        }
    }

    @Test
    public void testEqualsWithShortLength() throws IOException
    {
        ByteBuffer bb = ByteBufferUtil.bytes(s);
        checkEquals(bb);

        bb = fromStringWithPosition(s, 10, false);
        checkEquals(bb);

        bb = fromStringWithPosition(s, 10, true);
        checkEquals(bb);
    }

    private void checkEquals(ByteBuffer bb) throws IOException
    {
        DataOutputBuffer out = new DataOutputBuffer();
        ByteBufferUtil.writeWithShortLength(bb, out);

        DataInputStream in = new DataInputStream(new ByteArrayInputStream(out.toByteArray()));
        assert ByteBufferUtil.equalsWithShortLength(in, bb);

        int index = ThreadLocalRandom.current().nextInt(bb.remaining());

        in = new DataInputStream(new ByteArrayInputStream(out.toByteArray()));
        bb.put(bb.position() + index, (byte) (bb.get(index) ^ 0x55));
        assert !ByteBufferUtil.equalsWithShortLength(in, bb);
        bb.put(bb.position() + index, (byte) (bb.get(index) ^ 0x55));   // revert change

        in = new DataInputStream(new ByteArrayInputStream(out.toByteArray()));
        bb.limit(bb.position() + index);
        assert !ByteBufferUtil.equalsWithShortLength(in, bb);
    }
}
