//
//  Readchability.swift
//  Flicks
//
//  Created by Huynh Tri Dung on 7/10/16.
//  Copyright © 2016 Huynh Tri Dung. All rights reserved.
//

import SystemConfiguration
import Foundation

public let ReachabilityChangedNotification = "ReachabilityChangedNotification"

public class Reachability: NSObject {
    
    public typealias NetworkReachable = (Reachability) -> ()
    public typealias NetworkUneachable = (Reachability) -> ()
    
    public enum NetworkStatus: CustomStringConvertible {
        
        case NotReachable, ReachableViaWiFi, ReachableViaWWAN
        
        public var description: String {
            switch self {
            case .ReachableViaWWAN:
                return "Cellular"
            case .ReachableViaWiFi:
                return "WiFi"
            case .NotReachable:
                return "No Connection"
            }
        }
    }
    
    // MARK: - *** Public properties ***
    
    public var whenReachable: NetworkReachable?
    public var whenUnreachable: NetworkUneachable?
    public var reachableOnWWAN: Bool
    
    public var currentReachabilityStatus: NetworkStatus {
        if isReachable() {
            if isReachableViaWiFi() {
                return .ReachableViaWiFi
            }
            if isRunningOnDevice {
                return .ReachableViaWWAN
            }
        }
        
        //        var reachability:Reachability?
        //        reachability = Reachability(hostName:"192.168.1.23");
        //        reachability?.startNotifier();
        //        var netStatus = reachability?.currentReachabilityStatus()
        //        if(reachability?.currentReachabilityStatus().value == NotReachable.value)
        //        {
        //             return false
        //        }
        //        return true
        
        
        return .NotReachable
    }
    
    public var currentReachabilityString: String {
        return "\(currentReachabilityStatus)"
    }
    
    // MARK: - *** Initialisation methods ***
    public convenience init(hostname: String) {
        let ref = SCNetworkReachabilityCreateWithName(nil, (hostname as NSString).UTF8String)
        self.init(reachabilityRef: ref!)
    }
    
    public class func reachabilityForInternetConnection() -> Reachability {
        
        var zeroAddress = sockaddr_in(sin_len: __uint8_t(0), sin_family: sa_family_t(0), sin_port: in_port_t(0), sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
        zeroAddress.sin_len = UInt8(sizeofValue(zeroAddress))
        zeroAddress.sin_family = sa_family_t(AF_INET)
        
        let ref = withUnsafePointer(&zeroAddress) {
            SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, UnsafePointer($0))
        }
        return Reachability(reachabilityRef: ref!)
    }
    
    public class func reachabilityForLocalWiFi() -> Reachability {
        
        var localWifiAddress: sockaddr_in = sockaddr_in(sin_len: __uint8_t(0), sin_family: sa_family_t(0), sin_port: in_port_t(0), sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
        localWifiAddress.sin_len = UInt8(sizeofValue(localWifiAddress))
        localWifiAddress.sin_family = sa_family_t(AF_INET)
        
        // IN_LINKLOCALNETNUM is defined in <netinet/in.h> as 169.254.0.0
        let address: Int64 = 0xA9FE0000
        localWifiAddress.sin_addr.s_addr = in_addr_t(address.bigEndian)
        
        let ref = withUnsafePointer(&localWifiAddress) {
            SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, UnsafePointer($0))
        }
        return Reachability(reachabilityRef: ref!)
    }
    
