/*
 * 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.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ProcessBuilder.Redirect;
import java.net.DatagramSocket;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.channels.DatagramChannel;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.spi.AbstractSelectableChannel;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNull;

import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;

/**
 * Provides object-oriented access to file descriptors via {@link InputStream}, {@link Socket},
 * etc., depending on the file descriptor type.
 * <p>
 * Typical usage:
 * </p>
 * <pre><code>
 * FileDescriptor fd;
 *
 * // succeeds if fd refers to an AF_UNIX stream socket
 * AFUNIXSocket socket = FileDescriptorCast.using(fd).as(AFUNIXSocket.class);
 *
 * // succeeds if fd refers to an AF_UNIX datagram socket
 * AFUNIXDatagramChannel channel = FileDescriptorCast.using(fd).as(AFUNIXDatagramChannel.class);
 *
 * // always succeeds
 * InputStream in = FileDescriptorCast.using(fd).as(InputStream.class);
 * OutputStream in = FileDescriptorCast.using(fd).as(OutputStream.class);
 * </code></pre>
 * <p>
 * <b>Important notes</b>
 * <ol>
 * <li>On some platforms (e.g., Solaris, Illumos) you may need to re-apply a read timeout (e.g.,
 * using {@link Socket#setSoTimeout(int)}) after obtaining the socket.</li>
 * <li>You may lose Java port information for {@link AFSocketAddress} implementations that do not
 * encode this information directly (such as {@link AFUNIXSocketAddress} and
 * {@link AFTIPCSocketAddress}).</li>
 * <li>The "blocking" state of a socket may be forcibly changed to "blocking" when performing the
 * cast, especially when casting to {@link Socket}, {@link DatagramSocket} or {@link ServerSocket}
 * and any of their subclasses where "blocking" is the expected state.</li>
 * <li>When calling {@link #using(FileDescriptor)} for a {@link FileDescriptor} obtained from
 * another socket or other resource in the same JVM (i.e., not from another process), especially for
 * sockets provided by junixsocket itself, there is a chance that the garbage collector may clean up
 * the original socket at an opportune moment, thereby closing the resource underlying the shared
 * {@link FileDescriptor} prematurely.
 * <p>
 * This is considered an edge-case, and deliberately not handled automatically for performance and
 * portability reasons: We would have to do additional reference counting on all FileDescriptor
 * instances, either through patching {@code FileCleanable} or a shared data structure.
 * <p>
 * The issue can be prevented by keeping a reference to the original object, such as keeping it in
 * an enclosing try-with-resources block or as a member variable, for example. Alternatively, using
 * a "duplicate" file descriptor (via {@link #duplicating(FileDescriptor)}) circumvents this
 * problem, at the cost of using additional system resources.</li>
 * <li>As a consequence of the previous point: For {@link #using(FileDescriptor)}: when casting file
 * descriptors that belong to a junixsocket-controlled sockets, the target socket is configured in a
 * way such that garbage collection will not automatically close the target's underlying file
 * descriptor (but still potentially any file descriptors received from other processes via
 * ancillary messages).</li>
 * <li>The same restrictions as for {@link #using(FileDescriptor)} apply to
 * {@link #unsafeUsing(int)} as well.</li>
 * </ol>
 *
 * @author Christian Kohlschütter
 */
@SuppressWarnings("PMD.CouplingBetweenObjects")
public final class FileDescriptorCast implements FileDescriptorAccess {
  private static final Map<Class<?>, CastingProviderMap> PRIMARY_TYPE_PROVIDERS_MAP = Collections
      .synchronizedMap(new HashMap<>());

  private static final AFFunction<FileDescriptor, FileInputStream> FD_IS_PROVIDER = System
      .getProperty("osv.version") != null ? LenientFileInputStream::new : FileInputStream::new;

