#import <CoreBluetooth/CoreBluetooth.h>
#import <Foundation/Foundation.h>

extern "C" {

typedef void (*aidlab_ble_data_cb)(const uint8_t* data, int size);
typedef void (*aidlab_ble_event_cb)(int evt, const char* message);

void aidlab_ble_ios_set_data_callback(aidlab_ble_data_cb cb);
void aidlab_ble_ios_set_event_callback(aidlab_ble_event_cb cb);
bool aidlab_ble_ios_start(const char* device_name);
void aidlab_ble_ios_stop(void);
bool aidlab_ble_ios_write(const uint8_t* data, int size);
int aidlab_ble_ios_get_max_write_length(void);

}  // extern "C"

namespace {
static CBUUID* kAidlabServiceUuid() {
    return [CBUUID UUIDWithString:@"44366e80-cf3a-11e1-9ab4-0002a5d5c51b"];
}
static CBUUID* kAidlabCmdCharUuid() {
    return [CBUUID UUIDWithString:@"51366e80-cf3a-11e1-9ab4-0002a5d5c51b"];
}
static CBUUID* kDeviceInfoServiceUuid() {
    return [CBUUID UUIDWithString:@"180A"];
}
static CBUUID* kCurrentTimeServiceUuid() {
    return [CBUUID UUIDWithString:@"1805"];
}
static CBUUID* kCurrentTimeCharUuid() {
    return [CBUUID UUIDWithString:@"2A2B"];
}
static CBUUID* kFirmwareCharUuid() {
    return [CBUUID UUIDWithString:@"2A26"];
}
static CBUUID* kHardwareCharUuid() {
    return [CBUUID UUIDWithString:@"2A27"];
}
}  // namespace

@interface AidlabBleIosManager : NSObject <CBCentralManagerDelegate, CBPeripheralDelegate>
@property(nonatomic, strong) CBCentralManager* central;
@property(nonatomic, strong) CBPeripheral* peripheral;
@property(nonatomic, strong) CBCharacteristic* cmdCharacteristic;
@property(nonatomic, strong) CBCharacteristic* firmwareCharacteristic;
@property(nonatomic, strong) CBCharacteristic* hardwareCharacteristic;
@property(nonatomic, strong) CBCharacteristic* currentTimeCharacteristic;
@property(nonatomic, strong) dispatch_queue_t queue;
@property(nonatomic, strong) NSString* targetName;
@property(nonatomic) aidlab_ble_data_cb dataCb;
@property(nonatomic) aidlab_ble_event_cb eventCb;
@property(nonatomic) int64_t notifyPackets;
@end

@implementation AidlabBleIosManager

- (CBCharacteristicWriteType)cmdWriteType {
    if(!self.cmdCharacteristic) {
        return CBCharacteristicWriteWithResponse;
    }
    if(self.cmdCharacteristic.properties & CBCharacteristicPropertyWriteWithoutResponse) {
        return CBCharacteristicWriteWithoutResponse;
    }
    return CBCharacteristicWriteWithResponse;
}

- (void)emitEvent:(int)evt message:(NSString*)message {
    if(!self.eventCb) {
        return;
    }
    const char* cstr = message ? [message UTF8String] : "";
    self.eventCb(evt, cstr);
}

- (void)writeCurrentTime {
    if(!self.peripheral || !self.currentTimeCharacteristic) {
        return;
    }
    uint32_t ts = (uint32_t)[[NSDate date] timeIntervalSince1970];
    uint8_t bytes[4];
    bytes[0] = (uint8_t)(ts & 0xFF);
    bytes[1] = (uint8_t)((ts >> 8) & 0xFF);
    bytes[2] = (uint8_t)((ts >> 16) & 0xFF);
    bytes[3] = (uint8_t)((ts >> 24) & 0xFF);
    NSData* payload = [NSData dataWithBytes:bytes length:4];
    [self.peripheral writeValue:payload forCharacteristic:self.currentTimeCharacteristic type:CBCharacteristicWriteWithResponse];
    [self emitEvent:6 message:@"Set time sent"];
}

- (instancetype)init {
    self = [super init];
    if(self) {
        self.queue = dispatch_queue_create("com.aidlab.unity.ble.ios", DISPATCH_QUEUE_SERIAL);
        self.central = [[CBCentralManager alloc] initWithDelegate:self queue:self.queue];
        self.dataCb = nullptr;
        self.eventCb = nullptr;
        self.notifyPackets = 0;
    }
    return self;
}

- (BOOL)startWithName:(NSString*)name {
    self.targetName = name ?: @"Aidlab";
    if(self.central.state == CBManagerStatePoweredOn) {
        [self.central scanForPeripheralsWithServices:@[kAidlabServiceUuid()] options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @NO}];
    }
    return YES;
}

