/*
 * 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.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;

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

/**
 * Describes an {@link InetSocketAddress} that actually uses AF_UNIX sockets instead of AF_INET.
 *
 * The ability to specify a port number is not specified by AF_UNIX sockets, but we need it
 * sometimes, for example for RMI-over-AF_UNIX.
 *
 * @author Christian Kohlschütter
 */
@SuppressWarnings("PMD.ShortMethodName")
public final class AFUNIXSocketAddress extends AFSocketAddress {
  private static final long serialVersionUID = 1L; // do not change!

  private static final Charset ADDRESS_CHARSET = Charset.defaultCharset();

  @SuppressWarnings("null")
  static final AFAddressFamily<@NonNull AFUNIXSocketAddress> AF_UNIX = AFAddressFamily
      .registerAddressFamily("un", //
          AFUNIXSocketAddress.class, new AFSocketAddressConfig<AFUNIXSocketAddress>() {

            private final AFSocketAddressConstructor<AFUNIXSocketAddress> addrConstr =
                isUseDeserializationForInit() ? AFUNIXSocketAddress::newAFSocketAddress
                    : AFUNIXSocketAddress::new;

            @Override
            public AFUNIXSocketAddress parseURI(URI u, int port) throws SocketException {
              return AFUNIXSocketAddress.of(u, port);
            }

            @Override
            protected AFSocketAddressConstructor<AFUNIXSocketAddress> addressConstructor() {
              return addrConstr;
            }

            @Override
            protected String selectorProviderClassname() {
              return AFUNIXSelectorProvider.class.getName();
            }

            @Override
            protected Set<String> uriSchemes() {
              return new HashSet<>(Arrays.asList("unix", "http+unix", "https+unix"));
            }

            @Override
            protected SocketAddress nullBindAddress() throws IOException {
              return AFUNIXSocketAddress.ofNewTempFile();
            }
          });

