//
//  MFFGHSBluetoothPair.swift
//  MobileFirstFramework
//
//  Created by Chowdhury, Shohrab on 3/29/19.
//  Copyright © 2019 Verizon Wireless. All rights reserved.
//

import CoreBluetooth

public enum MFFGHSSignalStatus: NSNumber {
    case noBluetoothConnection = -1, noSignal = 1, poor, good, confirmedGood, confirmedPoor
}

public enum MFFGHSSignalStrengthCheckStatus: NSNumber {
    case idle = 0, processing, complete
}

public protocol MFFGHSBluetoothTestingProtocol {
    var continueTesting: Bool { get set }
}

public protocol MFFGHSBluetoothPairDelegate : NSObjectProtocol {
    
    /// Receive ble status ON/OFF
    func bluetoothStatus(isOn: Bool)
    
    /// New status that's more reliable as a callback if BLE is off
    func bluetoothOff()
    
    /// Bluetooth does not have access from the user
    func bluetoothPermissionsDenied()
    
    /// BLE found a device that doesn't match what it expects to find.
    func deviceIMEIMismatch(expected: String, found: String)

    /// An attempted pairing failed due to a broadcast naming mismatch.
    func onBluetoothDiscoveredNameFailed(expectedName: String, foundNames: [String])
    
    /// The bluetooth service was discovered.
    func onBluetoothDiscovered(expectedName: String, foundName: String)
    
    /// Check device is paired with phone
    func pairUpdate(isPaired: Bool)
    
    /// Check device is activated or not
    func updateActivatedStatus(isActivated: Bool)
    
    /// Receive current RSSI
    func updateCurrentSignalStatus(_ status: MFFGHSSignalStatus, strengthCheckStatus: MFFGHSSignalStrengthCheckStatus, rssi: Double?)
    
}

extension MFFGHSBluetoothPairDelegate {
    public func bluetoothStatus(isOn: Bool) {}
    public func bluetoothOff() {}
    public func bluetoothPermissionsDenied() {}
    public func deviceIMEIMismatch(expected: String, found: String) {}
    public func onBluetoothDiscoveredNameFailed(expectedName: String, foundNames: [String]) {}
    public func onBluetoothDiscovered(expectedName: String, foundName: String) {}
    public func pairUpdate(isPaired: Bool) {}
    public func updateActivatedStatus(isActivated:Bool) {}
    public func updateCurrentSignalStatus(_ status: MFFGHSSignalStatus, strengthCheckStatus: MFFGHSSignalStrengthCheckStatus, rssi: Double?) {}
}

class MFFGHSBluetoothPair: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, BluetoothParingProtocol {
    
    // !!Major change!! Some controllers are relying on bluetooth object being passed.
    static weak var instance:MFFGHSBluetoothPair?

    /// Weak instance returned. Controller that uses it should hold onto strong reference. When all controllers deallocate, this will also.
    static var sharedInstance: MFFGHSBluetoothPair {
        get {
            if let instance = instance {
                return instance
            } else {
                let newInstance = MFFGHSBluetoothPair()
                instance = newInstance
                return newInstance
            }
        }
    }
    
    // Bluetooth control objects.
    var centralManager: CBCentralManager?
    var peripheral: CBPeripheral?
    
    // Event delegate.
    weak var delegate: MFFGHSBluetoothPairDelegate?
    
    //debugger for event delegate
    var bluetoothDebugger: BluetoothDebugger?
    
    // Data from bluetooth connection
    var isPaired = false
    var isCPEActivated = false
    var currentSignalStatus: MFFGHSSignalStatus = .noBluetoothConnection
    var currentRSSIValue: Double?
    
    // Configured properties.
    var bluetoothConfig: BluetoothConfigModel? {
        didSet {
            if (oldValue != bluetoothConfig) { // TODO: Needs deep check for config changes.
                setupBluetoothScanner()
            }
        }
    }
    
    var signalStrengthObserveDuration: TimeInterval = 30
    var signalPassingPercentage: Double = 100
    var signalStrengthCheckStatus: MFFGHSSignalStrengthCheckStatus = .idle
    var signalStrengthObserverTimer: Timer?
    var lastSignalStatusTimeStamp: Date?
    var signalStrengthGoodTotalTime: Double = 0
    var signalStrengthFailTotalTime: Double = 0
    
