<commit_before>//
//  CentralProxy.swift
//
//  Copyright (c) 2016 <NAME>
//
//  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 CoreBluetooth

final class CentralProxy: NSObject {
    fileprivate lazy var asyncStateCallbacks: [AsyncCentralStateCallback] = []
    
    fileprivate var scanRequest: PeripheralScanRequest?
    
    fileprivate lazy var connectRequests: [UUID: ConnectPeripheralRequest] = [:]
    fileprivate lazy var disconnectRequests: [UUID: DisconnectPeripheralRequest] = [:]
    fileprivate lazy var peripherals = NSMapTable<NSString, Peripheral>.strongToWeakObjects()
    
    var centralManager: CBCentralManager!

    fileprivate(set) var centralQueue: DispatchQueue! = DispatchQueue.main
    
    override init() {
        super.init()
        
        self.centralManager = CBCentralManager(delegate: self, queue: nil)
    }
    
    init(stateRestoreIdentifier: String) {
        super.init()
        
        // MARK: in macOS before 10.13, the CBCentralManagerOptionRestoreIdentifierKey don't exist
        #if SB_XCODE9
            if #available(OSX 10.13, *) {
                self.centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: stateRestoreIdentifier])
            } else {
                self.centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
            }
        #else
            #if os(OSX)
                self.centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
            #else
                self.centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: stateRestoreIdentifier])
            #endif
        #endif
    }
    
    init(stateRestoreIdentifier: String?, queue: DispatchQueue) {
        super.init()

        self.centralQueue = queue
        
        // MARK: in macOS before 10.13, the CBCentralManagerOptionRestoreIdentifierKey don't exist
        #if SB_XCODE9
            if #available(OSX 10.13, *) {
                if let restoreIdentifier = stateRestoreIdentifier {
                    self.centralManager = CBCentralManager(delegate: self, queue: queue, options: [CBCentralManagerOptionRestoreIdentifierKey: restoreIdentifier])
                } else {
                    self.centralManager = CBCentralManager(delegate: self, queue: queue)
                }
            } else {
                self.centralManager = CBCentralManager(delegate: self, queue: queue, options: nil)
            }
        #else
            #if os(OSX)
                self.centralManager = CBCentralManager(delegate: self, queue: queue, options: nil)
            #else
                if let restoreIdentifier = stateRestoreIdentifier {
                    self.centralManager = CBCentralManager(delegate: self, queue: queue, options: [CBCentralManagerOptionRestoreIdentifierKey: restoreIdentifier])
                } else {
                    self.centralManager = CBCentralManager(delegate: self, queue: queue)
                }
            #endif
        #endif
    }
    
    fileprivate func postCentralEvent(_ event: NSNotification.Name, userInfo: [AnyHashable: Any]? = nil) {
        NotificationCenter.default.post(
            name: event,
            object: Central.sharedInstance,
            userInfo: userInfo)
    }
}

// MARK: Initialize Bluetooth requests
extension CentralProxy {
    func asyncState(_ completion: @escaping AsyncCentralStateCallback) {
        switch centralManager.state {
        case .unknown:
            self.asyncStateCallbacks.append(completion)
        case .resetting:
            self.asyncStateCallbacks.append(completion)
        case .unsupported:
            completion(.unsupported)
        case .unauthorized:
            completion(.unauthorized)
        case .poweredOff:
            completion(.poweredOff)
        case .poweredOn:
            completion(.poweredOn)
        }
    }
    
    func initializeBluetooth(_ completion: @escaping InitializeBluetoothCallback) {
        self.asyncState { (state) in
            switch state {
            case .unsupported:
                completion(.bluetoothUnavailable(reason: .unsupported))
            case .unauthorized:
                completion(.bluetoothUnavailable(reason: .unauthorized))
            case .poweredOff:
                completion(.bluetoothUnavailable(reason: .poweredOff))
            case .poweredOn:
                completion(nil)
            }
        }
    }
    
    func callAsyncCentralStateCallback(_ state: AsyncCentralState) {
        let callbacks = self.asyncStateCallbacks
        
        self.asyncStateCallbacks.removeAll()
        
        for callback in callbacks {
            callback(state)
        }
    }
}