- (void)stop {
    if(self.central) {
        [self.central stopScan];
    }
    if(self.peripheral) {
        [self.central cancelPeripheralConnection:self.peripheral];
    }
    self.peripheral = nil;
    self.cmdCharacteristic = nil;
    self.firmwareCharacteristic = nil;
    self.hardwareCharacteristic = nil;
    self.currentTimeCharacteristic = nil;
    self.dataCb = nullptr;
    self.eventCb = nullptr;
}

- (BOOL)writeBytes:(const uint8_t*)data size:(int)size {
    if(!self.peripheral || !self.cmdCharacteristic || !data || size <= 0) {
        return NO;
    }
    NSData* payload = [NSData dataWithBytes:data length:(NSUInteger)size];
    [self.peripheral writeValue:payload forCharacteristic:self.cmdCharacteristic type:[self cmdWriteType]];
    return YES;
}

- (int)maxWriteLen {
    if(!self.peripheral) {
        return 20;
    }
    return (int)[self.peripheral maximumWriteValueLengthForType:[self cmdWriteType]];
}

#pragma mark - CBCentralManagerDelegate

- (void)centralManagerDidUpdateState:(CBCentralManager*)central {
    (void)central;
    if(self.central.state == CBManagerStatePoweredOn) {
        [self.central scanForPeripheralsWithServices:@[kAidlabServiceUuid()] options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @NO}];
    } else {
        [self emitEvent:5 message:@"Bluetooth not powered on"];
    }
}

- (void)centralManager:(CBCentralManager*)central
    didDiscoverPeripheral:(CBPeripheral*)peripheral
        advertisementData:(NSDictionary<NSString*, id>*)advertisementData
                     RSSI:(NSNumber*)RSSI {
    (void)central;
    (void)RSSI;

    NSString* name = peripheral.name;
    NSString* advName = advertisementData[CBAdvertisementDataLocalNameKey];
    if(advName.length > 0) {
        name = advName;
    }

    if(name.length == 0) {
        return;
    }

    if(self.targetName.length > 0) {
        NSRange match = [name rangeOfString:self.targetName options:NSCaseInsensitiveSearch];
        if(match.location == NSNotFound) {
            return;
        }
    }

    [self.central stopScan];
    self.peripheral = peripheral;
    self.peripheral.delegate = self;
    [self.central connectPeripheral:peripheral options:nil];
}

- (void)centralManager:(CBCentralManager*)central didConnectPeripheral:(CBPeripheral*)peripheral {
    (void)central;
    if(peripheral != self.peripheral) {
        return;
    }
    [peripheral discoverServices:@[kAidlabServiceUuid(), kDeviceInfoServiceUuid(), kCurrentTimeServiceUuid()]];
}

- (void)centralManager:(CBCentralManager*)central didFailToConnectPeripheral:(CBPeripheral*)peripheral error:(NSError*)error {
    (void)central;
    (void)peripheral;
    [self emitEvent:5 message:[NSString stringWithFormat:@"Failed to connect: %@", error]];
}

- (void)centralManager:(CBCentralManager*)central didDisconnectPeripheral:(CBPeripheral*)peripheral error:(NSError*)error {
    (void)central;
    (void)error;
    if(peripheral == self.peripheral) {
        self.cmdCharacteristic = nil;
        self.firmwareCharacteristic = nil;
        self.hardwareCharacteristic = nil;
        self.currentTimeCharacteristic = nil;
        [self emitEvent:2 message:@"Disconnected"];
    }
}

#pragma mark - CBPeripheralDelegate

- (void)peripheral:(CBPeripheral*)peripheral didDiscoverServices:(NSError*)error {
    if(error) {
        [self emitEvent:5 message:[NSString stringWithFormat:@"Discover services error: %@", error]];
        return;
    }
    for(CBService* service in peripheral.services) {
        if([service.UUID isEqual:kAidlabServiceUuid()]) {
            [peripheral discoverCharacteristics:@[kAidlabCmdCharUuid()] forService:service];
        } else if([service.UUID isEqual:kDeviceInfoServiceUuid()]) {
            [peripheral discoverCharacteristics:@[kFirmwareCharUuid(), kHardwareCharUuid()] forService:service];
        } else if([service.UUID isEqual:kCurrentTimeServiceUuid()]) {
            [peripheral discoverCharacteristics:@[kCurrentTimeCharUuid()] forService:service];
        }
    }
}

