/*
 * 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 com.alipay.sofa.jraft.util.internal;

import java.lang.reflect.Field;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.security.AccessController;
import java.security.PrivilegedAction;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * For the {@link sun.misc.Unsafe} access.
 *
 * @author jiachun.fjc
 */
@SuppressWarnings("ConstantConditions")
public final class UnsafeUtil {

    private static final Logger         LOG                       = LoggerFactory.getLogger(UnsafeUtil.class);

    private static final Object         UNSAFE                    = getUnsafe0();

    private static final UnsafeAccessor UNSAFE_ACCESSOR           = getUnsafeAccessor0();

    private static final long           BYTE_ARRAY_BASE_OFFSET    = arrayBaseOffset(byte[].class);
    // Micro-optimization: we can assume a scale of 1 and skip the multiply
    // private static final long BYTE_ARRAY_INDEX_SCALE = 1;

    private static final long           BOOLEAN_ARRAY_BASE_OFFSET = arrayBaseOffset(boolean[].class);
    private static final long           BOOLEAN_ARRAY_INDEX_SCALE = arrayIndexScale(boolean[].class);

    private static final long           INT_ARRAY_BASE_OFFSET     = arrayBaseOffset(int[].class);
    private static final long           INT_ARRAY_INDEX_SCALE     = arrayIndexScale(int[].class);

    private static final long           LONG_ARRAY_BASE_OFFSET    = arrayBaseOffset(long[].class);
    private static final long           LONG_ARRAY_INDEX_SCALE    = arrayIndexScale(long[].class);

    private static final long           FLOAT_ARRAY_BASE_OFFSET   = arrayBaseOffset(float[].class);
    private static final long           FLOAT_ARRAY_INDEX_SCALE   = arrayIndexScale(float[].class);

    private static final long           DOUBLE_ARRAY_BASE_OFFSET  = arrayBaseOffset(double[].class);
    private static final long           DOUBLE_ARRAY_INDEX_SCALE  = arrayIndexScale(double[].class);

    private static final long           OBJECT_ARRAY_BASE_OFFSET  = arrayBaseOffset(Object[].class);
    private static final long           OBJECT_ARRAY_INDEX_SCALE  = arrayIndexScale(Object[].class);

    private static final long           BUFFER_ADDRESS_OFFSET     = objectFieldOffset(bufferAddressField());

    private static final long           STRING_VALUE_OFFSET       = objectFieldOffset(stringValueField());

    /**
     * Whether or not can use the unsafe api.
     */
    public static boolean hasUnsafe() {
        return UNSAFE != null;
    }

    /**
     * Get a {@link UnsafeAccessor} appropriate for the platform.
     */
    public static UnsafeAccessor getUnsafeAccessor() {
        return UNSAFE_ACCESSOR;
    }

    public static byte getByte(final Object target, final long offset) {
        return UNSAFE_ACCESSOR.getByte(target, offset);
    }

    public static void putByte(final Object target, final long offset, final byte value) {
        UNSAFE_ACCESSOR.putByte(target, offset, value);
    }

    public static int getInt(final Object target, final long offset) {
        return UNSAFE_ACCESSOR.getInt(target, offset);
    }

    public static void putInt(final Object target, final long offset, final int value) {
        UNSAFE_ACCESSOR.putInt(target, offset, value);
    }

    public static long getLong(final Object target, final long offset) {
        return UNSAFE_ACCESSOR.getLong(target, offset);
    }

    public static void putLong(final Object target, final long offset, final long value) {
        UNSAFE_ACCESSOR.putLong(target, offset, value);
    }

    public static boolean getBoolean(final Object target, final long offset) {
        return UNSAFE_ACCESSOR.getBoolean(target, offset);
    }

    public static void putBoolean(final Object target, final long offset, final boolean value) {
        UNSAFE_ACCESSOR.putBoolean(target, offset, value);
    }

    public static float getFloat(final Object target, final long offset) {
        return UNSAFE_ACCESSOR.getFloat(target, offset);
    }

    public static void putFloat(final Object target, final long offset, final float value) {
        UNSAFE_ACCESSOR.putFloat(target, offset, value);
    }

    public static double getDouble(final Object target, final long offset) {
        return UNSAFE_ACCESSOR.getDouble(target, offset);
    }

    public static void putDouble(final Object target, final long offset, final double value) {
        UNSAFE_ACCESSOR.putDouble(target, offset, value);
    }

    public static Object getObject(final Object target, final long offset) {
        return UNSAFE_ACCESSOR.getObject(target, offset);
    }