// MARK: Scan requests
private final class PeripheralScanRequest {
    let callback: PeripheralScanCallback
    
    init(callback: @escaping PeripheralScanCallback) {
        self.callback = callback
    }
}

extension CentralProxy {
    func scanWithTimeout(_ timeout: TimeInterval, serviceUUIDs: [CBUUID]?, options: [String : Any]?, _ callback: @escaping PeripheralScanCallback) {
        initializeBluetooth { [unowned self] (error) in
            if let error = error {
                callback(PeripheralScanResult.scanStopped(error: error))
            } else {
                if self.scanRequest != nil {
                    self.centralManager.stopScan()
                }
                
                let scanRequest = PeripheralScanRequest(callback: callback)
                self.scanRequest = scanRequest
                
                scanRequest.callback(.scanStarted)
                self.centralManager.scanForPeripherals(withServices: serviceUUIDs, options: options)

                weak var weakRequest: PeripheralScanRequest? = self.scanRequest
                self.centralQueue.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(timeout * 1000))) {
                    if let scanRequest = weakRequest {
                        self.stopScan()
                    }
                }
            }
        }
    }
    
    func stopScan(error: SBError? = nil) {
        self.centralManager.stopScan()
        if let scanRequest = self.scanRequest {
            self.scanRequest = nil
            scanRequest.callback(.scanStopped(error: error))
        }
    }
}

// MARK: Connect Peripheral requests
private final class ConnectPeripheralRequest {
    var callbacks: [ConnectPeripheralCallback] = []
    
    let peripheral: CBPeripheral
    
    init(peripheral: CBPeripheral, callback: @escaping ConnectPeripheralCallback) {
        self.callbacks.append(callback)
        
        self.peripheral = peripheral
    }
    
    func invokeCallbacks(error: Error?) {
        let result: SwiftyBluetooth.Result<Void> = {
            if let error = error {
                return .failure(error)
            } else {
                return .success()
            }
        }()
        for callback in callbacks {
            callback(result)
        }
    }
}

extension CentralProxy {
    func connect(peripheral: CBPeripheral, timeout: TimeInterval, _ callback: @escaping ConnectPeripheralCallback) {
        initializeBluetooth { [unowned self] (error) in
            if let error = error {
                callback(.failure(error))
                return
            }
            
            let uuid = peripheral.uuidIdentifier
            
            if let cbPeripheral = self.centralManager.retrievePeripherals(withIdentifiers: [uuid]).first , cbPeripheral.state == .connected {
                callback(.success())
                return
            }
            
            if let request = self.connectRequests[uuid] {
                request.callbacks.append(callback)
            } else {
                let request = ConnectPeripheralRequest(peripheral: peripheral, callback: callback)
                self.connectRequests[uuid] = request
                
                self.centralManager.connect(peripheral, options: nil)

                weak var weakRequest: ConnectPeripheralRequest? = request
                self.centralQueue.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(timeout * 1000))) {
                    if let connectRequest = weakRequest {
                        let uuid = connectRequest.peripheral.uuidIdentifier

                        self.connectRequests[uuid] = nil

                        self.centralManager.cancelPeripheralConnection(connectRequest.peripheral)

                        connectRequest.invokeCallbacks(error: SBError.operationTimedOut(operation: .connectPeripheral))
                    }
                }
            }
        }
    }
}

// MARK: Disconnect Peripheral requests
private final class DisconnectPeripheralRequest {
    var callbacks: [ConnectPeripheralCallback] = []
    
    let peripheral: CBPeripheral
    
    init(peripheral: CBPeripheral, callback: @escaping DisconnectPeripheralCallback) {
        self.callbacks.append(callback)
        
        self.peripheral = peripheral
    }
    
    func invokeCallbacks(error: Error?) {
        let result: SwiftyBluetooth.Result<Void> = {
            if let error = error {
                return .failure(error)
            } else {
                return .success()
            }
        }()
        for callback in callbacks {
            callback(result)
        }
    }
}