  private static final CastingProviderMap GLOBAL_PROVIDERS_FINAL = new CastingProviderMap() {

    @Override
    protected void addProviders() {
      // FileDescriptor and Object cannot be overridden
      addProvider(FileDescriptor.class, new CastingProvider<FileDescriptor>() {
        @Override
        public FileDescriptor provideAs(FileDescriptorCast fdc,
            Class<? super FileDescriptor> desiredType) throws IOException {
          return fdc.getFileDescriptor();
        }
      });
    }
  };

  private static final CastingProviderMap GLOBAL_PROVIDERS = new CastingProviderMap() {
    @Override
    protected void addProviders() {
      addProvider(WritableByteChannel.class, new CastingProvider<WritableByteChannel>() {
        @SuppressWarnings("resource")
        @Override
        public WritableByteChannel provideAs(FileDescriptorCast fdc,
            Class<? super WritableByteChannel> desiredType) throws IOException {
          return new FileOutputStream(fdc.getFileDescriptor()).getChannel();
        }
      });
      addProvider(ReadableByteChannel.class, new CastingProvider<ReadableByteChannel>() {
        @Override
        public ReadableByteChannel provideAs(FileDescriptorCast fdc,
            Class<? super ReadableByteChannel> desiredType) throws IOException {
          return FD_IS_PROVIDER.apply(fdc.getFileDescriptor()).getChannel();
        }
      });

      addProvider(FileChannel.class, new CastingProvider<FileChannel>() {
        @Override
        public FileChannel provideAs(FileDescriptorCast fdc, Class<? super FileChannel> desiredType)
            throws IOException {
          return RAFChannelProvider.getFileChannel(fdc.getFileDescriptor());
        }
      });

      addProvider(FileOutputStream.class, new CastingProvider<FileOutputStream>() {
        @Override
        public FileOutputStream provideAs(FileDescriptorCast fdc,
            Class<? super FileOutputStream> desiredType) throws IOException {
          return new FileOutputStream(fdc.getFileDescriptor());
        }
      });
      addProvider(FileInputStream.class, new CastingProvider<FileInputStream>() {
        @Override
        public FileInputStream provideAs(FileDescriptorCast fdc,
            Class<? super FileInputStream> desiredType) throws IOException {
          return FD_IS_PROVIDER.apply(fdc.getFileDescriptor());
        }
      });
      addProvider(FileDescriptor.class, new CastingProvider<FileDescriptor>() {
        @Override
        public FileDescriptor provideAs(FileDescriptorCast fdc,
            Class<? super FileDescriptor> desiredType) throws IOException {
          return fdc.getFileDescriptor();
        }
      });
      addProvider(Integer.class, new CastingProvider<Integer>() {
        @Override
        public Integer provideAs(FileDescriptorCast fdc, Class<? super Integer> desiredType)
            throws IOException {
          FileDescriptor fd = fdc.getFileDescriptor();
          int val = fd.valid() ? NativeUnixSocket.getFD(fd) : -1;
          if (val == -1) {
            throw new IOException("Not a valid file descriptor");
          }
          return val;
        }
      });

      if (AFSocket.supports(AFSocketCapability.CAPABILITY_FD_AS_REDIRECT)) {
        addProvider(Redirect.class, new CastingProvider<Redirect>() {
          @Override
          public Redirect provideAs(FileDescriptorCast fdc, Class<? super Redirect> desiredType)
              throws IOException {

            Redirect red = NativeUnixSocket.initRedirect(fdc.getFileDescriptor());
            if (red == null) {
              throw new ClassCastException("Cannot access file descriptor as " + desiredType);
            }
            return red;
          }
        });
      }
    }
  };

  private static final int FD_IN = getFdIfPossible(FileDescriptor.in);
  private static final int FD_OUT = getFdIfPossible(FileDescriptor.out);
  private static final int FD_ERR = getFdIfPossible(FileDescriptor.err);

  static {
    registerGenericSocketSupport();
  }

  private final FileDescriptor fdObj;

  private int localPort = 0;
  private int remotePort = 0;

  private final CastingProviderMap cpm;

