/*
 * 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.ByteArrayInputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.newsclub.net.unix.pool.ObjectPool;
import org.newsclub.net.unix.pool.ObjectPool.Lease;

import com.google.errorprone.annotations.Immutable;
import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;

/**
 * Some {@link SocketAddress} that is supported by junixsocket, such as {@link AFUNIXSocketAddress}.
 *
 * @author Christian Kohlschütter
 */
@Immutable
@SuppressWarnings({"PMD.CouplingBetweenObjects", "PMD.CyclomaticComplexity"})
public abstract class AFSocketAddress extends InetSocketAddress {
  private static final long serialVersionUID = 1L; // do not change!

  /**
   * Just a marker for "don't actually bind" (checked with "=="). Used in combination with a
   * superclass' bind method, which should trigger "setBound()", etc.
   */
  static final AFSocketAddress INTERNAL_DUMMY_BIND = new SentinelSocketAddress(0);
  static final AFSocketAddress INTERNAL_DUMMY_CONNECT = new SentinelSocketAddress(1);
  static final AFSocketAddress INTERNAL_DUMMY_DONT_CONNECT = new SentinelSocketAddress(2);

  private static final int SOCKADDR_NATIVE_FAMILY_OFFSET = NativeUnixSocket.isLoaded() //
      ? NativeUnixSocket.sockAddrNativeFamilyOffset() : -1;

  private static final int SOCKADDR_NATIVE_DATA_OFFSET = NativeUnixSocket.isLoaded() //
      ? NativeUnixSocket.sockAddrNativeDataOffset() : -1;

  private static final int SOCKADDR_MAX_LEN = NativeUnixSocket.isLoaded() //
      ? NativeUnixSocket.sockAddrLength(0) : 256;

  private static final Map<AFAddressFamily<?>, Map<Integer, Map<ByteBuffer, AFSocketAddress>>> ADDRESS_CACHE =
      new HashMap<>();

  static final ObjectPool<ByteBuffer> SOCKETADDRESS_BUFFER_TL = ObjectPool.newThreadLocalPool(
      () -> {
        return AFSocketAddress.newSockAddrDirectBuffer(SOCKADDR_MAX_LEN);
      }, (o) -> {
        o.clear();
        return true;
      });

  private static final boolean USE_DESERIALIZATION_FOR_INIT;

  static {
    String v = System.getProperty("org.newsclub.net.unix.AFSocketAddress.deserialize", "");
    USE_DESERIALIZATION_FOR_INIT = v.isEmpty() ? NativeLibraryLoader.isAndroid() : Boolean
        .parseBoolean(v);
  }

  /**
   * Some byte-level representation of this address, which can only be converted to a native
   * representation in combination with the domain ID.
   */
  @SuppressFBWarnings("JCIP_FIELD_ISNT_FINAL_IN_IMMUTABLE_CLASS")
  private byte[] bytes;

  /**
   * An {@link InetAddress}-wrapped representation of this address. Only created upon demand.
   */
  @SuppressFBWarnings("JCIP_FIELD_ISNT_FINAL_IN_IMMUTABLE_CLASS") // only modified during
                                                                  // construction/deserialization
  private InetAddress inetAddress = null; // derived from bytes

  /**
   * The system-native representation of this address, or {@code null}.
   */
  @SuppressWarnings("PMD.ImmutableField")
  private transient ByteBuffer nativeAddress;

  /**
   * The address family.
   */
  private transient AFAddressFamily<?> addressFamily;

  /**
   * Creates a new socket address.
   *
   * @param port The port.
   * @param socketAddress The socket address in junixsocket-specific byte-array representation.
   * @param nativeAddress The socket address in system-native representation.
   * @param af The address family.
   * @throws SocketException on error.
   */
  @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
  protected AFSocketAddress(int port, final byte[] socketAddress, Lease<ByteBuffer> nativeAddress,
      AFAddressFamily<?> af) throws SocketException {
    /*
     * Initializing the superclass with an unresolved hostname helps us pass the #equals and
     * #hashCode checks, which unfortunately are declared final in InetSocketAddress.
     *
     * Using a resolved address (with the address bit initialized) would be ideal, but resolved
     * addresses can only be IPv4 or IPv6 (at least as of Java 16 and earlier).
     */
    super(AFInetAddress.createUnresolvedHostname(socketAddress, af), port >= 0 && port <= 0xffff
        ? port : 0);
    initAFSocketAddress(this, port, socketAddress, nativeAddress, af);
  }

  /**
   * Only for {@link SentinelSocketAddress}.
   *
   * @param clazz The {@link SentinelSocketAddress} class.
   * @param port A sentinel port number.
   */
  @SuppressWarnings("PMD.UnusedFormalParameter")
  AFSocketAddress(Class<SentinelSocketAddress> clazz, int port) {
    super(InetAddress.getLoopbackAddress(), port);
    this.nativeAddress = null;
    this.bytes = new byte[0];
    this.addressFamily = null;
  }

