import Foundation

import CoreBluetooth
import os

struct TransferService {
    static let serviceUUID = CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
    static let rxCharacteristicUUID = CBUUID(string: "6E400002-B5A3-F393-E0A9-E50E24DCCA9E")
    static let txCharacteristicUUID = CBUUID(string: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
}

enum BluetoothLECentralError: Error {
    case noPeripheral
}

class DataCommunicationChannel: NSObject {
    var centralManager: CBCentralManager!

    var discoveredPeripheral: CBPeripheral?
    var discoveredPeripheralName: String?
    var rxCharacteristic: CBCharacteristic?
    var txCharacteristic: CBCharacteristic?
    var writeIterationsComplete = 0
    var connectionIterationsComplete = 0
    
    let defaultIterations = 5
    
    var accessoryDataHandler: ((Data, String) -> Void)?
    var accessoryConnectedHandler: ((String) -> Void)?
    var accessoryDisconnectedHandler: (() -> Void)?
    var accessoryNIServiceDiscoveredHandler: (() -> Void)?
    
    var bluetoothReady = false
    var shouldStartWhenReady = false

    let logger = os.Logger(subsystem: "com.example.apple-samplecode.NINearbyAccessorySample", category: "DataChannel")

    override init() {
        print("BC - Step:1 - Init")
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: true])
    }
    
    deinit {
        print("BC - Step:2 - deInit")
        centralManager.stopScan()
        //logger.info("Ricerca Stoppata")
    }
    
    func sendData(_ data: Data) throws {
        print("BC - Step:3 - sendData")
        if discoveredPeripheral == nil {
            throw BluetoothLECentralError.noPeripheral
        }
        writeData(data)
    }
    
    func start() {
        print("BC - Step:4 - start")
        if bluetoothReady {
            retrievePeripheral()
        } else {
            shouldStartWhenReady = true
        }
    }


    private func retrievePeripheral() {
        print("BC - Step:5 - retrievePeripheral")
        let connectedPeripherals: [CBPeripheral] = (centralManager.retrieveConnectedPeripherals(withServices: [TransferService.serviceUUID]))

        //logger.info("Trovata connessione con la periferica: \(connectedPeripherals)")

        if let connectedPeripheral = connectedPeripherals.last {
            logger.info("Connessione con la periferica \(connectedPeripheral)")
            self.discoveredPeripheral = connectedPeripheral
            centralManager.connect(connectedPeripheral, options: nil)
        } else {
            logger.info("Non Connesso. inizio ricerca...")
            // Because the app isn't connected to the peer, start scanning for peripherals.
            centralManager.scanForPeripherals(withServices: [TransferService.serviceUUID],
                                              options: [CBCentralManagerScanOptionAllowDuplicatesKey: true])
        }
    }

 
    private func cleanup() {
        print("BC - Step:6 - cleanUp")
        // Don't do anything if we're not connected
        guard let discoveredPeripheral = discoveredPeripheral,
              case .connected = discoveredPeripheral.state else { return }

        for service in (discoveredPeripheral.services ?? [] as [CBService]) {
            for characteristic in (service.characteristics ?? [] as [CBCharacteristic]) {
                if characteristic.uuid == TransferService.rxCharacteristicUUID && characteristic.isNotifying {
                    // It is notifying, so unsubscribe
                    self.discoveredPeripheral?.setNotifyValue(false, for: characteristic)
                }
            }
        }

        // When a connection exists without a subscriber, only disconnect.
        centralManager.cancelPeripheralConnection(discoveredPeripheral)
    }
    
    // Sends data to the peripheral.
    private func writeData(_ data: Data) {
        print("BC - Step:7 - writeData")
        guard let discoveredPeripheral = discoveredPeripheral,
              let transferCharacteristic = rxCharacteristic
        else { return }

        let mtu = discoveredPeripheral.maximumWriteValueLength(for: .withResponse)

        let bytesToCopy: size_t = min(mtu, data.count)

        var rawPacket = [UInt8](repeating: 0, count: bytesToCopy)
        data.copyBytes(to: &rawPacket, count: bytesToCopy)
        let packetData = Data(bytes: &rawPacket, count: bytesToCopy)

        let stringFromData = packetData.map { String(format: "0x%02x, ", $0) }.joined()
        logger.info("Scrittura \(bytesToCopy) bytes: \(String(describing: stringFromData))")

        discoveredPeripheral.writeValue(packetData, for: transferCharacteristic, type: .withResponse)

        writeIterationsComplete += 1
    }
}

