/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
 * This source file is part of the Cangjie project, licensed under Apache-2.0
 * with Runtime Library Exception.
 *
 * See https://cangjie-lang.cn/pages/LICENSE for license information.
 */

package std.net

import std.sync.*

/*
 * Represents a TCP streaming socket.
 *
 * Once an instance is created, it is not yet connected so should be connected explicitly via connect().
 *
 * Instances of this type should be explicitly closed even when the connect() hasn't been invoked.
 *
 * @see StreamingSocket for more details on how do streaming sockets work.
 */
public class TcpSocket <: StreamingSocket & Equatable<TcpSocket> & Hashable {
    // we need this due to identity support missing in CJ
    private let id = counter.fetchAdd(1)
    private static let counter = AtomicInt64(1)

    /**
     * Creates an internally precreated socket. See TcpServerSocket.accept.
     */
    TcpSocket(private let impl: SocketCommon<ActualTcpPlatformSocket>) {
        noDelay = true
    }

    /**
     * Create an unconnected TCP socket ready to connect to the specified address and port
     */
    public init(address: String, port: UInt16) {
        this(IPSocketAddress(resolveHelper(address), port))
    }

    /**
     * Create an unconnected TCP socket ready to connect to the specified address and port
     */
    public init(address: SocketAddress) {
        this(address, localAddress: None)
    }

    /**
     * Create an unconnected TCP socket ready to connect to the specified address and port
     * and optionally binding client socket to the particular localAddress (None to automatically find local address to bind)
     *
     * Specifying localAddress usually makes sense to give a hint, which network interface to use for connection.
     * If in doubt or don't know, specify None or use the constructor without localAddress.
     * Specifying localAddress does also configure SO_REUSEADDR by default for convenience ortherwise spurious "address already in use" may occur.
     * Use setSocketOptionBool(SocketOptions.SOL_SOCKET, SocketOptions.SO_REUSEADDR, false) to eliminate this option if needed.
     * Also note that local and remote address should always have the same address family: for example, both IPv4.
     */
    public init(address: SocketAddress, localAddress!: ?SocketAddress) {
        checkAddress(address, "address")
        if (let Some(localAddress) <- localAddress) {
            checkAddress(localAddress, "localAddress")
            if (address.family != localAddress.family) {
                throw IllegalArgumentException(
                    "remote address kind (${address.family}) should have the same address " +
                        " family as local (${localAddress.family})")
            }
        }
        throwIfIPv4ZeroOnWindows(address)

        this.impl = SocketCommon(SocketNet.TCP, address.family, SocketMode.StreamingMode)
        impl.remoteAddress = address
        if (let Some(localAddress) <- localAddress) {
            impl.localAddress = localAddress
            setSocketOptionBool(SOL_SOCKET, SOCK_REUSEADDR, true)
        }

        noDelay = true
    }

    /**
     * Remote address the socket will be or is currently connected to.
     *
     * @throws SocketException is the socket is already closed.
     */
    public override prop remoteAddress: SocketAddress {
        get() {
            impl.remoteAddress ?? SocketException.notYetConnected()
        }
    }

    /**
     * Local address the socket will be or currently is bound at.
     *
     * @throws SocketException is the socket is already closed
     * or no local address is available (local address was not provided during creation and the socket is not connected).
     */
    public override prop localAddress: SocketAddress {
        get() {
            impl.localAddress ?? SocketException.notYetConnected()
        }
    }

    /**
     * Read operation time limit or `None` for infinite read attempts.
     * The value specified here is actually the minimal amount of time before a read operation cancelled.
     * The actual time is not guaranteed but it will be never cancelled earlier than the specified timeout value.
     * If the duration is too big than it can be bumped to the infinite. When it's too small then if will be bumped to the minimal clock granularity.
     *
     * The default value is None.
     * @throws IllegalArgumentException if the specified timeout duration is negative.
     */
    public override mut prop readTimeout: ?Duration {
        get() {
            impl.readTimeout
        }
        set(timeout) {
            impl.readTimeout = timeout?.throwIfNegative("Read timeout").toNanosecondGranularity()
        }
    }