  private FileDescriptorCast(FileDescriptor fdObj, CastingProviderMap cpm) {
    this.fdObj = Objects.requireNonNull(fdObj);
    this.cpm = Objects.requireNonNull(cpm);
  }

  private static int getFdIfPossible(FileDescriptor fd) {
    if (!NativeUnixSocket.isLoaded()) {
      return -1;
    }
    try {
      if (!fd.valid()) {
        return -1;
      }
      return NativeUnixSocket.getFD(fd);
    } catch (IOException e) {
      return -1;
    }
  }

  private static void registerCastingProviders(Class<?> primaryType, CastingProviderMap cpm) {
    Objects.requireNonNull(primaryType);
    CastingProviderMap prev;
    if ((prev = PRIMARY_TYPE_PROVIDERS_MAP.put(primaryType, cpm)) != null) {
      PRIMARY_TYPE_PROVIDERS_MAP.put(primaryType, prev);
      throw new IllegalStateException("Already registered: " + primaryType);
    }
  }

  static <A extends AFSocketAddress> void registerCastingProviders(
      AFAddressFamilyConfig<A> config) {
    Class<? extends AFSocket<A>> socketClass = config.socketClass();
    Class<? extends AFDatagramSocket<A>> datagramSocketClass = config.datagramSocketClass();

    registerCastingProviders(socketClass, new CastingProviderMap() {

      @SuppressWarnings("null")
      @Override
      protected void addProviders() {
        addProviders(GLOBAL_PROVIDERS);

        final CastingProviderSocketOrChannel<AFSocket<A>> cpSocketOrChannel = (fdc, desiredType,
            isChannel) -> reconfigure(isChannel, AFSocket.newInstance(config.socketConstructor(),
                (AFSocketFactory<A>) null, fdc.getFileDescriptor(), fdc.localPort, fdc.remotePort));
        final CastingProviderSocketOrChannel<AFServerSocket<A>> cpServerSocketOrChannel = (fdc,
            desiredType, isChannel) -> reconfigure(isChannel, AFServerSocket.newInstance(config
                .serverSocketConstructor(), fdc.getFileDescriptor(), fdc.localPort,
                fdc.remotePort));

        registerGenericSocketProviders();

        addProvider(socketClass, (fdc, desiredType) -> cpSocketOrChannel.provideAs(fdc, desiredType,
            false));
        addProvider(config.serverSocketClass(), (fdc, desiredType) -> cpServerSocketOrChannel
            .provideAs(fdc, desiredType, false));
        addProvider(config.socketChannelClass(), (fdc, desiredType) -> cpSocketOrChannel.provideAs(
            fdc, AFSocket.class, true).getChannel());
        addProvider(config.serverSocketChannelClass(), (fdc, desiredType) -> cpServerSocketOrChannel
            .provideAs(fdc, AFServerSocket.class, true).getChannel());
      }
    });

    registerCastingProviders(datagramSocketClass, new CastingProviderMap() {

      @SuppressWarnings("null")
      @Override
      protected void addProviders() {
        addProviders(GLOBAL_PROVIDERS);

        final CastingProviderSocketOrChannel<AFDatagramSocket<A>> cpDatagramSocketOrChannel = (fdc,
            desiredType, isChannel) -> reconfigure(isChannel, AFDatagramSocket.newInstance(config
                .datagramSocketConstructor(), fdc.getFileDescriptor(), fdc.localPort,
                fdc.remotePort));

        registerGenericDatagramSocketProviders();

        addProvider(datagramSocketClass, (fdc, desiredType) -> cpDatagramSocketOrChannel.provideAs(
            fdc, desiredType, false));
        addProvider(config.datagramChannelClass(), (fdc, desiredType) -> cpDatagramSocketOrChannel
            .provideAs(fdc, AFDatagramSocket.class, true).getChannel());
      }
    });
  }

  private abstract static class CastingProviderMap {
    private final Map<Class<?>, CastingProvider<?>> providers = new HashMap<>();
    private final Set<Class<?>> classes = Collections.unmodifiableSet(providers.keySet());

