/*
 * 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.geode.internal.offheap;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;

import org.apache.geode.internal.SharedLibrary;
import org.apache.geode.pdx.internal.unsafe.UnsafeWrapper;

/**
 * This class supports allocating and freeing large amounts of addressable memory (i.e. slabs). It
 * also supports using an "address" to operate on the memory. Note that this class's implementation
 * is currently a singleton so all the methods on it are static.
 */
public class AddressableMemoryManager {
  private static final UnsafeWrapper unsafe;
  private static final int ARRAY_BYTE_BASE_OFFSET;
  private static final String reason;
  static {
    UnsafeWrapper tmp = null;
    String tmpReason = null;
    try {
      tmp = new UnsafeWrapper();
    } catch (RuntimeException ignore) {
      tmpReason = ignore.toString();
    } catch (Error ignore) {
      tmpReason = ignore.toString();
    }
    reason = tmpReason;
    unsafe = tmp;
    ARRAY_BYTE_BASE_OFFSET = unsafe != null ? unsafe.arrayBaseOffset(byte[].class) : 0;
  }

  public static long allocate(int size) {
    if (unsafe == null) {
      throw new OutOfMemoryError("Off-heap memory is not available because: " + reason);
    }
    try {
      return unsafe.allocateMemory(size);
    } catch (OutOfMemoryError err) {
      String msg = "Failed creating " + size + " bytes of off-heap memory during cache creation.";
      if (err.getMessage() != null && !err.getMessage().isEmpty()) {
        msg += " Cause: " + err.getMessage();
      }
      if (!SharedLibrary.is64Bit() && size >= (1024 * 1024 * 1024)) {
        msg +=
            " The JVM looks like a 32-bit one. For large amounts of off-heap memory a 64-bit JVM is needed.";
      }
      throw new OutOfMemoryError(msg);
    }
  }

  public static void free(long addr) {
    unsafe.freeMemory(addr);
  }

  public static Slab allocateSlab(int size) {
    return new SlabImpl(size);
  }

  public static byte readByte(long addr) {
    return unsafe.getByte(addr);
  }

  public static char readChar(long addr) {
    return unsafe.getChar(null, addr);
  }

  public static short readShort(long addr) {
    return unsafe.getShort(null, addr);
  }

  public static int readInt(long addr) {
    return unsafe.getInt(null, addr);
  }

  public static int readIntVolatile(long addr) {
    return unsafe.getIntVolatile(null, addr);
  }

  public static long readLong(long addr) {
    return unsafe.getLong(null, addr);
  }

  public static long readLongVolatile(long addr) {
    return unsafe.getLongVolatile(null, addr);
  }

  public static void writeByte(long addr, byte value) {
    unsafe.putByte(addr, value);
  }

  public static void writeInt(long addr, int value) {
    unsafe.putInt(null, addr, value);
  }

  public static void writeIntVolatile(long addr, int value) {
    unsafe.putIntVolatile(null, addr, value);
  }

  public static boolean writeIntVolatile(long addr, int expected, int value) {
    return unsafe.compareAndSwapInt(null, addr, expected, value);
  }

  public static void writeLong(long addr, long value) {
    unsafe.putLong(null, addr, value);
  }

  public static void writeLongVolatile(long addr, long value) {
    unsafe.putLongVolatile(null, addr, value);
  }

  public static boolean writeLongVolatile(long addr, long expected, long value) {
    return unsafe.compareAndSwapLong(null, addr, expected, value);
  }

  public static void readBytes(long addr, byte[] bytes, int bytesOffset, int size) {
    // Throwing an Error instead of using the "assert" keyword because passing < 0 to
    // copyMemory(...) can lead to a core dump with some JVMs and we don't want to
    // require the -ea JVM flag.
    if (size < 0) {
      throw new AssertionError("Size=" + size + ", but size must be >= 0");
    }

    assert bytesOffset >= 0 : "byteOffset=" + bytesOffset;
    assert bytesOffset + size <= bytes.length : "byteOffset=" + bytesOffset + ",size=" + size
        + ",bytes.length=" + bytes.length;

    if (size == 0) {
      return; // No point in wasting time copying 0 bytes
    }
    unsafe.copyMemory(null, addr, bytes, ARRAY_BYTE_BASE_OFFSET + bytesOffset, size);
  }

  public static void copyMemory(long srcAddr, long dstAddr, long size) {
    unsafe.copyMemory(srcAddr, dstAddr, size);
  }

