//
//  GMFGBluetoothPair.swift
//  MobileFirstFramework
//
//  Created by Chowdhury, Shohrab on 4/20/20.
//  Copyright © 2020 Verizon Wireless. All rights reserved.
//

import CoreBluetooth

class GMFGBluetoothPair: MFFGHSBluetoothPair {
    private static weak var ginstance: GMFGBluetoothPair?
    static var shared: GMFGBluetoothPair {
        get {
            if let ginstance = ginstance {
                return ginstance
            } else {
                let newInstance = GMFGBluetoothPair()
                ginstance = newInstance
                return newInstance
            }
        }
    }
    
    override var bluetoothConfig: BluetoothConfigModel? {
        didSet {
            if (oldValue != bluetoothConfig) { // TODO: Needs deep check for config changes.
                setupBluetoothScanner(showPowerAlert: true)
            }
        }
    }
    
    override func shutdown() {
        super.shutdown()
        GMFGBluetoothPair.ginstance = nil
    }
    
    typealias ResponseHandler = ([String : Any]) -> Void
    private var speedTestHandler = GMFGSpeedTestHandler()
    private var internetAccessHandler = GMFGPublicInternetAccessHandler()
    private var routerWifiHandler = GMFGRouterWifiHandler()
    private var operationModeHandler = GMFGOperationHandler()
    private var fotaHandler = GMFGFotaHandler()
    private var fiveGSignalHandler = GMFG5GSignalHandler()
    private var fiveGCBandSignalHandler = GMFG5GCBandSignalHandler()
    private var responseHandlers: [CBUUID: ResponseHandler] = [:]
       
    private var max5GSignal: Double?
    var max5GSignalSeen: Double? {
        set {
            if (max5GSignal == nil && newValue != nil) || (max5GSignal != nil && newValue == nil) {
                max5GSignal = newValue
                return
            }
            guard let value = newValue, let maxSignal = max5GSignal else { return }
            if value > maxSignal { max5GSignal = newValue }
        }
        get { return max5GSignal }
    }
    
    private var min5GSignal: Double?
    var min5GSignalSeen: Double? {
        set {
            if (min5GSignal == nil && newValue != nil) || (min5GSignal != nil && newValue == nil) {
                min5GSignal = newValue
                return
            }
            guard let value = newValue, let minSignal = min5GSignal else { return }
            if value < minSignal { min5GSignal = newValue }
        }
        get { return min5GSignal }
    }
    var samples5GSignalSeen = 0
               
    public class func isValidAdvertisedData(_ testData: String) -> Bool {
        return testData.contains(GMFGConstant.BLE.advertisePrefix) && testData.count == 20
    }
    
    // MARK:- Notification Functions
    func updateAllNotifyValue(_ shouldNotify: Bool) {
        if let _peripheral = peripheral, let services = _peripheral.services {
            for service in services {
                if let characteristics = service.characteristics {
                    for characteristic in characteristics {
                        if characteristic.isNotifying {
                            _peripheral.setNotifyValue(shouldNotify, for: characteristic)
                        }
                    }
                }
            }
        }
    }
    