    /**
     * Write operation time limit or `None` for infinite read attempts.
     *
     * The value specified here is actually the minimal amount of time before a write operation cancelled.
     * The actual time is not guaranteed but it will be never cancelled earlier than the specified timeout value.
     * If the duration is too big than it can be bumped to the infinite. When it's too small then if will be bumped to the minimal clock granularity.
     *
     * The default value is None.
     * @throws IllegalArgumentException if the specified timeout duration is negative.
     */
    public override mut prop writeTimeout: ?Duration {
        get() {
            impl.writeTimeout
        }
        set(timeout) {
            impl.writeTimeout = timeout?.throwIfNegative("Write timeout").toNanosecondGranularity()
        }
    }

    /**
     * Network interface name to bind at.
     * Despite it's a client TCP socket, we still do bind before
     * connect to occupy a local port and it is sometimes important
     * to bind at some particular network interface to try to
     * enforce the particular route.
     *
     * This option is a hint for the operating system that may decide to ignore the value
     * or reject an attempt to configure it, especially when it's not
     * allowed, unsupported or we specify a wrong name.
     */
    public mut prop bindToDevice: ?String {
        get() {
            impl.bindToDevice
        }
        set(newDevice) {
            impl.bindToDevice = newDevice
        }
    }

    /**
     * TCP Keep-Alive options or `None` if disabled.
     * If not configured, the operating system may decide to use some default keep-alive configuration.
     * Changing this option may have delayed effect or may be silently ignored or reinterpreted by the operating system
     * due to some reasons such as system configurations and/or missing support of particular features in the underlying TCP stack implementation.
     */
    public mut prop keepAlive: ?SocketKeepAliveConfig {
        get() {
            impl.keepAlive
        }
        set(newConfig) {
            impl.keepAlive = newConfig
        }
    }

    /**
     * `TCP_NODELAY`, true by default
     *
     * This option disables the Nagel's algorithm so any bytes chunk
     * written to the socket is scheduled for sending immediately without delay.
     * When this option is disabled, then Nagel's implementation does introduce time-based
     * delay before actually sending bytes. This is done to group outgoing byte chunks to bigger
     * TCP packets so there will be less quantity of them and the overhead decreases. In other words,
     * this is a time-based debouncing algorithm.
     * Despite that it looks reasonable, most applications already have proper buffering usually
     * on multiple application layers so this Nagel algorithm will simply introduce latency without
     * any benefits. So in this case TCP_NODELAY option is used to disable the debouncing.
     */
    public mut prop noDelay: Bool {
        get() {
            impl.getSocketOptionBool(IPPROTO_TCP, SOCK_TCP_NODELAY)
        }
        set(newState) {
            impl.setSocketOptionBool(IPPROTO_TCP, SOCK_TCP_NODELAY, newState)
        }
    }

    /**
     * TCP_QUICKACK, false by default
     *
     * This is similar to TCP_NODELAY but affects only TCP ACK and first response bytes chunk.
     * Usually (without TCP_QUICKACK), the TCP stack implementation does defer sending TCP ACK packet
     * until the first bytes will be sent (but with some time limit).
     * The idea is to group ACK and data bytes into a single batch, and reduce overhead.
     * Because of this, the remote peer doesn't get connection acknowledgement immediately but after some delay.
     * In some latency-sensitive or interactive protocols it is not acceptable.
     * So here TCP_QUCKACK option comes and provides a way to force sending TCP ACK immediately to reduce
     * the connection latency. However, generally this is not always good as we sacrify throughput a little bit
     * and increase the number of network packets, enlarging load on network hardware, switches.
     * Increasing the number of packets also leads to loss probability growth reducing robustness.
     * This is why this option could be good or bad depending on the usage scenario and environment.
     *
     * Not supported on windows and macOS
     */
    @When[os == "Linux"]
    public mut prop quickAcknowledge: Bool {
        get() {
            impl.getSocketOptionBool(IPPROTO_TCP, SOCK_TCP_QUICKACK)
        }
        set(newState) {
            impl.setSocketOptionBool(IPPROTO_TCP, SOCK_TCP_QUICKACK, newState)
        }
    }

