/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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 com.android.i18n.test.timezone.internal;

import android.icu.testsharding.MainTestShard;
import android.system.ErrnoException;
import android.system.OsConstants;
import com.android.i18n.timezone.internal.BufferIterator;
import com.android.i18n.timezone.internal.MemoryMappedFile;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.Arrays;
import java.util.function.Function;
import junit.framework.TestCase;

@MainTestShard
public class MemoryMappedFileTest extends TestCase {

    private File tempDir;

    @Override
    public void setUp() throws Exception {
        super.setUp();
        tempDir = TestIoUtils.createTemporaryDirectory("MemoryMappedFileTest");
    }

    public void testMmapRo_missingFile() throws Exception {
        try {
            MemoryMappedFile.mmapRO("doesNotExist");
            fail();
        } catch (ErrnoException e) {
            assertEquals(OsConstants.ENOENT, e.errno);
        }
    }

    public void testMmapRo_emptyFile() throws Exception {
        byte[] bytes = new byte[0];
        File file = createFile(bytes);
        try {
            MemoryMappedFile.mmapRO(file.getPath());
            fail();
        } catch (ErrnoException e) {
            assertEquals(OsConstants.EINVAL, e.errno);
        } finally {
            file.delete();
        }
    }

    public void testMmapRo() throws Exception {
        byte[] bytes = createBytes(10);
        File file = createFile(bytes);
        try (MemoryMappedFile memoryMappedFile = MemoryMappedFile.mmapRO(file.getPath())) {
            assertEquals(10, memoryMappedFile.size());
        } finally {
            file.delete();
        }
    }

    public void testMmapRo_close() throws Exception {
        byte[] bytes = createBytes(10);
        File file = createFile(bytes);
        MemoryMappedFile memoryMappedFile = MemoryMappedFile.mmapRO(file.getPath());
        memoryMappedFile.close();

        try {
            memoryMappedFile.bigEndianIterator();
            fail();
        } catch (IllegalStateException expected) {
        }

        try {
            memoryMappedFile.littleEndianIterator();
            fail();
        } catch (IllegalStateException expected) {
        }

        // Should not have any effect.
        memoryMappedFile.close();
    }

    public void testReadAfterCloseFails() throws Exception {
        byte[] bytes = createBytes(10);
        File file = createFile(bytes);
        MemoryMappedFile memoryMappedFile = MemoryMappedFile.mmapRO(file.getPath());
        BufferIterator iterator = memoryMappedFile.bigEndianIterator();
        memoryMappedFile.close();

        try {
            iterator.readByte();
            fail();
        } catch (IllegalStateException expected) {}
    }

    public void testReadByte() throws Exception {
        checkReadByte(MemoryMappedFile::bigEndianIterator);
        checkReadByte(MemoryMappedFile::littleEndianIterator);
    }

    private void checkReadByte(
            Function<MemoryMappedFile, BufferIterator> iteratorFactory) throws Exception {

        byte[] bytes = createBytes(10);
        File file = createFile(bytes);
        try {
            MemoryMappedFile mappedFile = MemoryMappedFile.mmapRO(file.getPath());
            BufferIterator iterator = iteratorFactory.apply(mappedFile);
            for (int i = 0; i < bytes.length; i++) {
                assertReadByteSucceeds(iterator, bytes[i]);
            }

            // Check skip.
            iterator.seek(0);
            for (int i = 0; i < bytes.length; i += 2) {
                assertReadByteSucceeds(iterator, bytes[i]);
                iterator.skip(1);
            }
        } finally {
            file.delete();
        }
    }

    public void testSeek() throws Exception {
        checkSeek(MemoryMappedFile::bigEndianIterator);
        checkSeek(MemoryMappedFile::littleEndianIterator);
    }

    private void checkSeek(
            Function<MemoryMappedFile, BufferIterator> iteratorFactory) throws Exception {

        byte[] bytes = createBytes(10);
        File file = createFile(bytes);
        try {
            MemoryMappedFile mappedFile = MemoryMappedFile.mmapRO(file.getPath());
            BufferIterator iterator = iteratorFactory.apply(mappedFile);
            seekRead(bytes, iterator, 2);

            seekRead(bytes, iterator, 0);

            seekRead(bytes, iterator, 1);

            seekRead(bytes, iterator, 9);

            seekReadExpectFailure(iterator, -1);

            seekRead(bytes, iterator, 1);

            seekReadExpectFailure(iterator, 10);
            seekReadExpectFailure(iterator, Integer.MAX_VALUE);
            seekReadExpectFailure(iterator, Integer.MIN_VALUE);
        } finally {
            file.delete();
        }
    }

