/*
 * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import java.lang.reflect.Field;
import sun.misc.Unsafe;

/*
 * @test
 * @summary Test Unsafe.copySwapMemory
 * @modules java.base/jdk.internal.misc
 */
public class CopySwap {
    private static final boolean DEBUG = Boolean.getBoolean("CopySwap.DEBUG");

    public static final long KB = 1024;
    public static final long MB = KB * 1024;
    public static final long GB = MB * 1024;

    private static final Unsafe UNSAFE;
    private static final int SMALL_COPY_SIZE = 32;
    private static final int BASE_ALIGNMENT = 16;

    static {
        try {
            Field f = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            UNSAFE = (sun.misc.Unsafe) f.get(null);
        } catch (Exception e) {
            throw new RuntimeException("Unable to get Unsafe instance.", e);
        }
    }

    private static long alignDown(long value, long alignment) {
        return value & ~(alignment - 1);
    }

    private static long alignUp(long value, long alignment) {
        return (value + alignment - 1) & ~(alignment - 1);
    }

    private static boolean isAligned(long value, long alignment) {
        return value == alignDown(value, alignment);
    }

    private static long toUnsignedLong(byte x) {
        return ((long) x) & 0xffL;
    }

    private static long toUnsignedLong(short x) {
        return ((long) x) & 0xffffL;
    }

    private static long toUnsignedLong(int x) {
        return ((long) x) & 0xffffffffL;
    }

    private CopySwap() {
    }

    /**
     * Generate verification data for a given offset
     *
     * The verification data is used to verify that the correct bytes
     * have indeed been copied and byte swapped.
     *
     * The data is generated based on the offset (in bytes) into the
     * source buffer. For a native buffer the offset is relative to
     * the base pointer. For a heap array it is relative to the
     * address of the first array element.
     *
     * This method will return the result of doing an elementSize byte
     * read starting at offset (in bytes).
     *
     * @param offset offset into buffer
     * @param elemSize size (in bytes) of the element
     *
     * @return the verification data, only the least significant
     * elemSize*8 bits are set, zero extended
     */
    private long getVerificationDataForOffset(long offset, long elemSize) {
        byte[] bytes = new byte[(int)elemSize];

        for (long i = 0; i < elemSize; i++) {
            bytes[(int)i] = (byte)(offset + i);
        }

        long o = UNSAFE.arrayBaseOffset(byte[].class);

        switch ((int)elemSize) {
        case 1: return toUnsignedLong(UNSAFE.getByte(bytes, o));
        case 2: return toUnsignedLong(UNSAFE.getShortUnaligned(bytes, o));
        case 4: return toUnsignedLong(UNSAFE.getIntUnaligned(bytes, o));
        case 8: return UNSAFE.getLongUnaligned(bytes, o);
        default: throw new IllegalArgumentException("Invalid element size: " + elemSize);
        }
    }

    /**
     * Verify byte swapped data
     *
     * @param ptr the data to verify
     * @param srcOffset the srcOffset (in bytes) from which the copy started,
     *        used as key to regenerate the verification data
     * @param dstOffset the offset (in bytes) in the array at which to start
     *        the verification, relative to the first element in the array
     * @param size size (in bytes) of data to to verify
     * @param elemSize size (in bytes) of the individual array elements
     *
     * @throws RuntimeException if an error is found
     */
    private void verifySwappedData(GenericPointer ptr, long srcOffset, long dstOffset, long size, long elemSize) {
        for (long offset = 0; offset < size; offset += elemSize) {
            long expectedUnswapped = getVerificationDataForOffset(srcOffset + offset, elemSize);
            long expected = byteSwap(expectedUnswapped, elemSize);

            long actual = getArrayElem(ptr, dstOffset + offset, elemSize);

            if (expected != actual) {
                throw new RuntimeException("srcOffset: 0x" + Long.toHexString(srcOffset) +
                                           " dstOffset: 0x" + Long.toHexString(dstOffset) +
                                           " size: 0x" + Long.toHexString(size) +
                                           " offset: 0x" + Long.toHexString(offset) +
                                           " expectedUnswapped: 0x" + Long.toHexString(expectedUnswapped) +
                                           " expected: 0x" + Long.toHexString(expected) +
                                           " != actual: 0x" + Long.toHexString(actual));
            }
        }
    }