- (void)peripheral:(CBPeripheral*)peripheral didDiscoverCharacteristicsForService:(CBService*)service error:(NSError*)error {
    if(error) {
        [self emitEvent:5 message:[NSString stringWithFormat:@"Discover characteristics error: %@", error]];
        return;
    }
    for(CBCharacteristic* ch in service.characteristics) {
        if([ch.UUID isEqual:kAidlabCmdCharUuid()]) {
            self.cmdCharacteristic = ch;
            [peripheral setNotifyValue:YES forCharacteristic:ch];
        } else if([ch.UUID isEqual:kFirmwareCharUuid()]) {
            self.firmwareCharacteristic = ch;
            [peripheral readValueForCharacteristic:ch];
        } else if([ch.UUID isEqual:kHardwareCharUuid()]) {
            self.hardwareCharacteristic = ch;
            [peripheral readValueForCharacteristic:ch];
        } else if([ch.UUID isEqual:kCurrentTimeCharUuid()]) {
            self.currentTimeCharacteristic = ch;
            [self writeCurrentTime];
        }
    }
}

- (void)peripheral:(CBPeripheral*)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic*)characteristic error:(NSError*)error {
    (void)peripheral;
    if(error) {
        [self emitEvent:5 message:[NSString stringWithFormat:@"Notify error: %@", error]];
        return;
    }
    if([characteristic.UUID isEqual:kAidlabCmdCharUuid()] && characteristic.isNotifying) {
        [self emitEvent:1 message:@"Connected"];
    }
}

- (void)peripheral:(CBPeripheral*)peripheral didUpdateValueForCharacteristic:(CBCharacteristic*)characteristic error:(NSError*)error {
    (void)peripheral;
    if(error) {
        [self emitEvent:5 message:[NSString stringWithFormat:@"Characteristic read/notify error: %@", error]];
        return;
    }
    NSData* value = characteristic.value;
    if(!value) {
        return;
    }

    if([characteristic.UUID isEqual:kAidlabCmdCharUuid()]) {
        self.notifyPackets += 1;
        if(self.dataCb) {
            self.dataCb((const uint8_t*)value.bytes, (int)value.length);
        }
        return;
    }

    NSString* str = [[NSString alloc] initWithData:value encoding:NSUTF8StringEncoding];
    if(!str) {
        str = @"";
    }
    if([characteristic.UUID isEqual:kFirmwareCharUuid()]) {
        [self emitEvent:3 message:str];
    } else if([characteristic.UUID isEqual:kHardwareCharUuid()]) {
        [self emitEvent:4 message:str];
    }
}

- (void)peripheral:(CBPeripheral*)peripheral didWriteValueForCharacteristic:(CBCharacteristic*)characteristic error:(NSError*)error {
    (void)peripheral;
    if(error) {
        [self emitEvent:5 message:[NSString stringWithFormat:@"Write error: %@", error]];
        if([characteristic.UUID isEqual:kCurrentTimeCharUuid()]) {
            [self emitEvent:7 message:[NSString stringWithFormat:@"Time set failed: %@", error]];
        }
        return;
    }
    if([characteristic.UUID isEqual:kCurrentTimeCharUuid()]) {
        [self emitEvent:7 message:@"Time set confirmed"];
    }
}

@end

namespace {
static AidlabBleIosManager* gManager = nullptr;
}  // namespace

extern "C" {

void aidlab_ble_ios_set_data_callback(aidlab_ble_data_cb cb) {
    if(!gManager) {
        gManager = [[AidlabBleIosManager alloc] init];
    }
    gManager.dataCb = cb;
}

void aidlab_ble_ios_set_event_callback(aidlab_ble_event_cb cb) {
    if(!gManager) {
        gManager = [[AidlabBleIosManager alloc] init];
    }
    gManager.eventCb = cb;
}

bool aidlab_ble_ios_start(const char* device_name) {
    if(!gManager) {
        gManager = [[AidlabBleIosManager alloc] init];
    }
    NSString* name = device_name ? [NSString stringWithUTF8String:device_name] : @"Aidlab";
    __block BOOL ok = YES;
    dispatch_sync(gManager.queue, ^{
        ok = [gManager startWithName:name];
    });
    return ok ? true : false;
}

void aidlab_ble_ios_stop(void) {
    if(!gManager) {
        return;
    }
    gManager.dataCb = nullptr;
    gManager.eventCb = nullptr;

    dispatch_async(gManager.queue, ^{
        [gManager stop];
    });
}

bool aidlab_ble_ios_write(const uint8_t* data, int size) {
    if(!gManager) {
        return false;
    }
    __block BOOL ok = NO;
    dispatch_sync(gManager.queue, ^{
        ok = [gManager writeBytes:data size:size];
    });
    return ok ? true : false;
}

int aidlab_ble_ios_get_max_write_length(void) {
    if(!gManager) {
        return 20;
    }
    __block int value = 20;
    dispatch_sync(gManager.queue, ^{
        value = [gManager maxWriteLen];
    });
    return value;
}

}  // extern "C"