    override init() {
        super.init()
        setupBluetoothScanner()
    }
    
    #if DEBUG
    deinit {
        print("bluetooth destroy")
    }
    #endif
    
    var bluetoothEnabled:Bool {
        #if targetEnvironment(simulator)
            return true
        #else
            return self.centralManager?.state == CBManagerState.poweredOn
        #endif
    }
    
    func scanForPeripherals(central:CBCentralManager) {
        let advertiseIds = getBleAdvertiseUUIDs()
        if advertiseIds.count > 0 {
            central.scanForPeripherals(withServices: advertiseIds, options: nil)
        } else {
            central.stopScan()
        }
    }

    func setupBluetoothScanner(showPowerAlert:Bool = false) {
        // Reset connection on new config.
        if let peripheral = self.peripheral {
            centralManager?.cancelPeripheralConnection(peripheral)
            self.peripheral = nil
            self.isPaired = false
            self.currentSignalStatus = .noBluetoothConnection
        }
        if self.centralManager == nil || showPowerAlert {
            centralManager = CBCentralManager(delegate: self, queue: DispatchQueue.main, options: [CBCentralManagerOptionShowPowerAlertKey: showPowerAlert])
        } else if (bluetoothEnabled) {
            scanForPeripherals(central: centralManager!)
        } // else wait for powered on status

        if GMFGTestScreenData.shared.isCPESimulated {
            #if DEBUG
            simulateBluetoothMessaging()
            #endif
        } else {
            #if DEBUG && targetEnvironment(simulator)
            simulateBluetoothMessaging()
            #endif
        }
    }
    
    #if DEBUG
    // For simulating RSSI changes.
    var timer: Timer?
    private func simulateBluetoothMessaging() {
        // If we don't get advertisement data, technically nothing will come.
        if getBleAdvertiseUUIDs().count == 0 { return }

        if timer != nil { return }

        var signalRotate = 0
        _ = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { (timer) in
            self.delegate?.bluetoothStatus(isOn: true)
            self.isCPEActivated = true
            self.delegate?.updateActivatedStatus(isActivated: self.isCPEActivated)
            self.isPaired = true
            self.delegate?.pairUpdate(isPaired: self.isPaired)
            signalRotate = (signalRotate + 1) % 5
            self.currentRSSIValue = Double(signalRotate)
            if (signalRotate >= 3) {
                self.currentRSSIValue = -76
                self.currentSignalStatus = .confirmedGood
            } else if (signalRotate >= 2) {
                self.currentRSSIValue = -86
                self.currentSignalStatus = .good
            } else if (signalRotate >= 1) {
                self.currentRSSIValue = -101
                self.currentSignalStatus = .poor
            } else {
                self.currentRSSIValue = -999
                self.currentSignalStatus = .noSignal
            }
            
            self.delegate?.updateCurrentSignalStatus(self.currentSignalStatus, strengthCheckStatus:self.signalStrengthCheckStatus, rssi: self.currentRSSIValue)
            self.bluetoothDebugger?.bluetoothStatus = true
            self.bluetoothDebugger?.isDeviceActivated = true
            self.bluetoothDebugger?.isDevicePaired = self.isPaired
            self.bluetoothDebugger?.updateCurrentSignalStatus(self.currentSignalStatus, strengthCheckStatus:self.signalStrengthCheckStatus, rssi: self.currentRSSIValue)
        }
    }
    #endif
    
    // Should be used when we detect scanning is no longer required.
    private func stopScanning() {
        if let peripheral = self.peripheral {
            centralManager?.cancelPeripheralConnection(peripheral)
            self.peripheral = nil;
        }
        centralManager?.stopScan()
        centralManager = nil
    }
    
    open func shutdown() {
        stopScanning()
        MFFGHSBluetoothPair.instance = nil
    }
    
    public func stopNotify5G(controller: MFFGHSBluetoothPairDelegate) {
        if let del = delegate, del === controller {
            delegate = nil
        }
        resetSignalStrengthCheckValue()
    }
    
