/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.datasketches.memory.internal;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

import sun.misc.Unsafe;

/**
 * Provides access to the sun.misc.Unsafe class and its key static fields.
 *
 * @author Lee Rhodes
 */
@SuppressWarnings({"restriction"})
public final class UnsafeUtil {
  public static final Unsafe unsafe;
  public static final String JDK; //must be at least "1.8"
  public static final int JDK_MAJOR; //8, 9, 10, 11, 12, etc

  //not an indicator of whether compressed references are used.
  public static final int ADDRESS_SIZE;

  //For 64-bit JVMs: these offsets vary depending on coop: 16 for JVM <= 32GB; 24 for JVM > 32GB.
  // Making this constant long-typed, rather than int, to exclude possibility of accidental overflow
  // in expressions like arrayLength * ARRAY_BYTE_BASE_OFFSET, where arrayLength is int-typed.
  // The same consideration for constants below: ARRAY_*_INDEX_SCALE, ARRAY_*_INDEX_SHIFT.
  public static final long ARRAY_BOOLEAN_BASE_OFFSET;
  public static final long ARRAY_BYTE_BASE_OFFSET;
  public static final long ARRAY_SHORT_BASE_OFFSET;
  public static final long ARRAY_CHAR_BASE_OFFSET;
  public static final long ARRAY_INT_BASE_OFFSET;
  public static final long ARRAY_LONG_BASE_OFFSET;
  public static final long ARRAY_FLOAT_BASE_OFFSET;
  public static final long ARRAY_DOUBLE_BASE_OFFSET;
  public static final long ARRAY_OBJECT_BASE_OFFSET;

  //@formatter:off

  // Setting those values directly instead of using unsafe.arrayIndexScale(), because it may be
  // beneficial for runtime execution, those values are backed into generated machine code as
  // constants. E. g. see https://shipilev.net/jvm-anatomy-park/14-constant-variables/
  public static final int ARRAY_BOOLEAN_INDEX_SCALE = 1;
  public static final int ARRAY_BYTE_INDEX_SCALE    = 1;
  public static final long ARRAY_SHORT_INDEX_SCALE  = 2;
  public static final long ARRAY_CHAR_INDEX_SCALE   = 2;
  public static final long ARRAY_INT_INDEX_SCALE    = 4;
  public static final long ARRAY_LONG_INDEX_SCALE   = 8;
  public static final long ARRAY_FLOAT_INDEX_SCALE  = 4;
  public static final long ARRAY_DOUBLE_INDEX_SCALE = 8;
  public static final long ARRAY_OBJECT_INDEX_SCALE;  // varies, 4 or 8 depending on coop

  //Used to convert "type" to bytes:  bytes = longs << LONG_SHIFT
  public static final int BOOLEAN_SHIFT    = 0;
  public static final int BYTE_SHIFT       = 0;
  public static final long SHORT_SHIFT     = 1;
  public static final long CHAR_SHIFT      = 1;
  public static final long INT_SHIFT       = 2;
  public static final long LONG_SHIFT      = 3;
  public static final long FLOAT_SHIFT     = 2;
  public static final long DOUBLE_SHIFT    = 3;
  public static final long OBJECT_SHIFT;     // varies, 2 or 3 depending on coop

  public static final String LS = System.getProperty("line.separator");

  //@formatter:on