    /**
     * Initialize an array with verification friendly data
     *
     * @param ptr pointer to the data to initialize
     * @param size size (in bytes) of the data
     * @param elemSize size (in bytes) of the individual elements
     */
    private void initVerificationData(GenericPointer ptr, long size, long elemSize) {
        for (long offset = 0; offset < size; offset++) {
            byte data = (byte)getVerificationDataForOffset(offset, 1);

            if (ptr.isOnHeap()) {
                UNSAFE.putByte(ptr.getObject(), ptr.getOffset() + offset, data);
            } else {
                UNSAFE.putByte(ptr.getOffset() + offset, data);
            }
        }
    }

    /**
     * Allocate a primitive array
     *
     * @param size size (in bytes) of all the array elements (elemSize * length)
     * @param elemSize the size of the array elements
     *
     * @return a newly allocated primitive array
     */
    Object allocArray(long size, long elemSize) {
        int length = (int)(size / elemSize);

        switch ((int)elemSize) {
        case 2: return new short[length];
        case 4: return new int[length];
        case 8: return new long[length];
        default:
            throw new IllegalArgumentException("Invalid element size: " + elemSize);
        }
    }

    /**
     * Get the value of a primitive array entry
     *
     * @param ptr pointer to the data
     * @param offset offset (in bytes) of the array element, relative to the first element in the array
     *
     * @return the array element, as an unsigned long
     */
    private long getArrayElem(GenericPointer ptr, long offset, long elemSize) {
        if (ptr.isOnHeap()) {
            Object o = ptr.getObject();
            int index = (int)(offset / elemSize);

            if (o instanceof short[]) {
                short[] arr = (short[])o;
                return toUnsignedLong(arr[index]);
            } else if (o instanceof int[]) {
                int[] arr = (int[])o;
                return toUnsignedLong(arr[index]);
            } else if (o instanceof long[]) {
                long[] arr = (long[])o;
                return arr[index];
            } else {
                throw new IllegalArgumentException("Invalid object type: " + o.getClass().getName());
            }
        } else {
            long addr = ptr.getOffset() + offset;

            switch ((int)elemSize) {
            case 1: return toUnsignedLong(UNSAFE.getByte(addr));
            case 2: return toUnsignedLong(UNSAFE.getShortUnaligned(null, addr));
            case 4: return toUnsignedLong(UNSAFE.getIntUnaligned(null, addr));
            case 8: return UNSAFE.getLongUnaligned(null, addr);
            default: throw new IllegalArgumentException("Invalid element size: " + elemSize);
            }
        }
    }

    private void putValue(long addr, long elemSize, long value) {
        switch ((int)elemSize) {
        case 1: UNSAFE.putByte(addr, (byte)value); break;
        case 2: UNSAFE.putShortUnaligned(null, addr, (short)value); break;
        case 4: UNSAFE.putIntUnaligned(null, addr, (int)value); break;
        case 8: UNSAFE.putLongUnaligned(null, addr, value); break;
        default: throw new IllegalArgumentException("Invalid element size: " + elemSize);
        }
    }

    /**
     * Get the size of the elements for an array
     *
     * @param o a primitive heap array
     *
     * @return the size (in bytes) of the individual array elements
     */
    private long getArrayElemSize(Object o) {
        if (o instanceof short[]) {
            return 2;
        } else if (o instanceof int[]) {
            return 4;
        } else if (o instanceof long[]) {
            return 8;
        } else {
            throw new IllegalArgumentException("Invalid object type: " + o.getClass().getName());
        }
    }

    /**
     * Byte swap a value
     *
     * @param value the value to swap, only the bytes*8 least significant bits are used
     * @param size size (in bytes) of the value
     *
     * @return the byte swapped value in the bytes*8 least significant bits
     */
    private long byteSwap(long value, long size) {
        switch ((int)size) {
        case 2: return toUnsignedLong(Short.reverseBytes((short)value));
        case 4: return toUnsignedLong(Integer.reverseBytes((int)value));
        case 8: return Long.reverseBytes(value);
        default: throw new IllegalArgumentException("Invalid element size: " + size);
        }
    }