    private static void seekRead(byte[] bytes, BufferIterator iterator, int offset) {
        iterator.seek(offset);
        assertEquals(offset, iterator.pos());
        assertReadByteSucceeds(iterator, bytes[offset]);
    }

    private static void seekReadExpectFailure(BufferIterator iterator, int offset) {
        iterator.seek(offset);
        assertReadByteFails(iterator);
    }

    public void testSkip() throws Exception {
        checkSkip(MemoryMappedFile::bigEndianIterator);
        checkSkip(MemoryMappedFile::littleEndianIterator);
    }

    private void checkSkip(
            Function<MemoryMappedFile, BufferIterator> iteratorFactory) throws Exception {

        byte[] bytes = createBytes(10);
        File file = createFile(bytes);
        try {
            MemoryMappedFile mappedFile = MemoryMappedFile.mmapRO(file.getPath());
            BufferIterator iterator = iteratorFactory.apply(mappedFile);
            iterator.skip(1);
            assertEquals(1, iterator.pos());
            assertReadByteSucceeds(iterator, bytes[1]);

            iterator.skip(-1);
            assertEquals(1, iterator.pos());
            assertReadByteSucceeds(iterator, bytes[1]);

            iterator.skip(2);
            assertEquals(4, iterator.pos());
            assertReadByteSucceeds(iterator, bytes[4]);

            iterator.skip(-2);
            assertEquals(3, iterator.pos());
            assertReadByteSucceeds(iterator, bytes[3]);

            iterator.skip(3);
            assertEquals(7, iterator.pos());
            assertReadByteSucceeds(iterator, bytes[7]);

            iterator.skip(-3);
            assertEquals(5, iterator.pos());
            assertReadByteSucceeds(iterator, bytes[5]);

            iterator.skip(4);
            assertEquals(10, iterator.pos());
            assertReadByteFails(iterator);

            iterator.skip(-1);
            assertEquals(9, iterator.pos());
            assertReadByteSucceeds(iterator, bytes[9]);
        } finally {
            file.delete();
        }
    }

    public void testReadShort_bigEndian() throws Exception {
        byte[] bytes = createBytes(10);
        File file = createFile(bytes);
        try {
            MemoryMappedFile mappedFile = MemoryMappedFile.mmapRO(file.getPath());
            BufferIterator iterator = mappedFile.bigEndianIterator();

            // Even offset
            short expectedValue = (short) ((bytes[0] << 8) | bytes[1]);
            assertReadShortSucceeds(iterator, expectedValue);

            checkShortFailureCases(iterator);

            // Odd offset.
            iterator.seek(1);
            expectedValue = (short) ((bytes[1] << 8) | bytes[2]);
            assertReadShortSucceeds(iterator, expectedValue);
        } finally {
            file.delete();
        }
    }

    public void testReadShort_littleEndian() throws Exception {
        byte[] bytes = createBytes(10);
        File file = createFile(bytes);
        try {
            MemoryMappedFile mappedFile = MemoryMappedFile.mmapRO(file.getPath());
            BufferIterator iterator = mappedFile.littleEndianIterator();

            // Even offset
            short expectedValue = (short) ((bytes[1] << 8) | bytes[0]);
            assertReadShortSucceeds(iterator, expectedValue);

            checkShortFailureCases(iterator);

            // Odd offset.
            iterator.seek(1);
            expectedValue = (short) ((bytes[2] << 8) | bytes[1]);
            assertReadShortSucceeds(iterator, expectedValue);
        } finally {
            file.delete();
        }
    }

    private static void checkShortFailureCases(BufferIterator iterator) {
        // Partly before bounds.
        iterator.seek(-1);
        assertReadShortFails(iterator);

        // Entirely before bounds.
        iterator.seek(-2);
        assertReadShortFails(iterator);

        // Partly after bounds.
        iterator.seek(9);
        assertReadShortFails(iterator);

        // Entirely after bounds.
        iterator.seek(10);
        assertReadShortFails(iterator);
    }

