/*
 * junixsocket
 *
 * Copyright 2009-2024 Christian Kohlschütter
 *
 * Licensed 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.newsclub.net.unix;

import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.spi.AbstractSelectableChannel;
import java.util.concurrent.atomic.AtomicBoolean;

import org.newsclub.net.unix.AFSelector.PollFd;

import com.kohlschutter.annotations.compiletime.ExcludeFromCodeCoverageGeneratedReport;
import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;

/**
 * JNI connector to native JNI C code.
 *
 * @author Christian Kohlschütter
 */
final class NativeUnixSocket {
  private static final AtomicBoolean LOADED = new AtomicBoolean(false);

  static final int DOMAIN_GENERIC = -1;
  static final int DOMAIN_UNIX = 1;
  static final int DOMAIN_TIPC = 30;
  static final int DOMAIN_VSOCK = 40;
  static final int DOMAIN_SYSTEM = 32;

  static final int SOCK_STREAM = 1;
  static final int SOCK_DGRAM = 2;
  static final int SOCK_RAW = 3;
  static final int SOCK_RDM = 4;
  static final int SOCK_SEQPACKET = 5;

  static final int OPT_LOOKUP_SENDER = 1;
  static final int OPT_PEEK = 2;
  static final int OPT_NON_BLOCKING = 4;

  /**
   * Indicator that the handle isn't referring to a socket but some other file descriptor.
   */
  static final int OPT_NON_SOCKET = 8;

  static final int OPT_DGRAM_MODE = 16;

  static final int BIND_OPT_REUSE = 1;

  static final int SOCKETSTATUS_INVALID = -1;
  static final int SOCKETSTATUS_UNKNOWN = 0;
  static final int SOCKETSTATUS_BOUND = 1;
  static final int SOCKETSTATUS_CONNECTED = 2;

  @SuppressWarnings("StaticAssignmentOfThrowable" /* errorprone */)
  private static Throwable initError = null;

  static final int SHUT_RD = 0;
  static final int SHUT_WR = 1;
  static final int SHUT_RD_WR = 2;

  @ExcludeFromCodeCoverageGeneratedReport(reason = "unreachable")
  private NativeUnixSocket() {
    throw new UnsupportedOperationException("No instances");
  }

  static {
    boolean loadSuccessful = false;
    try (NativeLibraryLoader nll = new NativeLibraryLoader()) {
      nll.loadLibrary();
      loadSuccessful = true;
    } catch (RuntimeException | Error e) {
      initError = e;
      StackTraceUtil.printStackTraceSevere(e);
    } finally {
      setLoaded(loadSuccessful);
    }

    AFAddressFamily.registerAddressFamily("generic", NativeUnixSocket.DOMAIN_GENERIC,
        "org.newsclub.net.unix.AFGenericSocketAddress");
    AFAddressFamily.registerAddressFamily("un", NativeUnixSocket.DOMAIN_UNIX,
        "org.newsclub.net.unix.AFUNIXSocketAddress");
    AFAddressFamily.registerAddressFamily("tipc", NativeUnixSocket.DOMAIN_TIPC,
        "org.newsclub.net.unix.AFTIPCSocketAddress");
    AFAddressFamily.registerAddressFamily("vsock", NativeUnixSocket.DOMAIN_VSOCK,
        "org.newsclub.net.unix.AFVSOCKSocketAddress");
    AFAddressFamily.registerAddressFamily("system", NativeUnixSocket.DOMAIN_SYSTEM,
        "org.newsclub.net.unix.AFSYSTEMSocketAddress");
  }

  static boolean isLoaded() {
    return LOADED.get();
  }

  static void ensureSupported() throws UnsupportedOperationException {
    if (!isLoaded()) {
      throw unsupportedException();
    }
  }

  static UnsupportedOperationException unsupportedException() {
    if (!isLoaded()) {
      return (UnsupportedOperationException) new UnsupportedOperationException(
          "junixsocket may not be fully supported on this platform").initCause(initError);
    } else {
      return null;
    }
  }