    /**
     * Verify data in a heap array which has *not* been byte swapped
     *
     * @param ptr the data to verify
     * @param startOffset the offset (in bytes) at which to start the verification
     * @param size size (in bytes) of the data to verify
     *
     * @throws RuntimeException if an error is found
     */
    private void verifyUnswappedData(GenericPointer ptr, long startOffset, long size) {
        for (long elemOffset = startOffset; elemOffset < startOffset + size; elemOffset++) {
            byte expected = (byte)getVerificationDataForOffset(elemOffset, 1);

            byte actual;
            if (ptr.isOnHeap()) {
                actual = UNSAFE.getByte(ptr.getObject(), ptr.getOffset() + elemOffset);
            } else {
                actual = UNSAFE.getByte(ptr.getOffset() + elemOffset);
            }

            if (expected != actual) {
                throw new RuntimeException("startOffset: 0x" + Long.toHexString(startOffset) +
                                           " size: 0x" + Long.toHexString(size) +
                                           " elemOffset: 0x" + Long.toHexString(elemOffset) +
                                           " expected: 0x" + Long.toHexString(expected) +
                                           " != actual: 0x" + Long.toHexString(actual));
            }
        }
    }


    /**
     * Copy and byte swap data from the source to the destination
     *
     * This method will pre-populate the whole source and destination
     * buffers with verification friendly data. It will then use
     * copySwapMemory to fill part of the destination buffer with
     * swapped data from the source. Some space (padding) will be
     * left before and after the data in the destination buffer, which
     * should not be touched/overwritten by the copy call.
     *
     * Note: Both source and destination buffers will be overwritten!
     *
     * @param src source buffer to copy from
     * @param srcOffset the offset (in bytes) in the source buffer, relative to
     *        the first array element, at which to start reading data
     * @param dst destination buffer to copy to
     * @param dstOffset the offset (in bytes) in the destination
     *        buffer, relative to the first array element, at which to
     *        start writing data
     * @param bufSize the size (in bytes) of the src and dst arrays
     * @param copyBytes the size (in bytes) of the copy to perform,
     *        must be a multiple of elemSize
     * @param elemSize the size (in bytes) of the elements to byte swap
     *
     * @throws RuntimeException if an error is found
     */
    private void testCopySwap(GenericPointer src, long srcOffset,
                              GenericPointer dst, long dstOffset,
                              long bufSize, long copyBytes, long elemSize) {
        if (!isAligned(copyBytes, elemSize)) {
            throw new IllegalArgumentException(
                "copyBytes (" + copyBytes + ") must be a multiple of elemSize (" + elemSize + ")");
        }
        if (src.isOnHeap() && !isAligned(srcOffset, elemSize)) {
            throw new IllegalArgumentException(
                "srcOffset (" + srcOffset + ") must be a multiple of elemSize (" + elemSize + ")");
        }
        if (dst.isOnHeap() && !isAligned(dstOffset, elemSize)) {
            throw new IllegalArgumentException(
                "dstOffset (" + dstOffset + ") must be a multiple of elemSize (" + elemSize + ")");
        }
        if (srcOffset + copyBytes > bufSize) {
            throw new IllegalArgumentException(
                "srcOffset (" + srcOffset + ") + copyBytes (" + copyBytes + ") > bufSize (" + bufSize + ")");
        }
        if (dstOffset + copyBytes > bufSize) {
            throw new IllegalArgumentException(
                "dstOffset (" + dstOffset + ") + copyBytes (" + copyBytes + ") > bufSize (" + bufSize + ")");
        }

        // Initialize the whole source buffer with a verification friendly pattern (no 0x00 bytes)
        initVerificationData(src, bufSize, elemSize);
        if (!src.equals(dst)) {
            initVerificationData(dst, bufSize, elemSize);
        }

        if (DEBUG) {
            System.out.println("===before===");
            for (int offset = 0; offset < bufSize; offset += elemSize) {
                long srcValue = getArrayElem(src, offset, elemSize);
                long dstValue = getArrayElem(dst, offset, elemSize);

                System.out.println("offs=0x" + Long.toHexString(toUnsignedLong(offset)) +
                                 " src=0x" + Long.toHexString(srcValue) +
                                 " dst=0x" + Long.toHexString(dstValue));
            }
        }

        // Copy & swap data into the middle of the destination buffer
        UNSAFE.copySwapMemory(src.getObject(),
                              src.getOffset() + srcOffset,
                              dst.getObject(),
                              dst.getOffset() + dstOffset,
                              copyBytes,
                              elemSize);

        if (DEBUG) {
            System.out.println("===after===");
            for (int offset = 0; offset < bufSize; offset += elemSize) {
                long srcValue = getArrayElem(src, offset, elemSize);
                long dstValue = getArrayElem(dst, offset, elemSize);

                System.out.println("offs=0x" + Long.toHexString(toUnsignedLong(offset)) +
                                 " src=0x" + Long.toHexString(srcValue) +
                                 " dst=0x" + Long.toHexString(dstValue));
            }
        }

        // Verify the the front padding is unchanged
        verifyUnswappedData(dst, 0, dstOffset);

        // Verify swapped data
        verifySwappedData(dst, srcOffset, dstOffset, copyBytes, elemSize);

        // Verify that the back back padding is unchanged
        long frontAndDataBytes = dstOffset + copyBytes;
        long trailingBytes = bufSize - frontAndDataBytes;
        verifyUnswappedData(dst, frontAndDataBytes, trailingBytes);
    }

