/*_############################################################################
  _## 
  _##  SNMP4J-UNIX-TRANSPORT - UnixDomainAddress.java  
  _## 
  _##  Copyright (C) 2021-2022  Frank Fock (SNMP4J.org)
  _##  
  _##  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.snmp4j.transport.unix;

import org.snmp4j.asn1.BERInputStream;
import org.snmp4j.smi.*;
import org.snmp4j.transport.TransportStateListener;

import java.io.IOException;
import java.io.OutputStream;
import java.io.Serial;
import java.net.ProtocolFamily;
import java.net.SocketAddress;
import java.net.StandardProtocolFamily;
import java.net.UnixDomainSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.List;

/**
 * Represents an address for communication using Unix domain sockets.
 * This class provides utilities for working with Unix domain socket addresses,
 * such as parsing, validation, and encoding/decoding in BER format.
 * It extends the {@code SMIAddress} class, inheriting SNMP-related behaviors
 * while focusing on Unix-specific socket address functionalities.
 */
public class UnixDomainAddress extends SMIAddress {

    @Serial
    private static final long serialVersionUID = -6151626307033468271L;

    /**
     * The maximum number of bytes a Unix domain socket path may have to fit into the {@code sun_path} member
     * of the {@code sockaddr_un} structure of the operating system, excluding the terminating NUL character.
     * The value is the largest capacity provided by any supported platform (Linux, whose {@code sun_path} has
     * 108 bytes), so that {@link #isValid()} never rejects a path that the operating system would accept.
     * Platforms with a smaller {@code sun_path} - macOS and the BSDs provide 104 bytes - reject accordingly
     * shorter paths when the socket is bound or connected, with a {@code SocketException} that reports the
     * path as being too long.
     */
    public static final int MAX_SOCKET_PATH_LENGTH = 107;

    private java.net.UnixDomainSocketAddress unixDomainAddress;

    private transient List<TransportStateListener> transportStateListeners;

    /**
     * Creates an empty UnixDomainAddress instance.
     */
    public UnixDomainAddress() {
    }

    /**
     * Creates a UnixDomainAddress instance from a given Unix domain socket path.
     *
     * @param unixDomainAddress the Unix domain socket path.
     */
    public UnixDomainAddress(Path unixDomainAddress) {
        this.unixDomainAddress = java.net.UnixDomainSocketAddress.of(unixDomainAddress);
    }

    /**
     * Creates a UnixDomainAddress instance from a given Unix domain socket address.
     *
     * @param unixDomainAddress the Unix domain socket address.
     */
    public UnixDomainAddress(java.net.UnixDomainSocketAddress unixDomainAddress) {
        this.unixDomainAddress = unixDomainAddress;
    }


    /**
     * Retrieves the Unix domain socket address.
     *
     * @return the Unix domain socket address.
     */
    public java.net.UnixDomainSocketAddress getUnixDomainAddress() {
        return unixDomainAddress;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        UnixDomainAddress that = (UnixDomainAddress) o;
        return getUnixDomainAddress().equals(that.getUnixDomainAddress());
    }

    @Override
    public int compareTo(Variable variable) {
        return unixDomainAddress.getPath().compareTo(((UnixDomainAddress)variable).unixDomainAddress.getPath());
    }

    @Override
    public int hashCode() {
        return unixDomainAddress.hashCode();
    }

    @Override
    public int getBERLength() {
        return new OctetString(unixDomainAddress.getPath().toString()).getBERLength();
    }

    @Override
    public void decodeBER(BERInputStream berInputStream) throws IOException {
        OctetString os = new OctetString();
        os.decodeBER(berInputStream);
        this.unixDomainAddress = java.net.UnixDomainSocketAddress.of(os.toString());
    }

    @Override
    public void encodeBER(OutputStream outputStream) throws IOException {
        OctetString os = new OctetString(unixDomainAddress.toString());
        os.encodeBER(outputStream);
    }

    @Override
    public int getSyntax() {
        return SMIConstants.SYNTAX_OCTET_STRING;
    }

    @Override
    public String toString() {
        return (unixDomainAddress == null) ? GenericAddress.TYPE_UNIX+":[]" : unixDomainAddress.toString();
    }

    @Override
    public int toInt() {
        throw new UnsupportedOperationException();
    }

