/*
 * 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.shared;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketImpl;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.geode.SystemFailure;

/**
 * Encapsulates native C/C++ calls via JNA. To obtain an instance of implementation for a platform,
 * use {@link NativeCalls#getInstance()}.
 *
 * @since GemFire 8.0
 */
public abstract class NativeCalls {

  /**
   * Static instance of NativeCalls implementation. This can be one of JNA implementations in
   * <code>NativeCallsJNAImpl</code> or can fall back to a generic implementation in case JNA is not
   * available for the platform.
   *
   * Note: this variable is deliberately not final so that other clients can plug in their own
   * native implementations of NativeCalls.
   */
  protected static NativeCalls instance;

  static {
    NativeCalls inst;
    try {
      // try to load JNA implementation first
      // we do it via reflection since some clients
      // may not have it
      final Class<?> c = Class.forName("org.apache.geode.internal.shared.NativeCallsJNAImpl");
      inst = (NativeCalls) c.getMethod("getInstance").invoke(null);
    } catch (VirtualMachineError e) {
      SystemFailure.initiateFailure(e);
      throw e;
    } catch (Throwable t) {
      SystemFailure.checkFailure();
      inst = null;
    }
    if (inst == null) {
      // fall back to generic implementation in case of a problem
      inst = new NativeCallsGeneric();
    }
    instance = inst;
  }

  public NativeCalls() {}

  /**
   * Get an instance of implementation of {@link NativeCalls} for the current platform.
   */
  public static NativeCalls getInstance() {
    return instance;
  }

  /**
   * Get the native kernel descriptor given the java Socket. This is a horribly implementation
   * dependent code checking various cases to get to the underlying kernel socket descriptor but
   * works for the JDK's we support or intend to support directly or indirectly (e.g. GCJ for ODBC
   * clients).
   *
   * @param sock the java socket
   * @param sockStream the {@link InputStream} of the java socket, if available
   *
   * @throws UnsupportedOperationException if the kernel descriptor could not be extracted
   */
  protected int getSocketKernelDescriptor(Socket sock, InputStream sockStream)
      throws UnsupportedOperationException {
    Method m;
    Field f;
    Object obj;
    FileDescriptor fd = null;
    // in some cases (for SSL) the Socket can be a wrapper one
    try {
      f = getAnyField(sock.getClass(), "self");
      if (f != null) {
        f.setAccessible(true);
        final Object self = f.get(sock);
        if (self instanceof Socket) {
          sock = (Socket) self;
          sockStream = sock.getInputStream();
        }
      }
    } catch (NoSuchFieldException fe) {
      // ignore if there is no such field
    } catch (RuntimeException re) {
      throw re;
    } catch (Exception ex) {
      throw new UnsupportedOperationException(ex);
    }

    // first try using FileInputStream
    if (sockStream instanceof FileInputStream) {
      try {
        fd = ((FileInputStream) sockStream).getFD();
      } catch (Exception e) {
        // go the fallback route
      }
    }
    // else fallback to SocketImpl route
    try {
      if (fd == null) {
        try {
          // package private Socket.getImpl() to get SocketImpl
          m = getAnyMethod(sock.getClass(), "getImpl");
        } catch (Exception ex) {
          try {
            m = getAnyMethod(sock.getClass(), "getPlainSocketImpl");
          } catch (Exception e) {
            // try forcing the InputStream route
            m = null;
            if (sockStream == null) {
              sockStream = sock.getInputStream();
              if (sockStream instanceof FileInputStream) {
                fd = ((FileInputStream) sockStream).getFD();
              }
            } else {
              throw e;
            }
          }
        }
        if (m != null) {
          m.setAccessible(true);
          final SocketImpl sockImpl = (SocketImpl) m.invoke(sock);
          if (sockImpl != null) {
            try {
              m = getAnyMethod(sockImpl.getClass(), "getFileDescriptor");
              if (m != null) {
                m.setAccessible(true);
                fd = (FileDescriptor) m.invoke(sockImpl);
              }
            } catch (NoSuchMethodException nme) {
              // go to field reflection route
            }
          }
        }
      }
      if (fd != null) {
        // get the kernel descriptor using reflection
        f = getAnyField(fd.getClass(), "fd");
        if (f != null) {
          f.setAccessible(true);
          obj = f.get(fd);
          if (obj instanceof Integer) {
            return ((Integer) obj).intValue();
          }
        }
      }
      throw new UnsupportedOperationException();
    } catch (SecurityException se) {
      throw new UnsupportedOperationException(se);
    } catch (RuntimeException re) {
      throw re;
    } catch (Exception ex) {
      throw new UnsupportedOperationException(ex);
    }
  }

  protected static Method getAnyMethod(Class<?> c, String name, Class<?>... parameterTypes)
      throws NoSuchMethodException, SecurityException {
    NoSuchMethodException firstEx = null;
    for (;;) {
      try {
        return c.getDeclaredMethod(name, parameterTypes);
      } catch (NoSuchMethodException nsme) {
        if (firstEx == null) {
          firstEx = nsme;
        }
        if ((c = c.getSuperclass()) == null) {
          throw firstEx;
        }
        // else continue searching in superClass
      }
    }
  }