    /**
     * Test various configurations copy-swapping from one buffer to the other
     *
     * @param src the source buffer to copy from
     * @param dst the destination buffer to copy to
     * @param size size (in bytes) of the buffers
     * @param elemSize size (in bytes) of the individual elements
     *
     * @throws RuntimeException if an error is found
     */
    public void testBufferPair(GenericPointer src, GenericPointer dst, long size, long elemSize) {
        // offset in source from which to start reading data
        for (long srcOffset = 0; srcOffset < size; srcOffset += (src.isOnHeap() ? elemSize : 1)) {

            // offset in destination at which to start writing data
            for (int dstOffset = 0; dstOffset < size; dstOffset += (dst.isOnHeap() ? elemSize : 1)) {

                // number of bytes to copy
                long maxCopyBytes = Math.min(size - srcOffset, size - dstOffset);
                for (long copyBytes = 0; copyBytes < maxCopyBytes; copyBytes += elemSize) {
                    try {
                        testCopySwap(src, srcOffset, dst, dstOffset, size, copyBytes, elemSize);
                    } catch (RuntimeException e) {
                        // Wrap the exception in another exception to catch the relevant configuration data
                        throw new RuntimeException("testBufferPair: " +
                                                   "src=" + src +
                                                   " dst=" + dst +
                                                   " elemSize=0x" + Long.toHexString(elemSize) +
                                                   " copyBytes=0x" + Long.toHexString(copyBytes) +
                                                   " srcOffset=0x" + Long.toHexString(srcOffset) +
                                                   " dstOffset=0x" + Long.toHexString(dstOffset),
                                                   e);
                    }
                }
            }
        }
    }

    /**
     * Test copying between various permutations of buffers
     *
     * @param buffers buffers to permute (src x dst)
     * @param size size (in bytes) of buffers
     * @param elemSize size (in bytes) of individual elements
     *
     * @throws RuntimeException if an error is found
     */
    public void testPermuteBuffers(GenericPointer[] buffers, long size, long elemSize) {
        for (int srcIndex = 0; srcIndex < buffers.length; srcIndex++) {
            for (int dstIndex = 0; dstIndex < buffers.length; dstIndex++) {
                testBufferPair(buffers[srcIndex], buffers[dstIndex], size, elemSize);
            }
        }
    }