  static {
    try {
      final Constructor<Unsafe> unsafeConstructor = Unsafe.class.getDeclaredConstructor();
      unsafeConstructor.setAccessible(true);
      unsafe = unsafeConstructor.newInstance();

      // Alternative, but may not work across different JVMs.
      //      Field field = Unsafe.class.getDeclaredField("theUnsafe");
      //      field.setAccessible(true);
      //      unsafe = (Unsafe) field.get(null);

    } catch (final InstantiationException | IllegalAccessException | IllegalArgumentException
        | InvocationTargetException | NoSuchMethodException e) {
      e.printStackTrace();
      throw new RuntimeException("Unable to acquire Unsafe. " + e);
    }

    //4 on 32-bit systems. 4 on 64-bit systems < 32GB, otherwise 8.
    //This alone is not an indicator of compressed ref (coop)
    ADDRESS_SIZE = unsafe.addressSize();

    ARRAY_BOOLEAN_BASE_OFFSET = unsafe.arrayBaseOffset(boolean[].class);
    ARRAY_BYTE_BASE_OFFSET = unsafe.arrayBaseOffset(byte[].class);
    ARRAY_SHORT_BASE_OFFSET = unsafe.arrayBaseOffset(short[].class);
    ARRAY_CHAR_BASE_OFFSET = unsafe.arrayBaseOffset(char[].class);
    ARRAY_INT_BASE_OFFSET = unsafe.arrayBaseOffset(int[].class);
    ARRAY_LONG_BASE_OFFSET = unsafe.arrayBaseOffset(long[].class);
    ARRAY_FLOAT_BASE_OFFSET = unsafe.arrayBaseOffset(float[].class);
    ARRAY_DOUBLE_BASE_OFFSET = unsafe.arrayBaseOffset(double[].class);
    ARRAY_OBJECT_BASE_OFFSET = unsafe.arrayBaseOffset(Object[].class);

    ARRAY_OBJECT_INDEX_SCALE = unsafe.arrayIndexScale(Object[].class);
    OBJECT_SHIFT = ARRAY_OBJECT_INDEX_SCALE == 4 ? 2 : 3;

    final String jdkVer = System.getProperty("java.version");
    final int[] p = parseJavaVersion(jdkVer);
    JDK = p[0] + "." + p[1];
    JDK_MAJOR = (p[0] == 1) ? p[1] : p[0];
  }

  private UnsafeUtil() {}

  /**
   * Returns first two number groups of the java version string.
   * @param jdkVer the java version string from System.getProperty("java.version").
   * @return first two number groups of the java version string.
   */
  public static int[] parseJavaVersion(final String jdkVer) {
    final int p0, p1;
    try {
      String[] parts = jdkVer.trim().split("[^0-9\\.]");//grab only number groups and "."
      parts = parts[0].split("\\."); //split out the number groups
      p0 = Integer.parseInt(parts[0]); //the first number group
      p1 = (parts.length > 1) ? Integer.parseInt(parts[1]) : 0; //2nd number group, or 0
    } catch (final NumberFormatException | ArrayIndexOutOfBoundsException  e) {
      throw new IllegalArgumentException("Improper Java -version string: " + jdkVer + "\n" + e);
    }
    checkJavaVersion(jdkVer, p0, p1);
    return new int[] {p0, p1};
  }

  public static void checkJavaVersion(final String jdkVer, final int p0, final int p1) {
    if ( (p0 < 1) || ((p0 == 1) && (p1 < 8)) || (p0 > 13)  ) {
      throw new IllegalArgumentException(
          "Unsupported JDK Major Version, must be one of 1.8, 8, 11, 17: " + jdkVer);
    }
  }

  public static long getFieldOffset(final Class<?> c, final String fieldName) {
    try {
      return unsafe.objectFieldOffset(c.getDeclaredField(fieldName));
    } catch (final NoSuchFieldException e) {
      throw new IllegalStateException(e + ": " + fieldName);
    }
  }

  /**
   * Like {@link Unsafe#arrayBaseOffset(Class)}, but caches return values for common array types.
   * Useful because calling {@link Unsafe#arrayBaseOffset(Class)} directly incurs more overhead.
   * @param c The given Class&lt;?&gt;.
   * @return the base-offset
   */
  public static long getArrayBaseOffset(final Class<?> c) {
    // Ordering here is roughly in order of what we expect to be most popular.
    if (c == byte[].class) {
      return ARRAY_BYTE_BASE_OFFSET;
    } else if (c == int[].class) {
      return ARRAY_INT_BASE_OFFSET;
    } else if (c == long[].class) {
      return ARRAY_LONG_BASE_OFFSET;
    } else if (c == float[].class) {
      return ARRAY_FLOAT_BASE_OFFSET;
    } else if (c == double[].class) {
      return ARRAY_DOUBLE_BASE_OFFSET;
    } else if (c == boolean[].class) {
      return ARRAY_BOOLEAN_BASE_OFFSET;
    } else if (c == short[].class) {
      return ARRAY_SHORT_BASE_OFFSET;
    } else if (c == char[].class) {
      return ARRAY_CHAR_BASE_OFFSET;
    } else if (c == Object[].class) {
      return ARRAY_OBJECT_BASE_OFFSET;
    } else {
      return unsafe.arrayBaseOffset(c);
    }
  }

}
