/*
 * 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.datastax.driver.core;

import java.lang.reflect.Method;
import jnr.ffi.LibraryLoader;
import jnr.ffi.Pointer;
import jnr.ffi.Runtime;
import jnr.ffi.Struct;
import jnr.ffi.annotations.Out;
import jnr.ffi.annotations.Transient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Helper class to deal with native system calls through the <a
 * href="https://github.com/jnr/jnr-ffi">JNR library</a>.
 *
 * <p>The driver can benefit from native system calls to improve its performance and accuracy in
 * some situations.
 *
 * <p>Currently, the following features may be used by the driver when available:
 *
 * <ol>
 *   <li>{@link #currentTimeMicros()}: thanks to a system call to {@code gettimeofday()}, the driver
 *       is able to generate timestamps with true microsecond precision (see {@link
 *       AtomicMonotonicTimestampGenerator} or {@link ThreadLocalMonotonicTimestampGenerator} for
 *       more information);
 *   <li>{@link #processId()}: thanks to a system call to {@code getpid()}, the driver has access to
 *       the JVM's process ID it is running under – which makes time-based UUID generation easier
 *       and more reliable (see {@link com.datastax.driver.core.utils.UUIDs UUIDs} for more
 *       information).
 * </ol>
 *
 * <p>The availability of the aforementioned system calls depends on the underlying operation
 * system's capabilities. For instance, {@code gettimeofday()} is not available under Windows
 * systems. You can check if any of the system calls exposed through this class is available by
 * calling {@link #isGettimeofdayAvailable()} or {@link #isGetpidAvailable()}.
 *
 * <p>Note: This class is public because it needs to be accessible from other packages of the Java
 * driver, but it is not meant to be used directly by client code.
 *
 * @see <a href="https://github.com/jnr/jnr-ffi">JNR library on Github</a>
 */
public final class Native {

  private static final Logger LOGGER = LoggerFactory.getLogger(Native.class);

  private static class LibCLoader {

    /**
     * Timeval struct.
     *
     * @see <a href="http://man7.org/linux/man-pages/man2/settimeofday.2.html">GETTIMEOFDAY(2)</a>
     */
    static class Timeval extends Struct {

      public final time_t tv_sec = new time_t();

      public final Unsigned32 tv_usec = new Unsigned32();

      public Timeval(Runtime runtime) {
        super(runtime);
      }
    }

    /** Interface for LIBC calls through JNR. Note that this interface must be declared public. */
    public interface LibC {

      /**
       * JNR call to {@code gettimeofday}.
       *
       * @param tv Timeval struct
       * @param unused Timezone struct (unused)
       * @return 0 for success, or -1 for failure
       * @see <a href="http://man7.org/linux/man-pages/man2/settimeofday.2.html">GETTIMEOFDAY(2)</a>
       */
      int gettimeofday(@Out @Transient Timeval tv, Pointer unused);
    }

    private static final LibC LIB_C;

    private static final Runtime LIB_C_RUNTIME;

    private static final boolean GETTIMEOFDAY_AVAILABLE;

    static {
      LibC libc;
      Runtime runtime = null;
      try {
        libc = LibraryLoader.create(LibC.class).load("c");
        runtime = Runtime.getRuntime(libc);
      } catch (Throwable t) {
        libc = null; // dereference proxy to library if runtime could not be loaded
        if (LOGGER.isDebugEnabled())
          LOGGER.debug(
              "Could not load JNR C Library, native system calls through this library will not be available",
              t);
        else
          LOGGER.info(
              "Could not load JNR C Library, native system calls through this library will not be available "
                  + "(set this logger level to DEBUG to see the full stack trace).");
      }
      LIB_C = libc;
      LIB_C_RUNTIME = runtime;
      boolean gettimeofday = false;
      if (LIB_C_RUNTIME != null) {
        try {
          gettimeofday = LIB_C.gettimeofday(new Timeval(LIB_C_RUNTIME), null) == 0;
        } catch (Throwable t) {
          if (LOGGER.isDebugEnabled())
            LOGGER.debug("Native calls to gettimeofday() not available on this system.", t);
          else
            LOGGER.info(
                "Native calls to gettimeofday() not available on this system "
                    + "(set this logger level to DEBUG to see the full stack trace).");
        }
      }
      GETTIMEOFDAY_AVAILABLE = gettimeofday;
    }
  }