    /**
     * Test copying of a specific element size
     *
     * @param size size (in bytes) of buffers to allocate
     * @param elemSize size (in bytes) of individual elements
     *
     * @throws RuntimeException if an error is found
     */
    private void testElemSize(long size, long elemSize) {
        long buf1Raw = 0;
        long buf2Raw = 0;

        try {
            buf1Raw = UNSAFE.allocateMemory(size + BASE_ALIGNMENT);
            long buf1 = alignUp(buf1Raw, BASE_ALIGNMENT);

            buf2Raw = UNSAFE.allocateMemory(size + BASE_ALIGNMENT);
            long buf2 = alignUp(buf2Raw, BASE_ALIGNMENT);

            GenericPointer[] buffers = {
                new GenericPointer(buf1),
                new GenericPointer(buf2),
                new GenericPointer(allocArray(size, elemSize)),
                new GenericPointer(allocArray(size, elemSize))
            };

            testPermuteBuffers(buffers, size, elemSize);
        } finally {
            if (buf1Raw != 0) {
                UNSAFE.freeMemory(buf1Raw);
            }
            if (buf2Raw != 0) {
                UNSAFE.freeMemory(buf2Raw);
            }
        }
    }

    /**
     * Verify that small copy swaps work
     */
    private void testSmallCopy() {
        int smallBufSize = SMALL_COPY_SIZE;

        // Test various element types and heap/native combinations
        for (long elemSize = 2; elemSize <= 8; elemSize <<= 1) {
            testElemSize(smallBufSize, elemSize);
        }
    }


    /**
     * Verify that large copy swaps work
     */
    private void testLargeCopy() {
        long size = 2 * GB + 8;
        long bufRaw = 0;

        // Check that a large native copy succeeds
        try {
            try {
                bufRaw = UNSAFE.allocateMemory(size + BASE_ALIGNMENT);
            } catch (OutOfMemoryError e) {
                // Accept failure, skip test
                return;
            }

            long buf = alignUp(bufRaw, BASE_ALIGNMENT);

            UNSAFE.copySwapMemory(null, buf, null, buf, size, 8);
        } catch (Exception e) {
            throw new RuntimeException("copySwapMemory of large buffer failed");
        } finally {
            if (bufRaw != 0) {
                UNSAFE.freeMemory(bufRaw);
            }
        }
    }

    /**
     * Run positive tests
     *
     * @throws RuntimeException if an error is found
     */
    private void testPositive() {
        testSmallCopy();
        testLargeCopy();
    }