extension CentralProxy {
    func disconnect(peripheral: CBPeripheral, timeout: TimeInterval, _ callback: @escaping DisconnectPeripheralCallback) {
        initializeBluetooth { [unowned self] (error) in
            
            if let error = error {
                callback(.failure(error))
                return
            }
            
            let uuid = peripheral.uuidIdentifier
            
            // MARK: in macOS before 10.13, the CBPeripheralState.disconnecting don't exist
            #if SB_XCODE9
                if #available(OSX 10.13, *) {
                    if let cbPeripheral = self.centralManager.retrievePeripherals(withIdentifiers: [uuid]).first,
                        (cbPeripheral.state == .disconnected || cbPeripheral.state == .disconnecting) {
                        callback(.success())
                        return
                    }
                } else {
                    if let cbPeripheral = self.centralManager.retrievePeripherals(withIdentifiers: [uuid]).first,
                        (cbPeripheral.state == .disconnected) {
                        callback(.success())
                        return
                    }
                }
            #else
                #if os(OSX)
                    if let cbPeripheral = self.centralManager.retrievePeripherals(withIdentifiers: [uuid]).first,
                        (cbPeripheral.state == .disconnected) {
                        callback(.success())
                        return
                    }
                #else
                    if let cbPeripheral = self.centralManager.retrievePeripherals(withIdentifiers: [uuid]).first,
                        (cbPeripheral.state == .disconnected || cbPeripheral.state == .disconnecting) {
                        callback(.success())
                        return
                    }
                #endif
            #endif

            if let request = self.disconnectRequests[uuid] {
                request.callbacks.append(callback)
            } else {
                let request = DisconnectPeripheralRequest(peripheral: peripheral, callback: callback)
                self.disconnectRequests[uuid] = request
                
                self.centralManager.cancelPeripheralConnection(peripheral)

                weak var weakRequest: DisconnectPeripheralRequest? = request
                self.centralQueue.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(timeout * 1000))) {
                    if let disconnectRequest = weakRequest {
                        let uuid = disconnectRequest.peripheral.uuidIdentifier

                        self.connectRequests[uuid] = nil

                        disconnectRequest.invokeCallbacks(error: SBError.operationTimedOut(operation: .disconnectPeripheral))
                    }
                }
            }
        }
    }
}

extension CentralProxy: CBCentralManagerDelegate {
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        postCentralEvent(Central.CentralStateChange, userInfo: ["state": central.state])
        