  private static class PosixLoader {

    public static final jnr.posix.POSIX POSIX;

    private static final boolean GETPID_AVAILABLE;

    static {
      jnr.posix.POSIX posix;
      try {
        // use reflection below to get the classloader a chance to load this class
        Class<?> posixHandler = Class.forName("jnr.posix.POSIXHandler");
        Class<?> defaultPosixHandler = Class.forName("jnr.posix.util.DefaultPOSIXHandler");
        Class<?> posixFactory = Class.forName("jnr.posix.POSIXFactory");
        Method getPOSIX = posixFactory.getMethod("getPOSIX", posixHandler, Boolean.TYPE);
        posix = (jnr.posix.POSIX) getPOSIX.invoke(null, defaultPosixHandler.newInstance(), true);
      } catch (Throwable t) {
        posix = null;
        if (LOGGER.isDebugEnabled())
          LOGGER.debug(
              "Could not load JNR POSIX Library, native system calls through this library will not be available.",
              t);
        else
          LOGGER.info(
              "Could not load JNR POSIX Library, native system calls through this library will not be available "
                  + "(set this logger level to DEBUG to see the full stack trace).");
      }
      POSIX = posix;
      boolean getpid = false;
      if (POSIX != null) {
        try {
          POSIX.getpid();
          getpid = true;
        } catch (Throwable t) {
          if (LOGGER.isDebugEnabled())
            LOGGER.debug("Native calls to getpid() not available on this system.", t);
          else
            LOGGER.info(
                "Native calls to getpid() not available on this system "
                    + "(set this logger level to DEBUG to see the full stack trace).");
        }
      }
      GETPID_AVAILABLE = getpid;
    }
  }

  /**
   * Returns {@code true} if JNR C library is loaded and a call to {@code gettimeofday} is possible
   * through this library on this system, and {@code false} otherwise.
   *
   * @return {@code true} if JNR C library is loaded and a call to {@code gettimeofday} is possible.
   */
  public static boolean isGettimeofdayAvailable() {
    try {
      return LibCLoader.GETTIMEOFDAY_AVAILABLE;
    } catch (NoClassDefFoundError e) {
      return false;
    }
  }

  /**
   * Returns {@code true} if JNR POSIX library is loaded and a call to {@code getpid} is possible
   * through this library on this system, and {@code false} otherwise.
   *
   * @return {@code true} if JNR POSIX library is loaded and a call to {@code getpid} is possible.
   */
  public static boolean isGetpidAvailable() {
    try {
      return PosixLoader.GETPID_AVAILABLE;
    } catch (NoClassDefFoundError e) {
      return false;
    }
  }

  /**
   * Returns the current timestamp with microsecond precision via a system call to {@code
   * gettimeofday}, through JNR C library.
   *
   * @return the current timestamp with microsecond precision.
   * @throws UnsupportedOperationException if JNR C library is not loaded or {@code gettimeofday} is
   *     not available.
   * @throws IllegalStateException if the call to {@code gettimeofday} did not complete with return
   *     code 0.
   */
  public static long currentTimeMicros() {
    if (!isGettimeofdayAvailable())
      throw new UnsupportedOperationException(
          "JNR C library not loaded or gettimeofday not available");
    LibCLoader.Timeval tv = new LibCLoader.Timeval(LibCLoader.LIB_C_RUNTIME);
    int res = LibCLoader.LIB_C.gettimeofday(tv, null);
    if (res != 0) throw new IllegalStateException("Call to gettimeofday failed with result " + res);
    return tv.tv_sec.get() * 1000000 + tv.tv_usec.get();
  }

  /**
   * Returns the JVM's process identifier (PID) via a system call to {@code getpid}.
   *
   * @return the JVM's process identifier (PID).
   * @throws UnsupportedOperationException if JNR POSIX library is not loaded or {@code getpid} is
   *     not available.
   */
  public static int processId() {
    if (!isGetpidAvailable())
      throw new UnsupportedOperationException(
          "JNR POSIX library not loaded or getpid not available");
    return PosixLoader.POSIX.getpid();
  }
}