  protected static Field getAnyField(Class<?> c, String name)
      throws NoSuchFieldException, SecurityException {
    NoSuchFieldException firstEx = null;
    for (;;) {
      try {
        return c.getDeclaredField(name);
      } catch (NoSuchFieldException nsfe) {
        if (firstEx == null) {
          firstEx = nsfe;
        }
        if ((c = c.getSuperclass()) == null) {
          throw firstEx;
        }
        // else continue searching in superClass
      }
    }
  }

  protected String getUnsupportedSocketOptionMessage(TCPSocketOptions opt) {
    return "setSocketOption(): socket option " + opt + " not supported by current platform "
        + getOSType();
  }

  /**
   * Get the {@link OSType} of current system.
   */
  public abstract OSType getOSType();

  /**
   * Get the value of given environment variable. This is different from
   * {@link System#getenv(String)} in that it returns the current value of the environment variable
   * in the process rather than from a static unmodifiable map created on the first call.
   *
   * @param name the name of the environment variable to be modified
   */
  public abstract String getEnvironment(String name);

  /**
   * Get the process ID of the current process.
   */
  public abstract int getProcessId();

  /**
   * Check whether a process with given ID is still running.
   *
   * @throws UnsupportedOperationException if no native API to determine the process status could be
   *         invoked
   */
  public abstract boolean isProcessActive(int processId) throws UnsupportedOperationException;

  /**
   * Kill the process with given process ID immediately (i.e. without giving it a chance to cleanup
   * properly).
   *
   * @param processId the PID of the process to be kill
   *
   * @throws UnsupportedOperationException if no native API to kill the process could be invoked
   */
  public abstract boolean killProcess(int processId) throws UnsupportedOperationException;

  /**
   * Perform the steps necessary to make the current JVM a proper UNIX daemon.
   *
   * @param callback register callback to be invoked on catching a SIGHUP signal; SIGHUP signal is
   *        ignored if the callback is null
   *
   * @throws UnsupportedOperationException if the native calls could not be completed for some
   *         reason or are not available
   * @throws IllegalStateException for a non-UNIX platform
   */
  public void daemonize(RehashServerOnSIGHUP callback)
      throws UnsupportedOperationException, IllegalStateException {
    throw new UnsupportedOperationException("daemonize() not available in base implementation");
  }

  public void preBlow(String path, long maxSize, boolean preAllocate) throws IOException {
    RandomAccessFile raf = new RandomAccessFile(path, "rw");
    try {
      raf.setLength(maxSize);
    } finally {
      raf.close();
    }
  }

  /**
   * This will return whether the path passed in as arg is part of a local file system or a remote
   * file system. This method is mainly used by the DiskCapacityMonitor thread and we don't want to
   * monitor remote fs available space as due to network problems/firewall issues the call to
   * getUsableSpace can hang. See bug #49155. On platforms other than Linux this will return false
   * even if it on local file system for now.
   */
  public boolean isOnLocalFileSystem(final String path) {
    return false;
  }

  /**
   * Set given extended socket options on a Java {@link Socket}.
   *
   * @throws UnsupportedOperationException if the native API to set the option could not be found or
   *         invoked
   *
   * @return the unsupported {@link TCPSocketOptions} for the current platform and the underlying
   *         exception
   *
   * @see TCPSocketOptions
   */
  public abstract Map<TCPSocketOptions, Throwable> setSocketOptions(Socket sock,
      InputStream sockStream, Map<TCPSocketOptions, Object> optValueMap)
      throws UnsupportedOperationException;

  // IPPROTO_TCP is used by setsockopt to denote a TCP option
  protected static final int OPT_IPPROTO_TCP = 6;
  // indicates an unsupported TCPSocketOptions enum
  protected static final int UNSUPPORTED_OPTION = Integer.MIN_VALUE;

  /**
   * A generic implementation of {@link #setSocketOptions} for POSIX like systems that requires the
   * child classes to implement a few platform specific methods.
   */
  protected Map<TCPSocketOptions, Throwable> setGenericSocketOptions(Socket sock,
      InputStream sockStream, Map<TCPSocketOptions, Object> optValueMap)
      throws UnsupportedOperationException {
    final Set<Map.Entry<TCPSocketOptions, Object>> optValueEntries = optValueMap.entrySet();
    for (Map.Entry<TCPSocketOptions, Object> e : optValueEntries) {
      TCPSocketOptions opt = e.getKey();
      Object value = e.getValue();
      // just to check for unsupported option
      getPlatformOption(opt);
      // all options currently require an integer argument
      if (value == null || !(value instanceof Integer)) {
        throw new IllegalArgumentException("bad argument type "
            + (value != null ? value.getClass().getName() : "NULL") + " for " + opt);
      }
    }

    Map<TCPSocketOptions, Throwable> failures = new HashMap<TCPSocketOptions, Throwable>(4);
    final int sockfd = getSocketKernelDescriptor(sock, sockStream);
    for (Map.Entry<TCPSocketOptions, Object> e : optValueEntries) {
      TCPSocketOptions opt = e.getKey();
      Object value = e.getValue();
      final int optName = getPlatformOption(opt);
      if (optName == UNSUPPORTED_OPTION) {
        failures.put(opt,
            new UnsupportedOperationException(getUnsupportedSocketOptionMessage(opt)));
        continue;
      }
      final int optSize = Integer.SIZE / Byte.SIZE;
      try {
        if (setPlatformSocketOption(sockfd, OPT_IPPROTO_TCP, optName, opt, (Integer) value,
            optSize) != 0) {
          failures.put(opt,
              new SocketException(getOSType() + ": error setting option " + opt + " to " + value));
        }
      } catch (NativeErrorException ne) {
        // check if the error indicates that option is not supported
        if (isNoProtocolOptionCode(ne.getErrorCode())) {
          failures.put(opt,
              new UnsupportedOperationException(getUnsupportedSocketOptionMessage(opt), ne));
        } else {
          final SocketException se =
              new SocketException(getOSType() + ": failed to set " + opt + " to " + value);
          se.initCause(ne);
          failures.put(opt, se);
        }
      }
    }
    return failures.size() > 0 ? failures : null;
  }