  public static void writeBytes(long addr, byte[] bytes, int bytesOffset, int size) {
    // Throwing an Error instead of using the "assert" keyword because passing < 0 to
    // copyMemory(...) can lead to a core dump with some JVMs and we don't want to
    // require the -ea JVM flag.
    if (size < 0) {
      throw new AssertionError("Size=" + size + ", but size must be >= 0");
    }

    assert bytesOffset >= 0 : "byteOffset=" + bytesOffset;
    assert bytesOffset + size <= bytes.length : "byteOffset=" + bytesOffset + ",size=" + size
        + ",bytes.length=" + bytes.length;

    if (size == 0) {
      return; // No point in wasting time copying 0 bytes
    }
    unsafe.copyMemory(bytes, ARRAY_BYTE_BASE_OFFSET + bytesOffset, null, addr, size);
  }

  public static void fill(long addr, int size, byte fill) {
    unsafe.setMemory(addr, size, fill);
  }

  @SuppressWarnings("rawtypes")
  private static volatile Class dbbClass = null;
  @SuppressWarnings("rawtypes")
  private static volatile Constructor dbbCtor = null;
  private static volatile boolean dbbCreateFailed = false;
  private static volatile Method dbbAddressMethod = null;
  private static volatile boolean dbbAddressFailed = false;

  /**
   * Returns the address of the Unsafe memory for the first byte of a direct ByteBuffer. If the
   * buffer is not direct or the address can not be obtained return 0.
   */
  @SuppressWarnings({"rawtypes", "unchecked"})
  public static long getDirectByteBufferAddress(ByteBuffer bb) {
    if (!bb.isDirect()) {
      return 0L;
    }
    if (dbbAddressFailed) {
      return 0L;
    }
    Method m = dbbAddressMethod;
    if (m == null) {
      Class c = dbbClass;
      if (c == null) {
        try {
          c = Class.forName("java.nio.DirectByteBuffer");
        } catch (ClassNotFoundException e) {
          // throw new IllegalStateException("Could not find java.nio.DirectByteBuffer", e);
          dbbCreateFailed = true;
          dbbAddressFailed = true;
          return 0L;
        }
        dbbClass = c;
      }
      try {
        m = c.getDeclaredMethod("address");
      } catch (NoSuchMethodException | SecurityException e) {
        // throw new IllegalStateException("Could not get method DirectByteBuffer.address()", e);
        dbbClass = null;
        dbbAddressFailed = true;
        return 0L;
      }
      m.setAccessible(true);
      dbbAddressMethod = m;
    }
    try {
      return (Long) m.invoke(bb);
    } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
      // throw new IllegalStateException("Could not create an invoke DirectByteBuffer.address()",
      // e);
      dbbClass = null;
      dbbAddressMethod = null;
      dbbAddressFailed = true;
      return 0L;
    }
  }

  /**
   * Create a direct byte buffer given its address and size. The returned ByteBuffer will be direct
   * and use the memory at the given address.
   *
   * @return the created direct byte buffer or null if it could not be created.
   */
  @SuppressWarnings({"rawtypes", "unchecked"})
  static ByteBuffer createDirectByteBuffer(long address, int size) {
    if (dbbCreateFailed) {
      return null;
    }
    Constructor ctor = dbbCtor;
    if (ctor == null) {
      Class c = dbbClass;
      if (c == null) {
        try {
          c = Class.forName("java.nio.DirectByteBuffer");
        } catch (ClassNotFoundException e) {
          // throw new IllegalStateException("Could not find java.nio.DirectByteBuffer", e);
          dbbCreateFailed = true;
          dbbAddressFailed = true;
          return null;
        }
        dbbClass = c;
      }
      try {
        ctor = c.getDeclaredConstructor(long.class, int.class);
      } catch (NoSuchMethodException | SecurityException e) {
        // throw new IllegalStateException("Could not get constructor DirectByteBuffer(long, int)",
        // e);
        dbbClass = null;
        dbbCreateFailed = true;
        return null;
      }
      ctor.setAccessible(true);
      dbbCtor = ctor;
    }
    try {
      return (ByteBuffer) ctor.newInstance(address, size);
    } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
        | InvocationTargetException e) {
      // throw new IllegalStateException("Could not create an instance using DirectByteBuffer(long,
      // int)", e);
      dbbClass = null;
      dbbCtor = null;
      dbbCreateFailed = true;
      return null;
    }
  }

}