    @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
    protected CastingProviderMap() {
      addProviders();

      addProviders(GLOBAL_PROVIDERS_FINAL);
    }

    @SuppressWarnings("null")
    protected void registerGenericSocketProviders() {
      final CastingProviderSocketOrChannel<AFSocket<AFGenericSocketAddress>> cpSocketOrChannelGeneric =
          (fdc, desiredType, isChannel) -> reconfigure(isChannel, AFSocket.newInstance(
              AFGenericSocket::new, (AFSocketFactory<AFGenericSocketAddress>) null, fdc
                  .getFileDescriptor(), fdc.localPort, fdc.remotePort));
      final CastingProviderSocketOrChannel<AFServerSocket<AFGenericSocketAddress>> cpServerSocketOrChannelGeneric =
          (fdc, desiredType, isChannel) -> reconfigure(isChannel, AFServerSocket.newInstance(
              AFGenericServerSocket::new, fdc.getFileDescriptor(), fdc.localPort, fdc.remotePort));

      addProvider(AFGenericSocket.class, (fdc, desiredType) -> cpSocketOrChannelGeneric.provideAs(
          fdc, desiredType, false));
      addProvider(AFGenericServerSocket.class, (fdc, desiredType) -> cpServerSocketOrChannelGeneric
          .provideAs(fdc, desiredType, false));
      addProvider(AFGenericSocketChannel.class, (fdc, desiredType) -> cpSocketOrChannelGeneric
          .provideAs(fdc, AFSocket.class, true).getChannel());
      addProvider(AFGenericServerSocketChannel.class, (fdc,
          desiredType) -> cpServerSocketOrChannelGeneric.provideAs(fdc, AFServerSocket.class, true)
              .getChannel());
    }

    @SuppressWarnings("null")
    protected void registerGenericDatagramSocketProviders() {
      final CastingProviderSocketOrChannel<AFDatagramSocket<AFGenericSocketAddress>> cpDatagramSocketOrChannelGeneric =
          (fdc, desiredType, isChannel) -> reconfigure(isChannel, AFDatagramSocket.newInstance(
              AFGenericDatagramSocket::new, fdc.getFileDescriptor(), fdc.localPort,
              fdc.remotePort));

      addProvider(AFDatagramSocket.class, (fdc, desiredType) -> cpDatagramSocketOrChannelGeneric
          .provideAs(fdc, desiredType, false));
      addProvider(AFDatagramChannel.class, (fdc, desiredType) -> cpDatagramSocketOrChannelGeneric
          .provideAs(fdc, AFDatagramSocket.class, true).getChannel());
    }

    protected abstract void addProviders();

    protected final <T> void addProvider(Class<T> type, CastingProvider<?> cp) {
      Objects.requireNonNull(type);

      addProvider0(type, cp);
    }

    private void addProvider0(Class<?> type, CastingProvider<?> cp) {
      if (providers.put(type, cp) != cp) { // NOPMD
        for (Class<?> cl : type.getInterfaces()) {
          addProvider0(cl, cp);
        }
        Class<?> scl = type.getSuperclass();
        if (scl != null) {
          addProvider0(scl, cp);
        }
      }
    }

    protected final void addProviders(CastingProviderMap other) {
      if (other == null || other == this) { // NOPMD
        return;
      }
      this.providers.putAll(other.providers);
    }

    @SuppressWarnings("unchecked")
    public <T> CastingProvider<? extends T> get(Class<T> desiredType) {
      return (CastingProvider<? extends T>) providers.get(desiredType);
    }
  }

  @FunctionalInterface
  private interface CastingProvider<T> {
    T provideAs(FileDescriptorCast fdc, Class<? super T> desiredType) throws IOException;
  }

  @FunctionalInterface
  private interface CastingProviderSocketOrChannel<T> {
    T provideAs(FileDescriptorCast fdc, Class<? super T> desiredType, boolean isChannel)
        throws IOException;
  }