    public void testReadInt_bigEndian() throws Exception {
        byte[] bytes = createBytes(10);
        File file = createFile(bytes);
        try {
            MemoryMappedFile mappedFile = MemoryMappedFile.mmapRO(file.getPath());
            BufferIterator iterator = mappedFile.bigEndianIterator();

            // Even offset
            int expectedValue = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
            assertReadIntSucceeds(iterator, expectedValue);

            checkIntFailureCases(iterator);

            // Odd offset.
            iterator.seek(1);
            expectedValue = (bytes[1] << 24) | (bytes[2] << 16) | (bytes[3] << 8) | bytes[4];
            assertReadIntSucceeds(iterator, expectedValue);
        } finally {
            file.delete();
        }
    }

    public void testReadInt_littleEndian() throws Exception {
        byte[] bytes = createBytes(10);
        File file = createFile(bytes);
        try {
            MemoryMappedFile mappedFile = MemoryMappedFile.mmapRO(file.getPath());
            BufferIterator iterator = mappedFile.littleEndianIterator();

            // Even offset
            int expectedValue = (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0];
            assertReadIntSucceeds(iterator, expectedValue);

            checkIntFailureCases(iterator);

            // Odd offset.
            iterator.seek(1);
            expectedValue = (bytes[4] << 24) | (bytes[3] << 16) | (bytes[2] << 8) | bytes[1];
            assertReadIntSucceeds(iterator, expectedValue);
        } finally {
            file.delete();
        }
    }

    private static void checkIntFailureCases(BufferIterator iterator) {
        // Partly before bounds.
        iterator.seek(-1);
        assertReadIntFails(iterator);

        // Entirely before bounds.
        iterator.seek(-4);
        assertReadIntFails(iterator);

        // Partly after bounds.
        iterator.seek(7);
        assertReadIntFails(iterator);

        // Entirely after bounds.
        iterator.seek(10);
        assertReadIntFails(iterator);
    }

    public void testReadIntArray() throws Exception {
        checkReadIntArray(MemoryMappedFile::bigEndianIterator, ByteOrder.BIG_ENDIAN);
        checkReadIntArray(MemoryMappedFile::littleEndianIterator, ByteOrder.LITTLE_ENDIAN);
    }

    private void checkReadIntArray(
            Function<MemoryMappedFile, BufferIterator> iteratorFactory,
            ByteOrder byteOrdering) throws Exception {

        byte[] testBytes = createBytes(12);
        File file = createFile(testBytes);
        try {
            MemoryMappedFile mappedFile = MemoryMappedFile.mmapRO(file.getPath());
            BufferIterator iterator = iteratorFactory.apply(mappedFile);

            // Even offsets.
            iterator.seek(4);
            assertReadIntArraySucceeds(iterator, testBytes, byteOrdering, 2 /* intCount */);

            iterator.seek(0);
            assertReadIntArraySucceeds(iterator, testBytes, byteOrdering, 3 /* intCount */);

            checkIntArrayZeroReadCases(iterator);

            // Odd offsets.
            iterator.seek(1);
            assertReadIntArraySucceeds(iterator, testBytes, byteOrdering, 2 /* intCount */);
            iterator.seek(3);
            assertReadIntArraySucceeds(iterator, testBytes, byteOrdering, 2 /* intCount */);
        } finally {
            file.delete();
        }
    }

    private static void checkIntArrayZeroReadCases(BufferIterator iterator) {
        // Zero length reads do nothing.
        int posBeforeRead = iterator.pos();
        int[] dstWithExistingValues = new int[] { 111, 222 };
        iterator.readIntArray(dstWithExistingValues, 0, 0);
        assertEquals(posBeforeRead, iterator.pos());
        assertArrayEquals(new int[] { 111, 222 }, dstWithExistingValues);

        try {
            iterator.readIntArray(null, 0, 0);
            fail();
        } catch (NullPointerException expected) {
        }
        assertEquals(posBeforeRead, iterator.pos());

        int[] dst = new int[2];

        // Partly before bounds.
        iterator.seek(-1);
        assertReadIntArrayFails(iterator, dst, 0, 1);

        // Entirely before bounds.
        iterator.seek(-2);
        assertReadIntArrayFails(iterator, dst, 0, 1);

        // Partly after bounds.
        iterator.seek(9);
        assertReadIntArrayFails(iterator, dst, 0, 1);

        // Entirely after bounds.
        iterator.seek(12);
        assertReadIntArrayFails(iterator, dst, 0, 1);

        // dst too small.
        assertReadIntArrayFails(iterator, dst, 0, 3); // dst can only hold 2 ints

        // offset leaves dst too small.
        assertReadIntArrayFails(iterator, dst, 1, 2);

        // Invalid offset
        assertReadIntArrayFails(iterator, dst, -1, 2);
        assertReadIntArrayFails(iterator, dst, 2, 2);

        // Null dst
        try {
            iterator.readIntArray(null, 0, 1);
            fail();
        } catch (NullPointerException expected) {
        }
    }