    public func setup(config: BluetoothConfigModel, delegate: BluetoothDebuggerDelegate) {
        bluetoothDebugger = BluetoothDebugger(config: config, delegate: delegate)
        bluetoothConfig = config
    }

    public func startNotify5G(controller: MFFGHSBluetoothPairDelegate) {
        delegate = controller
        // Initialize
        delegate?.pairUpdate(isPaired: isPaired)
        delegate?.updateActivatedStatus(isActivated: isCPEActivated)
        delegate?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue)
        
        if let state = centralManager?.state {
            bluetoothDebugger?.bluetoothState = state
        }
        bluetoothDebugger?.bluetoothStatus = bluetoothEnabled
        bluetoothDebugger?.isDevicePaired = isPaired
        bluetoothDebugger?.isDeviceActivated = isCPEActivated
        bluetoothDebugger?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus:signalStrengthCheckStatus, rssi: currentRSSIValue)
    }
           
    // MARK:- Bluetooth delegate
    
    public func centralManagerDidUpdateState(_ central: CBCentralManager) {
        bluetoothDebugger?.bluetoothState = central.state
        switch (central.state) {
        case .poweredOn:
            delegate?.bluetoothStatus(isOn: true)
            bluetoothDebugger?.bluetoothStatus = true
            scanForPeripherals(central: central)
        case .poweredOff, .unsupported, .unauthorized, .unknown:
            delegate?.bluetoothStatus(isOn: false)
            bluetoothDebugger?.bluetoothStatus = false
        default:
            print("bluetooth not powered on")
        }
    }
    
    public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        if findPeripheralName(getBleAdvertiseName(), in: advertisementData) {
            self.peripheral = peripheral
            peripheral.delegate = self
            central.connect(peripheral, options: nil)
            central.stopScan()
        } else {
            if let foundName = advertisementData[CBAdvertisementDataLocalNameKey] as? String, foundName.hasPrefix(GMFGConstant.BLE.advertisePrefix) {
                delegate?.deviceIMEIMismatch(expected: getBleAdvertiseName(), found: foundName)
                bluetoothDebugger?.addDeviceIMEIMismatch(found: foundName)
            }
        }
    }
    
    private func findPeripheralName(_ name: String, in advertisementData: [String : Any]) -> Bool {
        var found = false
        var foundAdvertisedNames: [String] = []
        
        // Check the local data name. (Where it should be. Used for testing app.)
        if let localServiceName = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
            if localServiceName == name {
                found = true
            } else {
                foundAdvertisedNames.append(localServiceName)
            }
        }
        // Check the service data. (Where it is actually implemented by CPE.)
        if !found, let avdService = advertisementData[CBAdvertisementDataServiceDataKey] as? [CBUUID:Data] {
            for key in avdService.keys {
                if let rawName = avdService[key],
                    let advertisedName = String(bytes: rawName, encoding: .utf8) {
                    if (advertisedName == name) {
                        found = true
                        break
                    } else {
                        foundAdvertisedNames.append(advertisedName)
                    }
                }
            }
        }
        
        if found {
            // Currently exact equality passes. Later can adjust to be actual advertised name if the requirement relaxes.
            delegate?.onBluetoothDiscovered(expectedName: name, foundName: name)
            bluetoothDebugger?.deviceDiscovered = name
            let value: [String : Any] = [
                "blePairStatus": "discovered",
                "expectedBroadcastName": name,
                "foundBroadcastName": name
                ]
            track(value)
            
            return true;
        } else {
            delegate?.onBluetoothDiscoveredNameFailed(expectedName: name, foundNames: foundAdvertisedNames)
            bluetoothDebugger?.addDiscoveredNameFailures(foundNames: foundAdvertisedNames)
            let value: [String : Any] = [
                "blePairStatus": "mismatch",
                "expectedBroadcastName": name,
                "foundBroadcastName": foundAdvertisedNames.joined(separator: " / ").prefix(500)
                ]
            track(value)
            
            return false
        }
    }
    
    public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        isPaired = true
        delegate?.pairUpdate(isPaired: true)
        bluetoothDebugger?.isDevicePaired = true
        peripheral.discoverServices(getBleAdvertiseUUIDs()) // For testing app which can only advertise on 1 service.
        peripheral.discoverServices(getSeviceUUIDs())
    }
    
    public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        for service in peripheral.services ?? [] {
            peripheral.discoverCharacteristics(getCharacteristicUUIDs(serviceId: service.uuid), for: service)
        }
    }
    
    public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        for characteristic in service.characteristics ?? [] {
            peripheral.setNotifyValue(true, for: characteristic)
        }
    }

    public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        
        let cpeResponse = formatCharacteristicData(characteristic)

        handleActivated(response: cpeResponse)
        
        handleFiveGSignal(response: cpeResponse)
    }
    
    public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { }
    
    public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        // We only connect to 1 peripheral. Using that assumption below.
        isPaired = false
        currentSignalStatus = .noBluetoothConnection
        currentRSSIValue = nil
        delegate?.pairUpdate(isPaired: false)
        delegate?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue)
        bluetoothDebugger?.isDevicePaired = false
        bluetoothDebugger?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue)
        self.peripheral = nil
        scanForPeripherals(central: central)
    }
    
    func formatCharacteristicData(_ characteristic: CBCharacteristic) -> [String: Any] {
        guard
            let advertiseData = characteristic.value, !advertiseData.isEmpty,
            let advertiseDict = try? JSONSerialization.jsonObject(with: advertiseData, options: []) as? [String: Any]
            else {
                return [:]
            }
        return advertiseDict
    }
    
    // MARK: Handle Activation
    func handleActivated(response: [String: Any]) {
        if let activation = response["Activation"] as? [String: Any] {
            isCPEActivated = activation.stringForkey("Status") == "Activated"
            delegate?.updateActivatedStatus(isActivated: isCPEActivated)
            bluetoothDebugger?.isDeviceActivated = isCPEActivated
        }
    }
    
    // MARK: Handle FiveGSignal
    func handleFiveGSignal(response: [String: Any]) {
        if let fivegSignal = response["5GSignal"] as? [String: Any] {
            if let rssiValue = parseRSRPFromJSON(fiveGData: fivegSignal) {
                currentRSSIValue = rssiValue
                
                // This need to call before currentSignalStatus set with new status. This is to gather the amount of time on the previous signal status before the update.
                if signalStrengthCheckStatus == .processing {
                    //lastSignalStatusTimeStamp = lastSignalStatusTimeStamp != nil ? lastSignalStatusTimeStamp : Date()
                    checkSignalStrengthContinueRequired()
                }
                
                if GMFGTestScreenData.shared.isEnable5GSignal {
                    currentSignalStatus = currentSignalStatus == .confirmedGood ? .confirmedGood : .good
                } else {
                    if rssiValue >= higherThreshold || rssiValue <= lowerThreshold {
                        // Future reference: -999 means no signal from CPE. Need to make sure it will always be included in check.
                        // Invalid value. We should not be above the upper limits or beneath the lower limits.
                        currentSignalStatus = .noSignal
                    } else if rssiValue >= rssiThreshold {
                        currentSignalStatus = currentSignalStatus == .confirmedGood ? .confirmedGood : .good
                    } else {
                        currentSignalStatus = .poor
                    }
                }
            } else {
                currentSignalStatus = .noSignal // Invalid value or doesn't exist.
            }
            
            delegate?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue)
            bluetoothDebugger?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue)
        }
    }

    // MARK:- Handle Signal History
    
    private func resetSignalStrengthCheckValue() {
        signalStrengthCheckStatus = .idle
        signalStrengthObserverTimer?.invalidate()
        signalStrengthObserverTimer = nil
        lastSignalStatusTimeStamp = nil
        signalStrengthGoodTotalTime = 0
        signalStrengthFailTotalTime = 0
    }
    
    func checkSignalStrengthContinueRequired() {
        let lastTrackDate = lastSignalStatusTimeStamp ?? Date()
        if  [.good, .confirmedGood].contains(currentSignalStatus) {
            signalStrengthGoodTotalTime += Date().timeIntervalSince(lastTrackDate)
        } else {
            signalStrengthFailTotalTime += Date().timeIntervalSince(lastTrackDate)
        }
        lastSignalStatusTimeStamp = Date()
        evaluateSignalStrength()
    }
    
    private func evaluateSignalStrength()  {
        let maxFailPercentage = 100 - signalPassingPercentage
        let maxFailTime = (maxFailPercentage / 100) * signalStrengthObserveDuration
        let maxPassTime = (signalPassingPercentage / 100) * signalStrengthObserveDuration
        
        #if DEBUG
        print("[ MFFGHSBluetoothPair ] Good Signal Time >>>> \(signalStrengthGoodTotalTime)")
        print("[ MFFGHSBluetoothPair ]  Bad Signal Time >>>> \(signalStrengthFailTotalTime)")
        #endif
        
        if signalStrengthFailTotalTime > maxFailTime {
            // already Fail, stop testing
            resetSignalStrengthCheckValue()
            signalStrengthCheckStatus = .complete
            currentSignalStatus = .confirmedPoor
            delegate?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue)
            bluetoothDebugger?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue)
        } else if signalStrengthGoodTotalTime >= maxPassTime || signalStrengthCheckStatus == .complete {
            // already PASS. stop testing and handle
            resetSignalStrengthCheckValue()
            signalStrengthCheckStatus = .complete
            currentSignalStatus = .confirmedGood
            delegate?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue)
            bluetoothDebugger?.updateCurrentSignalStatus(currentSignalStatus, strengthCheckStatus: signalStrengthCheckStatus, rssi: currentRSSIValue)
        } else {
            //contiue
        }
    }
    
    public func startSignalStrengthCheckValue(duration: TimeInterval, percentage: Double){
        if signalStrengthCheckStatus != .processing {
            resetSignalStrengthCheckValue()
            signalStrengthObserveDuration = duration
            signalPassingPercentage = percentage
            if currentSignalStatus == .confirmedGood {
                currentSignalStatus = .good
            }
            signalStrengthCheckStatus = .processing
            lastSignalStatusTimeStamp = Date()
            signalStrengthObserverTimer = Timer(timeInterval: (signalStrengthObserveDuration + 0.3), repeats: false) { (timer) in
                #if DEBUG
                print("[ MFFGHSBluetoothPair ] Signal Test Timeout")
                #endif
                self.signalStrengthCheckStatus = .complete
                self.checkSignalStrengthContinueRequired()
            }
            RunLoop.main.add(signalStrengthObserverTimer!, forMode: .common)
        }
    }
    
    // MARK:- Utility Functions
    func parseRSRPFromJSON(fiveGData: [String: Any]) -> Double? {
        return fiveGData["SS-RSRP"] as? Double ?? nil
    }
    
    // MARK:- Analytics
    func track(_ data: [String: Any]) {
        if let _delegate = delegate {
            trackGemini(value: data.merging(["delegateName": String(describing: _delegate)]) { $1 }, logType: .BLE)
        }
    }

    // MARK: BluetoothConfigModel methods
    var rssiThreshold: Double {
        return bluetoothConfig?.bleSignalThreshold ?? 0
    }
    
    var lowerThreshold: Double {
        return bluetoothConfig?.bleSignalLowerBound ?? 0
    }
    
    var higherThreshold: Double {
        return bluetoothConfig?.bleSignalUpperBound ?? 0
    }
    
    func getBleAdvertiseUUIDs() -> [CBUUID] {
        return bluetoothConfig?.peripherals.map{ $0.uuid } ?? []
    }
    
    open func getBleAdvertiseName() -> String {
        return bluetoothConfig?.advertisedData ?? ""
    }
    
    func getSeviceUUIDs() -> [CBUUID] {
        return bluetoothConfig?.services.map { $0.uuid } ?? []
    }
    
    func getCharacteristicUUIDs(serviceId: CBUUID) -> [CBUUID] {
        return bluetoothConfig?.services.first?.characteristics.map { $0.uuid } ?? []
    }
}