    public static void putObject(final Object target, final long offset, final Object value) {
        UNSAFE_ACCESSOR.putObject(target, offset, value);
    }

    public static byte getByte(final byte[] target, final long index) {
        return UNSAFE_ACCESSOR.getByte(target, BYTE_ARRAY_BASE_OFFSET + index);
    }

    public static void putByte(final byte[] target, final long index, final byte value) {
        UNSAFE_ACCESSOR.putByte(target, BYTE_ARRAY_BASE_OFFSET + index, value);
    }

    public static int getInt(final int[] target, final long index) {
        return UNSAFE_ACCESSOR.getInt(target, INT_ARRAY_BASE_OFFSET + (index * INT_ARRAY_INDEX_SCALE));
    }

    public static void putInt(final int[] target, final long index, final int value) {
        UNSAFE_ACCESSOR.putInt(target, INT_ARRAY_BASE_OFFSET + (index * INT_ARRAY_INDEX_SCALE), value);
    }

    public static long getLong(final long[] target, final long index) {
        return UNSAFE_ACCESSOR.getLong(target, LONG_ARRAY_BASE_OFFSET + (index * LONG_ARRAY_INDEX_SCALE));
    }

    public static void putLong(final long[] target, final long index, final long value) {
        UNSAFE_ACCESSOR.putLong(target, LONG_ARRAY_BASE_OFFSET + (index * LONG_ARRAY_INDEX_SCALE), value);
    }

    public static boolean getBoolean(final boolean[] target, final long index) {
        return UNSAFE_ACCESSOR.getBoolean(target, BOOLEAN_ARRAY_BASE_OFFSET + (index * BOOLEAN_ARRAY_INDEX_SCALE));
    }

    public static void putBoolean(final boolean[] target, final long index, final boolean value) {
        UNSAFE_ACCESSOR.putBoolean(target, BOOLEAN_ARRAY_BASE_OFFSET + (index * BOOLEAN_ARRAY_INDEX_SCALE), value);
    }

    public static float getFloat(final float[] target, final long index) {
        return UNSAFE_ACCESSOR.getFloat(target, FLOAT_ARRAY_BASE_OFFSET + (index * FLOAT_ARRAY_INDEX_SCALE));
    }

    public static void putFloat(final float[] target, final long index, final float value) {
        UNSAFE_ACCESSOR.putFloat(target, FLOAT_ARRAY_BASE_OFFSET + (index * FLOAT_ARRAY_INDEX_SCALE), value);
    }

    public static double getDouble(final double[] target, final long index) {
        return UNSAFE_ACCESSOR.getDouble(target, DOUBLE_ARRAY_BASE_OFFSET + (index * DOUBLE_ARRAY_INDEX_SCALE));
    }

    public static void putDouble(final double[] target, final long index, final double value) {
        UNSAFE_ACCESSOR.putDouble(target, DOUBLE_ARRAY_BASE_OFFSET + (index * DOUBLE_ARRAY_INDEX_SCALE), value);
    }

    public static Object getObject(final Object[] target, final long index) {
        return UNSAFE_ACCESSOR.getObject(target, OBJECT_ARRAY_BASE_OFFSET + (index * OBJECT_ARRAY_INDEX_SCALE));
    }

    public static void putObject(final Object[] target, final long index, final Object value) {
        UNSAFE_ACCESSOR.putObject(target, OBJECT_ARRAY_BASE_OFFSET + (index * OBJECT_ARRAY_INDEX_SCALE), value);
    }

    public static byte getByte(final long address) {
        return UNSAFE_ACCESSOR.getByte(address);
    }

    public static void putByte(final long address, final byte value) {
        UNSAFE_ACCESSOR.putByte(address, value);
    }

    public static int getInt(final long address) {
        return UNSAFE_ACCESSOR.getInt(address);
    }

    public static void putInt(final long address, final int value) {
        UNSAFE_ACCESSOR.putInt(address, value);
    }

    public static long getLong(final long address) {
        return UNSAFE_ACCESSOR.getLong(address);
    }

    public static void putLong(final long address, final long value) {
        UNSAFE_ACCESSOR.putLong(address, value);
    }

    public static byte getByteVolatile(final byte[] target, final long index) {
        return UNSAFE_ACCESSOR.getByteVolatile(target, BYTE_ARRAY_BASE_OFFSET + index);
    }

    public static void putByteVolatile(final byte[] target, final long index, final byte value) {
        UNSAFE_ACCESSOR.putByteVolatile(target, BYTE_ARRAY_BASE_OFFSET + index, value);
    }

