//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// A DNS resolver built on top of the libc `getaddrinfo` function.
///
/// This is the lowest-common-denominator resolver available to NIO. It's not really a very good
/// solution because the `getaddrinfo` call blocks during the DNS resolution, meaning that this resolver
/// will block a thread for as long as it takes to perform the getaddrinfo call. To prevent it from blocking `EventLoop`
/// threads, it will offload the blocking `getaddrinfo` calls to a `DispatchQueue`.
/// One advantage from leveraging `getaddrinfo` is the automatic conformance to RFC 6724, which removes some of the work
/// needed to implement it.
///
/// This resolver is a single-use object: it can only be used to perform a single host resolution.

import Dispatch

#if os(Linux) || os(FreeBSD) || os(Android)
import CNIOLinux
#endif

#if os(Windows)
import let WinSDK.AF_INET
import let WinSDK.AF_INET6

import func WinSDK.FreeAddrInfoW
import func WinSDK.GetAddrInfoW
import func WinSDK.gai_strerrorA

import struct WinSDK.ADDRESS_FAMILY
import struct WinSDK.ADDRINFOW
import struct WinSDK.SOCKADDR_IN
import struct WinSDK.SOCKADDR_IN6
#endif

// A thread-specific variable where we store the offload queue if we're on an `SelectableEventLoop`.
let offloadQueueTSV = ThreadSpecificVariable<DispatchQueue>()


internal class GetaddrinfoResolver: Resolver {
    private let v4Future: EventLoopPromise<[SocketAddress]>
    private let v6Future: EventLoopPromise<[SocketAddress]>
    private let aiSocktype: NIOBSDSocket.SocketType
    private let aiProtocol: CInt

    /// Create a new resolver.
    ///
    /// - parameters:
    ///     - loop: The `EventLoop` whose thread this resolver will block.
    ///     - aiSocktype: The sock type to use as hint when calling getaddrinfo.
    ///     - aiProtocol: the protocol to use as hint when calling getaddrinfo.
    init(loop: EventLoop, aiSocktype: NIOBSDSocket.SocketType, aiProtocol: CInt) {
        self.v4Future = loop.makePromise()
        self.v6Future = loop.makePromise()
        self.aiSocktype = aiSocktype
        self.aiProtocol = aiProtocol
    }

    /// Initiate a DNS A query for a given host.
    ///
    /// Due to the nature of `getaddrinfo`, we only actually call the function once, in the AAAA query.
    /// That means this just returns the future for the A results, which in practice will always have been
    /// satisfied by the time this function is called.
    ///
    /// - parameters:
    ///     - host: The hostname to do an A lookup on.
    ///     - port: The port we'll be connecting to.
    /// - returns: An `EventLoopFuture` that fires with the result of the lookup.
    func initiateAQuery(host: String, port: Int) -> EventLoopFuture<[SocketAddress]> {
        return v4Future.futureResult
    }

    /// Initiate a DNS AAAA query for a given host.
    ///
    /// Due to the nature of `getaddrinfo`, we only actually call the function once, in this function.
    /// That means this function call actually blocks: sorry!
    ///
    /// - parameters:
    ///     - host: The hostname to do an AAAA lookup on.
    ///     - port: The port we'll be connecting to.
    /// - returns: An `EventLoopFuture` that fires with the result of the lookup.
    func initiateAAAAQuery(host: String, port: Int) -> EventLoopFuture<[SocketAddress]> {
        self.offloadQueue().async {
            self.resolveBlocking(host: host, port: port)
        }
        return v6Future.futureResult
    }

    private func offloadQueue() -> DispatchQueue {
        if let offloadQueue = offloadQueueTSV.currentValue {
            return offloadQueue
        } else {
            if MultiThreadedEventLoopGroup.currentEventLoop != nil {
                // Okay, we're on an SelectableEL thread. Let's stuff our queue into the thread local.
                let offloadQueue = DispatchQueue(label: "io.swiftnio.GetaddrinfoResolver.offloadQueue")
                offloadQueueTSV.currentValue = offloadQueue
                return offloadQueue
            } else {
                return DispatchQueue.global()
            }
        }
    }

    /// Cancel all outstanding DNS queries.
    ///
    /// This method is called whenever queries that have not completed no longer have their
    /// results needed. The resolver should, if possible, abort any outstanding queries and
    /// clean up their state.
    ///
    /// In the getaddrinfo case this is a no-op, as the resolver blocks.
    func cancelQueries() { }

    /// Perform the DNS queries and record the result.
    ///
    /// - parameters:
    ///     - host: The hostname to do the DNS queries on.
    ///     - port: The port we'll be connecting to.
    private func resolveBlocking(host: String, port: Int) {
#if os(Windows)
        host.withCString(encodedAs: UTF16.self) { wszHost in
            String(port).withCString(encodedAs: UTF16.self) { wszPort in
                var pResult: UnsafeMutablePointer<ADDRINFOW>?

                var aiHints: ADDRINFOW = ADDRINFOW()
                aiHints.ai_socktype = self.aiSocktype.rawValue
                aiHints.ai_protocol = self.aiProtocol

                let iResult = GetAddrInfoW(wszHost, wszPort, &aiHints, &pResult)
                guard iResult == 0 else {
                    self.fail(SocketAddressError.unknown(host: host, port: port))
                    return
                }

                if let pResult = pResult {
                    self.parseAndPublishResults(pResult, host: host)
                    FreeAddrInfoW(pResult)
                } else {
                    self.fail(SocketAddressError.unsupported)
                }
            }
        }
#else
        var info: UnsafeMutablePointer<addrinfo>?

        var hint = addrinfo()
        hint.ai_socktype = self.aiSocktype.rawValue
        hint.ai_protocol = self.aiProtocol
        guard getaddrinfo(host, String(port), &hint, &info) == 0 else {
            self.fail(SocketAddressError.unknown(host: host, port: port))
            return
        }

        if let info = info {
            self.parseAndPublishResults(info, host: host)
            freeaddrinfo(info)
        } else {
            /* this is odd, getaddrinfo returned NULL */
            self.fail(SocketAddressError.unsupported)
        }
#endif
    }

    /// Parses the DNS results from the `addrinfo` linked list.
    ///
    /// - parameters:
    ///     - info: The pointer to the first of the `addrinfo` structures in the list.
    ///     - host: The hostname we resolved.
#if os(Windows)
    internal typealias CAddrInfo = ADDRINFOW
#else
    internal typealias CAddrInfo = addrinfo
#endif

    private func parseAndPublishResults(_ info: UnsafeMutablePointer<CAddrInfo>, host: String) {
        var v4Results: [SocketAddress] = []
        var v6Results: [SocketAddress] = []

        var info: UnsafeMutablePointer<CAddrInfo> = info
        while true {
            switch NIOBSDSocket.AddressFamily(rawValue: info.pointee.ai_family) {
            case .inet:
                info.pointee.ai_addr.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { ptr in
                    v4Results.append(.init(ptr.pointee, host: host))
                }
            case .inet6:
                info.pointee.ai_addr.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { ptr in
                    v6Results.append(.init(ptr.pointee, host: host))
                }
            default:
                self.fail(SocketAddressError.unsupported)
                return
            }

            guard let nextInfo = info.pointee.ai_next else {
                break
            }

            info = nextInfo
        }

        v6Future.succeed(v6Results)
        v4Future.succeed(v4Results)
    }

    /// Record an error and fail the lookup process.
    ///
    /// - parameters:
    ///     - error: The error encountered during lookup.
    private func fail(_ error: Error) {
        self.v6Future.fail(error)
        self.v4Future.fail(error)
    }
}
