/*
 * Licensed 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 io.airlift.slice;

import org.testng.Assert;
import org.testng.annotations.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;

import static io.airlift.slice.SizeOf.SIZE_OF_BYTE;
import static io.airlift.slice.SizeOf.SIZE_OF_DOUBLE;
import static io.airlift.slice.SizeOf.SIZE_OF_FLOAT;
import static io.airlift.slice.SizeOf.SIZE_OF_INT;
import static io.airlift.slice.SizeOf.SIZE_OF_LONG;
import static io.airlift.slice.SizeOf.SIZE_OF_SHORT;
import static io.airlift.slice.Slices.EMPTY_SLICE;
import static io.airlift.slice.Slices.utf8Slice;
import static java.lang.Double.doubleToLongBits;
import static java.lang.Double.longBitsToDouble;
import static java.lang.Float.floatToIntBits;
import static java.lang.Float.intBitsToFloat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

public class TestSlice
{
    @Test
    public void testFillAndClear()
    {
        for (byte size = 0; size < 100; size++) {
            Slice slice = allocate(size);
            for (int i = 0; i < slice.length(); i++) {
                assertEquals(slice.getByte(i), (byte) 0);
            }
            slice.fill((byte) 0xA5);
            for (int i = 0; i < slice.length(); i++) {
                assertEquals(slice.getByte(i), (byte) 0xA5);
            }
            slice.clear();
            for (int i = 0; i < slice.length(); i++) {
                assertEquals(slice.getByte(i), (byte) 0);
            }
        }
    }

    @Test
    public void testEqualsHashCodeCompare()
    {
        for (int size = 0; size < 100; size++) {
            // self equals
            Slice slice = allocate(size);
            assertSlicesEquals(slice, slice);

            // equals other all zero
            Slice other = allocate(size);
            assertSlicesEquals(slice, other);

            // equals self fill pattern
            slice = allocate(size); // create a new slice since slices cache the hash code value
            slice.fill((byte) 0xA5);
            assertSlicesEquals(slice, slice);

            // equals other fill pattern
            other = allocate(size); // create a new slice since slices cache the hash code value
            other.fill((byte) 0xA5);
            assertSlicesEquals(slice, other);

            // different types
            assertNotEquals(slice, new Object());
            //noinspection MisorderedAssertEqualsArgumentsTestNG
            assertNotEquals(new Object(), slice);

            // different sizes
            Slice oneBigger = allocate(size + 1);
            oneBigger.fill((byte) 0xA5);
            assertNotEquals(slice, oneBigger);
            assertNotEquals(oneBigger, slice);
            assertTrue(slice.compareTo(oneBigger) < 0);
            assertTrue(oneBigger.compareTo(slice) > 0);
            assertFalse(slice.equals(0, size, oneBigger, 0, size + 1));
            assertFalse(oneBigger.equals(0, size + 1, slice, 0, size));
            assertTrue(slice.compareTo(0, size, oneBigger, 0, size + 1) < 0);
            assertTrue(oneBigger.compareTo(0, size + 1, slice, 0, size) > 0);

            // different in one byte
            for (int i = 1; i < slice.length(); i++) {
                slice.setByte(i - 1, 0xA5);
                assertTrue(slice.equals(i - 1, size - i, other, i - 1, size - i));
                slice.setByte(i, 0xFF);
                assertNotEquals(slice, other);
                assertFalse(slice.equals(i, size - i, other, i, size - i));
                assertTrue(slice.compareTo(0, size, oneBigger, 0, size + 1) > 0);
            }

            // compare with empty slice
            if (slice.length() > 0) {
                assertNotEquals(slice, EMPTY_SLICE);
                //noinspection MisorderedAssertEqualsArgumentsTestNG
                assertNotEquals(EMPTY_SLICE, slice);

                assertFalse(slice.equals(0, size, EMPTY_SLICE, 0, 0));
                assertFalse(EMPTY_SLICE.equals(0, 0, slice, 0, size));

                assertTrue(slice.compareTo(0, size, EMPTY_SLICE, 0, 0) > 0);
                assertTrue(EMPTY_SLICE.compareTo(0, 0, slice, 0, size) < 0);

                try {
                    //noinspection ResultOfMethodCallIgnored
                    slice.equals(0, size, EMPTY_SLICE, 0, size);
                    fail("expected IndexOutOfBoundsException");
                }
                catch (IndexOutOfBoundsException expected) {
                }
                try {
                    //noinspection ResultOfMethodCallIgnored
                    EMPTY_SLICE.equals(0, size, slice, 0, size);
                    fail("expected IndexOutOfBoundsException");
                }
                catch (IndexOutOfBoundsException expected) {
                }

                try {
                    slice.compareTo(0, size, EMPTY_SLICE, 0, size);
                    fail("expected IndexOutOfBoundsException");
                }
                catch (IndexOutOfBoundsException expected) {
                }
                try {
                    EMPTY_SLICE.compareTo(0, size, slice, 0, size);
                    fail("expected IndexOutOfBoundsException");
                }
                catch (IndexOutOfBoundsException expected) {
                }
            }
        }
    }

    private static void assertSlicesEquals(Slice slice, Slice other)
    {
        int size = slice.length();

        assertEquals(slice, other);
        assertTrue(slice.equals(0, size, other, 0, size));
        assertEquals(slice.hashCode(), other.hashCode());
        assertEquals(slice.hashCode(), other.hashCode(0, size));
        assertEquals(slice.compareTo(other), 0);
        assertEquals(slice.compareTo(0, size, other, 0, size), 0);
        for (int i = 0; i < slice.length(); i++) {
            assertTrue(slice.equals(i, size - i, other, i, size - i));
            assertEquals(slice.hashCode(i, size - i), other.hashCode(i, size - i));
            assertEquals(slice.compareTo(i, size - i, other, i, size - i), 0);
        }
        for (int i = 0; i < slice.length(); i++) {
            assertTrue(slice.equals(0, size - i, other, 0, size - i));
            assertEquals(slice.hashCode(0, size - i), other.hashCode(0, size - i));
            assertEquals(slice.compareTo(0, size - i, other, 0, size - i), 0);
        }
    }

    @Test
    public void testToString()
    {
        assertEquals(Slices.copiedBuffer("apple", UTF_8).toString(UTF_8), "apple");

        for (int size = 0; size < 100; size++) {
            for (int index = 0; index < size; index++) {
                assertToStrings(allocate(size), index);
            }
        }
    }

    @Test
    public void testUtf8Conversion()
    {
        String s = "apple \u2603 snowman";
        Slice slice = Slices.copiedBuffer(s, UTF_8);

        assertEquals(Slices.utf8Slice(s), slice);
        assertEquals(slice.toStringUtf8(), s);
        assertEquals(Slices.utf8Slice(s).toStringUtf8(), s);
    }

    @SuppressWarnings("CharUsedInArithmeticContext")
    private static void assertToStrings(Slice slice, int index)
    {
        // fill slice with FF
        slice.fill((byte) 0xFF);

        // set and get the value
        char[] chars = new char[(slice.length() - index) / 2];
        for (int i = 0; i < chars.length; i++) {
            chars[i] = (char) ('a' + (i % 26));
        }
        String string = new String(chars);
        Slice value = Slices.copiedBuffer(string, UTF_8);
        slice.setBytes(index, value);
        assertEquals(slice.toString(index, value.length(), UTF_8), string);

        for (int length = 0; length < value.length(); length++) {
            slice.fill((byte) 0xFF);
            slice.setBytes(index, value, 0, length);
            assertEquals(slice.toString(index, length, UTF_8), string.substring(0, length));
        }
    }

    @Test
    public void testByte()
    {
        for (byte size = 0; size < 100; size++) {
            for (byte index = 0; index < (size - SIZE_OF_BYTE); index++) {
                assertByte(allocate(size), index);
            }
        }
    }

    private static void assertByte(Slice slice, byte index)
    {
        // fill slice with FF
        slice.fill((byte) 0xFF);

        // set and get unsigned value
        slice.setByte(index, 0xA5);
        assertEquals(slice.getUnsignedByte(index), 0x0000_00A5);

        // set and get the value
        slice.setByte(index, 0xA5);
        assertEquals(slice.getByte(index), (byte) 0xA5);

        try {
            slice.getByte(-1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getByte((slice.length() - SIZE_OF_BYTE) + 1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getByte(slice.length());
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getByte(slice.length() + 1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }
    }

    @Test
    public void testShort()
    {
        for (short size = 0; size < 100; size++) {
            for (short index = 0; index < (size - SIZE_OF_SHORT); index++) {
                assertShort(allocate(size), index);
            }
        }
    }

    private static void assertShort(Slice slice, short index)
    {
        // fill slice with FF
        slice.fill((byte) 0xFF);

        // set and get the value
        slice.setShort(index, 0xAA55);
        assertEquals(slice.getShort(index), (short) 0xAA55);

        try {
            slice.getShort(-1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getShort((slice.length() - SIZE_OF_SHORT) + 1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getShort(slice.length());
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getShort(slice.length() + 1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }
    }

    @Test
    public void testInt()
    {
        for (int size = 0; size < 100; size++) {
            for (int index = 0; index < (size - SIZE_OF_INT); index++) {
                assertInt(allocate(size), index);
            }
        }
    }

    private static void assertInt(Slice slice, int index)
    {
        // fill slice with FF
        slice.fill((byte) 0xFF);

        // set and get the value
        slice.setInt(index, 0xAAAA_5555);
        assertEquals(slice.getInt(index), 0xAAAA_5555);

        try {
            slice.getInt(-1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getInt((slice.length() - SIZE_OF_INT) + 1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getInt(slice.length());
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getInt(slice.length() + 1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }
    }

    @Test
    public void testLong()
    {
        for (int size = 0; size < 100; size++) {
            for (int index = 0; index < (size - SIZE_OF_LONG); index++) {
                assertLong(allocate(size), index);
            }
        }
    }

    private static void assertLong(Slice slice, int index)
    {
        // fill slice with FF
        slice.fill((byte) 0xFF);

        // set and get the value
        slice.setLong(index, 0xAAAA_AAAA_5555_5555L);
        assertEquals(slice.getLong(index), 0xAAAA_AAAA_5555_5555L);

        try {
            slice.getLong(-1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getLong((slice.length() - SIZE_OF_LONG) + 1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getLong(slice.length());
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getLong(slice.length() + 1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }
    }

    @Test
    public void testFloat()
    {
        for (int size = 0; size < 100; size++) {
            for (int index = 0; index < (size - SIZE_OF_FLOAT); index++) {
                assertFloat(allocate(size), index);
            }
        }
    }

    private static void assertFloat(Slice slice, int index)
    {
        // fill slice with FF
        slice.fill((byte) 0xFF);

        // set and get the value
        slice.setFloat(index, intBitsToFloat(0xAAAA_5555));
        assertEquals(floatToIntBits(slice.getFloat(index)), 0xAAAA_5555);

        try {
            slice.getFloat(-1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getFloat((slice.length() - SIZE_OF_FLOAT) + 1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getFloat(slice.length());
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getFloat(slice.length() + 1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }
    }

    @Test
    public void testDouble()
    {
        for (int size = 0; size < 100; size++) {
            for (int index = 0; index < (size - SIZE_OF_DOUBLE); index++) {
                assertDouble(allocate(size), index);
            }
        }
    }

    private static void assertDouble(Slice slice, int index)
    {
        // fill slice with FF
        slice.fill((byte) 0xFF);

        // set and get the value
        slice.setDouble(index, longBitsToDouble(0xAAAA_AAAA_5555_5555L));
        assertEquals(doubleToLongBits(slice.getDouble(index)), 0xAAAA_AAAA_5555_5555L);

        try {
            slice.getDouble(-1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getDouble((slice.length() - SIZE_OF_DOUBLE) + 1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getDouble(slice.length());
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }

        try {
            slice.getDouble(slice.length() + 1);
            fail("expected IndexOutOfBoundsException");
        }
        catch (IndexOutOfBoundsException e) {
        }
    }

    @Test
    public void testBytesArray()
    {
        for (int size = 0; size < 100; size++) {
            for (int index = 0; index < size; index++) {
                assertBytesArray(allocate(size), index);
            }
        }
    }

    private static void assertBytesArray(Slice slice, int index)
    {
        // fill slice with FF
        slice.fill((byte) 0xFF);

        byte[] value = new byte[slice.length()];
        Arrays.fill(value, (byte) 0xFF);
        assertEquals(slice.getBytes(), value);

        // set and get the value
        value = new byte[(slice.length() - index) / 2];
        for (int i = 0; i < value.length; i++) {
            value[i] = (byte) i;
        }
        slice.setBytes(index, value);
        assertEquals(slice.getBytes(index, value.length), value);

        for (int length = 0; length < value.length; length++) {
            slice.fill((byte) 0xFF);
            slice.setBytes(index, value, 0, length);
            assertEquals(slice.getBytes(index, length), Arrays.copyOf(value, length));
        }
    }

    @Test
    public void testBytesSlice()
    {
        for (int size = 0; size < 100; size++) {
            for (int index = 0; index < size; index++) {
                assertBytesSlice(allocate(size), index);
            }
        }
    }

    private void assertBytesSlice(Slice slice, int index)
    {
        // fill slice with FF
        slice.fill((byte) 0xFF);

        // compare to self slice
        assertEquals(slice.slice(0, slice.length()), slice);
        Slice value = allocate(slice.length());
        slice.getBytes(0, value, 0, slice.length());
        assertEquals(value, slice);

        // set and get the value
        value = allocate((slice.length() - index) / 2);
        for (int i = 0; i < value.length(); i++) {
            value.setByte(i, i);
        }

        // check by slicing out the region
        slice.setBytes(index, value);
        assertEquals(value, slice.slice(index, value.length()));

        // check by getting out the region
        Slice tempValue = allocate(value.length());
        slice.getBytes(index, tempValue, 0, tempValue.length());
        assertEquals(tempValue, slice.slice(index, tempValue.length()));
        assertTrue(tempValue.equals(0, tempValue.length(), slice, index, tempValue.length()));

        for (int length = 0; length < value.length(); length++) {
            slice.fill((byte) 0xFF);
            slice.setBytes(index, value, 0, length);

            // check by slicing out the region
            assertEquals(value.slice(0, length), slice.slice(index, length));
            assertTrue(value.equals(0, length, slice, index, length));

            // check by getting out the region
            tempValue = allocate(length);
            slice.getBytes(index, tempValue);
            assertEquals(tempValue, slice.slice(index, length));
            assertTrue(tempValue.equals(0, length, slice, index, length));
        }
    }

    @Test
    public void testBytesStreams()
            throws Exception
    {
        for (int size = 0; size < 100; size++) {
            for (int index = 0; index < size; index++) {
                assertBytesStreams(allocate(size), index);
            }
        }
        assertBytesStreams(allocate(16 * 1024), 3);
    }

    private static void assertBytesStreams(Slice slice, int index)
            throws Exception
    {
        // fill slice with FF
        slice.fill((byte) 0xFF);

        byte[] value = new byte[slice.length()];
        Arrays.fill(value, (byte) 0xFF);
        assertEquals(slice.getBytes(), value);

        // set and get the value
        value = new byte[(slice.length() - index) / 2];
        for (int i = 0; i < value.length; i++) {
            value[i] = (byte) i;
        }
        slice.setBytes(index, new ByteArrayInputStream(value), value.length);
        assertEquals(slice.getBytes(index, value.length), value);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        slice.getBytes(index, out, value.length);
        assertEquals(slice.getBytes(index, value.length), out.toByteArray());

        for (int length = 0; length < value.length; length++) {
            slice.fill((byte) 0xFF);
            slice.setBytes(index, new ByteArrayInputStream(value), length);
            assertEquals(slice.getBytes(index, length), Arrays.copyOf(value, length));

            out = new ByteArrayOutputStream();
            slice.getBytes(index, out, length);
            assertEquals(slice.getBytes(index, length), out.toByteArray());
        }
    }

    @Test
    public void testMemoryMappedReads()
            throws IOException
    {
        Path path = Files.createTempFile("longs", null);
        List<Long> values = createRandomLongs(20000);

        Slice output = allocate(values.size() * (int) SIZE_OF_LONG);
        for (int i = 0; i < values.size(); i++) {
            output.setLong(i * SIZE_OF_LONG, values.get(i));
        }

        Files.write(path, output.getBytes());

        Slice slice = Slices.mapFileReadOnly(path.toFile());
        for (int i = 0; i < values.size(); i++) {
            long actual = slice.getLong(i * SIZE_OF_LONG);
            long expected = values.get(i);
            assertEquals(actual, expected);
        }

        assertEquals(slice.getBytes(), output.getBytes());
    }

    @Test
    public void testCopyOf()
            throws Exception
    {
        // slightly stronger guarantees for empty slice
        assertSame(Slices.copyOf(EMPTY_SLICE), EMPTY_SLICE);
        assertSame(Slices.copyOf(Slices.utf8Slice("hello world"), 1, 0), EMPTY_SLICE);

        Slice slice = Slices.utf8Slice("hello world");
        assertEquals(Slices.copyOf(slice), slice);
        assertEquals(Slices.copyOf(slice, 1, 3), slice.slice(1, 3));

        // verify it's an actual copy
        Slice original = Slices.utf8Slice("hello world");
        Slice copy = Slices.copyOf(original);

        original.fill((byte) 0);
        assertEquals(copy, Slices.utf8Slice("hello world"));

        // read before beginning
        try {
            Slices.copyOf(slice, -1, slice.length());
            Assert.fail();
        }
        catch (IndexOutOfBoundsException ignored) {
        }

        // read after end
        try {
            Slices.copyOf(slice, slice.length() + 1, 1);
            Assert.fail();
        }
        catch (IndexOutOfBoundsException ignored) {
        }

        // start before but extend past end
        try {
            Slices.copyOf(slice, 1, slice.length());
            Assert.fail();
        }
        catch (IndexOutOfBoundsException ignored) {
        }


    }

    private static List<Long> createRandomLongs(int count)
    {
        Random random = new Random();
        List<Long> list = new ArrayList<>(count);
        for (int i = 0; i < count; i++) {
            list.add(random.nextLong());
        }
        return Collections.unmodifiableList(list);
    }

    protected Slice allocate(int size)
    {
        return Slices.allocate(size);
    }
}