    public static int getIntVolatile(final int[] target, final long index) {
        return UNSAFE_ACCESSOR.getIntVolatile(target, INT_ARRAY_BASE_OFFSET + (index * INT_ARRAY_INDEX_SCALE));
    }

    public static void putIntVolatile(final int[] target, final long index, final int value) {
        UNSAFE_ACCESSOR.putIntVolatile(target, INT_ARRAY_BASE_OFFSET + (index * INT_ARRAY_INDEX_SCALE), value);
    }

    public static long getLongVolatile(final long[] target, final long index) {
        return UNSAFE_ACCESSOR.getLongVolatile(target, LONG_ARRAY_BASE_OFFSET + (index * LONG_ARRAY_INDEX_SCALE));
    }

    public static void putLongVolatile(final long[] target, final long index, final long value) {
        UNSAFE_ACCESSOR.putLongVolatile(target, LONG_ARRAY_BASE_OFFSET + (index * LONG_ARRAY_INDEX_SCALE), value);
    }

    public static boolean getBooleanVolatile(final boolean[] target, final long index) {
        return UNSAFE_ACCESSOR.getBooleanVolatile(target, BOOLEAN_ARRAY_BASE_OFFSET
                                                          + (index * BOOLEAN_ARRAY_INDEX_SCALE));
    }

    public static void putBooleanVolatile(final boolean[] target, final long index, final boolean value) {
        UNSAFE_ACCESSOR.putBooleanVolatile(target, BOOLEAN_ARRAY_BASE_OFFSET + (index * BOOLEAN_ARRAY_INDEX_SCALE),
            value);
    }

    public static float getFloatVolatile(final float[] target, final long index) {
        return UNSAFE_ACCESSOR.getFloatVolatile(target, FLOAT_ARRAY_BASE_OFFSET + (index * FLOAT_ARRAY_INDEX_SCALE));
    }

    public static void putFloatVolatile(final float[] target, final long index, final float value) {
        UNSAFE_ACCESSOR.putFloatVolatile(target, FLOAT_ARRAY_BASE_OFFSET + (index * FLOAT_ARRAY_INDEX_SCALE), value);
    }

    public static double getDoubleVolatile(final double[] target, final long index) {
        return UNSAFE_ACCESSOR.getDoubleVolatile(target, DOUBLE_ARRAY_BASE_OFFSET + (index * DOUBLE_ARRAY_INDEX_SCALE));
    }

    public static void putDoubleVolatile(final double[] target, final long index, final double value) {
        UNSAFE_ACCESSOR.putDoubleVolatile(target, DOUBLE_ARRAY_BASE_OFFSET + (index * DOUBLE_ARRAY_INDEX_SCALE), value);
    }

    public static Object getObjectVolatile(final Object[] target, final long index) {
        return UNSAFE_ACCESSOR.getObjectVolatile(target, OBJECT_ARRAY_BASE_OFFSET + (index * OBJECT_ARRAY_INDEX_SCALE));
    }

    public static void putObjectVolatile(final Object[] target, final long index, final Object value) {
        UNSAFE_ACCESSOR.putObjectVolatile(target, OBJECT_ARRAY_BASE_OFFSET + (index * OBJECT_ARRAY_INDEX_SCALE), value);
    }

    /**
     * Reports the offset of the first element in the storage allocation of a
     * given array class.
     */
    public static int arrayBaseOffset(final Class<?> clazz) {
        return hasUnsafe() ? UNSAFE_ACCESSOR.arrayBaseOffset(clazz) : -1;
    }

    /**
     * Reports the scale factor for addressing elements in the storage
     * allocation of a given array class.
     */
    public static int arrayIndexScale(final Class<?> clazz) {
        return hasUnsafe() ? UNSAFE_ACCESSOR.arrayIndexScale(clazz) : -1;
    }

    /**
     * Returns the offset of the provided field, or {@code -1} if {@code sun.misc.Unsafe} is not
     * available.
     */
    public static long objectFieldOffset(final Field field) {
        return field == null || hasUnsafe() ? UNSAFE_ACCESSOR.objectFieldOffset(field) : -1;
    }

    /**
     * Returns the offset of the provided class and fieldName, or {@code -1} if {@code sun.misc.Unsafe} is not
     * available.
     */
    public static long objectFieldOffset(final Class<?> clazz, final String fieldName) {
        try {
            return objectFieldOffset(clazz.getDeclaredField(fieldName));
        } catch (final NoSuchFieldException e) {
            UNSAFE_ACCESSOR.throwException(e);
        }
        return -1; // never get here
    }