  static Throwable retrieveInitError() {
    return initError;
  }

  static void initPre() {
    // in some environments, JNI FindClass won't find these classes unless we resolve them first
    tryResolveClass(AbstractSelectableChannel.class.getName());
    tryResolveClass("java.lang.ProcessBuilder$RedirectPipeImpl");
    tryResolveClass(InetSocketAddress.class.getName());
    tryResolveClass(InvalidArgumentSocketException.class.getName());
    tryResolveClass(AddressUnavailableSocketException.class.getName());
    tryResolveClass(OperationNotSupportedSocketException.class.getName());
    tryResolveClass(NoSuchDeviceSocketException.class.getName());
    tryResolveClass(BrokenPipeSocketException.class.getName());
    tryResolveClass(ConnectionResetSocketException.class.getName());
    tryResolveClass(SocketClosedException.class.getName());
  }

  private static void tryResolveClass(String className) {
    try {
      Class.forName(className);
    } catch (Exception e) {
      // ignore
    }
  }

  @SuppressFBWarnings("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION")
  static native void init() throws Exception;

  @SuppressFBWarnings("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION")
  static native void destroy() throws Exception;

  /**
   * Can be used to check (without side-effects) if the library has been loaded.
   *
   * Terminates normally if so; throws {@link UnsatisfiedLinkError} if not.
   */
  static native void noop();

  static native int capabilities();

  static native byte[] sockname(int domain, FileDescriptor fd, boolean peer);

  static native long bind(ByteBuffer sockaddr, int sockaddrLen, FileDescriptor fd, int options)
      throws IOException;

  static native void listen(FileDescriptor fd, int backlog) throws IOException;

  static native boolean accept(ByteBuffer sockaddr, int sockaddrLen, FileDescriptor fdServer,
      FileDescriptor fd, long inode, int timeout) throws IOException;

  static native boolean connect(ByteBuffer sockaddr, int sockaddrLen, FileDescriptor fd, long inode)
      throws IOException;

  static native boolean finishConnect(FileDescriptor fd) throws IOException;

  static native void disconnect(FileDescriptor fd) throws IOException;

  static native int socketStatus(FileDescriptor fd) throws IOException;

  static native FileDescriptor duplicate(FileDescriptor fdSource, FileDescriptor fdTarget)
      throws IOException;

  static native Class<?> primaryType(FileDescriptor fd) throws IOException;

  /**
   * Reads data from an {@link AFSocketImpl}.
   *
   * @param fd The corresponding file descriptor.
   * @param buf The buffer to read into, or {@code null} if a single byte should be read.
   * @param off The buffer offset.
   * @param len The maximum number of bytes to read. Must be 1 if {@code buf} is {@code null}.
   * @param options Options.
   * @param ancillaryDataSupport The ancillary data support instance, or {@code null}.
   * @return The number of bytes read, -1 if nothing could be read, or the byte itself iff
   *         {@code buf} was {@code null}.
   * @throws IOException upon error.
   */
  static native int read(FileDescriptor fd, byte[] buf, int off, int len, int options,
      AncillaryDataSupport ancillaryDataSupport, int timeoutMillis) throws IOException;

  /**
   * Writes data to an {@link AFSocketImpl}.
   *
   * @param fd The corresponding file descriptor.
   * @param buf The buffer to write from, or {@code null} if a single byte should be written.
   * @param off The buffer offset, or the byte to write if {@code buf} is {@code null}.
   * @param len The number of bytes to write. Must be 1 if {@code buf} is {@code null}.
   * @param options Options.
   * @param ancillaryDataSupport The ancillary data support instance, or {@code null}.
   * @return The number of bytes written (which could be 0).
   * @throws IOException upon error.
   */
  static native int write(FileDescriptor fd, byte[] buf, int off, int len, int options,
      AncillaryDataSupport ancillaryDataSupport) throws IOException;