    @When[os != "Linux"]
    @Deprecated
    public mut prop quickAcknowledge: Bool {
        get() {
            impl.getSocketOptionBool(IPPROTO_TCP, SOCK_TCP_QUICKACK)
        }
        set(newState) {
            impl.setSocketOptionBool(IPPROTO_TCP, SOCK_TCP_QUICKACK, newState)
        }
    }

    /**
     * SO_LINGER duration, the default is system-dependant. `None` if linger is disabled.
     *
     * When a socket is closed, if there are pending outgoing bytes, we are waiting
     * for the linger time before aborting connection. If the time is out but bytes were not sent yet,
     * then usually the connection get aborted (via reset / TCP reset).
     *
     * When the linger is disabled (`None`), then the connection will be aborted immediately:
     * depending on the presence of pending outgoing bytes, it will be either terminated successully (FIN-ACK)
     * or reset (RST).
     *
     * @throws IllegalArgumentException if the specified timeout duration is negative.
     */
    public mut prop linger: ?Duration {
        get() {
            impl.getLinger()
        }
        set(newLinger) {
            impl.setLinger(newLinger)
        }
    }

    /**
     * SO_SNDBUF option, providing a way to specify hint for the underlying
     * native socket implementation about the desired outgoing buffer size.
     *
     * Changing this option is not guaranteed to have any effect since it's
     * completely up to the operating system.
     *
     * Reading this property could also provide non-realistic values on some systems in
     * some cases so no logic should strictly rely on this value.
     *
     * @throws IllegalArgumentException if the specified buffer size is negative or 0.
     */
    public mut prop sendBufferSize: Int64 {
        get() { impl.getSendBufferSize() }
        set(newSize) { impl.setSendBufferSize(newSize) }
    }

    /**
     * SO_RCVBUF option, providing a way to specify hint for the underlying
     * native socket implementation about the desired receive buffer size.
     *
     * Changing this option is not guaranteed to have any effect since it's
     * completely up to the operating system.
     *
     * Reading this property could also provide non-realistic values on some systems in
     * some cases so no logic should strictly rely on this value.
     *
     * @throws IllegalArgumentException if the specified buffer size is negative or 0.
     */
    public mut prop receiveBufferSize: Int64 {
        get() { impl.getReceiveBufferSize() }
        set(newSize) { impl.setReceiveBufferSize(newSize) }
    }

    /**
     * Read at least one byte to the specified buffer waiting for incoming data if necessary.
     *
     * Returns number of bytes written to the buffer or 0 when the remote peer closed the stream
     * or also 0 when the socket is closed.
     *
     * @throws IllegalArgumentException if the specified buffer is empty
     * @throws SocketTimeoutException if the waiting time has expired.
     * @throws SocketException when the connection is broken
     */
    public override func read(buffer: Array<Byte>): Int64 {
        impl.read(buffer)
    }

    /**
     * Write the payload bytes to the socket waiting for the output buffer space if necessary.
     *
     * The provided bytes are copied and transmitted asynchronously so returning from this function
     * doesn't guarantee actual data delivery. When the link is poor or the remote peer is unable
     * to handle data fast enough, the send buffer may overflow and in this case an attempt
     * to write more bytes may block coroutine here until enough bytes will be transmitted to
     * get enough send buffer space to write the payload. If the payload is too big to fit the send
     * buffer, then the payload will be fragmented and sent chunk by chunk.
     *
     * Also note that due to the nature of IP and TCP having packet fragmentation,
     * the underlying network may split the payload into smaller parts of any size
     * so it is not guaranteed that the the whole payload will be delivered to the remote peer at once.
     *
     * Despite TCP provides delivery acknowledge, the delivery here means that bytes were transmitted
     * to the remote peer and it doesn't gurantee that the remote application actually received
     * and processed bytes. Also, this function only schedule the payload for sending.
     * Therefore, successfully returning write() invocation doesn't mean that bytes were
     * deliverd. To get guaranteed delivery, use application-level acknowledges instead.
     *
     * @throws IllegalArgumentException if the specified buffer is empty
     * @throws SocketTimeoutException if the waiting time has expired.
     * @throws SocketException when the socket is closed or the connection is broken
     */
    public override func write(payload: Array<Byte>): Unit {
        impl.write(payload)
    }