    /**
     * Gets the offset of the {@code address} field of the given
     * direct {@link ByteBuffer}.
     */
    public static long addressOffset(final ByteBuffer buffer) {
        return UNSAFE_ACCESSOR.getLong(buffer, BUFFER_ADDRESS_OFFSET);
    }

    public static void throwException(final Throwable t) {
        UNSAFE_ACCESSOR.throwException(t);
    }

    /**
     * Returns a new {@link String} backed by the given {@code chars}.
     * The char array should not be mutated any more after calling
     * this function.
     */
    public static String moveToString(final char[] chars) {
        if (STRING_VALUE_OFFSET == -1) {
            // In the off-chance that this JDK does not implement String as we'd expect, just do a copy.
            return new String(chars);
        }
        final String str;
        try {
            str = (String) UNSAFE_ACCESSOR.allocateInstance(String.class);
        } catch (final InstantiationException e) {
            // This should never happen, but return a copy as a fallback just in case.
            return new String(chars);
        }
        UNSAFE_ACCESSOR.putObject(str, STRING_VALUE_OFFSET, chars);
        return str;
    }

    /**
     * Returns the system {@link ClassLoader}.
     */
    public static ClassLoader getSystemClassLoader() {
        if (System.getSecurityManager() == null) {
            return ClassLoader.getSystemClassLoader();
        } else {
            return AccessController.doPrivileged((PrivilegedAction<ClassLoader>) ClassLoader::getSystemClassLoader);
        }
    }

    /**
     * Finds the address field within a direct {@link Buffer}.
     */
    private static Field bufferAddressField() {
        return field(Buffer.class, "address", long.class);
    }

    /**
     * Finds the value field within a {@link String}.
     */
    private static Field stringValueField() {
        return field(String.class, "value", char[].class);
    }