    /**
     * Run negative tests, testing corner cases and the various exceptions
     *
     * @throws RuntimeException if an error is found
     */
    private void testNegative() {
        long bufRaw = 0;

        try {
            bufRaw = UNSAFE.allocateMemory(1024);
            long buf = alignUp(bufRaw, BASE_ALIGNMENT);
            short[] arr = new short[16];

            // Check various illegal element sizes
            for (int elemSize = 2; elemSize <= 8; elemSize <<= 1) {
                long[] illegalSizes = { -1, 1, elemSize - 1, elemSize + 1, elemSize * 2 - 1 };
                for (long size : illegalSizes) {
                    try {
                        // Check that illegal elemSize throws an IAE
                        UNSAFE.copySwapMemory(null, buf, null, buf, size, elemSize);
                        throw new RuntimeException("copySwapMemory failed to throw IAE for size=" + size + " elemSize=" + elemSize);
                    } catch (IllegalArgumentException e) {
                        // good
                    }
                }
            }

            try {
                // Check that negative srcOffset throws an IAE
                UNSAFE.copySwapMemory(arr, -1, arr, UNSAFE.arrayBaseOffset(arr.getClass()), 16, 2);
                throw new RuntimeException("copySwapMemory failed to throw IAE for srcOffset=-1");
            } catch (IllegalArgumentException e) {
                // good
            }

            try {
                // Check that negative dstOffset throws an IAE
                UNSAFE.copySwapMemory(arr, UNSAFE.arrayBaseOffset(arr.getClass()), arr, -1, 16, 2);
                throw new RuntimeException("copySwapMemory failed to throw IAE for destOffset=-1");
            } catch (IllegalArgumentException e) {
                // good
            }

            long illegalElemSizes[] = { 0, 1, 3, 5, 6, 7, 9, 10, -1 };
            for (long elemSize : illegalElemSizes) {
                try {
                    // Check that elemSize 1 throws an IAE
                    UNSAFE.copySwapMemory(null, buf, null, buf, 16, elemSize);
                    throw new RuntimeException("copySwapMemory failed to throw NPE");
                } catch (IllegalArgumentException e) {
                    // good
                }
            }

            try {
                // Check that a NULL source throws NPE
                UNSAFE.copySwapMemory(null, 0, null, buf, 16, 2);
                throw new RuntimeException("copySwapMemory failed to throw NPE");
            } catch (NullPointerException e) {
                // good
            }

            try {
                // Check that a NULL destination throws NPE
                UNSAFE.copySwapMemory(null, buf, null, 0, 16, 2);
                throw new RuntimeException("copySwapMemory failed to throw NPE");
            } catch (NullPointerException e) {
                // good
            }

            try {
                // Check that a reference array destination throws IAE
                UNSAFE.copySwapMemory(null, buf, new Object[16], UNSAFE.arrayBaseOffset(Object[].class), 16, 8);
                throw new RuntimeException("copySwapMemory failed to throw NPE");
            } catch (IllegalArgumentException e) {
                // good
            }

            // Check that invalid source & dest pointers throw IAEs (only relevant on 32-bit platforms)
            if (UNSAFE.addressSize() == 4) {
                long invalidPtr = (long)1 << 35; // Pick a random bit in upper 32 bits

                try {
                    // Check that an invalid (not 32-bit clean) source pointer throws IAE
                    UNSAFE.copySwapMemory(null, invalidPtr, null, buf, 16, 2);
                    throw new RuntimeException("copySwapMemory failed to throw IAE for srcOffset 0x" +
                                               Long.toHexString(invalidPtr));
                } catch (IllegalArgumentException e) {
                    // good
                }

                try {
                    // Check that an invalid (not 32-bit clean) source pointer throws IAE
                    UNSAFE.copySwapMemory(null, buf, null, invalidPtr, 16, 2);
                    throw new RuntimeException("copySwapMemory failed to throw IAE for destOffset 0x" +
                                               Long.toHexString(invalidPtr));
                } catch (IllegalArgumentException e) {
                    // good
                }
            }
        } finally {
            if (bufRaw != 0) {
                UNSAFE.freeMemory(bufRaw);
            }
        }
    }

    /**
     * Run all tests
     *
     * @throws RuntimeException if an error is found
     */
    private void test() {
        testPositive();
        testNegative();
    }

    public static void main(String[] args) {
        CopySwap cs = new CopySwap();
        cs.test();
    }

    /**
     * Helper class to represent a "pointer" - either a heap array or
     * a pointer to a native buffer.
     *
     * In the case of a native pointer, the Object is null and the offset is
     * the absolute address of the native buffer.
     *
     * In the case of a heap object, the Object is a primitive array, and
     * the offset will be set to the base offset to the first element, meaning
     * the object and the offset together form a double-register pointer.
     */
    static class GenericPointer {
        private final Object o;
        private final long offset;

        private GenericPointer(Object o, long offset) {
            this.o = o;
            this.offset = offset;
        }

        public String toString() {
            return "GenericPointer(o={" + o + "}, offset=0x" + Long.toHexString(offset) + ")";
        }

        public boolean equals(Object other) {
            if (!(other instanceof GenericPointer)) {
                return false;
            }

            GenericPointer otherp = (GenericPointer)other;

            return o == otherp.o && offset == otherp.offset;
        }

        GenericPointer(Object o) {
            this(o, UNSAFE.arrayBaseOffset(o.getClass()));
        }

        GenericPointer(long offset) {
            this(null, offset);
        }

        public boolean isOnHeap() {
            return o != null;
        }

        public Object getObject() {
            return o;
        }

        public long getOffset() {
            return offset;
        }
    }
}