        switch central.state.rawValue {
        case 0: // .unknown
            self.stopScan(error: .scanningEndedUnexpectedly)
        case 1: // .resetting
            self.stopScan(error: .scanningEndedUnexpectedly)
        case 2: // .unsupported
            self.callAsyncCentralStateCallback(.unsupported)
            self.stopScan(error: .scanningEndedUnexpectedly)
        case 3: // .unauthorized
            self.callAsyncCentralStateCallback(.unauthorized)
            self.stopScan(error: .scanningEndedUnexpectedly)
        case 4: // .poweredOff
            self.callAsyncCentralStateCallback(.poweredOff)
            self.stopScan(error: .scanningEndedUnexpectedly)
        case 5: // .poweredOn
            self.callAsyncCentralStateCallback(.poweredOn)
        default:
            fatalError("Unsupported BLE CentralState")
        }
    }
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        let uuid = peripheral.uuidIdentifier
        guard let request = connectRequests[uuid] else {
            return
        }
        
        connectRequests[uuid] = nil
        
        request.invokeCallbacks(error: nil)
    }
    
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        let uuid = peripheral.uuidIdentifier
        guard let request = disconnectRequests[uuid] else {
            return
        }
        
        disconnectRequests[uuid] = nil
        
        request.invokeCallbacks(error: error)
    }
    
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        let uuid = peripheral.uuidIdentifier
        guard let request = connectRequests[uuid] else {
            return
        }
        
        let resolvedError: Error = error ?? SBError.peripheralFailedToConnectReasonUnknown
        
        connectRequests[uuid] = nil
        
        request.invokeCallbacks(error: resolvedError)
    }
    
    func centralManager(_ central: CBCentralManager,
                        didDiscover peripheral: CBPeripheral,
                        advertisementData: [String: Any],
                        rssi RSSI: NSNumber)
    {
        guard let scanRequest = scanRequest else {
            return
        }

        // MARK: fixed the bug: after creating Peripheral for existed CBPeripheral last Peripheral replaces CBPeripheral.delegate
        //       and old Peripherals don't receive a response
        //
        let cbPeripheral = peripheral

        let key = NSString(string: cbPeripheral.uuidIdentifier.uuidString)
        var peripheral = self.peripherals.object(forKey: key)
        if peripheral == nil {
            peripheral = Peripheral(peripheral: cbPeripheral)
            self.peripherals.setObject(peripheral, forKey: key)
        }
        //
        
        var rssiOptional: Int? = Int(RSSI)
        if let rssi = rssiOptional, rssi == 127 {
            rssiOptional = nil
        }
        
        scanRequest.callback(.scanResult(peripheral: peripheral!, advertisementData: advertisementData, RSSI: rssiOptional))
    }
    
    func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) {
        // MARK: in macOS before 10.13, the CBCentralManagerOptionRestoreIdentifierKey don't exist
        #if SB_XCODE9
            if #available(OSX 10.13, *) {
                let peripherals = ((dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral]) ?? []).map { Peripheral(peripheral: $0) }
                postCentralEvent(Central.CentralManagerWillRestoreState, userInfo: ["peripherals": peripherals])
            }
        #else
            #if os(OSX)
            #else
                let peripherals = ((dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral]) ?? []).map { Peripheral(peripheral: $0) }
                postCentralEvent(Central.CentralManagerWillRestoreState, userInfo: ["peripherals": peripherals])
            #endif
        #endif
    }

}
<commit_msg>Fixed warning: 'Value was defined but never used; consider replacing with boolean test'<commit_after>//
//  CentralProxy.swift
//
//  Copyright (c) 2016 <NAME>
//
//  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 CoreBluetooth

final class CentralProxy: NSObject {
    fileprivate lazy var asyncStateCallbacks: [AsyncCentralStateCallback] = []
    
    fileprivate var scanRequest: PeripheralScanRequest?
    
    fileprivate lazy var connectRequests: [UUID: ConnectPeripheralRequest] = [:]
    fileprivate lazy var disconnectRequests: [UUID: DisconnectPeripheralRequest] = [:]
    fileprivate lazy var peripherals = NSMapTable<NSString, Peripheral>.strongToWeakObjects()
    
    var centralManager: CBCentralManager!

    fileprivate(set) var centralQueue: DispatchQueue! = DispatchQueue.main
    
    override init() {
        super.init()
        
        self.centralManager = CBCentralManager(delegate: self, queue: nil)
    }
    
    init(stateRestoreIdentifier: String) {
        super.init()
        
        // MARK: in macOS before 10.13, the CBCentralManagerOptionRestoreIdentifierKey don't exist
        #if SB_XCODE9
            if #available(OSX 10.13, *) {
                self.centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: stateRestoreIdentifier])
            } else {
                self.centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
            }
        #else
            #if os(OSX)
                self.centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
            #else
                self.centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: stateRestoreIdentifier])
            #endif
        #endif
    }
    
    init(stateRestoreIdentifier: String?, queue: DispatchQueue) {
        super.init()

        self.centralQueue = queue
        
        // MARK: in macOS before 10.13, the CBCentralManagerOptionRestoreIdentifierKey don't exist
        #if SB_XCODE9
            if #available(OSX 10.13, *) {
                if let restoreIdentifier = stateRestoreIdentifier {
                    self.centralManager = CBCentralManager(delegate: self, queue: queue, options: [CBCentralManagerOptionRestoreIdentifierKey: restoreIdentifier])
                } else {
                    self.centralManager = CBCentralManager(delegate: self, queue: queue)
                }
            } else {
                self.centralManager = CBCentralManager(delegate: self, queue: queue, options: nil)
            }
        #else
            #if os(OSX)
                self.centralManager = CBCentralManager(delegate: self, queue: queue, options: nil)
            #else
                if let restoreIdentifier = stateRestoreIdentifier {
                    self.centralManager = CBCentralManager(delegate: self, queue: queue, options: [CBCentralManagerOptionRestoreIdentifierKey: restoreIdentifier])
                } else {
                    self.centralManager = CBCentralManager(delegate: self, queue: queue)
                }
            #endif
        #endif
    }
    
    fileprivate func postCentralEvent(_ event: NSNotification.Name, userInfo: [AnyHashable: Any]? = nil) {
        NotificationCenter.default.post(
            name: event,
            object: Central.sharedInstance,
            userInfo: userInfo)
    }
}

