// --------------------------------------------------------------------------
//
//  Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/)
//
// The MIT License (MIT)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the ""Software""), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//
// --------------------------------------------------------------------------

import Foundation

#if !os(watchOS)

    import SystemConfiguration

    public class ReachabilityManager: ReachabilityManagerType {
        // MARK: - Properties

        public var networkReachabilityStatus: NetworkReachabilityStatus {
            guard let flags = flags else { return .unknown }
            return networkReachabilityStatusForFlags(flags)
        }

        /// The dispatch queue to execute the `listener` closure on.
        private var listenerQueue = DispatchQueue.main

        var listener: ReachabilityStatusListener?
        var previousStatus = NetworkReachabilityStatus.unknown

        private let reachability: SCNetworkReachability
        private var previousFlags: SCNetworkReachabilityFlags
        private var flags: SCNetworkReachabilityFlags? {
            var flags = SCNetworkReachabilityFlags()

            if SCNetworkReachabilityGetFlags(reachability, &flags) {
                return flags
            }

            return nil
        }

        // MARK: - Initialization

        /// Creates a `Reachability` instance with the specified host.
        ///
        /// - parameter host: The host used to evaluate network reachability.
        ///
        /// - returns: The new `Reachability` instance.
        public convenience init?(host: String) {
            guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }
            self.init(reachability: reachability)
        }

        /// Creates a `Reachability` instance that monitors the address 0.0.0.0.
        ///
        /// ReachabilityManager treats the 0.0.0.0 address as a special token that causes it to monitor the
        /// general routing status of the device, both IPv4 and IPv6.
        ///
        /// - returns: The new `Reachability` instance.
        public convenience init?() {
            var address = sockaddr_in()
            address.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
            address.sin_family = sa_family_t(AF_INET)

            guard let reachability = withUnsafePointer(to: &address, { pointer in
                pointer.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout<sockaddr>.size) {
                    SCNetworkReachabilityCreateWithAddress(nil, $0)
                }
            }) else { return nil }

            self.init(reachability: reachability)
        }

        private init(reachability: SCNetworkReachability) {
            self.reachability = reachability
            self.previousFlags = SCNetworkReachabilityFlags()
        }

        deinit {
            stopListening()
        }

        // MARK: - Listening

        public func registerListener(_ listener: @escaping ReachabilityStatusListener) {
            self.listener = { status in
                defer { self.previousStatus = status }
                guard self.previousStatus != status else { return }
                listener(status)
            }
        }

        @discardableResult
        public func startListening() -> Bool {
            var context = SCNetworkReachabilityContext(
                version: 0,
                info: nil,
                retain: nil,
                release: nil,
                copyDescription: nil
            )
            context.info = Unmanaged.passUnretained(self).toOpaque()

            let callbackEnabled = SCNetworkReachabilitySetCallback(
                reachability, { _, flags, info in
                    let reachability = Unmanaged<ReachabilityManager>.fromOpaque(info!).takeUnretainedValue()
                    reachability.notifyListener(flags)
                },
                &context
            )

            let queueEnabled = SCNetworkReachabilitySetDispatchQueue(reachability, listenerQueue)

            listenerQueue.async {
                self.previousFlags = SCNetworkReachabilityFlags()
                self.notifyListener(self.flags ?? SCNetworkReachabilityFlags())
            }

            return callbackEnabled && queueEnabled
        }

        public func stopListening() {
            SCNetworkReachabilitySetCallback(reachability, nil, nil)
            SCNetworkReachabilitySetDispatchQueue(reachability, nil)
        }

        // MARK: - Internal - Listener Notification

        func notifyListener(_ flags: SCNetworkReachabilityFlags) {
            guard previousFlags != flags else { return }
            previousFlags = flags

            listener?(networkReachabilityStatusForFlags(flags))
        }

        // MARK: - Internal - Network Reachability Status

        func networkReachabilityStatusForFlags(_ flags: SCNetworkReachabilityFlags) -> NetworkReachabilityStatus {
            guard isNetworkReachable(with: flags) else { return .notReachable }

            var networkStatus: NetworkReachabilityStatus = .reachable(.ethernetOrWiFi)

            #if os(iOS)
                if flags.contains(.isWWAN) { networkStatus = .reachable(.wwan) }
            #endif

            return networkStatus
        }

        func isNetworkReachable(with flags: SCNetworkReachabilityFlags) -> Bool {
            let isReachable = flags.contains(.reachable)
            let needsConnection = flags.contains(.connectionRequired)
            let canConnectAutomatically = flags.contains(.connectionOnDemand) || flags.contains(.connectionOnTraffic)
            let canConnectWithoutUserInteraction = canConnectAutomatically && !flags.contains(.interventionRequired)

            return isReachable && (!needsConnection || canConnectWithoutUserInteraction)
        }
    }

#endif