  @SuppressWarnings({"cast", "this-escape"})
  private static void initAFSocketAddress(AFSocketAddress addr, int port,
      final byte[] socketAddress, Lease<ByteBuffer> nativeAddress, AFAddressFamily<?> af)
      throws SocketException {
    if (socketAddress.length == 0) {
      throw new SocketException("Illegal address length: " + socketAddress.length);
    }

    addr.nativeAddress = nativeAddress == null ? null : (ByteBuffer) (Object) nativeAddress.get()
        .duplicate().rewind();
    if (port < -1) {
      throw new IllegalArgumentException("port out of range");
    } else if (port > 0xffff) {
      if (!NativeUnixSocket.isLoaded()) {
        throw (SocketException) new SocketException(
            "Cannot set SocketAddress port - junixsocket JNI library is not available").initCause(
                NativeUnixSocket.unsupportedException());
      }
      NativeUnixSocket.setPort1(addr, port);
    }

    addr.bytes = socketAddress.clone();
    addr.addressFamily = af;
  }

  /**
   * Returns a new {@link AFSocketAddress} instance via deserialization. This is a trick to
   * workaround certain environments that do not allow the construction of {@link InetSocketAddress}
   * instances without trying DNS resolution.
   *
   * @param <A> The subclass (must be a direct subclass of {@link AFSocketAddress}).
   * @param port The port to use.
   * @param socketAddress The junixsocket representation of the socket address.
   * @param nativeAddress The system-native representation of the socket address, or {@code null}.
   * @param af The address family, corresponding to the subclass
   * @param constructor The constructor to use as fallback
   * @return The new instance.
   * @throws SocketException on error.
   */
  protected static <A extends AFSocketAddress> A newDeserializedAFSocketAddress(int port,
      final byte[] socketAddress, Lease<ByteBuffer> nativeAddress, AFAddressFamily<A> af,
      AFSocketAddressConstructor<A> constructor) throws SocketException {
    String hostname = AFInetAddress.createUnresolvedHostname(socketAddress, af);
    if (hostname == null || hostname.isEmpty()) {
      return constructor.newAFSocketAddress(port, socketAddress, nativeAddress);
    }
    try (ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(AFSocketAddress
        .craftSerializedObject(af.getSocketAddressClass(), hostname, (port >= 0 && port <= 0xffff
            ? port : 0))))) {
      @SuppressWarnings("unchecked")
      A addr = (A) oin.readObject();
      initAFSocketAddress(addr, port, socketAddress, nativeAddress, af);
      return addr;
    } catch (SocketException e) {
      throw e;
    } catch (ClassNotFoundException | IOException e) {
      throw (SocketException) new SocketException("Unexpected deserialization problem").initCause(
          e);
    }
  }

  /**
   * Creates a byte-representation of a serialized {@link AFSocketAddress} instance, overriding
   * hostname and port, which allows bypassing DNS resolution.
   *
   * @param className The actual subclass.
   * @param hostname The hostname to use (must not be empty or null).
   * @param port The port to use.
   * @return The byte representation.
   */
  private static byte[] craftSerializedObject(Class<? extends AFSocketAddress> className,
      String hostname, int port) {
    ByteBuffer bb = ByteBuffer.allocate(768);
    bb.putShort((short) 0xaced); // STREAM_MAGIC
    bb.putShort((short) 5); // STREAM_VERSION
    bb.put((byte) 0x73); // TC_OBJECT
    bb.put((byte) 0x72); // TC_CLASSDESC

    putShortLengthUtf8(bb, className.getName());
    bb.putLong(1); // serialVersionUID of subclass (expected to be 1)
    bb.putInt(0x02000078);
    bb.put((byte) 0x72);

    putShortLengthUtf8(bb, AFSocketAddress.class.getName());
    bb.putLong(serialVersionUID); // serialVersionUID of AFSocketAddress
    bb.putInt(0x0300025B);
    putShortLengthUtf8(bb, "bytes");

    bb.putInt(0x7400025B);
    bb.putShort((short) 0x424C);

    putShortLengthUtf8(bb, "inetAddress");
    bb.put((byte) 0x74);

    putShortLengthEncodedClassName(bb, InetAddress.class);

    bb.putShort((short) 0x7872);
    putShortLengthUtf8(bb, InetSocketAddress.class.getName());
    bb.putLong(5076001401234631237L); // NOPMD InetSocketAddress serialVersionUID

    bb.putInt(0x03000349);
    putShortLengthUtf8(bb, "port");

    bb.put((byte) 0x4C);
    putShortLengthUtf8(bb, "addr");

    bb.putInt(0x71007E00);
    bb.putShort((short) 0x034C);
    putShortLengthUtf8(bb, "hostname");
    bb.put((byte) 0x74);

    putShortLengthEncodedClassName(bb, String.class);

    bb.putShort((short) 0x7872);
    putShortLengthUtf8(bb, SocketAddress.class.getName());
    bb.putLong(5215720748342549866L); // NOPMD SocketAddress serialVersionUID

    bb.putInt(0x02000078);
    bb.put((byte) 0x70);
    bb.putInt(port);

    bb.putShort((short) 0x7074);
    putShortLengthUtf8(bb, hostname);

    bb.putInt(0x78707077);
    bb.put((byte) 0x0B);

    putShortLengthUtf8(bb, "undefined");

    bb.put((byte) 0x78); // TC_ENDBLOCKDATA
    bb.flip();

    byte[] buf = new byte[bb.remaining()];
    bb.get(buf);
    return buf;
  }

  private static void putShortLengthEncodedClassName(ByteBuffer bb, Class<?> klazz) {
    putShortLengthUtf8(bb, "L" + klazz.getName().replace('.', '/') + ";");
  }

  private static void putShortLengthUtf8(ByteBuffer bb, String s) {
    byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);
    bb.putShort((short) utf8.length);
    bb.put(utf8);
  }

  /**
   * Checks if {@link AFSocketAddress} instantiation should be performed via deserialization.
   *
   * @return {@code true} if so.
   * @see #newDeserializedAFSocketAddress(int, byte[], Lease, AFAddressFamily,
   *      AFSocketAddressConstructor)
   */
  protected static boolean isUseDeserializationForInit() {
    return USE_DESERIALIZATION_FOR_INIT;
  }

  /**
   * Checks if the address can be resolved to a {@link File}.
   *
   * @return {@code true} if the address has a filename.
   */
  public abstract boolean hasFilename();

  /**
   * Returns the {@link File} corresponding with this address, if possible.
   *
   * A {@link FileNotFoundException} is thrown if there is no filename associated with the address,
   * which applies to addresses in the abstract namespace, for example.
   *
   * @return The filename.
   * @throws FileNotFoundException if the address is not associated with a filename.
   */
  public abstract File getFile() throws FileNotFoundException;

  /**
   * Returns the corresponding {@link AFAddressFamily}.
   *
   * @return The address family instance.
   */
  public final AFAddressFamily<?> getAddressFamily() {
    return addressFamily;
  }

  /**
   * Wraps the socket name/peer name of a file descriptor as an {@link InetAddress}.
   *
   * @param fdesc The file descriptor.
   * @param peerName If {@code true}, the remote peer name (instead of the local name) is retrieved.
   * @param af The address family.
   * @return The {@link InetAddress}.
   */
  protected static final InetAddress getInetAddress(FileDescriptor fdesc, boolean peerName,
      AFAddressFamily<?> af) {
    if (!fdesc.valid()) {
      return null;
    }
    byte[] addr = NativeUnixSocket.sockname(af.getDomain(), fdesc, peerName);
    if (addr == null) {
      return null;
    }
    return AFInetAddress.wrapAddress(addr, af);
  }

  /**
   * Gets the socket name/peer name of a file descriptor as an {@link AFSocketAddress}.
   *
   * @param <A> The corresponding address type.
   * @param fdesc The file descriptor.
   * @param requestPeerName If {@code true}, the remote peer name (instead of the local name) is
   *          retrieved.
   * @param port The port.
   * @param af The address family.
   * @return The {@link InetAddress}.
   */
  protected static final <A extends AFSocketAddress> @Nullable A getSocketAddress(
      FileDescriptor fdesc, boolean requestPeerName, int port, AFAddressFamily<A> af) {
    if (!fdesc.valid()) {
      return null;
    }
    byte[] addr = NativeUnixSocket.sockname(af.getDomain(), fdesc, requestPeerName);
    if (addr == null) {
      return null;
    }
    try {
      // FIXME we could infer the "port" from the path if the socket factory supports that
      return AFSocketAddress.unwrap(AFInetAddress.wrapAddress(addr, af), port, af);
    } catch (SocketException e) {
      throw new IllegalStateException(e);
    }
  }

  static final AFSocketAddress preprocessSocketAddress(
      Class<? extends AFSocketAddress> supportedAddressClass, SocketAddress endpoint,
      AFSocketAddressFromHostname<?> afh) throws SocketException {
    Objects.requireNonNull(endpoint);
    if (endpoint instanceof SentinelSocketAddress) {
      return (SentinelSocketAddress) endpoint;
    }

    if (!(endpoint instanceof AFSocketAddress)) {
      if (afh != null) {
        if (endpoint instanceof InetSocketAddress) {
          InetSocketAddress isa = (InetSocketAddress) endpoint;

          String hostname = isa.getHostString();
          if (afh.isHostnameSupported(hostname)) {
            try {
              endpoint = afh.addressFromHost(hostname, isa.getPort());
            } catch (SocketException e) {
              throw e;
            }
          }
        }
      }
      endpoint = mapOrFail(endpoint, supportedAddressClass);
    }

    Objects.requireNonNull(endpoint);

    if (!supportedAddressClass.isAssignableFrom(endpoint.getClass())) {
      throw new IllegalArgumentException("Can only connect to endpoints of type "
          + supportedAddressClass.getName() + ", got: " + endpoint.getClass() + ": " + endpoint);
    }

    return (AFSocketAddress) endpoint;
  }

  /**
   * Returns the (non-native) byte-level representation of this address.
   *
   * @return The byte array.
   */
  protected final byte[] getBytes() {
    return bytes; // NOPMD
  }

  /**
   * Returns a "special" {@link InetAddress} that contains information about this
   * {@link AFSocketAddress}.
   *
   * IMPORTANT: This {@link InetAddress} does not properly compare (using
   * {@link InetAddress#equals(Object)} and {@link InetAddress#hashCode()}). It should be used
   * exclusively to circumvent existing APIs like {@link DatagramSocket} that only accept/return
   * {@link InetAddress} and not arbitrary {@link SocketAddress} types.
   *
   * @return The "special" {@link InetAddress}.
   */
  public final InetAddress wrapAddress() {
    return AFInetAddress.wrapAddress(bytes, getAddressFamily());
  }

  /**
   * A reference to the constructor of an AFSocketAddress subclass.
   *
   * @param <T> The actual subclass.
   * @author Christian Kohlschütter
   */
  @FunctionalInterface
  protected interface AFSocketAddressConstructor<T extends AFSocketAddress> {
    /**
     * Constructs a new AFSocketAddress instance.
     *
     * @param port The port.
     * @param socketAddress The socket address in junixsocket-specific byte-array representation.
     * @param nativeAddress The socket address in system-native representation.
     * @return The instance.
     * @throws SocketException on error.
     */
    @NonNull
    T newAFSocketAddress(int port, byte[] socketAddress, Lease<ByteBuffer> nativeAddress)
        throws SocketException;
  }

  /**
   * Resolves a junixsocket-specific byte-array representation of an {@link AFSocketAddress} to an
   * actual {@link AFSocketAddress} instance, possibly reusing a cached instance.
   *
   * @param <A> The concrete {@link AFSocketAddress} that is supported by this type.
   * @param socketAddress The socket address in junixsocket-specific byte-array representation.
   * @param port The port.
   * @param af The address family.
   * @return The instance.
   * @throws SocketException on error.
   */
  @SuppressWarnings({"unchecked", "null"})
  protected static final <A extends AFSocketAddress> A resolveAddress(final byte[] socketAddress,
      int port, AFAddressFamily<A> af) throws SocketException {
    if (socketAddress.length == 0) {
      throw new SocketException("Address cannot be empty");
    }

    if (port == -1) {
      port = 0;
    }

    try (Lease<ByteBuffer> lease = SOCKETADDRESS_BUFFER_TL.take()) {
      ByteBuffer direct = lease.get();
      int limit = NativeUnixSocket.isLoaded() ? NativeUnixSocket.bytesToSockAddr(af.getDomain(),
          direct, socketAddress) : -1;
      if (limit == -1) {
        // not supported, but we can still create an address
        return af.getAddressConstructor().newAFSocketAddress(port, socketAddress, null);
      } else if (limit > SOCKADDR_MAX_LEN) {
        throw new IllegalStateException("Unexpected address length");
      }
      direct.rewind();
      direct.limit(limit);

      A instance;
      synchronized (AFSocketAddress.class) {
        Map<ByteBuffer, AFSocketAddress> map;
        Map<Integer, Map<ByteBuffer, AFSocketAddress>> mapPorts = ADDRESS_CACHE.get(af);
        if (mapPorts == null) {
          instance = null;
          mapPorts = new HashMap<>();
          map = new HashMap<>();
          mapPorts.put(port, map);
          ADDRESS_CACHE.put(af, mapPorts);
        } else {
          map = mapPorts.get(port);
          if (map == null) {
            instance = null;
            map = new HashMap<>();
            mapPorts.put(port, map);
          } else {
            instance = (A) map.get(direct);
          }
        }

        if (instance == null) {
          ByteBuffer key = newSockAddrKeyBuffer(limit);
          key.put(direct);
          key = key.asReadOnlyBuffer();

          instance = af.getAddressConstructor().newAFSocketAddress(port, socketAddress, ObjectPool
              .unpooledLease(key));

          map.put(key, instance);
        }
      }
      return instance;
    }
  }

  @SuppressWarnings("null")
  static final <A extends AFSocketAddress> A ofInternal(ByteBuffer socketAddressBuffer,
      AFAddressFamily<A> af) throws SocketException {
    synchronized (AFSocketAddress.class) {
      socketAddressBuffer.rewind();

      Map<Integer, Map<ByteBuffer, AFSocketAddress>> mapPorts = ADDRESS_CACHE.get(af);
      if (mapPorts != null) {
        Map<ByteBuffer, AFSocketAddress> map = mapPorts.get(0); // FIXME get port, something like
                                                                // sockAddrToPort
        if (map != null) {
          @SuppressWarnings("unchecked")
          A address = (A) map.get(socketAddressBuffer);
          if (address != null) {
            return address;
          }
        }
      }

      try (Lease<ByteBuffer> leasedBuffer = socketAddressBuffer.isDirect() ? null
          : getNativeAddressDirectBuffer(Math.min(socketAddressBuffer.limit(), SOCKADDR_MAX_LEN))) {
        if (leasedBuffer != null) {
          ByteBuffer buf = leasedBuffer.get();
          buf.put(socketAddressBuffer);
          socketAddressBuffer = buf;
        }

        byte[] sockAddrToBytes = NativeUnixSocket.sockAddrToBytes(af.getDomain(),
            socketAddressBuffer);
        if (sockAddrToBytes == null) {
          return null;
        } else {
          return AFSocketAddress.resolveAddress(sockAddrToBytes, 0, af);
        }
      }
    }
  }

  /**
   * Wraps an address as an {@link InetAddress}.
   *
   * @param af The address family.
   * @return The {@link InetAddress}.
   */
  protected final synchronized InetAddress getInetAddress(AFAddressFamily<?> af) {
    if (inetAddress == null) {
      inetAddress = AFInetAddress.wrapAddress(bytes, af);
    }
    return inetAddress;
  }

  /**
   * Wraps this address as an {@link InetAddress}.
   *
   * @return The {@link InetAddress}.
   */
  protected final InetAddress getInetAddress() {
    return getInetAddress(getAddressFamily());
  }

  @SuppressWarnings("null")
  static final @NonNull ByteBuffer newSockAddrDirectBuffer(int length) {
    return ByteBuffer.allocateDirect(length);
  }

  @SuppressWarnings("null")
  static final @NonNull ByteBuffer newSockAddrKeyBuffer(int length) {
    return ByteBuffer.allocate(length);
  }

  /**
   * Returns an {@link AFSocketAddress} given a special {@link InetAddress} that encodes the byte
   * sequence of an AF_UNIX etc. socket address, like those returned by {@link #wrapAddress()}.
   *
   * @param <A> The corresponding address type.
   * @param address The "special" {@link InetAddress}.
   * @param port The port (use 0 for "none").
   * @param af The address family.
   * @return The {@link AFSocketAddress} instance.
   * @throws SocketException if the operation fails, for example when an unsupported address is
   *           specified.
   */
  @SuppressWarnings("null")
  @NonNull
  protected static final <A extends AFSocketAddress> A unwrap(InetAddress address, int port,
      AFAddressFamily<A> af) throws SocketException {
    Objects.requireNonNull(address);
    return resolveAddress(AFInetAddress.unwrapAddress(address, af), port, af);
  }

  /**
   * Returns an {@link AFSocketAddress} given a special {@link InetAddress} hostname that encodes
   * the byte sequence of an AF_UNIX etc. socket address, like those returned by
   * {@link #wrapAddress()}.
   *
   * @param <A> The corresponding address type.
   * @param hostname The "special" hostname, as provided by {@link InetAddress#getHostName()}.
   * @param port The port (use 0 for "none").
   * @param af The address family.
   * @return The {@link AFSocketAddress} instance.
   * @throws SocketException if the operation fails, for example when an unsupported address is
   *           specified.
   */
  @SuppressWarnings("null")
  @NonNull
  protected static final <A extends AFSocketAddress> A unwrap(String hostname, int port,
      AFAddressFamily<A> af) throws SocketException {
    Objects.requireNonNull(hostname);
    return resolveAddress(AFInetAddress.unwrapAddress(hostname, af), port, af);
  }

  static final int unwrapAddressDirectBufferInternal(ByteBuffer socketAddressBuffer,
      SocketAddress address) throws SocketException {
    if (!NativeUnixSocket.isLoaded()) {
      throw new SocketException("Unsupported operation; junixsocket native library is not loaded");
    }
    Objects.requireNonNull(address);

    address = AFSocketAddress.mapOrFail(address, AFSocketAddress.class);
    AFSocketAddress socketAddress = (AFSocketAddress) address;

    byte[] addr = socketAddress.getBytes();
    int domain = socketAddress.getAddressFamily().getDomain();

    int len = NativeUnixSocket.bytesToSockAddr(domain, socketAddressBuffer, addr);
    if (len == -1) {
      throw new SocketException("Unsupported domain");
    }
    return len;
  }

  /**
   * Returns a thread-local direct ByteBuffer containing the native socket address representation of
   * this {@link AFSocketAddress}.
   *
   * @return The direct {@link ByteBuffer}.
   */
  final Lease<ByteBuffer> getNativeAddressDirectBuffer() throws SocketException {
    ByteBuffer address = nativeAddress;
    if (address == null) {
      throw (SocketException) new SocketException("Cannot access native address").initCause(
          NativeUnixSocket.unsupportedException());
    }
    address = address.duplicate();

    Lease<ByteBuffer> lease = getNativeAddressDirectBuffer(address.limit());
    ByteBuffer direct = lease.get();
    address.position(0);
    direct.put(address);

    return lease;
  }

  static final Lease<ByteBuffer> getNativeAddressDirectBuffer(int limit) {
    Lease<ByteBuffer> lease = SOCKETADDRESS_BUFFER_TL.take();
    ByteBuffer direct = lease.get();
    direct.position(0);
    direct.limit(limit);
    return lease;
  }

  /**
   * Checks if the given address is supported by this address family.
   *
   * @param addr The address.
   * @param af The address family.
   * @return {@code true} if supported.
   */
  protected static final boolean isSupportedAddress(InetAddress addr, AFAddressFamily<?> af) {
    return AFInetAddress.isSupportedAddress(addr, af);
  }

  /**
   * Writes the native (system-level) representation of this address to the given buffer.
   *
   * The position of the target buffer will be at the end (i.e., after) the written data.
   *
   * @param buf The target buffer.
   * @throws IOException on error.
   */
  public final void writeNativeAddressTo(ByteBuffer buf) throws IOException {
    if (nativeAddress == null) {
      throw (SocketException) new SocketException("Cannot access native address").initCause(
          NativeUnixSocket.unsupportedException());
    }
    buf.put(nativeAddress);
  }

  /**
   * Creates a new socket connected to this address.
   *
   * @return The socket instance.
   * @throws IOException on error.
   */
  public AFSocket<?> newConnectedSocket() throws IOException {
    AFSocket<?> socket = getAddressFamily().newSocket();
    socket.connect(this);
    return socket;
  }

  /**
   * Creates a new server socket bound to this address.
   *
   * @return The server socket instance.
   * @throws IOException on error.
   */
  public AFServerSocket<?> newBoundServerSocket() throws IOException {
    AFServerSocket<?> serverSocket = getAddressFamily().newServerSocket();
    serverSocket.bind(this);
    return serverSocket;
  }

  /**
   * Creates a new server socket force-bound to this address (i.e., any additional call to
   * {@link ServerSocket#bind(SocketAddress)} will ignore the passed address and use this one
   * instead.
   *
   * @return The server socket instance.
   * @throws IOException on error.
   */
  public AFServerSocket<?> newForceBoundServerSocket() throws IOException {
    AFServerSocket<?> serverSocket = getAddressFamily().newServerSocket();
    serverSocket.forceBindAddress(this).bind(this);
    return serverSocket;
  }

  /**
   * Tries to parse the given URI and return a corresponding {@link AFSocketAddress} for it.
   *
   * NOTE: Only certain URI schemes are supported, such as {@code unix://} (for
   * {@link AFUNIXSocketAddress}) and {@code tipc://} for {@link AFTIPCSocketAddress}.
   *
   * @param u The URI.
   * @return The address.
   * @throws SocketException on error.
   * @see AFAddressFamily#uriSchemes()
   */
  @SuppressWarnings("PMD.ShortMethodName")
  public static AFSocketAddress of(URI u) throws SocketException {
    return of(u, -1);
  }

  /**
   * Tries to parse the given URI and return a corresponding {@link AFSocketAddress} for it.
   *
   * NOTE: Only certain URI schemes are supported, such as {@code unix://} (for
   * {@link AFUNIXSocketAddress}) and {@code tipc://} for {@link AFTIPCSocketAddress}.
   *
   * @param u The URI.
   * @param overridePort The port to forcibly use, or {@code -1} for "don't override".
   * @return The address.
   * @throws SocketException on error.
   * @see AFAddressFamily#uriSchemes()
   */
  @SuppressWarnings("PMD.ShortMethodName")
  public static AFSocketAddress of(URI u, int overridePort) throws SocketException {
    AFAddressFamily<?> af = AFAddressFamily.getAddressFamily(u);
    if (af == null) {
      throw new SocketException("Cannot resolve AFSocketAddress from URI scheme: " + u.getScheme());
    }
    return af.parseURI(u, overridePort);
  }

  /**
   * Tries to create a URI based on this {@link AFSocketAddress}.
   *
   * @param scheme The target scheme.
   * @param template An optional template to reuse certain parameters (e.g., the "path" component
   *          for an {@code http} request), or {@code null}.
   * @return The URI.
   * @throws IOException on error.
   */
  public URI toURI(String scheme, URI template) throws IOException {
    throw new IOException("Unsupported operation");
  }

  /**
   * Returns a address string that can be used with {@code socat}'s {@code SOCKET-CONNECT},
   * {@code SOCKET-LISTEN}, {@code SOCKET-DATAGRAM}, etc., address types, or {@code null} if the
   * address type is not natively supported by this platform.
   *
   * This call is mostly suited for debugging purposes. The resulting string is specific to the
   * platform the code is executed on, and thus may be different among platforms.
   *
   * @param socketType The socket type, or {@code null} to omit from string.
   * @param socketProtocol The socket protocol, or {@code null} to omit from string.
   * @return The string (such as 1:0:x2f746d702f796f).
   * @throws IOException on error (a {@link SocketException} is thrown if the native address cannot
   *           be accessed).
   */
  public @Nullable @SuppressWarnings("PMD.NPathComplexity") String toSocatAddressString(
      AFSocketType socketType, AFSocketProtocol socketProtocol) throws IOException {

    if (SOCKADDR_NATIVE_FAMILY_OFFSET == -1 || SOCKADDR_NATIVE_DATA_OFFSET == -1) {
      return null;
    }
    if (nativeAddress == null) {
      throw (SocketException) new SocketException("Cannot access native address").initCause(
          NativeUnixSocket.unsupportedException());
    }
    if (socketProtocol != null && socketProtocol.getId() != 0) {
      throw new IOException("Protocol not (yet) supported"); // FIXME support additional protocols
    }

    int family = (nativeAddress.get(SOCKADDR_NATIVE_FAMILY_OFFSET) & 0xFF);
    int type = socketType == null ? -1 : NativeUnixSocket.sockTypeToNative(socketType.getId());
    StringBuilder sb = new StringBuilder();
    sb.append(family);
    if (type != -1) {
      sb.append(':');
      sb.append(type);
    }
    if (socketProtocol != null) {
      sb.append(':');
      sb.append(socketProtocol.getId()); // FIXME needs native conversion
    }
    sb.append(":x");
    int n = nativeAddress.limit();
    while (n > 1 && nativeAddress.get(n - 1) == 0) {
      n--;
    }
    for (int pos = SOCKADDR_NATIVE_DATA_OFFSET; pos < n; pos++) {
      byte b = nativeAddress.get(pos);
      sb.append(String.format(Locale.ENGLISH, "%02x", b));
    }
    return sb.toString();
  }

  /**
   * Checks if the given address could cover another address.
   *
   * By default, this is only true if both addresses are regarded equal using
   * {@link #equals(Object)}.
   *
   * However, implementations may support "wildcard" addresses, and this method would compare a
   * wildcard address against some non-wildcard address, for example.
   *
   * @param other The other address that could be covered by this address.
   * @return {@code true} if the other address could be covered.
   */
  public boolean covers(AFSocketAddress other) {
    return this.equals(other);
  }

  /**
   * Custom serialization: Reference {@link AFAddressFamily} instance by identifier string.
   *
   * @param in The {@link ObjectInputStream}.
   * @throws ClassNotFoundException on error.
   * @throws IOException on error.
   */
  @SuppressFBWarnings("MC_OVERRIDABLE_METHOD_CALL_IN_READ_OBJECT") // https://github.com/spotbugs/spotbugs/issues/2957
  private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
    in.defaultReadObject();

    String af = in.readUTF();
    if ("undefined".equals(af)) {
      this.addressFamily = null;
    } else {
      this.addressFamily = Objects.requireNonNull(AFAddressFamily.getAddressFamily(af),
          "address family");
    }
  }

  /**
   * Custom serialization: Reference {@link AFAddressFamily} instance by identifier string.
   *
   * @param out The {@link ObjectOutputStream}.
   * @throws IOException on error.
   */
  private void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject();
    out.writeUTF(addressFamily == null ? "undefined" : addressFamily.getJuxString());
  }

  /**
   * Returns a string representation of the argument as an unsigned decimal value.
   * <p>
   * Works like {@link Integer#toUnsignedString(int)}; added to allow execution on Java 1.7.
   *
   * @param i The value.
   * @return The string.
   */
  static String toUnsignedString(int i) {
    return Long.toString(toUnsignedLong(i));
  }

  /**
   * Returns a string representation of the first argument as an unsigned integer value in the radix
   * specified by the second argument; added to allow execution on Java 1.7.
   *
   * @param i The value.
   * @param radix The radix.
   * @return The string.
   */
  static String toUnsignedString(int i, int radix) {
    return Long.toUnsignedString(toUnsignedLong(i), radix);
  }

  private static long toUnsignedLong(long x) {
    return x & 0xffffffffL;
  }

  /**
   * Parses the string argument as an unsigned integer in the radix specified by the second
   * argument. Works like {@link Integer#parseUnsignedInt(String, int)}; added to allow execution on
   * Java 1.7.
   *
   * @param s The string.
   * @param radix The radix.
   * @return The integer.
   * @throws NumberFormatException on parse error.
   */
  protected static int parseUnsignedInt(String s, int radix) throws NumberFormatException {
    if (s == null || s.isEmpty()) {
      throw new NumberFormatException("Cannot parse null or empty string");
    }

    int len = s.length();
    if (s.startsWith("-")) {
      throw new NumberFormatException("Illegal leading minus sign on unsigned string " + s);
    }

    if (len <= 5 || (radix == 10 && len <= 9)) {
      return Integer.parseInt(s, radix);
    } else {
      long ell = Long.parseLong(s, radix);
      if ((ell & 0xffff_ffff_0000_0000L) == 0) {
        return (int) ell;
      } else {
        throw new NumberFormatException("String value exceeds " + "range of unsigned int: " + s);
      }
    }
  }

  /**
   * Checks if the given {@link SocketAddress} can be mapped to an {@link AFSocketAddress}. This is
   * the case if the address either already is an {@link AFSocketAddress}, {@code null}, or
   * something that has an equivalent representation, such as {@code UnixDomainSocketAddress}.
   *
   * @param addr The address.
   * @return {@code true} if mappable.
   */
  public static boolean canMap(SocketAddress addr) {
    return canMap(addr, AFSocketAddress.class);
  }

  /**
   * Checks if the given {@link SocketAddress} can be mapped to a specific {@link AFSocketAddress}
   * subclass. This is the case if the address either already is such an {@link AFSocketAddress},
   * {@code null}, or something that has an equivalent representation, such as
   * {@code UnixDomainSocketAddress}.
   *
   * @param addr The address.
   * @param targetAddressClass The target address class to map to.
   * @return {@code true} if mappable.
   */
  public static boolean canMap(SocketAddress addr,
      Class<? extends AFSocketAddress> targetAddressClass) {
    if (addr == null) {
      return true;
    } else if (targetAddressClass.isAssignableFrom(addr.getClass())) {
      return true;
    }
    AFSupplier<? extends AFSocketAddress> supplier = SocketAddressUtil.supplyAFSocketAddress(addr);
    if (supplier == null) {
      return false;
    }
    AFSocketAddress afAddr = supplier.get();
    if (afAddr == null) {
      return false;
    }
    return (targetAddressClass.isAssignableFrom(afAddr.getClass()));
  }

  /**
   * Maps the given address to an {@link AFSocketAddress}.
   *
   * @param addr The address.
   * @return The {@link AFSocketAddress}.
   * @throws IllegalArgumentException if the address could not be mapped.
   * @see #canMap(SocketAddress,Class)
   */
  public static AFSocketAddress mapOrFail(SocketAddress addr) {
    return mapOrFail(addr, AFSocketAddress.class);
  }

  /**
   * Maps the given address to a specific {@link AFSocketAddress} type.
   *
   * @param addr The address.
   * @param targetAddressClass The target address class.
   * @param <A> The target address type.
   * @return The {@link AFSocketAddress}.
   * @throws IllegalArgumentException if the address could not be mapped.
   * @see #canMap(SocketAddress,Class)
   */
  @SuppressWarnings("null")
  public static <A extends AFSocketAddress> A mapOrFail(SocketAddress addr,
      Class<A> targetAddressClass) {
    if (addr == null) {
      return null;
    } else if (targetAddressClass.isAssignableFrom(addr.getClass())) {
      return targetAddressClass.cast(addr);
    }

    AFSupplier<? extends AFSocketAddress> supplier = SocketAddressUtil.supplyAFSocketAddress(addr);
    if (supplier == null) {
      throw new IllegalArgumentException("Can only bind to endpoints of type "
          + AFSocketAddress.class.getName() + ": " + addr);
    }
    AFSocketAddress afAddr = supplier.get();
    if (afAddr == null || !targetAddressClass.isAssignableFrom(afAddr.getClass())) {
      throw new IllegalArgumentException("Can only bind to endpoints of type "
          + AFSocketAddress.class.getName() + ", and this specific address is unsupported: "
          + addr);
    }
    return targetAddressClass.cast(afAddr);
  }
}
