//
//  bluetoothSerial.swift
//  USB Meter
//
//  Created by Bogdan Timofte on 17/03/2020.
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
//
//  https://github.com/hoiberg/HM10-BluetoothSerial-iOS
import CoreBluetooth

final class BluetoothSerial : NSObject, ObservableObject {
    
    enum AdministrativeState {
        case down
        case up
    }
    enum OperationalState: Int, Comparable {
        case peripheralNotConnected
        case peripheralConnectionPending
        case peripheralConnected
        case peripheralReady

        static func < (lhs: OperationalState, rhs: OperationalState) -> Bool {
            return lhs.rawValue < rhs.rawValue
        }
    }
    
    private var administrativeState = AdministrativeState.down
    private var operationalState = OperationalState.peripheralNotConnected {
        didSet {
            delegate?.opertionalStateChanged(to: operationalState)
        }
    }
    
    var macAddress: MACAddress
    private var manager: CBCentralManager
    private var radio: BluetoothRadio
    @Published var RSSI: Int
    
    private var expectedResponseLength = 0
    private var wdTimer: Timer?
        
    var peripheral: CBPeripheral
    
    /// The characteristic 0xFFE1 we need to write to, of the connectedPeripheral
    private var writeCharacteristic: CBCharacteristic?
    
    private var buffer = Data()
    
    weak var delegate: SerialPortDelegate?
    
    init( peripheral: CBPeripheral, radio: BluetoothRadio, with macAddress: MACAddress, managedBy manager: CBCentralManager, RSSI: Int ) {

        self.peripheral = peripheral
        self.macAddress = macAddress
        self.radio = radio
        self.manager = manager
        self.RSSI = RSSI
        Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: {_ in
            if peripheral.state == .connected {
                peripheral.readRSSI()
            }
        })
        super.init()
        peripheral.delegate = self
    }
    
    func connect() {
        administrativeState = .up
        if operationalState < .peripheralConnected {
            operationalState = .peripheralConnectionPending
            track("Connect caled")
            manager.connect(peripheral, options: nil)
        } else {
            track("Peripheral allready connected: \(operationalState)")
        }
    }
    
    func disconnect() {
        if operationalState >= .peripheralConnected {
            manager.cancelPeripheralConnection(peripheral)
            buffer.removeAll()
        }
    }
        
    /**
     Send data
     
     - parameter data: Data to be sent.
     - parameter expectedResponseLength: Optional If message sent require a respnse the length for that response must be provideed. Incomming data will be buffered before calling delegate.didReceiveData
     */
    func write(_ data: Data, expectedResponseLength: Int = 0) {
        //track("\(self.expectedResponseLength)")
        //track(data.hexEncodedStringValue)
        guard operationalState == .peripheralReady else {
            track("Guard: \(operationalState)")
            return
        }
        guard self.expectedResponseLength == 0 else {
            track("Guard: \(self.expectedResponseLength)")
            return
        }
        
        self.expectedResponseLength = expectedResponseLength
        
//        track("Sending...")
        switch radio {
        case .BT18 :
            peripheral.writeValue(data, for: writeCharacteristic!, type: .withoutResponse)
        case .PW0316 :
            peripheral.writeValue(data, for: writeCharacteristic!, type: .withResponse)
        default:
            track("Radio \(radio) Not Implemented!")
        }
//        track("Sent!")
        if self.expectedResponseLength != 0 {
            setWDT()
        }
    }
        
    func connectionEstablished () {
        track("")
        operationalState = .peripheralConnected
        peripheral.discoverServices(BluetoothRadioServicesUUIDS[radio])
    }
    
    func connectionClosed () {
        track("")
        operationalState = .peripheralNotConnected
        expectedResponseLength = 0
        writeCharacteristic = nil
    }

    func setWDT() {
        wdTimer?.invalidate()
        wdTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false, block: {_ in
            track("Response timeout. Expected: \(self.expectedResponseLength) - buffer: \(self.buffer.count)")
            self.expectedResponseLength = 0
            self.disconnect()
        })
    }

}

//  MARK:   CBPeripheralDelegate
extension BluetoothSerial : CBPeripheralDelegate {