extension DataCommunicationChannel: CBCentralManagerDelegate {

    internal func centralManagerDidUpdateState(_ central: CBCentralManager) {
        print("BC - Step:8 - centralManagerDidUpdateState")
        switch central.state {
            
        // Begin communicating with the peripheral.
        case .poweredOn:
            //logger.info("CBManager è accesso")
            bluetoothReady = true
            if shouldStartWhenReady {
                start()
            }
        // In your app, deal with the following states as necessary.
        case .poweredOff:
            logger.error("CBManager is not powered on")
            return
        case .resetting:
            logger.error("CBManager is resetting")
            return
        case .unauthorized:
            handleCBUnauthorized()
            return
        case .unknown:
            logger.error("CBManager state is unknown")
            return
        case .unsupported:
            logger.error("Bluetooth is not supported on this device")
            return
        @unknown default:
            logger.error("A previously unknown central manager state occurred")
            return
        }
    }

    // Reacts to the varying causes of Bluetooth restriction.
    internal func handleCBUnauthorized() {
        switch CBManager.authorization {
        case .denied:
            // In your app, consider sending the user to Settings to change authorization.
            logger.error("The user denied Bluetooth access.")
        case .restricted:
            logger.error("Bluetooth is restricted")
        default:
            logger.error("Unexpected authorization")
        }
    }

    // Reacts to transfer service UUID discovery.
    // Consider checking the RSSI value before attempting to connect.
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
                        advertisementData: [String: Any], rssi RSSI: NSNumber) {
        print("BC - Step:9 - didDiscover")
        //logger.info("Trovata periferica: \( String(describing: peripheral.name)) at\(RSSI.intValue)")
        
        // Check if the app recognizes the in-range peripheral device.
        if discoveredPeripheral != peripheral {
            
            // Save a local copy of the peripheral so Core Bluetooth doesn't
            // deallocate it.
            discoveredPeripheral = peripheral
            
            // Connect to the peripheral.
            logger.info("Connessione con la periferica \(peripheral)")
            
            let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String
            discoveredPeripheralName = name ?? "Unknown"
            centralManager.connect(peripheral, options: nil)
        }
    }

    // Reacts to connection failure.
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        print("BC - Step:10 - didFailToConnect")
        logger.error("Failed to connect to \(peripheral). \( String(describing: error))")
        cleanup()
    }

    // Discovers the services and characteristics to find the 'TransferService'
    // characteristic after peripheral connection.
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("BC - Step:11 - didConnect")
        if let didConnectHandler = accessoryConnectedHandler {
            didConnectHandler(discoveredPeripheralName!)
        }
        
        logger.info("Periferica connessa")
        
        // Stop scanning.
        centralManager.stopScan()
        //logger.info("Ricerca stoppata")
        
        
        // Set the iteration info.
        connectionIterationsComplete += 1
        writeIterationsComplete = 0
        
        // Set the `CBPeripheral` delegate to receive callbacks for its services discovery.
        peripheral.delegate = self
        
        // Search only for services that match the service UUID.
        peripheral.discoverServices([TransferService.serviceUUID])
    }

    // Cleans up the local copy of the peripheral after disconnection.
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        print("BC - Step:12 - didDisconnectPeripheral")
        logger.info("Periferica disconnessa")
        discoveredPeripheral = nil
        discoveredPeripheralName = nil
        
        if let didDisconnectHandler = accessoryDisconnectedHandler {
            didDisconnectHandler()
        }
        
        // Resume scanning after disconnection.
        if connectionIterationsComplete < defaultIterations {
            retrievePeripheral()
        } else {
            logger.info("Iterazione della connessione completata")
        }
    }

}