  /**
   * Creates a {@link FileDescriptorCast} using the given file descriptor.
   * <p>
   * Note that if any resource that also references this {@link FileDescriptor} is
   * garbage-collected, the cleanup for that object may close the referenced {@link FileDescriptor},
   * thereby resulting in premature connection losses, etc. See {@link #duplicating(FileDescriptor)}
   * for a solution to this problem.
   *
   * @param fdObj The file descriptor.
   * @return The {@link FileDescriptorCast} instance.
   * @throws IOException on error, especially if the given file descriptor is invalid or
   *           unsupported.
   */
  public static FileDescriptorCast using(FileDescriptor fdObj) throws IOException {
    if (!fdObj.valid()) {
      throw new IOException("Not a valid file descriptor");
    }

    Class<?> primaryType = NativeUnixSocket.isLoaded() ? NativeUnixSocket.primaryType(fdObj) : null;
    if (primaryType == null) {
      primaryType = FileDescriptor.class;
    }

    triggerInit();

    CastingProviderMap map = PRIMARY_TYPE_PROVIDERS_MAP.get(primaryType);
    return new FileDescriptorCast(fdObj, map == null ? GLOBAL_PROVIDERS : map);
  }

  /**
   * Creates a {@link FileDescriptorCast} using a <em>duplicate</em> of the given file descriptor.
   * <p>
   * Duplicating a file descriptor is performed at the system-level, which means an additional file
   * descriptor pointing to the same resource as the original is created by the operating system.
   * <p>
   * The advantage of using {@link #duplicating(FileDescriptor)} over {@link #using(FileDescriptor)}
   * is that neither implicit garbage collection nor an explicit call to {@link Closeable#close()}
   * on a resource owning the original {@link FileDescriptor} affects the availability of the
   * resource from the target of the cast.
   *
   * @param fdObj The file descriptor to duplicate.
   * @return The {@link FileDescriptorCast} instance.
   * @throws IOException on error, especially if the given file descriptor is invalid or
   *           unsupported, or if duplicating fails or is unsupported.
   */
  public static FileDescriptorCast duplicating(FileDescriptor fdObj) throws IOException {
    if (!fdObj.valid()) {
      throw new IOException("Not a valid file descriptor");
    }

    FileDescriptor duplicate = NativeUnixSocket.duplicate(fdObj, new FileDescriptor());
    if (duplicate == null) {
      throw new IOException("Could not duplicate file descriptor");
    }
    return using(duplicate);
  }

  /**
   * Creates a {@link FileDescriptorCast} using the given native file descriptor value.
   * <p>
   * This method is inherently unsafe as it may
   * <ol>
   * <li>make assumptions on the internal system representation of a file descriptor (which differs
   * between Windows and Unix, for example).</li>
   * <li>provide access to resources that are otherwise not accessible</li>
   * </ol>
   * <p>
   * Note that attempts are made to reuse {@link FileDescriptor#in}, {@link FileDescriptor#out}, and
   * {@link FileDescriptor#err}, respectively.
   *
   * @param fd The system-native file descriptor value.
   * @return The {@link FileDescriptorCast} instance.
   * @throws IOException on error, especially if the given file descriptor is invalid or
   *           unsupported, or when "unsafe" operations are unavailable or manually disabled for the
   *           current environment.
   */
  @Unsafe
  public static FileDescriptorCast unsafeUsing(int fd) throws IOException {
    AFSocket.ensureUnsafeSupported();

    FileDescriptor fdObj;
    if (fd == -1) {
      throw new IOException("Not a valid file descriptor");
    } else if (fd == FD_IN) {
      fdObj = FileDescriptor.in;
    } else if (fd == FD_OUT) {
      fdObj = FileDescriptor.out;
    } else if (fd == FD_ERR) {
      fdObj = FileDescriptor.err;
    } else {
      fdObj = null;
    }

    if (fdObj != null) {
      int check = getFdIfPossible(fdObj);
      if (fd == check) {
        return using(fdObj);
      }
    }

    fdObj = new FileDescriptor();
    NativeUnixSocket.initFD(fdObj, fd);

    return using(fdObj);
  }