  static native int receive(FileDescriptor fd, ByteBuffer directBuffer, int offset, int length,
      ByteBuffer directSocketAddressOut, int options, AncillaryDataSupport ancillaryDataSupport,
      int timeoutMillis) throws IOException;

  static native int send(FileDescriptor fd, ByteBuffer directBuffer, int offset, int length,
      ByteBuffer directSocketAddress, int addrLen, int options,
      AncillaryDataSupport ancillaryDataSupport) throws IOException;

  static native void close(FileDescriptor fd) throws IOException;

  static native void shutdown(FileDescriptor fd, int mode) throws IOException;

  static native int getSocketOptionInt(FileDescriptor fd, int optionId) throws IOException;

  static native void setSocketOptionInt(FileDescriptor fd, int optionId, int value)
      throws IOException;

  static native <T> T getSocketOption(FileDescriptor fd, int level, int optionName,
      Class<T> valueType) throws IOException;

  static native void setSocketOption(FileDescriptor fd, int level, int optionName, Object value)
      throws IOException;

  static native int available(FileDescriptor fd, ByteBuffer buf) throws IOException;

  static native AFUNIXSocketCredentials peerCredentials(FileDescriptor fd,
      AFUNIXSocketCredentials creds) throws IOException;

  static native void initServerImpl(ServerSocket serverSocket, AFSocketImpl<?> impl)
      throws IOException;

  static native void createSocket(FileDescriptor fdesc, int domain, int type) throws IOException;

  static native void setPort(SocketAddress addr, int port);

  static native void initFD(FileDescriptor fdesc, int fd) throws IOException;

  static native int getFD(FileDescriptor fdesc) throws IOException;

  static native void copyFileDescriptor(FileDescriptor source, FileDescriptor target)
      throws IOException;

  static native void attachCloseable(FileDescriptor fdesc, Closeable closeable)
      throws SocketException;

  static native int maxAddressLength();

  static native int sockAddrLength(int domain);

  static native int ancillaryBufMinLen();

  static native byte[] sockAddrToBytes(int domain, ByteBuffer sockAddrDirect);

  static native int bytesToSockAddr(int domain, ByteBuffer sockAddrDirectBuf, byte[] address);

  @SuppressFBWarnings("THROWS_METHOD_THROWS_RUNTIMEEXCEPTION")
  static void setPort1(SocketAddress addr, int port) throws SocketException {
    if (port < 0) {
      throw new IllegalArgumentException("port out of range:" + port);
    }

    try {
      setPort(addr, port);
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw (SocketException) new SocketException("Could not set port").initCause(e);
    }
  }

  static native Socket currentRMISocket();

  static native boolean initPipe(FileDescriptor source, FileDescriptor sink, boolean selectable)
      throws IOException;

  static native int poll(PollFd pollFd, int timeout) throws IOException;

  static native void configureBlocking(FileDescriptor fd, boolean blocking) throws IOException;

  /**
   * Checks if the given file descriptor describes a blocking socket.
   *
   * @param fd The file descriptor to check
   * @return 0 = non-blocking, 1 = blocking, 2 = indeterminate (needs reconfiguration)
   * @throws IOException on error.
   */
  static native int checkBlocking(FileDescriptor fd) throws IOException;

  static native void socketPair(int domain, int type, FileDescriptor fd, FileDescriptor fd2);

  static native Redirect initRedirect(FileDescriptor fd);

  static native void deregisterSelectionKey(AbstractSelectableChannel chann, SelectionKey key);

  static native byte[] tipcGetNodeId(int peer) throws IOException;

  static native byte[] tipcGetLinkName(int peer, int bearerId) throws IOException;

  static native int sockAddrNativeDataOffset();

  static native int sockAddrNativeFamilyOffset();

  static native int sockTypeToNative(int type) throws IOException;

  static native int vsockGetLocalCID() throws IOException;

  static native int systemResolveCtlId(FileDescriptor fd, String ctlName) throws IOException;

  static void setLoaded(boolean successful) {
    LOADED.compareAndSet(false, successful);
  }
}