    @discardableResult func sendNotifications(for characteristic: CharacteristicType, shouldNotify: Bool) -> Bool {
        if let _peripheral = peripheral, let _characteristic = getCharacteristic(peripheral: _peripheral, fromType: characteristic) {
            _peripheral.setNotifyValue(shouldNotify, for: _characteristic)
            return true
        }
        return false
    }
    
    
    /// This will ensure that the handlers are registered only once
    ///  All this logic was originally happening in a case statement
    ///  therefore each comparison / lookup was happening everytime
    ///  the callback occurred. This is registering a characteric to a handler
    ///  which happens 1 time and used then with a dictionary lookup for the handler.
    /// - Parameter peripheral: comes in from delegate callback
    func ensureResponseHandlers(peripheral: CBPeripheral) {
        //compare local peripheral vs one passed in
        //make sure bluetoothConfig is set
        //make sure there are no responseHandlers registered
        guard let localPeripheral = self.peripheral,
                peripheral.identifier == localPeripheral.identifier,
                responseHandlers.isEmpty,
                bluetoothConfig != nil else { return }
        
        // activation
        if let uuid = getCharacteristic(peripheral: peripheral, fromType: .activation)?.uuid {
            responseHandlers[uuid] = handleActivated(response:)
        }

        // speedTest
        if let uuid = getCharacteristic(peripheral: peripheral, fromType: .speedTest)?.uuid {
            responseHandlers[uuid] = speedTestHandler.response(response:)
        }
        if let uuid = getCharacteristic(peripheral: peripheral, fromType: .speedTestResults)?.uuid {
            responseHandlers[uuid] = speedTestHandler.response(response:)
        }
        
        //internetAccess
        if let uuid = getCharacteristic(peripheral: peripheral, fromType: .internetAccess)?.uuid {
            responseHandlers[uuid] = internetAccessHandler.response(response:)
        }
        
        //fota
        if let uuid = getCharacteristic(peripheral: peripheral, fromType: .fotaCheck)?.uuid {
            responseHandlers[uuid] = fotaHandler.response(response:)
        }
        if let uuid = getCharacteristic(peripheral: peripheral, fromType: .fotaStatus)?.uuid {
            responseHandlers[uuid] = fotaHandler.response(response:)
        }
       
        //routerWifi
        if let uuid = getCharacteristic(peripheral: peripheral, fromType: .routerWiFi)?.uuid {
            responseHandlers[uuid] = routerWifiHandler.response(response:)
        }

        //fiveGCBandSignal
        if let uuid = getCharacteristic(peripheral: peripheral, fromType: .fiveGCBandSignal)?.uuid {
            responseHandlers[uuid] = fiveGCBandSignalHandler.response(response:)
        }

        //fiveGSignal
        if let uuid = getCharacteristic(peripheral: peripheral, fromType: .fiveGSignal)?.uuid {
            func fiveGResponse(response: [String: Any]){
                updateFiveGStatistics(cpeResponse: response)
                handleFiveGSignal(response: response)
                fiveGSignalHandler.response(response: response)
            }
            responseHandlers[uuid] = fiveGResponse(response:)
        }
    }

    // MARK:- CBPeripheralDelegate
    override public func centralManagerDidUpdateState(_ central: CBCentralManager) {
        bluetoothDebugger?.bluetoothState = central.state
        if central.state == .unauthorized {
            delegate?.bluetoothPermissionsDenied() //User denied BLE
            bluetoothDebugger?.isBluetoothPermissionDenied = true
        } else if central.state == .poweredOff {
            delegate?.bluetoothOff() //User turned off BLE
        }
        super.centralManagerDidUpdateState(central)
    }
    
    override func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        let cpeResponse = formatCharacteristicData(characteristic)
        track("Message From CPE", additionalData: cpeResponse.merging(["CBUUID":characteristic.uuid.uuidString], uniquingKeysWith: { $1 }))
        
        //make sure responseHandlers are registered
        ensureResponseHandlers(peripheral: peripheral)
        