    // MARK: - *** Notifier methods ***
    public func startNotifier() -> Bool {
        
        reachabilityObject = self
        let reachability = self.reachabilityRef!
        #if DEBUG
            print(reachability)
        #endif
        previousReachabilityFlags = reachabilityFlags
        if let timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, timer_queue) {
            dispatch_source_set_timer(timer, dispatch_walltime(nil, 0), 500 * NSEC_PER_MSEC, 100 * NSEC_PER_MSEC)
            dispatch_source_set_event_handler(timer, { [unowned self] in
                self.timerFired()
                })
            
            dispatch_timer = timer
            dispatch_resume(timer)
            
            return true
        } else {
            return false
        }
    }
    
    public func stopNotifier() {
        
        reachabilityObject = nil
        
        if let timer = dispatch_timer {
            dispatch_source_cancel(timer)
            dispatch_timer = nil
        }
        
    }
    
    // MARK: - *** Connection test methods ***
    public func isReachable() -> Bool {
        return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in
            return self.isReachableWithFlags(flags)
        })
    }
    
    public func isReachableViaWWAN() -> Bool {
        
        if isRunningOnDevice {
            return isReachableWithTest() { flags -> Bool in
                // Check we're REACHABLE
                if self.isReachable(flags) {
                    
                    // Now, check we're on WWAN
                    if self.isOnWWAN(flags) {
                        return true
                    }
                }
                return false
            }
        }
        return false
    }
    
    public func isReachableViaWiFi() -> Bool {
        
        return isReachableWithTest() { flags -> Bool in
            
            // Check we're reachable
            if self.isReachable(flags) {
                
                if self.isRunningOnDevice {
                    // Check we're NOT on WWAN
                    if self.isOnWWAN(flags) {
                        return false
                    }
                }
                return true
            }
            
            return false
        }
    }
    
    // MARK: - *** Private methods ***
    private var isRunningOnDevice: Bool = {
        #if (arch(i386) || arch(x86_64)) && os(iOS)
            return false
        #else
            return true
        #endif
    }()
    
    private var reachabilityRef: SCNetworkReachability?
    private var reachabilityObject: AnyObject?
    private var dispatch_timer: dispatch_source_t?
    private lazy var timer_queue: dispatch_queue_t = {
        return dispatch_queue_create("uk.co.joylordsystems.reachability_timer_queue", nil)
    }()
    private var previousReachabilityFlags: SCNetworkReachabilityFlags?
    
    private init(reachabilityRef: SCNetworkReachability) {
        reachableOnWWAN = true
        self.reachabilityRef = reachabilityRef
    }
    
    func timerFired() {
        let currentReachabilityFlags = reachabilityFlags
        if let _previousReachabilityFlags = previousReachabilityFlags {
            #if DEBUG
                print(_previousReachabilityFlags)
            #endif
            if currentReachabilityFlags != previousReachabilityFlags {
                dispatch_async(dispatch_get_main_queue(), { [unowned self] in
                    self.reachabilityChanged(currentReachabilityFlags)
                    self.previousReachabilityFlags = currentReachabilityFlags
                    })
            }
        }
    }
    
    private func reachabilityChanged(flags: SCNetworkReachabilityFlags) {
        if isReachableWithFlags(flags) {
            if let block = whenReachable {
                block(self)
            }
        } else {
            if let block = whenUnreachable {
                block(self)
            }
        }
        
        NSNotificationCenter.defaultCenter().postNotificationName(ReachabilityChangedNotification, object:self)
    }
    
    private func isReachableWithFlags(flags: SCNetworkReachabilityFlags) -> Bool {
        
        let reachable = isReachable(flags)
        
        if !reachable {
            return false
        }
        
        if isConnectionRequiredOrTransient(flags) {
            return false
        }
        
        if isRunningOnDevice {
            if isOnWWAN(flags) && !reachableOnWWAN {
                // We don't want to connect when on 3G.
                return false
            }
        }
        
        return true
    }
    
    private func isReachableWithTest(test: (SCNetworkReachabilityFlags) -> (Bool)) -> Bool {
        var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags.ConnectionAutomatic
        let gotFlags = SCNetworkReachabilityGetFlags(reachabilityRef!, &flags) != false
        if gotFlags {
            return test(flags)
        }
        
        return false
    }
    
    // WWAN may be available, but not active until a connection has been established.
    // WiFi may require a connection for VPN on Demand.
    private func isConnectionRequired() -> Bool {
        return connectionRequired()
    }
    
    private func connectionRequired() -> Bool {
        return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in
            return self.isConnectionRequired(flags)
        })
    }
    
    // Dynamic, on demand connection?
    private func isConnectionOnDemand() -> Bool {
        return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in
            return self.isConnectionRequired(flags) //&& self.isConnectionOnTrafficOrDemand(flags)
        })
    }
    
    // Is user intervention required?
    private func isInterventionRequired() -> Bool {
        return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in
            return self.isConnectionRequired(flags) && self.isInterventionRequired(flags)
        })
    }
    
    private func isOnWWAN(flags: SCNetworkReachabilityFlags) -> Bool {
        #if os(iOS)
            return true
            //return flags.rawValue & SCNetworkReachabilityFlags(kSCNetworkReachabilityFlagsIsWWAN) != 0
        #else
            return false
        #endif
    }
    private func isReachable(flags: SCNetworkReachabilityFlags) -> Bool {
        return flags.rawValue & UInt32(kSCNetworkFlagsReachable) != 0
        //return flags &  SCNetworkReachabilityFlags(kSCNetworkReachabilityFlagsReachable) != 0
    }
    private func isConnectionRequired(flags: SCNetworkReachabilityFlags) -> Bool {
        return flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired) != 0
        //return flags & SCNetworkReachabilityFlags(kSCNetworkReachabilityFlagsConnectionRequired) != 0
    }
    private func isInterventionRequired(flags: SCNetworkReachabilityFlags) -> Bool {
        return flags.rawValue & UInt32(kSCNetworkFlagsInterventionRequired) != 0
        //return flags & SCNetworkReachabilityFlags(kSCNetworkReachabilityFlagsInterventionRequired) != 0
    }
    
    /*private func isConnectionOnTraffic(flags: SCNetworkReachabilityFlags) -> Bool {
     return flags.rawValue & UInt32(kSCNetworkFlagsConnectionAutomatic) != 0
     //return flags & SCNetworkReachabilityFlags(kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0
     }*/
    
    /*private func isConnectionOnDemand(flags: SCNetworkReachabilityFlags) -> Bool {
     return flags & SCNetworkReachabilityFlags(kSCNetworkReachabilityFlagsConnectionOnDemand) != 0
     }*/
    
    /*func isConnectionOnTrafficOrDemand(flags: SCNetworkReachabilityFlags) -> Bool {
     return flags & SCNetworkReachabilityFlags(kSCNetworkReachabilityFlagsConnectionOnTraffic | kSCNetworkReachabilityFlagsConnectionOnDemand) != 0
     }*/
    
    private func isTransientConnection(flags: SCNetworkReachabilityFlags) -> Bool {
        return flags.rawValue & UInt32(kSCNetworkFlagsTransientConnection) != 0
        //return flags & SCNetworkReachabilityFlags(kSCNetworkReachabilityFlagsTransientConnection) != 0
    }
    
    private func isLocalAddress(flags: SCNetworkReachabilityFlags) -> Bool
    {
        return flags.rawValue & UInt32(kSCNetworkFlagsIsLocalAddress) != 0
        //return flags && SCNetworkReachabilityFlags(kSCNetworkReachabilityFlagsIsLocalAddress) != 0
    }
    
    private func isDirect(flags: SCNetworkReachabilityFlags) -> Bool {
        return flags.rawValue & UInt32(kSCNetworkFlagsIsDirect) != 0
        //return flags & SCNetworkReachabilityFlags(kSCNetworkReachabilityFlagsIsDirect) != 0
    }
    
    private func isConnectionRequiredOrTransient(flags: SCNetworkReachabilityFlags) -> Bool {
        let testcase = UInt32(kSCNetworkFlagsConnectionRequired | kSCNetworkFlagsTransientConnection)
        //let testcase = SCNetworkReachabilityFlags(kSCNetworkReachabilityFlagsConnectionRequired | kSCNetworkReachabilityFlagsTransientConnection)
        return flags.rawValue & testcase == testcase
    }
    
    private var reachabilityFlags: SCNetworkReachabilityFlags {
        var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags.ConnectionAutomatic
        let gotFlags = SCNetworkReachabilityGetFlags(reachabilityRef!, &flags) != false
        if gotFlags {
            return flags
        }
        return flags
    }
    
    override public var description: String {
        
        var W: String
        if isRunningOnDevice {
            W = isOnWWAN(reachabilityFlags) ? "W" : "-"
        } else {
            W = "X"
        }
        let R = isReachable(reachabilityFlags) ? "R" : "-"
        let c = isConnectionRequired(reachabilityFlags) ? "c" : "-"
        let t = isTransientConnection(reachabilityFlags) ? "t" : "-"
        let i = isInterventionRequired(reachabilityFlags) ? "i" : "-"
        //let C = isConnectionOnTraffic(reachabilityFlags) ? "C" : "-"
        //let D = isConnectionOnDemand(reachabilityFlags) ? "D" : "-"
        let l = isLocalAddress(reachabilityFlags) ? "l" : "-"
        let d = isDirect(reachabilityFlags) ? "d" : "-"
        return "\(W)\(R) \(c)\(t)\(i)\(l)\(d)"
        //return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)"
    }
    
    deinit {
        stopNotifier()
        
        reachabilityRef = nil
        whenReachable = nil
        whenUnreachable = nil
    }
}