// MARK: Initialize Bluetooth requests
extension CentralProxy {
    func asyncState(_ completion: @escaping AsyncCentralStateCallback) {
        switch centralManager.state {
        case .unknown:
            self.asyncStateCallbacks.append(completion)
        case .resetting:
            self.asyncStateCallbacks.append(completion)
        case .unsupported:
            completion(.unsupported)
        case .unauthorized:
            completion(.unauthorized)
        case .poweredOff:
            completion(.poweredOff)
        case .poweredOn:
            completion(.poweredOn)
        }
    }
    
    func initializeBluetooth(_ completion: @escaping InitializeBluetoothCallback) {
        self.asyncState { (state) in
            switch state {
            case .unsupported:
                completion(.bluetoothUnavailable(reason: .unsupported))
            case .unauthorized:
                completion(.bluetoothUnavailable(reason: .unauthorized))
            case .poweredOff:
                completion(.bluetoothUnavailable(reason: .poweredOff))
            case .poweredOn:
                completion(nil)
            }
        }
    }
    
    func callAsyncCentralStateCallback(_ state: AsyncCentralState) {
        let callbacks = self.asyncStateCallbacks
        
        self.asyncStateCallbacks.removeAll()
        
        for callback in callbacks {
            callback(state)
        }
    }
}

// MARK: Scan requests
private final class PeripheralScanRequest {
    let callback: PeripheralScanCallback
    
    init(callback: @escaping PeripheralScanCallback) {
        self.callback = callback
    }
}

extension CentralProxy {
    func scanWithTimeout(_ timeout: TimeInterval, serviceUUIDs: [CBUUID]?, options: [String : Any]?, _ callback: @escaping PeripheralScanCallback) {
        initializeBluetooth { [unowned self] (error) in
            if let error = error {
                callback(PeripheralScanResult.scanStopped(error: error))
            } else {
                if self.scanRequest != nil {
                    self.centralManager.stopScan()
                }
                
                let scanRequest = PeripheralScanRequest(callback: callback)
                self.scanRequest = scanRequest
                
                scanRequest.callback(.scanStarted)
                self.centralManager.scanForPeripherals(withServices: serviceUUIDs, options: options)

                weak var weakRequest: PeripheralScanRequest? = self.scanRequest
                self.centralQueue.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(timeout * 1000))) {
                    if let _ = weakRequest {
                        self.stopScan()
                    }
                }
            }
        }
    }
    
    func stopScan(error: SBError? = nil) {
        self.centralManager.stopScan()
        if let scanRequest = self.scanRequest {
            self.scanRequest = nil
            scanRequest.callback(.scanStopped(error: error))
        }
    }
}

// MARK: Connect Peripheral requests
private final class ConnectPeripheralRequest {
    var callbacks: [ConnectPeripheralCallback] = []
    
    let peripheral: CBPeripheral
    
    init(peripheral: CBPeripheral, callback: @escaping ConnectPeripheralCallback) {
        self.callbacks.append(callback)
        
        self.peripheral = peripheral
    }
    
    func invokeCallbacks(error: Error?) {
        let result: SwiftyBluetooth.Result<Void> = {
            if let error = error {
                return .failure(error)
            } else {
                return .success()
            }
        }()
        for callback in callbacks {
            callback(result)
        }
    }
}