// An extention to implement `CBPeripheralDelegate` methods.
extension DataCommunicationChannel: CBPeripheralDelegate {
    
    // Reacts to peripheral services invalidation.
    func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
        print("BC - Step:13 - didModifyServices")

        for service in invalidatedServices where service.uuid == TransferService.serviceUUID {
            logger.error("Transfer service is invalidated - rediscover services")
            peripheral.discoverServices([TransferService.serviceUUID])
        }
    }

    // Reacts to peripheral services discovery.
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        print("BC - Step:14 - didDiscoverServices")
        if let error = error {
            logger.error("Error discovering services: \(error.localizedDescription)")
            cleanup()
            return
        }
        //logger.info("Servizio di Ricerica. Ricerca delle caratteristiche..")
        // Check the newly filled peripheral services array for more services.
        guard let peripheralServices = peripheral.services else { return }
        for service in peripheralServices {
            peripheral.discoverCharacteristics([TransferService.rxCharacteristicUUID, TransferService.txCharacteristicUUID], for: service)
        }
    }

    // Subscribes to a discovered characteristic, which lets the peripheral know we want the data it contains.
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        print("BC - Step:15 - didDiscoverCharactericsFor")
        
        // Deal with errors (if any).
        if let error = error {
            logger.error("Error discovering characteristics: \(error.localizedDescription)")
            cleanup()
            return
        }

        // Check the newly filled peripheral services array for more services.
        guard let serviceCharacteristics = service.characteristics else { return }
        for characteristic in serviceCharacteristics where characteristic.uuid == TransferService.rxCharacteristicUUID {
            // Subscribe to the transfer service's `rxCharacteristic`.
            rxCharacteristic = characteristic
            //logger.info("Trovate caratteristiche: \(characteristic)")
            peripheral.setNotifyValue(true, for: characteristic)
        }

        for characteristic in serviceCharacteristics where characteristic.uuid == TransferService.txCharacteristicUUID {
            // Subscribe to the transfer service's `txCharacteristic`.
            txCharacteristic = characteristic
            //logger.info("Trovate caratteristiche: \(characteristic)")
            peripheral.setNotifyValue(true, for: characteristic)
        }

        // Wait for the peripheral to send data.
    }

    // Reacts to data arrival through the characteristic notification.
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        print("BC - Step:16 - didUpdateValueFor")
        // Check if the peripheral reported an error.
        if let error = error {
            logger.error("Error discovering characteristics:\(error.localizedDescription)")
            cleanup()
            return
        }
        guard let characteristicData = characteristic.value else { return }
    
        //let str = characteristicData.map { String(format: "0x%02x, ", $0) }.joined()
        //logger.info("Ricevuto \(characteristicData.count) bytes: \(str)")
        
        if let dataHandler = self.accessoryDataHandler, let accessoryName = discoveredPeripheralName {
            dataHandler(characteristicData, accessoryName)
        }
    }

    // Reacts to the subscription status.
    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
        // Check if the peripheral reported an error.
        if let error = error {
            logger.error("Error changing notification state: \(error.localizedDescription)")
            return
        }

        if characteristic.isNotifying {
            // Indicates the notification began.
            //logger.info("Inizio Connessione con \(characteristic)")
        } else {
            // Because the notification stopped, disconnect from the peripheral.
            //logger.info("Stoppata connesione con \(characteristic). Disconesso")
            cleanup()
        }
        
        if let didNIServiceDiscoveredHandler = accessoryNIServiceDiscoveredHandler {
            didNIServiceDiscoveredHandler()
        }
        
    }
}