    //  MARK:   didReadRSSI
    func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
        if error != nil {
            track( "Error: \(error!)" )
        }
        self.RSSI = RSSI.intValue
    }
    
    //  MARK:   didDiscoverServices
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        track("\(String(describing: peripheral.services))")
        if error != nil {
            track( "Error: \(error!)" )
        }
        switch radio {
        case .BT18:
            // discover the 0xFFE1 characteristic for all services (though there should only be one(service))
            for service in peripheral.services! {
                switch service.uuid {
                case CBUUID(string: "FFE0"):
                    // check whether the characteristic we're looking for (0xFFE1) is present - just to be sure
                    peripheral.discoverCharacteristics([CBUUID(string: "FFE1")], for: service)
                default:
                    track ("Unexpected service discovered: '\(service)'")
                }
            }
        case .PW0316:
            for service in peripheral.services! {
                //track("\(service.uuid)")
                switch service.uuid {
                case CBUUID(string: "FFE0"):
                    //track("\(service.uuid)")
                    // check whether the characteristic we're looking for (0xFFE4) is present - just to be sure
                    peripheral.discoverCharacteristics([CBUUID(string: "FFE4")], for: service)
                    break
                case CBUUID(string: "FFE5"):
                    //track("\(service.uuid)")
                    // check whether the characteristic we're looking for (0xFFE9) is present - just to be sure
                    peripheral.discoverCharacteristics([CBUUID(string: "FFE9")], for: service)
                default:
                    track ("Unexpected service discovered: '\(service)'")
                }
            }
            break;
        default:
            track("Radio \(radio) Not Implemented!")
        }
    }
    
    //  MARK:   didDiscoverCharacteristicsFor
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        if error != nil {
            track( "Error: \(error!)" )
        }
        track("\(String(describing: service.characteristics))")
        switch radio {
        case .BT18:
            // check whether the characteristic we're looking for (0xFFE1) is present - just to be sure
            for characteristic in service.characteristics! {
                //track(characteristic.debugDescription)
                switch characteristic.uuid {
                case CBUUID(string: "FFE1"):
                    // subscribe to this value (so we'll get notified when there is serial data for us..)
                    peripheral.setNotifyValue(true, for: characteristic)
                    // keep a reference to this characteristic so we can write to it
                    writeCharacteristic = characteristic
                    // Change State
                    operationalState = .peripheralReady
                default:
                    track ("Unexpected characteristic discovered: '\(characteristic)'")
                }
            }
        case .PW0316:
            for characteristic in service.characteristics! {
                switch characteristic.uuid {
                case CBUUID(string: "FFE9"): //TX
                    //track("characteristic FFE9: \(characteristic.properties & CBCharacteristicProperties.write)")
                    writeCharacteristic = characteristic
                    operationalState = .peripheralReady
                case CBUUID(string: "FFE4"): //RX
                    peripheral.setNotifyValue(true, for: characteristic)
                    //track("characteristic FFE4: \(characteristic.properties)")
                default:
                    track ("Unexpected characteristic discovered: '\(characteristic)'")
                }
                
            }
            break;
        default:
            track("Radio \(radio) Not Implemented!")
        }
    }
    
    //  MARK:   didUpdateValueFor
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
//        track("")
        if error != nil {
            track( "Error: \(error!)" )
        }
        buffer.append( characteristic.value ?? Data() )
//        track("\n\(buffer.hexEncodedStringValue)")
        switch buffer.count {
        case let x where x < expectedResponseLength:
            setWDT()
            //track("buffering")
            break;
        case let x where x == expectedResponseLength:
            //track("buffer ready")
            wdTimer?.invalidate()
            expectedResponseLength = 0
            delegate?.didReceiveData(buffer)
            buffer.removeAll()
        case let x where x > expectedResponseLength:
            // MARK: De unde stim că asta a fost tot? Probabil o deconectare ar rezolva problema
            wdTimer?.invalidate()
            expectedResponseLength = 0
            buffer.removeAll()
            track("Buffer Overflow")
        default:
            track("This is not possible!")
        }
    }

    func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
        if error != nil { track( "Error: \(error!)" ) }
    }
    
    func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) {
        //track("")
    }
    
}

// MARK: SerialPortDelegate
protocol SerialPortDelegate: AnyObject {
    // MARK: State Changed
    func opertionalStateChanged( to newOperationalState: BluetoothSerial.OperationalState )
    // MARK: Data was received
    func didReceiveData(_ data: Data)
}

// MARK: SerialPortDelegate Optionals
extension SerialPortDelegate {
}