    /**
     * Gets the field with the given name within the class, or
     * {@code null} if not found. If found, the field is made accessible.
     */
    private static Field field(final Class<?> clazz, final String fieldName, final Class<?> expectedType) {
        Field field;
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
            if (!field.getType().equals(expectedType)) {
                return null;
            }
        } catch (final Throwable t) {
            // Failed to access the fields.
            field = null;
        }
        return field;
    }

    private static UnsafeAccessor getUnsafeAccessor0() {
        return hasUnsafe() ? new UnsafeAccessor(UNSAFE) : null;
    }

    private static Object getUnsafe0() {
        Object unsafe;
        try {
            final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
            final Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
            unsafeField.setAccessible(true);
            unsafe = unsafeField.get(null);
        } catch (final Throwable t) {
            if (LOG.isWarnEnabled()) {
                LOG.warn("sun.misc.Unsafe.theUnsafe: unavailable.", t);
            }
            unsafe = null;
        }
        return unsafe;
    }

    public static class UnsafeAccessor {

        private final sun.misc.Unsafe unsafe;

        UnsafeAccessor(Object unsafe) {
            this.unsafe = (sun.misc.Unsafe) unsafe;
        }

        /**
         * Returns the {@link sun.misc.Unsafe}'s instance.
         */
        public sun.misc.Unsafe getUnsafe() {
            return unsafe;
        }

        public byte getByte(final Object target, final long offset) {
            return this.unsafe.getByte(target, offset);
        }

        public void putByte(final Object target, final long offset, final byte value) {
            this.unsafe.putByte(target, offset, value);
        }

        public short getShort(final Object target, final long offset) {
            return this.unsafe.getShort(target, offset);
        }

        public void putShort(final Object target, final long offset, final short value) {
            this.unsafe.putShort(target, offset, value);
        }

        public int getInt(final Object target, final long offset) {
            return this.unsafe.getInt(target, offset);
        }

        public void putInt(final Object target, final long offset, final int value) {
            this.unsafe.putInt(target, offset, value);
        }

        public long getLong(final Object target, final long offset) {
            return this.unsafe.getLong(target, offset);
        }

        public void putLong(final Object target, final long offset, final long value) {
            this.unsafe.putLong(target, offset, value);
        }

        public boolean getBoolean(final Object target, final long offset) {
            return this.unsafe.getBoolean(target, offset);
        }

        public void putBoolean(final Object target, final long offset, final boolean value) {
            this.unsafe.putBoolean(target, offset, value);
        }

        public float getFloat(final Object target, final long offset) {
            return this.unsafe.getFloat(target, offset);
        }

        public void putFloat(final Object target, final long offset, final float value) {
            this.unsafe.putFloat(target, offset, value);
        }

        public double getDouble(final Object target, final long offset) {
            return this.unsafe.getDouble(target, offset);
        }

        public void putDouble(final Object target, final long offset, final double value) {
            this.unsafe.putDouble(target, offset, value);
        }

        public Object getObject(final Object target, final long offset) {
            return this.unsafe.getObject(target, offset);
        }

        public void putObject(final Object target, final long offset, final Object value) {
            this.unsafe.putObject(target, offset, value);
        }

        public byte getByte(final long address) {
            return this.unsafe.getByte(address);
        }

        public void putByte(final long address, final byte value) {
            this.unsafe.putByte(address, value);
        }

        public short getShort(final long address) {
            return this.unsafe.getShort(address);
        }

        public void putShort(final long address, final short value) {
            this.unsafe.putShort(address, value);
        }

        public int getInt(final long address) {
            return this.unsafe.getInt(address);
        }

        public void putInt(final long address, final int value) {
            this.unsafe.putInt(address, value);
        }

        public long getLong(final long address) {
            return this.unsafe.getLong(address);
        }

        public void putLong(final long address, final long value) {
            this.unsafe.putLong(address, value);
        }

        public void copyMemory(final Object srcBase, final long srcOffset, final Object dstBase, final long dstOffset,
                               final long bytes) {
            this.unsafe.copyMemory(srcBase, srcOffset, dstBase, dstOffset, bytes);
        }

        public void copyMemory(final long srcAddress, final long dstAddress, final long bytes) {
            this.unsafe.copyMemory(srcAddress, dstAddress, bytes);
        }

        public byte getByteVolatile(final Object target, final long offset) {
            return this.unsafe.getByteVolatile(target, offset);
        }

        public void putByteVolatile(final Object target, final long offset, final byte value) {
            this.unsafe.putByteVolatile(target, offset, value);
        }

        public short getShortVolatile(final Object target, final long offset) {
            return this.unsafe.getShortVolatile(target, offset);
        }

        public void putShortVolatile(final Object target, final long offset, final short value) {
            this.unsafe.putShortVolatile(target, offset, value);
        }

        public int getIntVolatile(final Object target, final long offset) {
            return this.unsafe.getIntVolatile(target, offset);
        }

        public void putIntVolatile(final Object target, final long offset, final int value) {
            this.unsafe.putIntVolatile(target, offset, value);
        }

        public long getLongVolatile(final Object target, final long offset) {
            return this.unsafe.getLongVolatile(target, offset);
        }

        public void putLongVolatile(final Object target, final long offset, final long value) {
            this.unsafe.putLongVolatile(target, offset, value);
        }

        public boolean getBooleanVolatile(final Object target, final long offset) {
            return this.unsafe.getBooleanVolatile(target, offset);
        }

        public void putBooleanVolatile(final Object target, final long offset, final boolean value) {
            this.unsafe.putBooleanVolatile(target, offset, value);
        }

        public float getFloatVolatile(final Object target, final long offset) {
            return this.unsafe.getFloatVolatile(target, offset);
        }

        public void putFloatVolatile(final Object target, final long offset, final float value) {
            this.unsafe.putFloatVolatile(target, offset, value);
        }

        public double getDoubleVolatile(final Object target, final long offset) {
            return this.unsafe.getDoubleVolatile(target, offset);
        }

        public void putDoubleVolatile(final Object target, final long offset, final double value) {
            this.unsafe.putDoubleVolatile(target, offset, value);
        }

        public Object getObjectVolatile(final Object target, final long offset) {
            return this.unsafe.getObjectVolatile(target, offset);
        }

        public void putObjectVolatile(final Object target, final long offset, final Object value) {
            this.unsafe.putObjectVolatile(target, offset, value);
        }

        /**
         * Reports the offset of the first element in the storage allocation of a
         * given array class.
         */
        public int arrayBaseOffset(final Class<?> clazz) {
            return this.unsafe != null ? this.unsafe.arrayBaseOffset(clazz) : -1;
        }

        /**
         * Reports the scale factor for addressing elements in the storage
         * allocation of a given array class.
         */
        public int arrayIndexScale(final Class<?> clazz) {
            return this.unsafe != null ? this.unsafe.arrayIndexScale(clazz) : -1;
        }

        /**
         * Returns the offset of the provided field, or {@code -1} if {@code sun.misc.Unsafe} is not
         * available.
         */
        public long objectFieldOffset(final Field field) {
            return field == null || this.unsafe == null ? -1 : this.unsafe.objectFieldOffset(field);
        }

        public Object allocateInstance(final Class<?> clazz) throws InstantiationException {
            return this.unsafe.allocateInstance(clazz);
        }

        public void throwException(final Throwable t) {
            this.unsafe.throwException(t);
        }
    }

    private UnsafeUtil() {
    }
}