        if let responseHandler = responseHandlers[characteristic.uuid] {
            responseHandler(cpeResponse)
        }
    }
    
    override public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        if let peripheralName = advertisementData[CBAdvertisementDataLocalNameKey] as? String, peripheralName.hasPrefix(GMFGConstant.BLE.advertisePrefix) {
            track("Scanning found peripheral \(peripheralName) with RSSI \(RSSI).")
        } else {
            track("Scanning found peripheral, but not a VZ5G device")
        }
        super.centralManager(central, didDiscover: peripheral, advertisementData: advertisementData, rssi: RSSI)
    }
    
    override public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        track("Connected to \(String(describing: peripheral.name))")
        super.centralManager(central, didConnect: peripheral)
    }
    
    override public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        track("Disconnected from \(String(describing: peripheral.name))")
        fiveGSignalHandler.deviceNotReady()
        super.centralManager(central, didDisconnectPeripheral: peripheral, error: error)
    }
    
    override public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if let error = error {
            track("Discover Service Error: \(error.localizedDescription)")
        }
        super.peripheral(peripheral, didDiscoverServices: error)
    }
    
    public func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
        if let error = error {
            track("Read error: \(error.localizedDescription) with RSSI: \(RSSI)")
        }
    }
    
    public func peripheral(_ peripheral: CBPeripheral, didOpen channel: CBL2CAPChannel?, error: Error?) {
        if let error = error {
            track("Error opening channel: \(error.localizedDescription)")
        }
    }
    
    public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) {
        if let error = error {
            track("Error writing to characteristic: \(String(describing: descriptor.characteristic?.uuid.uuidString)) holding value: \(String(describing: descriptor.value)) with error: \(error.localizedDescription)")
        }
    }
    
    public func peripheral(_ peripheral: CBPeripheral, didDiscoverIncludedServicesFor service: CBService, error: Error?) {
        if let error = error {
            track("Error in discovered device characteristics: \(error.localizedDescription)")
        }
    }
    
    override func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        track(error?.localizedDescription ?? "Unknown", function: "Central Manager Failed Connection")
    }
    
    override func scanForPeripherals(central: CBCentralManager) {
        if !central.isScanning {
            track("Starting scan for peripherals")
            central.scanForPeripherals(withServices: nil, options: nil)
        } else {
            track("Already Scanning for peripherals")
        }
    }
        
    //5GSignal Characteristic
    private func updateFiveGStatistics(cpeResponse: [String: Any]) {
        if let fivegSignal = cpeResponse["5GSignal"] as? [String: Any] {
            if let rssiValue = parseRSRPFromJSON(fiveGData: fivegSignal) {
                max5GSignalSeen = rssiValue
                min5GSignalSeen = rssiValue
                samples5GSignalSeen = samples5GSignalSeen + 1
            }
        }
    }
    
    // MARK:- Speed Test Functions
    
    public func startSpeedTest(_ speedTestDelegate: GMFGSpeedTestDelegate, withNotifications: Bool) {
        speedTestHandler.setDelegate(delegates: [speedTestDelegate, bluetoothDebugger])
        if let _peripheral = peripheral, let _cTriggerSpeedTest = getCharacteristic(peripheral: _peripheral, fromType: .speedTest) {
            if withNotifications {
                updateAllNotifyValue(false) //Turn them all off to reduce traffic.
                _ = sendNotifications(for: .speedTestResults, shouldNotify: true)
            }
            track("Starting Speed Test")
            speedTestHandler.start(_peripheral, _cTriggerSpeedTest)
        } else {
            let errorMessage = peripheral == nil ? GMFGConstant.BLE.LogMsg.peripheralError : GMFGConstant.BLE.LogMsg.characteristicError
            track(errorMessage)
            speedTestHandler.deviceNotReady()
        }
    }
    
    public func stopSpeedTest() {
        speedTestHandler.reset()
        if let _peripheral = peripheral, let _cTriggerSpeedTest = getCharacteristic(peripheral: _peripheral, fromType: .speedTest) {
            _ = sendNotifications(for: .speedTestResults, shouldNotify: false)
            track("Speed Test Stopping")
            speedTestHandler.stop(_peripheral, _cTriggerSpeedTest)
        } else {
            let errorMessage = peripheral == nil ? GMFGConstant.BLE.LogMsg.peripheralError : GMFGConstant.BLE.LogMsg.characteristicError
            track(errorMessage)
            speedTestHandler.deviceNotReady()
        }
    }
    
    //MARK:- Router WIFI Info
    public func fetchRouterWifi(delegate: GMFGRouterWifiDelegate) {
        routerWifiHandler.setDelegate(delegates: [delegate, bluetoothDebugger])
        if let _peripheral = peripheral, let _cRouterWifi = getCharacteristic(peripheral: _peripheral, fromType: .routerWiFi) {
            track("Started fetching Wifi info")
            routerWifiHandler.fetchRouterWifiCredentials(_peripheral, _cRouterWifi)
        } else {
            let errorMessage = peripheral == nil ? GMFGConstant.BLE.LogMsg.peripheralError : GMFGConstant.BLE.LogMsg.characteristicError
            track(errorMessage)
            routerWifiHandler.deviceNotReady()
        }
    }
    
    // MARK:- Public Internet Functions
    public func checkInternetStatus(delegate: GMFGPublicInternetAccessDelegate) {
        internetAccessHandler.setDelegate(delegates: [delegate, bluetoothDebugger])
        if let _peripheral = peripheral, let _cPublicInternetAccess = getCharacteristic(peripheral: _peripheral, fromType: .internetAccess) {
            track("Getting Internet Access")
            internetAccessHandler.getPublicInternetAccess(_peripheral, _cPublicInternetAccess)
        } else {
            let errorMessage = peripheral == nil ? GMFGConstant.BLE.LogMsg.peripheralError : GMFGConstant.BLE.LogMsg.characteristicError
            track(errorMessage)
            internetAccessHandler.deviceNotReady()
        }
    }
    
    // MARK:- Operational Mode Functions
    @discardableResult
    public func setOpMode(mode: GMFGOperationMode, disableBLE: Bool = false) -> Bool {
        let additionalLogData: [String: Any] = ["Mode": mode.rawValue, "DisableBLE": disableBLE]
        if let _peripheral = peripheral, let _cOperationMode = getCharacteristic(peripheral: _peripheral, fromType: .operationMode) {
            track("Disabling BLE", additionalData: additionalLogData)
            return operationModeHandler.setOpMode(_peripheral, _cOperationMode, mode: mode, disableBLE: disableBLE)
        }
        let errorMessage = peripheral == nil ? GMFGConstant.BLE.LogMsg.peripheralError : GMFGConstant.BLE.LogMsg.characteristicError
        track(errorMessage, additionalData: additionalLogData)
        return false
    }
    
    // MARK:- FOTA Functions
    public func startGetFirmwareVersion(delegate: GMFGFotaDelegate) {
        bluetoothDebugger?.isCheckingFirmware = true
        fotaHandler.setDelegate(delegates: [delegate, bluetoothDebugger])
        if let _peripheral = peripheral, let _cFota = getCharacteristic(peripheral: _peripheral, fromType: .fotaStatus) {
            track("Started getting firmware version from device")
            fotaHandler.requestVersion(_peripheral, _cFota)
        } else {
            let errorMessage = peripheral == nil ? GMFGConstant.BLE.LogMsg.peripheralError : GMFGConstant.BLE.LogMsg.characteristicError
            track(errorMessage)
            fotaHandler.deviceNotReady()
        }
    }
    
    public func stopGetFirmwareVersion() {
        bluetoothDebugger?.isCheckingFirmware = false
        fotaHandler.reset()
        track("Stopped getting firmware version")
    }
    
    public func startFirmwareUpgrade(delegate: GMFGFotaDelegate, useNotifications: Bool) {
        fotaHandler.setDelegate(delegates: [delegate, bluetoothDebugger])
        if let _peripheral = peripheral, let _cFota = getCharacteristic(peripheral: _peripheral, fromType: .fotaCheck), getCharacteristic(peripheral: _peripheral,fromType: .fotaStatus) != nil {
            track("Starting firmware update")
            updateAllNotifyValue(false)
            _ = self.sendNotifications(for: .fotaStatus, shouldNotify: useNotifications)
            fotaHandler.startUpdate(_peripheral, _cFota)
        } else {
            let errorMessage = peripheral == nil ? GMFGConstant.BLE.LogMsg.peripheralError : GMFGConstant.BLE.LogMsg.characteristicError
            track(errorMessage)
            fotaHandler.deviceNotReady()
        }
    }
    
    public func readFOTAStatus(delegate: GMFGFotaDelegate) {
        fotaHandler.setDelegate(delegates: [delegate, bluetoothDebugger])
        if let _peripheral = peripheral, let _cFota = getCharacteristic(peripheral: _peripheral, fromType: .fotaStatus) {
            track("Checking FOTA status")
            fotaHandler.requestVersion(_peripheral, _cFota)
        } else {
            let errorMessage = peripheral == nil ? GMFGConstant.BLE.LogMsg.peripheralError : GMFGConstant.BLE.LogMsg.characteristicError
            track(errorMessage)
            fotaHandler.deviceNotReady()
        }
    }
    
    public func stopFirmwareUpgrade() {
        fotaHandler.reset()
        track("Stopped firmware update")
    }
    
    public func isFotaInProgress() -> Bool {
        if let params = GMFGStorageManager.retrieve(key: GMFGConstant.fiveGParams), let inProgress = params[GMFGConstant.BLE.fotaInProgress] as? Bool {
            return inProgress
        }
        return false
    }
    
    public func setFotaInProgress(_ inProgress: Bool) {
        GMFGStorageManager.addEntries(dictionary: [GMFGConstant.BLE.fotaInProgress: inProgress])
    }
    
    public func getOldFirmwareVersion() -> String? {
        if let params = GMFGStorageManager.retrieve(key: GMFGConstant.fiveGParams), let oldFirmware = params[GMFGConstant.BLE.firmwareVersion] as? String {
            return oldFirmware
        }
        return nil
    }
    
    public func isFirmwareNewer(_ newFirmware: String) -> Bool {
       if getOldFirmwareVersion() == newFirmware {
           return false
       }
       return true
   }
    
    // MARK:- 5G Signal Handler
    public func start5GSignalUpdates(_ delegate: GMFG5GSignalDelegate, useNotifications: Bool) {
        fiveGSignalHandler.setDelegate(delegates: [delegate, bluetoothDebugger])
        if let _peripheral = peripheral, let _c5GSignal = getCharacteristic(peripheral: _peripheral, fromType: .fiveGSignal) {
            track("Getting 5G Signal")
            sendNotifications(for: .fiveGSignal, shouldNotify: useNotifications)
            fiveGSignalHandler.get5GSignal(_peripheral, _c5GSignal)
        } else {
            let errorMessage = peripheral == nil ? GMFGConstant.BLE.LogMsg.peripheralError : GMFGConstant.BLE.LogMsg.characteristicError
            track(errorMessage)
            fiveGSignalHandler.deviceNotReady()
        }
    }
    
    public func stop5GSignalUpdates() {
        fiveGSignalHandler.reset()
        track("Stop getting 5G Signal")
        updateAllNotifyValue(false)
    }
    
    // MARK:- 5G CBand Signal Handler
    public func start5GCBandSignalUpdates(_ delegate: GMFG5GCBandSignalDelegate, useNotifications: Bool) {
        fiveGCBandSignalHandler.setDelegate(delegates:  [delegate, bluetoothDebugger])
        if let _peripheral = peripheral, let _c5GSignal = getCharacteristic(peripheral: _peripheral, fromType: .fiveGCBandSignal) {
            track("Getting 5G C-Band Signal")
            sendNotifications(for: .fiveGCBandSignal, shouldNotify: useNotifications)
            fiveGCBandSignalHandler.get5GSignal(_peripheral, _c5GSignal)
        } else {
            let errorMessage = peripheral == nil ? GMFGConstant.BLE.LogMsg.peripheralError : GMFGConstant.BLE.LogMsg.characteristicError
            track(errorMessage)
            fiveGCBandSignalHandler.deviceNotReady()
        }
    }
    
    public func stop5GCBandSignalUpdates() {
        fiveGCBandSignalHandler.reset()
        track("Stop getting 5G C-Band Signal")
        updateAllNotifyValue(false)
    }
    
    // MARK:- Utility Functions
    override func parseRSRPFromJSON(fiveGData: [String: Any]) -> Double? {
        return (fiveGData["RSRP"] as? NSString)?.doubleValue ?? nil
    }
    
    // MARK:- BluetoothConfigModel Functions
    override func getBleAdvertiseName() -> String {
        let remoteAdvData = bluetoothConfig?.advertisedData ?? ""
        if GMFGBluetoothPair.isValidAdvertisedData(remoteAdvData)  {
            GMFGStorageManager.addEntries(dictionary: [GMFGConstant.BLE.advertisedData: remoteAdvData])
            return remoteAdvData
        } else if let localData = GMFGStorageManager.retrieve(key: GMFGConstant.fiveGParams), let localAdvData = localData[GMFGConstant.BLE.advertisedData] as? String {
            track(GMFGConstant.BLE.LogMsg.cachedAdvertised)
            return localAdvData
        }
        return remoteAdvData
    }
    
    func getCharacteristic(peripheral: CBPeripheral, fromType: CharacteristicType) -> CBCharacteristic? {
        guard let service = bluetoothConfig?.services.first else {
            track("SERVICE was empty in the BLE Module JSON")
            return nil
        }
        
        guard let expectedCharacteristicUUID = service.characteristics.first(where: {$0.name == fromType})?.uuid else {
            track("No characteristic found for key \(fromType.rawValue) in the BLE Module JSON")
            return nil
        }
        
        guard let expectedServiceUUID = getSeviceUUIDs().first else {
            track("No services found in the BLE Module JSON")
            return nil
        }
        
        guard peripheral.state == .connected else {
            track("Tried to get a characteristic from the peripheral, but it is still in state: \(peripheral.state.rawValue)")
            return nil
        }
        
        guard let services = peripheral.services else {
            return nil
        }
        
        if services.count != 1 {
            track("Found \(services.count) services!")
        }
        
        guard let service = services.first(where: {$0.uuid == expectedServiceUUID}) else {
            track("Could not find service: \(expectedServiceUUID) on device.")
            return nil
        }
        
        guard let characteristic = service.characteristics?.first(where: {$0.uuid == expectedCharacteristicUUID}) else {
            track("Could not find characteristic: \(expectedCharacteristicUUID) on device.")
            return nil
        }
        return characteristic
    }

}