extension CentralProxy {
    func connect(peripheral: CBPeripheral, timeout: TimeInterval, _ callback: @escaping ConnectPeripheralCallback) {
        initializeBluetooth { [unowned self] (error) in
            if let error = error {
                callback(.failure(error))
                return
            }
            
            let uuid = peripheral.uuidIdentifier
            
            if let cbPeripheral = self.centralManager.retrievePeripherals(withIdentifiers: [uuid]).first , cbPeripheral.state == .connected {
                callback(.success())
                return
            }
            
            if let request = self.connectRequests[uuid] {
                request.callbacks.append(callback)
            } else {
                let request = ConnectPeripheralRequest(peripheral: peripheral, callback: callback)
                self.connectRequests[uuid] = request
                
                self.centralManager.connect(peripheral, options: nil)

                weak var weakRequest: ConnectPeripheralRequest? = request
                self.centralQueue.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(timeout * 1000))) {
                    if let connectRequest = weakRequest {
                        let uuid = connectRequest.peripheral.uuidIdentifier

                        self.connectRequests[uuid] = nil

                        self.centralManager.cancelPeripheralConnection(connectRequest.peripheral)

                        connectRequest.invokeCallbacks(error: SBError.operationTimedOut(operation: .connectPeripheral))
                    }
                }
            }
        }
    }
}

// MARK: Disconnect Peripheral requests
private final class DisconnectPeripheralRequest {
    var callbacks: [ConnectPeripheralCallback] = []
    
    let peripheral: CBPeripheral
    
    init(peripheral: CBPeripheral, callback: @escaping DisconnectPeripheralCallback) {
        self.callbacks.append(callback)
        
        self.peripheral = peripheral
    }
    
    func invokeCallbacks(error: Error?) {
        let result: SwiftyBluetooth.Result<Void> = {
            if let error = error {
                return .failure(error)
            } else {
                return .success()
            }
        }()
        for callback in callbacks {
            callback(result)
        }
    }
}

extension CentralProxy {
    func disconnect(peripheral: CBPeripheral, timeout: TimeInterval, _ callback: @escaping DisconnectPeripheralCallback) {
        initializeBluetooth { [unowned self] (error) in
            
            if let error = error {
                callback(.failure(error))
                return
            }
            
            let uuid = peripheral.uuidIdentifier
            
            // MARK: in macOS before 10.13, the CBPeripheralState.disconnecting don't exist
            #if SB_XCODE9
                if #available(OSX 10.13, *) {
                    if let cbPeripheral = self.centralManager.retrievePeripherals(withIdentifiers: [uuid]).first,
                        (cbPeripheral.state == .disconnected || cbPeripheral.state == .disconnecting) {
                        callback(.success())
                        return
                    }
                } else {
                    if let cbPeripheral = self.centralManager.retrievePeripherals(withIdentifiers: [uuid]).first,
                        (cbPeripheral.state == .disconnected) {
                        callback(.success())
                        return
                    }
                }
            #else
                #if os(OSX)
                    if let cbPeripheral = self.centralManager.retrievePeripherals(withIdentifiers: [uuid]).first,
                        (cbPeripheral.state == .disconnected) {
                        callback(.success())
                        return
                    }
                #else
                    if let cbPeripheral = self.centralManager.retrievePeripherals(withIdentifiers: [uuid]).first,
                        (cbPeripheral.state == .disconnected || cbPeripheral.state == .disconnecting) {
                        callback(.success())
                        return
                    }
                #endif
            #endif

            if let request = self.disconnectRequests[uuid] {
                request.callbacks.append(callback)
            } else {
                let request = DisconnectPeripheralRequest(peripheral: peripheral, callback: callback)
                self.disconnectRequests[uuid] = request
                
                self.centralManager.cancelPeripheralConnection(peripheral)

                weak var weakRequest: DisconnectPeripheralRequest? = request
                self.centralQueue.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(timeout * 1000))) {
                    if let disconnectRequest = weakRequest {
                        let uuid = disconnectRequest.peripheral.uuidIdentifier

                        self.connectRequests[uuid] = nil

                        disconnectRequest.invokeCallbacks(error: SBError.operationTimedOut(operation: .disconnectPeripheral))
                    }
                }
            }
        }
    }
}