    /**
     * Connects to the remote peer within the specified timeout.
     * If the timeout is `None`, then connection attempts will continue without time limit.
     * Please note that this function doesn't do retry so if the server peer does reject connection, we get error despite the timeout duration.
     *
     * This function also does bind first before doing connect so there is no need to invoke bind
     *
     * @throws SocketException if the connection cannot be established.
     * @throws SocketTimeoutException if the waiting time has expired.
     * @throws IllegalArgumentException if the specified timeout duration is negative.
     */
    public func connect(timeout!: ?Duration = None): Unit {
        impl.connect(timeout?.throwIfNegative("Timeout"))
    }

    /**
     * Read the specified socket option writing the result to value buffer
     * of the specified valueLength (in bytes).
     * Before invoking this function valueLength should be initialized with the buffer size
     * After invoking this function valueLength will contain the actual result
     * size in bytes.
     *
     * See SocketOptions for popular option constants.
     *
     * Throws an exception if failed (when getsockopt returns -1).
     */
    public func getSocketOption(level: Int32, option: Int32, value: CPointer<Unit>, valueLength: CPointer<UIntNative>): Unit {
        unsafe { impl.getSocketOption(level, option, value, valueLength) }
    }

    /**
     * Write the specified socket option from value buffer having valueLength
     * size in bytes.
     *
     * See SocketOptions for popular option constants.
     *
     * Throws an exception if failed (when setsockopt returns -1).
     */
    public func setSocketOption(level: Int32, option: Int32, value: CPointer<Unit>, valueLength: UIntNative): Unit {
        unsafe { impl.setSocketOption(level, option, value, valueLength) }
    }

    /**
     * Read the specified socket option returning it's value as IntNative result.
     *
     * See SocketOptions for popular option constants.
     *
     * Throws an exception if failed (when getsockopt returns -1) or if the result
     * has size different from IntNative.
     */
    public func getSocketOptionIntNative(level: Int32, option: Int32): IntNative {
        impl.getSocketOptionIntNative(level, option)
    }

    /**
     * Write a numeric IntNative value to the specified socket option.
     *
     * See SocketOptions for popular option constants.
     *
     * Throws an exception if failed (when setsockopt returns -1), for example
     * when the option size is different from IntNative.
     */
    public func setSocketOptionIntNative(level: Int32, option: Int32, value: IntNative): Unit {
        impl.setSocketOptionIntNative(level, option, value)
    }

    /**
     * Read the specified socket option returning it's value as a boolean value
     * converting it from an IntNative.
     *
     * See SocketOptions for popular option constants.
     *
     * The conversion is defined as 0 => false, other values => true.
     *
     * Throws an exception if failed (when getsockopt returns -1) or if the result
     * has size different from IntNative.
     */
    public func getSocketOptionBool(level: Int32, option: Int32): Bool {
        impl.getSocketOptionBool(level, option)
    }

    /**
     * Write a boolean value to the specified socket option converting it to IntNative.
     *
     * See SocketOptions for popular option constants.
     *
     * The conversion is defined as false => 0, true => 1
     *
     * Throws an exception if failed (when setsockopt returns -1), for example
     * when the option size is different from IntNative.
     */
    public func setSocketOptionBool(level: Int32, option: Int32, value: Bool): Unit {
        impl.setSocketOptionBool(level, option, value)
    }

    /**
     * Close the socket releasing all resources. All operations except for close() and isClose() are no longer available.
     * This function is reentrant.
     **/
    public func close(): Unit {
        impl.close()
    }

    /**
     * Checks whether this socket has been explicitly closed via close()
     **/
    public func isClosed(): Bool {
        impl.isClosed()
    }

    public override operator func ==(other: TcpSocket): Bool {
        id == other.id
    }

    public override operator func !=(other: TcpSocket): Bool {
        id != other.id
    }

    public override func hashCode(): Int64 {
        id.hashCode()
    }

    public override func toString(): String {
        "TcpSocket(${impl.toString()})"
    }

    private static func checkAddress(address: SocketAddress, name: String): SocketAddress {
        if (address.family != AddressFamily.INET && address.family != AddressFamily.INET6) {
            throw IllegalArgumentException(
                "${name} should be either IPv4 or IPv6 but got ${address.family}: ${address}")
        }
        return address
    }
}