    public void testReadByteArray() throws Exception {
        checkReadByteArray(MemoryMappedFile::bigEndianIterator);
        checkReadByteArray(MemoryMappedFile::littleEndianIterator);
    }

    private void checkReadByteArray(
            Function<MemoryMappedFile, BufferIterator> iteratorFactory) throws Exception {

        byte[] testBytes = createBytes(12);
        File file = createFile(testBytes);
        try {
            MemoryMappedFile mappedFile = MemoryMappedFile.mmapRO(file.getPath());
            BufferIterator iterator = iteratorFactory.apply(mappedFile);

            // Even offsets.
            iterator.seek(4);
            assertReadByteArraySucceeds(iterator, testBytes, 2 /* intCount */);

            iterator.seek(0);
            assertReadByteArraySucceeds(iterator, testBytes, 3 /* intCount */);

            checkByteArrayZeroReadCases(iterator);

            // Odd offsets.
            iterator.seek(1);
            assertReadByteArraySucceeds(iterator, testBytes, 2 /* intCount */);
            iterator.seek(3);
            assertReadByteArraySucceeds(iterator, testBytes, 2 /* intCount */);
        } finally {
            file.delete();
        }
    }

    private static void checkByteArrayZeroReadCases(BufferIterator iterator) {
        // Zero length reads do nothing.
        int posBeforeRead = iterator.pos();
        byte[] dstWithExistingValues = new byte[] { 11, 22, 33, 44, 55, 66, 77, 88 };
        iterator.readByteArray(dstWithExistingValues, 0, 0);
        assertEquals(posBeforeRead, iterator.pos());
        assertArrayEquals(new byte[] { 11, 22, 33, 44, 55, 66, 77, 88 }, dstWithExistingValues);

        try {
            iterator.readByteArray(null, 0, 0);
            fail();
        } catch (NullPointerException expected) {
        }
        assertEquals(posBeforeRead, iterator.pos());

        byte[] dst = new byte[10];

        // Before bounds.
        iterator.seek(-1);
        assertReadByteArrayFails(iterator, dst, 0, 1);

        // After bounds.
        iterator.seek(12);
        assertReadByteArrayFails(iterator, dst, 0, 1);

        // dst too small.
        assertReadByteArrayFails(iterator, dst, 0, 11); // dst can only hold 10 bytes

        // offset leaves dst too small.
        assertReadByteArrayFails(iterator, dst, 1, 10);

        // Invalid offset
        assertReadByteArrayFails(iterator, dst, -1, 2);
        assertReadByteArrayFails(iterator, dst, 2, 2);

        // Null dst
        try {
            iterator.readByteArray(null, 0, 1);
            fail();
        } catch (NullPointerException expected) {
        }
    }

    private static void assertReadByteArrayFails(
            BufferIterator iterator, byte[] dst, int offset, int intCount) {

        int posBefore = iterator.pos();
        try {
            iterator.readByteArray(dst, offset, intCount);
            fail();
        } catch (IndexOutOfBoundsException expected) {
        }
        assertEquals(posBefore, iterator.pos());
    }

    private static void assertReadByteArraySucceeds(
            BufferIterator iterator, byte[] underlyingData, int byteCount) {

        int posBefore = iterator.pos();

        // Create a byte[] containing book-end bytes we don't expect to be touched:
        // [Byte.MAX_VALUE, {the bytes we expect from underlyingData from posBefore onward},
        // Byte.MIN_VALUE].
        byte[] expectedBytes = new byte[byteCount + 2];
        expectedBytes[0] = Byte.MAX_VALUE;
        expectedBytes[byteCount - 1] = Byte.MIN_VALUE;
        System.arraycopy(underlyingData, posBefore, expectedBytes, 1, byteCount);

        // Get the true data.
        byte[] dst = new byte[byteCount + 2];
        // Copy the two bytes we expect to be untouched.
        dst[0] = expectedBytes[0];
        dst[byteCount - 1] = expectedBytes[byteCount - 1];
        // Do the read.
        iterator.readByteArray(dst, 1, byteCount);

        assertArrayEquals(expectedBytes, dst);
        assertEquals(posBefore + byteCount, iterator.pos());
    }