  private AFUNIXSocketAddress(int port, final byte[] socketAddress, Lease<ByteBuffer> nativeAddress)
      throws SocketException {
    super(port, socketAddress, nativeAddress, AF_UNIX);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
   * file and port. <b>Legacy constructor, do not use!</b>
   *
   * @param socketFile The socket to connect to.
   * @throws SocketException if the operation fails.
   * @deprecated Use {@link #of(File)} instead.
   * @see #of(File)
   */
  @Deprecated
  public AFUNIXSocketAddress(File socketFile) throws SocketException {
    this(socketFile, 0);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
   * file. <b>Legacy constructor, do not use!</b>
   *
   * @param socketFile The socket to connect to.
   * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
   * @throws SocketException if the operation fails.
   * @deprecated Use {@link #of(File, int)} instead.
   * @see #of(File, int)
   */
  @Deprecated
  public AFUNIXSocketAddress(File socketFile, int port) throws SocketException {
    this(port, of(socketFile, port).getPathAsBytes(), of(socketFile, port)
        .getNativeAddressDirectBuffer());
  }

  static AFUNIXSocketAddress newAFSocketAddress(int port, final byte[] socketAddress,
      Lease<ByteBuffer> nativeAddress) throws SocketException {
    return newDeserializedAFSocketAddress(port, socketAddress, nativeAddress, AF_UNIX,
        AFUNIXSocketAddress::new);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
   * file.
   *
   * @param socketFile The socket to connect to.
   * @return A corresponding {@link AFUNIXSocketAddress} instance.
   * @throws SocketException if the operation fails.
   */
  public static AFUNIXSocketAddress of(final File socketFile) throws SocketException {
    return of(socketFile, 0);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
   * file, assigning the given port to it.
   *
   * @param socketFile The socket to connect to.
   * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
   * @return A corresponding {@link AFUNIXSocketAddress} instance.
   * @throws SocketException if the operation fails.
   */
  public static AFUNIXSocketAddress of(final File socketFile, int port) throws SocketException {
    return of(socketFile.getPath().getBytes(ADDRESS_CHARSET), port);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
   * byte sequence.
   *
   * NOTE: By specifying a byte array that starts with a zero byte, you indicate that the abstract
   * namespace is to be used. This feature is not available on all target platforms.
   *
   * @param socketAddress The socket address (as bytes).
   * @return A corresponding {@link AFUNIXSocketAddress} instance.
   * @throws SocketException if the operation fails.
   * @see AFUNIXSocketAddress#inAbstractNamespace(String)
   */
  public static AFUNIXSocketAddress of(final byte[] socketAddress) throws SocketException {
    return of(socketAddress, 0);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
   * byte sequence, assigning the given port to it.
   *
   * NOTE: By specifying a byte array that starts with a zero byte, you indicate that the abstract
   * namespace is to be used. This feature is not available on all target platforms.
   *
   * @param socketAddress The socket address (as bytes).
   * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
   * @return A corresponding {@link AFUNIXSocketAddress} instance.
   * @throws SocketException if the operation fails.
   * @see AFUNIXSocketAddress#inAbstractNamespace(String,int)
   */
  public static AFUNIXSocketAddress of(final byte[] socketAddress, int port)
      throws SocketException {
    return AFSocketAddress.resolveAddress(socketAddress, port, AF_UNIX);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
   * path.
   *
   * @param socketPath The socket to connect to.
   * @return A corresponding {@link AFUNIXSocketAddress} instance.
   * @throws SocketException if the operation fails.
   */
  public static AFUNIXSocketAddress of(Path socketPath) throws SocketException {
    return of(socketPath, 0);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} that points to the AF_UNIX socket specified by the given
   * path, assigning the given port to it.
   *
   * @param socketPath The socket to connect to.
   * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
   * @return A corresponding {@link AFUNIXSocketAddress} instance.
   * @throws SocketException if the operation fails.
   */
  public static AFUNIXSocketAddress of(Path socketPath, int port) throws SocketException {
    if (!PathUtil.isPathInDefaultFileSystem(socketPath)) {
      throw new SocketException("Path is not in the default file system");
    }

    return of(socketPath.toString().getBytes(ADDRESS_CHARSET), port);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} for the given URI, if possible.
   *
   * @param u The URI.
   * @return The address.
   * @throws SocketException if the operation fails.
   */
  public static AFUNIXSocketAddress of(URI u) throws SocketException {
    return of(u, -1);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} for the given URI, if possible.
   *
   * @param u The URI.
   * @param overridePort The port to forcibly use, or {@code -1} for "don't override".
   * @return The address.
   * @throws SocketException if the operation fails.
   */
  public static AFUNIXSocketAddress of(URI u, int overridePort) throws SocketException {
    switch (u.getScheme()) {
      case "file":
      case "unix":
        String path = u.getPath();
        if (path == null || path.isEmpty()) {
          String auth = u.getAuthority();
          if (auth != null && !auth.isEmpty() && u.getRawSchemeSpecificPart().indexOf('@') == -1) {
            path = auth;
          } else {
            throw new SocketException("Cannot find UNIX socket path component from URI: " + u);
          }
        }
        return of(new File(path), overridePort != -1 ? overridePort : u.getPort());
      case "http+unix":
      case "https+unix":
        HostAndPort hp = HostAndPort.parseFrom(u);
        return of(new File(hp.getHostname()), overridePort != -1 ? overridePort : hp.getPort());
      default:
        throw new SocketException("Invalid URI");
    }
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} that points to a temporary, non-existent but accessible
   * path in the file system.
   *
   * @return A corresponding {@link AFUNIXSocketAddress} instance.
   * @throws IOException if the operation fails.
   */
  public static AFUNIXSocketAddress ofNewTempFile() throws IOException {
    return ofNewTempPath(0);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} that points to a temporary, non-existent but accessible
   * path in the file system, assigning the given port to it.
   *
   * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
   * @return A corresponding {@link AFUNIXSocketAddress} instance.
   * @throws IOException if the operation fails.
   */
  public static AFUNIXSocketAddress ofNewTempPath(int port) throws IOException {
    return of(newTempPath(true), port);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} based on the given {@link SocketAddress}.
   *
   * This either simply casts an existing {@link AFUNIXSocketAddress}, or converts a
   * {@code UnixDomainSocketAddress} to it.
   *
   * @param address The address to convert.
   * @return A corresponding {@link AFUNIXSocketAddress} instance.
   * @throws SocketException if the operation fails.
   */
  public static AFUNIXSocketAddress of(SocketAddress address) throws IOException {
    AFUNIXSocketAddress addr = unwrap(Objects.requireNonNull(address));
    if (addr == null) {
      throw new SocketException("Could not convert SocketAddress to AFUNIXSocketAddress");
    }
    return addr;
  }

  static File newTempPath(boolean deleteOnExit) throws IOException {
    File f = File.createTempFile("jux", ".sock");
    if (deleteOnExit) {
      f.deleteOnExit(); // always delete on exit to clean-up sockets created under that name
    }
    if (!f.delete() && f.exists()) {
      throw new IOException("Could not delete temporary file that we just created: " + f);
    }
    return f;
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} given a special {@link InetAddress} that encodes the
   * byte sequence of an AF_UNIX socket address, like those returned by {@link #wrapAddress()}.
   *
   * @param address The "special" {@link InetAddress}.
   * @param port The port (use 0 for "none").
   * @return The {@link AFUNIXSocketAddress} instance.
   * @throws SocketException if the operation fails, for example when an unsupported address is
   *           specified.
   */
  public static AFUNIXSocketAddress unwrap(InetAddress address, int port) throws SocketException {
    return AFSocketAddress.unwrap(address, port, AF_UNIX);
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} given a generic {@link SocketAddress}.
   *
   * @param address The address to unwrap.
   * @return The {@link AFUNIXSocketAddress} instance.
   * @throws SocketException if the operation fails, for example when an unsupported address is
   *           specified.
   */
  public static AFUNIXSocketAddress unwrap(SocketAddress address) throws SocketException {
    Objects.requireNonNull(address);
    AFSupplier<AFUNIXSocketAddress> supplier = supportedAddressSupplier(address);
    if (supplier == null) {
      throw new SocketException("Unsupported address");
    }
    return supplier.get();
  }

  /**
   * Returns an {@link AFUNIXSocketAddress} given a special {@link InetAddress} hostname that
   * encodes the byte sequence of an AF_UNIX socket address, like those returned by
   * {@link #wrapAddress()}.
   *
   * @param hostname The "special" hostname, as provided by {@link InetAddress#getHostName()}.
   * @param port The port (use 0 for "none").
   * @return The {@link AFUNIXSocketAddress} instance.
   * @throws SocketException if the operation fails, for example when an unsupported address is
   *           specified.
   */
  public static AFUNIXSocketAddress unwrap(String hostname, int port) throws SocketException {
    return AFSocketAddress.unwrap(hostname, port, AF_UNIX);
  }

  /**
   * Convenience method to create an {@link AFUNIXSocketAddress} in the abstract namespace.
   *
   * The returned socket address will use the byte representation of this identifier (using the
   * system's default character encoding), prefixed with a null byte (to indicate the abstract
   * namespace is used).
   *
   * @param name The identifier in the abstract namespace, without trailing zero or @.
   * @return The address.
   * @throws SocketException if the operation fails.
   */
  public static AFUNIXSocketAddress inAbstractNamespace(String name) throws SocketException {
    return inAbstractNamespace(name, 0);
  }

  /**
   * Convenience method to create an {@link AFUNIXSocketAddress} in the abstract namespace.
   *
   * The returned socket address will use the byte representation of this identifier (using the
   * system's default character encoding), prefixed with a null byte (to indicate the abstract
   * namespace is used).
   *
   * @param name The identifier in the abstract namespace, without trailing zero or @.
   * @param port The port associated with this socket, or {@code 0} when no port should be assigned.
   * @return The address.
   * @throws SocketException if the operation fails.
   */
  public static AFUNIXSocketAddress inAbstractNamespace(String name, int port)
      throws SocketException {
    byte[] bytes = name.getBytes(ADDRESS_CHARSET);
    byte[] addr = new byte[bytes.length + 1];
    System.arraycopy(bytes, 0, addr, 1, bytes.length);
    return AFUNIXSocketAddress.of(addr, port);
  }

  private static String prettyPrint(byte[] data) {
    final int dataLength = data.length;
    if (dataLength == 0) {
      return "";
    }
    StringBuilder sb = new StringBuilder(dataLength + 16);
    for (int i = 0; i < dataLength; i++) {
      byte c = data[i];
      if (c >= 32 && c < 127) {
        sb.append((char) c);
      } else {
        sb.append("\\x");
        sb.append(String.format(Locale.ENGLISH, "%02x", c));
      }
    }
    return sb.toString();
  }

  @Override
  public String toString() {
    int port = getPort();
    return getClass().getName() + "[" + (port == 0 ? "" : "port=" + port + ";") + "path="
        + prettyPrint(getBytes()) + "]";
  }

  /**
   * Returns the path to the UNIX domain socket, as a human-readable string using the default
   * encoding.
   *
   * For addresses in the abstract namespace, the US_ASCII encoding is used; zero-bytes are
   * converted to '@', other non-printable bytes are converted to '.'
   *
   * @return The path.
   * @see #getPathAsBytes()
   */
  public String getPath() {
    byte[] bytes = getBytes();
    if (bytes.length == 0) {
      return "";
    } else if (bytes[0] != 0) {
      return new String(bytes, ADDRESS_CHARSET);
    }

    byte[] by = bytes.clone();
    for (int i = 0; i < by.length; i++) {
      byte b = by[i];
      if (b == 0) {
        by[i] = '@';
      } else if (b >= 32 && b < 127) {
        // print as-is
      } else {
        by[i] = '.';
      }
    }
    return new String(by, StandardCharsets.US_ASCII);
  }

  /**
   * Returns the {@link Charset} used to encode/decode {@link AFUNIXSocketAddress}es.
   *
   * This is usually the system default charset, unless that is {@link StandardCharsets#US_ASCII}
   * (7-bit), in which case {@link StandardCharsets#ISO_8859_1} is used instead.
   *
   * @return The charset.
   */
  public static Charset addressCharset() {
    return ADDRESS_CHARSET;
  }

  /**
   * Returns the path to the UNIX domain socket, as bytes.
   *
   * @return The path.
   * @see #getPath()
   */
  public byte[] getPathAsBytes() {
    return getBytes().clone();
  }

  /**
   * Checks if the address is in the abstract namespace (or, for Haiku OS, in the internal
   * namespace).
   *
   * @return {@code true} if the address is in the abstract namespace.
   */
  public boolean isInAbstractNamespace() {
    byte[] bytes = getBytes();
    return bytes.length > 0 && bytes[0] == 0;
  }

  @Override
  public boolean hasFilename() {
    byte[] bytes = getBytes();
    return bytes.length > 0 && bytes[0] != 0;
  }

  @Override
  public File getFile() throws FileNotFoundException {
    if (isInAbstractNamespace()) {
      throw new FileNotFoundException("Socket is in abstract namespace");
    }
    byte[] bytes = getBytes();

    if (bytes.length == 0) {
      throw new FileNotFoundException("No name");
    }
    return new File(new String(bytes, ADDRESS_CHARSET));
  }

  /**
   * Checks if an {@link InetAddress} can be unwrapped to an {@link AFUNIXSocketAddress}.
   *
   * @param addr The instance to check.
   * @return {@code true} if so.
   * @see #wrapAddress()
   * @see #unwrap(InetAddress, int)
   */
  public static boolean isSupportedAddress(InetAddress addr) {
    return AFInetAddress.isSupportedAddress(addr, AF_UNIX);
  }

  /**
   * Checks if a {@link SocketAddress} can be unwrapped to an {@link AFUNIXSocketAddress}.
   *
   * @param addr The instance to check.
   * @return {@code true} if so.
   * @see #unwrap(InetAddress, int)
   */
  public static boolean isSupportedAddress(SocketAddress addr) {
    return supportedAddressSupplier(addr) != null;
  }

  /**
   * Checks if the given address can be unwrapped to an {@link AFUNIXSocketAddress}, and if so,
   * returns a supplier function; if not, {@code null} is returned.
   *
   * @param addr The address.
   * @return The supplier, or {@code null}.
   */
  static AFSupplier<AFUNIXSocketAddress> supportedAddressSupplier(SocketAddress addr) {
    if (addr == null) {
      return null;
    } else if (addr instanceof AFUNIXSocketAddress) {
      return () -> ((AFUNIXSocketAddress) addr);
    } else {
      return SocketAddressUtil.supplyAFUNIXSocketAddress(addr);
    }
  }

  /**
   * Returns the corresponding {@link AFAddressFamily}.
   *
   * @return The address family instance.
   */
  @SuppressWarnings("null")
  public static AFAddressFamily<AFUNIXSocketAddress> addressFamily() {
    return AFUNIXSelectorProvider.getInstance().addressFamily();
  }

  @Override
  public URI toURI(String scheme, URI template) throws IOException {
    switch (scheme) {
      case "unix":
      case "file":
        try {
          if (getPort() > 0 && !"file".equals(scheme)) {
            return new URI(scheme, null, "localhost", getPort(), getPath(), null, (String) null);
          } else {
            return new URI(scheme, null, null, -1, getPath(), null, null);
          }
        } catch (URISyntaxException e) {
          throw new IOException(e);
        }
      case "http+unix":
      case "https+unix":
        HostAndPort hp = new HostAndPort(getPath(), getPort());
        return hp.toURI(scheme, template);
      default:
        return super.toURI(scheme, template);
    }
  }

  @Override
  public AFUNIXSocket newConnectedSocket() throws IOException {
    return (AFUNIXSocket) super.newConnectedSocket();
  }

  @Override
  public AFUNIXServerSocket newBoundServerSocket() throws IOException {
    return (AFUNIXServerSocket) super.newBoundServerSocket();
  }

  @Override
  public AFUNIXServerSocket newForceBoundServerSocket() throws IOException {
    return (AFUNIXServerSocket) super.newForceBoundServerSocket();
  }
}