    @Override
    public long toLong() {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object clone() {
        return new UnixDomainAddress(unixDomainAddress.getPath());
    }

    @Override
    public OID toSubIndex(boolean impliedLength) {
        return new OctetString(unixDomainAddress.toString()).toSubIndex(impliedLength);
    }

    @Override
    public void fromSubIndex(OID oid, boolean b) {
        OctetString os = OctetString.fromIndex(oid, 0, b);
        this.unixDomainAddress = java.net.UnixDomainSocketAddress.of(Path.of(os.toString()));
    }

    /**
     * Checks whether this address has a socket path that can be used as a Unix domain socket path. Whether
     * the current process is permitted to use that socket is <em>not</em> checked here, because the necessary
     * permissions depend on the role of the address: use {@link #isBindable()} for an address to listen on and
     * {@link #isConnectable()} for an address to send to.
     *
     * @return
     *    {@code true} if this address has a non-empty socket path whose length does not exceed
     *    {@link #MAX_SOCKET_PATH_LENGTH}, {@code false} otherwise.
     */
    @Override
    public boolean isValid() {
        return (unixDomainAddress != null) && isValidUnixDomainSocketPath(unixDomainAddress.getPath());
    }

    /**
     * Checks whether the supplied path can be used as the path of a Unix domain socket, that is, whether it is
     * a non-empty path that fits into the {@code sun_path} member of the {@code sockaddr_un} structure.
     *
     * @param path
     *    the socket path to check, must not be {@code null}.
     * @return
     *    {@code true} if the path can be used as a Unix domain socket path, {@code false} otherwise.
     */
    private static boolean isValidUnixDomainSocketPath(Path path) {
        String socketPath = path.toString();
        // An empty path denotes an unnamed socket that is bound automatically to a system chosen path, thus it
        // cannot be used to address a SNMP entity.
        return !socketPath.isEmpty() &&
                (socketPath.getBytes(StandardCharsets.UTF_8).length <= MAX_SOCKET_PATH_LENGTH);
    }

    /**
     * Checks whether the current process is permitted to bind (i.e. listen on) this address. Binding creates
     * the socket file, thus the directory that contains it needs to exist and the process needs write and
     * search (execute) permission on it. A socket file that already exists needs to be removed before the
     * socket can be bound, which requires the same permissions.
     * <p>
     * The result is a snapshot of the current file system state and the effective permissions of the current
     * process. It can change before the socket is actually bound, therefore a failing bind operation has to be
     * handled regardless of the result of this method.
     *
     * @return
     *    {@code true} if this address is {@link #isValid() valid} and its socket file can be created in an
     *    existing and writable directory, {@code false} otherwise.
     */
    public boolean isBindable() {
        if (!isValid()) {
            return false;
        }
        Path directory = getParentDirectory();
        return (directory != null) && Files.isDirectory(directory) &&
                Files.isWritable(directory) && Files.isExecutable(directory);
    }

    /**
     * Checks whether the current process is permitted to connect to this address. Connecting to a Unix domain
     * socket requires write permission on the socket file itself - unlike binding it, it does not require write
     * permission on the directory that contains the socket file. Read permission is not needed.
     * <p>
     * The result is a snapshot of the current file system state and the effective permissions of the current
     * process. It can change before the socket is actually connected, therefore a failing connect operation has
     * to be handled regardless of the result of this method.
     *
     * @return
     *    {@code true} if this address is {@link #isValid() valid} and its socket file exists and is writable,
     *    {@code false} otherwise.
     */
    public boolean isConnectable() {
        if (!isValid()) {
            return false;
        }
        Path path = getPath();
        return Files.exists(path) && !Files.isDirectory(path) && Files.isWritable(path);
    }

    /**
     * Gets the directory that contains (or, for an address to be bound, will contain) the socket file of this
     * address. A relative socket path is resolved against the current working directory of the JVM.
     *
     * @return
     *    the parent directory of the socket path, or {@code null} if this address has no path or its path
     *    denotes a file system root.
     */
    public Path getParentDirectory() {
        Path path = getPath();
        return (path == null) ? null : path.toAbsolutePath().getParent();
    }

    @Override
    public boolean parseAddress(String s) {
        if (s == null) {
            return false;
        }
        java.net.UnixDomainSocketAddress unixDomainSocketAddress;
        try {
            unixDomainSocketAddress = java.net.UnixDomainSocketAddress.of(s);
        } catch (InvalidPathException ipx) {
            // For example, a path that contains a NUL character.
            return false;
        }
        if (isValidUnixDomainSocketPath(unixDomainSocketAddress.getPath())) {
            this.unixDomainAddress = unixDomainSocketAddress;
            return true;
        }
        return false;
    }

    @Override
    public void setValue(String s) {
        this.unixDomainAddress = java.net.UnixDomainSocketAddress.of(s);
    }

    @Override
    public void setValue(byte[] bytes) {
        OctetString os = new OctetString(bytes);
        this.unixDomainAddress = java.net.UnixDomainSocketAddress.of(os.toString());
    }

    @Override
    public byte[] toByteArray() {
        return unixDomainAddress.toString().getBytes(StandardCharsets.UTF_8);
    }

    /**
     * Retrieves the Unix domain socket path.
     *
     * @return the Unix domain socket path.
     */
    public Path getPath() {
        if (unixDomainAddress != null) {
            return unixDomainAddress.getPath();
        }
        return null;
    }

    @Override
    public ProtocolFamily getFamily() {
        return StandardProtocolFamily.UNIX;
    }

    @Override
    public SocketAddress getSocketAddress() {
        return unixDomainAddress;
    }

}