    private static void assertReadIntArrayFails(
            BufferIterator iterator, int[] dst, int offset, int intCount) {

        int posBefore = iterator.pos();
        try {
            iterator.readIntArray(dst, offset, intCount);
            fail();
        } catch (IndexOutOfBoundsException expected) {
        }
        assertEquals(posBefore, iterator.pos());
    }

    private static void assertReadIntArraySucceeds(
            BufferIterator iterator, byte[] underlyingData, ByteOrder byteOrder, int intCount) {

        int posBefore = iterator.pos();

        // Create an int[] containing book-end ints we don't expect to be touched:
        // [Integer.MAX_VALUE, {the ints we expect from underlyingData from posBefore onward},
        // Integer.MIN_VALUE].

        // Create an IntBuffer containing the ints we'd expect from underlyingData from posBefore
        // onward.
        ByteBuffer byteBuffer = ByteBuffer.wrap(underlyingData);
        byteBuffer.position(posBefore);
        IntBuffer expectedIntsBuffer = byteBuffer.slice().order(byteOrder).asIntBuffer();
        assertEquals(byteOrder, expectedIntsBuffer.order());

        // Copy the ints we expect.
        int[] expectedInts = new int[intCount + 2];
        expectedInts[0] = Integer.MAX_VALUE;
        expectedInts[intCount - 1] = Integer.MIN_VALUE;
        expectedIntsBuffer.get(expectedInts, 1, intCount);

        // Get the true data.
        int[] dst = new int[intCount + 2];
        dst[0] = expectedInts[0];
        dst[intCount - 1] = expectedInts[intCount - 1];
        iterator.readIntArray(dst, 1, intCount);

        assertArrayEquals(expectedInts, dst);
        assertEquals(posBefore + (intCount * Integer.BYTES), iterator.pos());
    }

    private static void assertReadIntFails(BufferIterator iterator) {
        int posBefore = iterator.pos();
        try {
            iterator.readInt();
            fail();
        } catch (IndexOutOfBoundsException expected) {
        }
        assertEquals(posBefore, iterator.pos());
    }

    private static void assertReadIntSucceeds(BufferIterator iterator, int expectedValue) {
        int posBefore = iterator.pos();
        assertEquals(expectedValue, iterator.readInt());
        assertEquals(posBefore + Integer.BYTES, iterator.pos());
    }

    private static void assertReadShortFails(BufferIterator iterator) {
        int posBefore = iterator.pos();
        try {
            iterator.readShort();
            fail();
        } catch (IndexOutOfBoundsException expected) {
        }
        assertEquals(posBefore, iterator.pos());
    }

    private static void assertReadShortSucceeds(BufferIterator iterator, short expectedValue) {
        int posBefore = iterator.pos();
        assertEquals(expectedValue, iterator.readShort());
        assertEquals(posBefore + Short.BYTES, iterator.pos());
    }

    private static void assertReadByteFails(BufferIterator iterator) {
        int posBefore = iterator.pos();
        try {
            iterator.readByte();
            fail();
        } catch (IndexOutOfBoundsException expected) {
        }
        // Must not advance pos.
        assertEquals(posBefore, iterator.pos());
    }

    private static void assertReadByteSucceeds(BufferIterator iterator, byte expectedValue) {
        int posBefore = iterator.pos();
        assertEquals(expectedValue, iterator.readByte());
        assertEquals(posBefore + 1, iterator.pos());
    }

    private static void assertArrayEquals(int[] expected, int[] actual) {
        assertEquals(Arrays.toString(expected), Arrays.toString(actual));
    }

    private static void assertArrayEquals(byte[] expected, byte[] actual) {
        assertEquals(Arrays.toString(expected), Arrays.toString(actual));
    }

    private static byte[] createBytes(int byteCount) {
        byte[] bytes = new byte[byteCount];
        for (int i = 0; i < byteCount; i++) {
            bytes[i] = (byte) i;
        }
        return bytes;
    }

    private File createFile(byte[] bytes) throws Exception {
        File file = File.createTempFile("bytes", null, tempDir);
        try (FileOutputStream fos = new FileOutputStream(file)) {
            fos.write(bytes);
        }
        return file;
    }
}