  private static void triggerInit() {
    for (AFAddressFamily<?> family : new AFAddressFamily<?>[] {
        AFUNIXSocketAddress.addressFamily(), //
        AFTIPCSocketAddress.addressFamily(), //
        AFVSOCKSocketAddress.addressFamily(), //
        AFSYSTEMSocketAddress.addressFamily(), //
    }) {
      Objects.requireNonNull(family.getClass()); // trigger init
    }
  }

  /**
   * Registers the given port number as the "local port" for this file descriptor.
   *
   * Important: This only changes the state of this instance. The actual file descriptor is not
   * affected.
   *
   * @param port The port to assign to (must be &gt;= 0).
   * @return This instance.
   */
  public FileDescriptorCast withLocalPort(int port) {
    if (port < 0) {
      throw new IllegalArgumentException();
    }
    this.localPort = port;
    return this;
  }

  /**
   * Registers the given port number as the "remote port" for this file descriptor.
   *
   * Important: This only changes the state of this instance. The actual file descriptor is not
   * affected.
   *
   * @param port The port to assign to (must be &gt;= 0).
   * @return This instance.
   */
  public FileDescriptorCast withRemotePort(int port) {
    if (port < 0) {
      throw new IllegalArgumentException();
    }
    this.remotePort = port;
    return this;
  }

  /**
   * Casts this instance to the desired type.
   *
   * @param <K> The desired type.
   * @param desiredType The class of the desired type.
   * @return s An instance of the desired type.
   * @throws IOException if there was a problem while casting.
   * @throws ClassCastException if the cast cannot be legally made.
   * @see #availableTypes()
   * @see #isAvailable(Class)
   */
  @SuppressWarnings("PMD.ShortMethodName")
  public @NonNull <K> K as(Class<K> desiredType) throws IOException {
    Objects.requireNonNull(desiredType);

    CastingProvider<? extends K> provider = cpm.get(desiredType);
    if (provider != null) {
      K obj = desiredType.cast(provider.provideAs(this, desiredType));
      Objects.requireNonNull(obj);
      return obj;
    } else {
      throw new ClassCastException("Cannot access file descriptor as " + desiredType);
    }
  }

  /**
   * Checks if the instance can be cast as the given desired type (using {@link #as(Class)}).
   *
   * @param desiredType The class of the desired type.
   * @return {@code true} if the cast can be made.
   * @throws IOException on error.
   * @see #as(Class)
   */
  public boolean isAvailable(Class<?> desiredType) throws IOException {
    return cpm.providers.containsKey(desiredType);
  }

  /**
   * Returns a collection of available types this instance can be cast to (using
   * {@link #as(Class)}).
   *
   * @return The collection of available types.
   */
  public Set<Class<?>> availableTypes() {
    return cpm.classes;
  }

  @Override
  @SuppressFBWarnings("EI_EXPOSE_REP")
  public FileDescriptor getFileDescriptor() {
    return fdObj;
  }

  private static final class LenientFileInputStream extends FileInputStream {
    private LenientFileInputStream(FileDescriptor fdObj) {
      super(fdObj);
    }

    @Override
    public int available() throws IOException {
      try {
        return super.available();
      } catch (IOException e) {
        String msg = e.getMessage();
        if ("Invalid seek".equals(msg)) {
          // OSv may not like FileInputStream#availabe() on pipe fds.
          return 0;
        }
        throw e;
      }
    }
  }