  protected int getPlatformOption(TCPSocketOptions opt) throws UnsupportedOperationException {
    // no generic POSIX specification for this
    throw new UnsupportedOperationException(
        "setSocketOption not supported for generic POSIX platform");
  }

  protected int setPlatformSocketOption(int sockfd, int level, int optName, TCPSocketOptions opt,
      Integer optVal, int optSize) throws UnsupportedOperationException, NativeErrorException {
    // no generic POSIX specification for this
    throw new UnsupportedOperationException(
        "setSocketOption not supported for generic POSIX platform");
  }

  protected boolean isNoProtocolOptionCode(int errno) throws UnsupportedOperationException {
    // no generic POSIX specification for this
    throw new UnsupportedOperationException(
        "setSocketOption not supported for generic POSIX platform");
  }

  /**
   * Callback invoked when an OS-level SIGHUP signal is caught after handler has been installed by
   * {@link NativeCalls#daemonize}. This is provided to allow for re-reading configuration files or
   * any other appropriate actions on receiving HUP signal as is the convention in other servers.
   *
   * @since GemFire 8.0
   */
  public interface RehashServerOnSIGHUP {

    /**
     * Perform the actions required to "rehash" the server.
     */
    void rehash();
  }

  /**
   * whether o/s supports high resolution clock or equivalent perf counter.
   *
   * @return true if implemented, otherwise false.
   */
  public boolean isNativeTimerEnabled() {
    return false;
  }

  /**
   * This is fall back for jni based library implementation of NanoTimer which is more efficient
   * than current impl through jna.
   *
   * Linux impls create temporary timespec object and marshals that for invoking native api.
   * Shouldn't be used if to be called too many times, instead jni implementation is more desirable.
   *
   * @return nanosecond precision performance counter.
   */
  public long nanoTime(int clock_id) {
    return System.nanoTime();
  }

  public long clockResolution(int clock_id) {
    return 0;
  }

  public boolean isTTY() {
    return false;
  }

  /**
   * A generic fallback implementation of {@link NativeCalls} when no JNA based implementation could
   * be initialized (e.g. if JNA itself does not provide an implementation for the platform, or JNA
   * is not found).
   *
   * @since GemFire 8.0
   */
  public static class NativeCallsGeneric extends NativeCalls {

    private static final boolean isWin;

    static {
      isWin = System.getProperty("os.name").indexOf("Windows") >= 0;
    }

    /**
     * @see NativeCalls#getOSType()
     */
    @Override
    public OSType getOSType() {
      return isWin ? OSType.WIN : OSType.GENERIC;
    }

    /**
     * @see NativeCalls#getEnvironment(String)
     */
    @Override
    public String getEnvironment(final String name) {
      return System.getenv(name);
    }

    /**
     * @see NativeCalls#getProcessId()
     */
    @Override
    public int getProcessId() {
      final String name = java.lang.management.ManagementFactory.getRuntimeMXBean().getName();
      final int idx = name.indexOf('@');
      if (idx > 0) {
        try {
          return Integer.parseInt(name.substring(0, idx));
        } catch (NumberFormatException nfe) {
          // something changed in the RuntimeMXBean name
        }
      }
      return 0;
    }

    /**
     * @see NativeCalls#isProcessActive(int)
     */
    @Override
    public boolean isProcessActive(int processId) throws UnsupportedOperationException {
      throw new UnsupportedOperationException(
          "isProcessActive() not available in generic implementation");
    }

    /**
     * @see NativeCalls#killProcess(int)
     */
    @Override
    public boolean killProcess(int processId) throws UnsupportedOperationException {
      throw new UnsupportedOperationException(
          "killProcess() not available in generic implementation");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map<TCPSocketOptions, Throwable> setSocketOptions(Socket sock, InputStream sockStream,
        Map<TCPSocketOptions, Object> optValueMap) throws UnsupportedOperationException {
      throw new UnsupportedOperationException("setting native socket options "
          + optValueMap.keySet() + " not possible in generic implementation");
    }
  }
}
