/*
 * 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.db.index.sasi.utils;

import java.io.*;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

import org.apache.cassandra.db.marshal.LongType;
import org.apache.cassandra.io.util.FileUtils;

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

public class MappedBufferTest
{
    @Test
    public void testBasicWriteThenRead() throws Exception
    {
        long numLongs = 10000;
        final MappedBuffer buffer = createTestFile(numLongs);

        Assert.assertEquals(0, buffer.position());
        for (long i = 0; i < numLongs; i++)
        {
            Assert.assertEquals(i * 8, buffer.position());
            Assert.assertEquals(i, buffer.getLong());
        }

        buffer.position(0);
        for (long i = 0; i < numLongs; i++)
        {
            Assert.assertEquals(i, buffer.getLong(i * 8));
            Assert.assertEquals(0, buffer.position());
        }

        // read all the numbers as shorts (all numbers fit into four bytes)
        for (long i = 0; i < Math.min(Integer.MAX_VALUE, numLongs); i++)
            Assert.assertEquals(i, buffer.getInt((i * 8) + 4));

        // read all the numbers as shorts (all numbers fit into two bytes)
        for (long i = 0; i < Math.min(Short.MAX_VALUE, numLongs); i++) {
            Assert.assertEquals(i, buffer.getShort((i * 8) + 6));
        }

        // read all the numbers that can be represented as a single byte
        for (long i = 0; i < 128; i++)
            Assert.assertEquals(i, buffer.get((i * 8) + 7));

        buffer.close();
    }

    @Test
    public void testDuplicate() throws Exception
    {
        long numLongs = 10;
        final MappedBuffer buffer1 = createTestFile(numLongs);

        Assert.assertEquals(0, buffer1.getLong());
        Assert.assertEquals(1, buffer1.getLong());

        final MappedBuffer buffer2 = buffer1.duplicate();

        Assert.assertEquals(2, buffer1.getLong());
        Assert.assertEquals(2, buffer2.getLong());

        buffer2.position(0);
        Assert.assertEquals(3, buffer1.getLong());
        Assert.assertEquals(0, buffer2.getLong());
    }

    @Test
    public void testLimit() throws Exception
    {
        long numLongs =  10;
        final MappedBuffer buffer1 = createTestFile(numLongs);

        MappedBuffer buffer2 = buffer1.duplicate().position(16).limit(32);
        buffer1.position(0).limit(16);
        List<Long> longs = new ArrayList<>(4);

        while (buffer1.hasRemaining())
            longs.add(buffer1.getLong());

        while (buffer2.hasRemaining())
            longs.add(buffer2.getLong());

        Assert.assertArrayEquals(new Long[]{0L, 1L, 2L, 3L}, longs.toArray());
    }

    @Test(expected = IllegalArgumentException.class)
    public void testPositionGreaterThanLimit() throws Exception
    {
        final MappedBuffer buffer = createTestFile(1);

        buffer.limit(4);

        try
        {
            buffer.position(buffer.limit() + 1);
        }
        finally
        {
            buffer.close();
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testNegativePosition() throws Exception
    {
        try (MappedBuffer buffer = createTestFile(1))
        {
            buffer.position(-1);
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testLimitGreaterThanCapacity() throws Exception
    {
        try (MappedBuffer buffer = createTestFile(1))
        {
            buffer.limit(buffer.capacity() + 1);
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testLimitLessThanPosition() throws Exception
    {
        final MappedBuffer buffer = createTestFile(1);

        buffer.position(1);

        try
        {
            buffer.limit(0);
        }
        finally
        {
            buffer.close();
        }
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetRelativeUnderflow() throws Exception
    {
        final MappedBuffer buffer = createTestFile(1);

        buffer.position(buffer.limit());
        try
        {
            buffer.get();
        }
        finally
        {
            buffer.close();
        }

    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetAbsoluteGreaterThanCapacity() throws Exception
    {
        try (MappedBuffer buffer = createTestFile(1))
        {
            buffer.get(buffer.limit());
        }
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetAbsoluteNegativePosition() throws Exception
    {
        try (MappedBuffer buffer = createTestFile(1))
        {
            buffer.get(-1);
        }
    }


    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetShortRelativeUnderflow() throws Exception
    {
        final MappedBuffer buffer = createTestFile(1);

        buffer.position(buffer.capacity() - 1);
        try
        {
            buffer.getShort();
        }
        finally
        {
            buffer.close();
        }

    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetShortAbsoluteGreaterThanCapacity() throws Exception
    {
        final MappedBuffer buffer = createTestFile(1);

        Assert.assertEquals(8, buffer.capacity());
        try
        {
            buffer.getShort(buffer.capacity() - 1);
        }
        finally
        {
            buffer.close();
        }
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetShortAbsoluteNegativePosition() throws Exception
    {
        try (MappedBuffer buffer = createTestFile(1))
        {
            buffer.getShort(-1);
        }
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetIntRelativeUnderflow() throws Exception
    {
        final MappedBuffer buffer = createTestFile(1);

        buffer.position(buffer.capacity() - 3);
        try
        {
            buffer.getInt();
        }
        finally
        {
            buffer.close();
        }
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetIntAbsoluteGreaterThanCapacity() throws Exception
    {
        final MappedBuffer buffer = createTestFile(1);

        Assert.assertEquals(8, buffer.capacity());
        try
        {
            buffer.getInt(buffer.capacity() - 3);
        }
        finally
        {
            buffer.close();
        }
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetIntAbsoluteNegativePosition() throws Exception
    {
        try (MappedBuffer buffer = createTestFile(1))
        {
            buffer.getInt(-1);
        }
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetLongRelativeUnderflow() throws Exception
    {
        final MappedBuffer buffer = createTestFile(1);

        buffer.position(buffer.capacity() - 7);
        try
        {
            buffer.getLong();
        }
        finally
        {
            buffer.close();
        }

    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetLongAbsoluteGreaterThanCapacity() throws Exception
    {
        final MappedBuffer buffer = createTestFile(1);

        Assert.assertEquals(8, buffer.capacity());
        try
        {
            buffer.getLong(buffer.capacity() - 7);
        }
        finally
        {
            buffer.close();
        }
    }

    @Test(expected = IndexOutOfBoundsException.class)
    public void testGetLongAbsoluteNegativePosition() throws Exception
    {
        try (MappedBuffer buffer = createTestFile(1))
        {
            buffer.getLong(-1);
        }
    }

    @Test
    public void testGetPageRegion() throws Exception
    {
        ThreadLocalRandom random = ThreadLocalRandom.current();

        int numLongs = 1000;
        int byteSize = 8;
        int capacity = numLongs * byteSize;
        try (MappedBuffer buffer = createTestFile(numLongs))
        {
            for (int i = 0; i < 1000; i++)
            {
                // offset, length are always aligned on sizeof(long)
                int offset = random.nextInt(0, 1000 * byteSize - byteSize) & ~(byteSize - 1);
                int length = Math.min(capacity, random.nextInt(byteSize, capacity - offset) & ~(byteSize - 1));

                ByteBuffer region = buffer.getPageRegion(offset, length);
                for (int j = offset; j < (offset + length); j += 8)
                    Assert.assertEquals(j / 8, region.getLong(j));
            }
        }
    }

    @Test (expected = IllegalArgumentException.class)
    public void testMisalignedRegionAccess() throws Exception
    {
        try (MappedBuffer buffer = createTestFile(100, 8, 4, 0))
        {
            buffer.getPageRegion(13, 27);
        }
    }

    @Test
    public void testSequentialIterationWithPadding() throws Exception
    {
        long numValues = 1000;
        int maxPageBits = 6; // 64 bytes page
        int[] paddings = new int[] { 0, 3, 5, 7, 9, 11, 13 };

        // test different page sizes, with different padding and types
        for (int numPageBits = 3; numPageBits <= maxPageBits; numPageBits++)
        {
            for (int typeSize = 2; typeSize <= 8; typeSize *= 2)
            {
                for (int padding : paddings)
                {
                    try (MappedBuffer buffer = createTestFile(numValues, typeSize, numPageBits, padding))
                    {
                        long offset = 0;
                        for (long j = 0; j < numValues; j++)
                        {
                            switch (typeSize)
                            {
                                case 2:
                                    Assert.assertEquals(j, buffer.getShort(offset));
                                    break;

                                case 4:
                                    Assert.assertEquals(j, buffer.getInt(offset));
                                    break;

                                case 8:
                                    Assert.assertEquals(j, buffer.getLong(offset));
                                    break;

                                default:
                                    throw new AssertionError();
                            }

                            offset += typeSize + padding;
                        }
                    }
                }
            }
        }
    }

    @Test
    public void testSequentialIteration() throws IOException
    {
        long numValues = 1000;
        for (int typeSize = 2; typeSize <= 8; typeSize *= 2)
        {
            try (MappedBuffer buffer = createTestFile(numValues, typeSize, 16, 0))
            {
                for (int j = 0; j < numValues; j++)
                {
                    Assert.assertEquals(j * typeSize, buffer.position());

                    switch (typeSize)
                    {
                        case 2:
                            Assert.assertEquals(j, buffer.getShort());
                            break;

                        case 4:
                            Assert.assertEquals(j, buffer.getInt());
                            break;

                        case 8:
                            Assert.assertEquals(j, buffer.getLong());
                            break;

                        default:
                            throw new AssertionError();
                    }
                }
            }
        }
    }

    @Test
    public void testCompareToPage() throws IOException
    {
        long numValues = 100;
        int typeSize = 8;

        try (MappedBuffer buffer = createTestFile(numValues))
        {
            for (long i = 0; i < numValues * typeSize; i += typeSize)
            {
                long value = i / typeSize;
                Assert.assertEquals(0, buffer.comparePageTo(i, typeSize, LongType.instance, LongType.instance.decompose(value)));
            }
        }
    }

    @Test
    public void testOpenWithoutPageBits() throws IOException
    {
        File tmp = File.createTempFile("mapped-buffer", "tmp");
        tmp.deleteOnExit();

        RandomAccessFile file = new RandomAccessFile(tmp, "rw");

        long numValues = 1000;
        for (long i = 0; i < numValues; i++)
            file.writeLong(i);

        file.getFD().sync();

        try (MappedBuffer buffer = new MappedBuffer(file))
        {
            Assert.assertEquals(numValues * 8, buffer.limit());
            Assert.assertEquals(numValues * 8, buffer.capacity());

            for (long i = 0; i < numValues; i++)
            {
                Assert.assertEquals(i * 8, buffer.position());
                Assert.assertEquals(i, buffer.getLong());
            }
        }
        finally
        {
            FileUtils.closeQuietly(file);
        }
    }

    @Test(expected = IllegalArgumentException.class)
    public void testIncorrectPageSize() throws Exception
    {
        new MappedBuffer(null, 33);
    }

    private MappedBuffer createTestFile(long numCount) throws IOException
    {
        return createTestFile(numCount, 8, 16, 0);
    }

    private MappedBuffer createTestFile(long numCount, int typeSize, int numPageBits, int padding) throws IOException
    {
        final File testFile = File.createTempFile("mapped-buffer-test", "db");
        testFile.deleteOnExit();

        RandomAccessFile file = new RandomAccessFile(testFile, "rw");

        for (long i = 0; i < numCount; i++)
        {

            switch (typeSize)
            {
                case 1:
                    file.write((byte) i);
                    break;

                case 2:
                    file.writeShort((short) i);
                    break;

                case 4:
                    file.writeInt((int) i);
                    break;

                case 8:
                    // bunch of longs
                    file.writeLong(i);
                    break;

                default:
                    throw new IllegalArgumentException("unknown byte size: " + typeSize);
            }

            for (int j = 0; j < padding; j++)
                file.write(0);
        }

        file.getFD().sync();

        try
        {
            return new MappedBuffer(file, numPageBits);
        }
        finally
        {
            FileUtils.closeQuietly(file);
        }
    }

}