extension CentralProxy: CBCentralManagerDelegate {
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        postCentralEvent(Central.CentralStateChange, userInfo: ["state": central.state])
        
        switch central.state.rawValue {
        case 0: // .unknown
            self.stopScan(error: .scanningEndedUnexpectedly)
        case 1: // .resetting
            self.stopScan(error: .scanningEndedUnexpectedly)
        case 2: // .unsupported
            self.callAsyncCentralStateCallback(.unsupported)
            self.stopScan(error: .scanningEndedUnexpectedly)
        case 3: // .unauthorized
            self.callAsyncCentralStateCallback(.unauthorized)
            self.stopScan(error: .scanningEndedUnexpectedly)
        case 4: // .poweredOff
            self.callAsyncCentralStateCallback(.poweredOff)
            self.stopScan(error: .scanningEndedUnexpectedly)
        case 5: // .poweredOn
            self.callAsyncCentralStateCallback(.poweredOn)
        default:
            fatalError("Unsupported BLE CentralState")
        }
    }
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        let uuid = peripheral.uuidIdentifier
        guard let request = connectRequests[uuid] else {
            return
        }
        
        connectRequests[uuid] = nil
        
        request.invokeCallbacks(error: nil)
    }
    
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        let uuid = peripheral.uuidIdentifier
        guard let request = disconnectRequests[uuid] else {
            return
        }
        
        disconnectRequests[uuid] = nil
        
        request.invokeCallbacks(error: error)
    }
    
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        let uuid = peripheral.uuidIdentifier
        guard let request = connectRequests[uuid] else {
            return
        }
        
        let resolvedError: Error = error ?? SBError.peripheralFailedToConnectReasonUnknown
        
        connectRequests[uuid] = nil
        
        request.invokeCallbacks(error: resolvedError)
    }
    
    func centralManager(_ central: CBCentralManager,
                        didDiscover peripheral: CBPeripheral,
                        advertisementData: [String: Any],
                        rssi RSSI: NSNumber)
    {
        guard let scanRequest = scanRequest else {
            return
        }

        // MARK: fixed the bug: after creating Peripheral for existed CBPeripheral last Peripheral replaces CBPeripheral.delegate
        //       and old Peripherals don't receive a response
        //
        let cbPeripheral = peripheral

        let key = NSString(string: cbPeripheral.uuidIdentifier.uuidString)
        var peripheral = self.peripherals.object(forKey: key)
        if peripheral == nil {
            peripheral = Peripheral(peripheral: cbPeripheral)
            self.peripherals.setObject(peripheral, forKey: key)
        }
        //
        
        var rssiOptional: Int? = Int(RSSI)
        if let rssi = rssiOptional, rssi == 127 {
            rssiOptional = nil
        }
        
        scanRequest.callback(.scanResult(peripheral: peripheral!, advertisementData: advertisementData, RSSI: rssiOptional))
    }
    
    func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) {
        // MARK: in macOS before 10.13, the CBCentralManagerOptionRestoreIdentifierKey don't exist
        #if SB_XCODE9
            if #available(OSX 10.13, *) {
                let peripherals = ((dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral]) ?? []).map { Peripheral(peripheral: $0) }
                postCentralEvent(Central.CentralManagerWillRestoreState, userInfo: ["peripherals": peripherals])
            }
        #else
            #if os(OSX)
            #else
                let peripherals = ((dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral]) ?? []).map { Peripheral(peripheral: $0) }
                postCentralEvent(Central.CentralManagerWillRestoreState, userInfo: ["peripherals": peripherals])
            #endif
        #endif
    }

}

{'fraction_non_alphanumeric': 0.07731726545782183, 'fraction_numerical': 0.0035476968127041335, 'mean_word_length': 1.964938642624593, 'pattern_counts': {'":': 6, '<': 11, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6707116', 'n_tokens_mistral': 9318, 'n_tokens_neox': 8547, 'n_words': 2095}