  /**
   * Add support for otherwise unsupported sockets.
   */
  private static void registerGenericSocketSupport() {
    registerCastingProviders(Socket.class, new CastingProviderMap() {

      @Override
      protected void addProviders() {
        addProviders(GLOBAL_PROVIDERS);

        registerGenericSocketProviders();
      }
    });

    registerCastingProviders(DatagramSocket.class, new CastingProviderMap() {
      @Override
      protected void addProviders() {
        addProviders(GLOBAL_PROVIDERS);

        registerGenericDatagramSocketProviders();
      }
    });
  }

  @SuppressWarnings("null")
  private static <S extends AFSocket<?>> S reconfigure(boolean isChannel, S socket)
      throws IOException {
    reconfigure(isChannel, socket.getChannel());
    socket.getAFImpl().getCore().disableCleanFd();
    return socket;
  }

  @SuppressWarnings("null")
  private static <S extends AFServerSocket<?>> S reconfigure(boolean isChannel, S socket)
      throws IOException {
    reconfigure(isChannel, socket.getChannel());
    socket.getAFImpl().getCore().disableCleanFd();
    return socket;
  }

  @SuppressWarnings("null")
  private static <S extends AFDatagramSocket<?>> S reconfigure(boolean isChannel, S socket)
      throws IOException {
    reconfigure(isChannel, socket.getChannel());
    socket.getAFImpl().getCore().disableCleanFd();
    return socket;
  }

  /**
   * Reconfigures the Java-side of the socket/socket channel such that its state is compatible with
   * the native socket's state. This is necessary to properly configure blocking/non-blocking state,
   * as that is cached on the Java side.
   * <p>
   * If {@code isChannel} is false, then we want to cast to a {@link Socket}, {@link DatagramSocket}
   * or {@link ServerSocket}, which means blocking I/O is desired. If the underlying native socket
   * is configured non-blocking, we need to reset the state to "blocking" accordingly.
   * <p>
   * If {@code isChannel} is true, then we want to cast to a {@link SocketChannel},
   * {@link DatagramChannel} or {@link ServerSocketChannel}, in which case the blocking state should
   * be preserved, if possible. It is then up to the user to check blocking state via
   * {@link AbstractSelectableChannel#isBlocking()} prior to using the socket.
   * <p>
   * Note that on Windows, it may be impossible to query the blocking state from an external socket,
   * so the state is always forcibly set to "blocking".
   *
   * @param <S> The type.
   * @param isChannel The desired cast type (socket=set to blocking, or channel=preserve state).
   * @param socketChannel The channel.
   * @throws IOException on error.
   */
  private static <@NonNull S extends AFSomeSocketChannel> void reconfigure(boolean isChannel,
      S socketChannel) throws IOException {
    if (isChannel) {
      reconfigureKeepBlockingState(socketChannel);
    } else {
      reconfigureSetBlocking(socketChannel);
    }
  }

  private static <@NonNull S extends AFSomeSocketChannel> void reconfigureKeepBlockingState(
      S socketChannel) throws IOException {
    int result = NativeUnixSocket.checkBlocking(socketChannel.getFileDescriptor());

    boolean blocking;
    switch (result) {
      case 0:
        blocking = false;
        break;
      case 1:
        blocking = true;
        break;
      case 2:
        // need to reconfigure/forcibly override any cached result -> set to blocking by default
        socketChannel.configureBlocking(false);
        socketChannel.configureBlocking(true);
        return;
      default:
        throw new OperationNotSupportedSocketException("Invalid blocking state");
    }

    socketChannel.configureBlocking(blocking);
  }

  private static <@NonNull S extends AFSomeSocketChannel> void reconfigureSetBlocking(
      S socketChannel) throws IOException {
    int result = NativeUnixSocket.checkBlocking(socketChannel.getFileDescriptor());

    switch (result) {
      case 0:
        // see below
        break;
      case 1:
        // already blocking, nothing to do
        return;
      case 2:
        // need to reconfigure/forcibly override any cached result -> set to blocking by default
        // see below
        break;
      default:
        throw new OperationNotSupportedSocketException("Invalid blocking state");
    }

    socketChannel.configureBlocking(false);
    socketChannel.configureBlocking(true);
  }
}
