//
//  UnityBluetoothLE.h
//  Unity-iPhone
//
//  Created by Tony Pitman on 03/05/2014.
//
//

#import "UnityBluetoothLE.h"

const char _messageDelimeter = '~';

extern "C" {
    
    UnityBluetoothLE *_unityBluetoothLE = nil;
    
    void _iOSBluetoothLELogString (NSString *message) {
        
        NSLog (message);
    }
    
    void _iOSBluetoothLELog (char *message) {
        
        _iOSBluetoothLELogString ([NSString stringWithFormat:@"%s", message]);
    }
    
    void _iOSBluetoothLEInitialize (BOOL asCentral, BOOL asPeripheral) {
        
        _unityBluetoothLE = [UnityBluetoothLE new];
        [_unityBluetoothLE initialize:asCentral asPeripheral:asPeripheral];
    }
    
    void _iOSBluetoothLEDeInitialize () {
        
        if (_unityBluetoothLE != nil) {
            
            [_unityBluetoothLE deInitialize];
            [_unityBluetoothLE release];
            _unityBluetoothLE = nil;
            
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", "DeInitialized");
        }
    }
    
    void _iOSBluetoothLEPauseMessages (BOOL pause) {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE pauseMessages:pause];
    }
    
    void _iOSBluetoothLEScanForPeripheralsWithServices (char *serviceUUIDsStringRaw, bool allowDuplicates, bool rssiOnly, bool clearPeripheralList, int recordType) {
        
        if (_unityBluetoothLE != nil)
        {
            _unityBluetoothLE._rssiOnly = rssiOnly;
            
            NSMutableArray *actualUUIDs = nil;
            
            if (serviceUUIDsStringRaw != nil)
            {
                NSString *serviceUUIDsString = [NSString stringWithFormat:@"%s", serviceUUIDsStringRaw];
                NSArray *serviceUUIDs = [serviceUUIDsString componentsSeparatedByString:@"|"];
                
                if (serviceUUIDs.count > 0)
                {
                    actualUUIDs = [[NSMutableArray alloc] init];
                    
                    for (NSString* sUUID in serviceUUIDs)
                        [actualUUIDs addObject:[CBUUID UUIDWithString:sUUID]];
                }
            }
            
            NSDictionary *options = nil;
            if (allowDuplicates)
                options = @{ CBCentralManagerScanOptionAllowDuplicatesKey: @YES };
            
            [_unityBluetoothLE scanForPeripheralsWithServices:actualUUIDs options:options clearPeripheralList:clearPeripheralList recordType:recordType];
        }
    }

    void _iOSBluetoothLEStopScan () {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE stopScan];
    }
    
    void _iOSBluetoothLERetrieveListOfPeripheralsWithServices (char *serviceUUIDsStringRaw) {
        
        if (_unityBluetoothLE != nil)
        {
            NSMutableArray *actualUUIDs = nil;
            
            if (serviceUUIDsStringRaw != nil)
            {
                NSString *serviceUUIDsString = [NSString stringWithFormat:@"%s", serviceUUIDsStringRaw];
                NSArray *serviceUUIDs = [serviceUUIDsString componentsSeparatedByString:@"|"];
                
                if (serviceUUIDs.count > 0)
                {
                    actualUUIDs = [[NSMutableArray alloc] init];
                    
                    for (NSString* sUUID in serviceUUIDs)
                        [actualUUIDs addObject:[CBUUID UUIDWithString:sUUID]];
                }
            }
            
            [_unityBluetoothLE retrieveListOfPeripheralsWithServices:actualUUIDs];
        }
    }
    
    void _iOSBluetoothLEConnectToPeripheral (char *name) {
        
        if (_unityBluetoothLE && name != nil)
            [_unityBluetoothLE connectToPeripheral:[NSString stringWithFormat:@"%s", name]];
    }
    
    void _iOSBluetoothLEDisconnectPeripheral (char *name) {
        
        if (_unityBluetoothLE && name != nil)
            [_unityBluetoothLE disconnectPeripheral:[NSString stringWithFormat:@"%s", name]];
    }
    
    void _iOSBluetoothLEReadCharacteristic (char *name, char *service, char *characteristic) {
        
        if (_unityBluetoothLE && name != nil && service != nil && characteristic != nil)
            [_unityBluetoothLE readCharacteristic:[NSString stringWithFormat:@"%s", name] service:[NSString stringWithFormat:@"%s", service] characteristic:[NSString stringWithFormat:@"%s", characteristic]];
    }
    
    void _iOSBluetoothLEWriteCharacteristic (char *name, char *service, char *characteristic, unsigned char *data, int length, BOOL withResponse) {
        
        if (_unityBluetoothLE && name != nil && service != nil && characteristic != nil && data != nil && length > 0)
            [_unityBluetoothLE writeCharacteristic:[NSString stringWithFormat:@"%s", name] service:[NSString stringWithFormat:@"%s", service] characteristic:[NSString stringWithFormat:@"%s", characteristic] data:[NSData dataWithBytes:data length:length] withResponse:withResponse];
    }
    
    void _iOSBluetoothLESubscribeCharacteristic (char *name, char *service, char *characteristic) {
        
        if (_unityBluetoothLE && name != nil && service != nil && characteristic != nil)
            [_unityBluetoothLE subscribeCharacteristic:[NSString stringWithFormat:@"%s", name] service:[NSString stringWithFormat:@"%s", service] characteristic:[NSString stringWithFormat:@"%s", characteristic]];
    }
    
    void _iOSBluetoothLEUnSubscribeCharacteristic (char *name, char *service, char *characteristic) {
        
        if (_unityBluetoothLE && name != nil && service != nil && characteristic != nil)
            [_unityBluetoothLE unsubscribeCharacteristic:[NSString stringWithFormat:@"%s", name] service:[NSString stringWithFormat:@"%s", service] characteristic:[NSString stringWithFormat:@"%s", characteristic]];
    }
    
    void _iOSBluetoothLEDisconnectAll () {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE disconnectAll];
    }
    
    void _iOSBluetoothLERequestMtu (char *name, int mtu) {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE requestMtu:[NSString stringWithFormat:@"%s", name] mtu:mtu];
    }
    
    void _iOSBluetoothLEReadRSSI (char *name) {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE readRSSI:[NSString stringWithFormat:@"%s", name]];
    }

#if !TARGET_OS_TV
    void _iOSBluetoothLEScanForBeacons (char *proximityUUIDsStringRaw) {
        
        if (_unityBluetoothLE != nil)
        {
            NSMutableArray *actualUUIDs = nil;
            
            if (proximityUUIDsStringRaw != nil)
            {
                NSString *proximityUUIDsString = [NSString stringWithFormat:@"%s", proximityUUIDsStringRaw];
                NSArray *proximityUUIDs = [proximityUUIDsString componentsSeparatedByString:@"|"];
                
                if (proximityUUIDs.count > 0)
                {
                    NSMutableArray<CLBeaconRegion *> *beaconRegions = [[NSMutableArray<CLBeaconRegion *> alloc] init];
                    
                    for (NSString* sUUID in proximityUUIDs)
                    {
                        NSArray *parts = [sUUID componentsSeparatedByString:@":"];
                        if (parts.count == 2)
                        {
                            if (@available(iOS 13.0, *)) {
                                CLBeaconRegion *beaconRegion = [[CLBeaconRegion alloc] initWithUUID:[[NSUUID alloc] initWithUUIDString:parts[0]] identifier:parts[1]];
                                [beaconRegions addObject:beaconRegion];
                            } else {
                                CLBeaconRegion *beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:[[NSUUID alloc] initWithUUIDString:parts[0]] identifier:parts[1]];
                                [beaconRegions addObject:beaconRegion];
                            }
                            
                            [_unityBluetoothLE scanForBeacons:beaconRegions];
                        }
                        else
                        {
                            NSString *message = [NSString stringWithFormat:@"Error~iBeacon Scanning missing identifiers"];
                            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
                        }
                    }
                }
                else
                {
                    NSString *message = [NSString stringWithFormat:@"Error~iBeacon Scanning requires proximity UUIDs"];
                    UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
                }
            }
            else
            {
                NSString *message = [NSString stringWithFormat:@"Error~iBeacon Scanning requires proximity UUIDs"];
                UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
            }
        }
    }
    
    void _iOSBluetoothLEStopBeaconScan () {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE stopBeaconScan];
    }

    void _iOSBluetoothLEPeripheralName (char *newName) {
        
        if (_unityBluetoothLE != nil && newName != nil)
            [_unityBluetoothLE peripheralName:[[NSString alloc] initWithUTF8String:newName]];
    }
    
    void _iOSBluetoothLECreateService (char *uuid, BOOL primary) {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE createService:[NSString stringWithFormat:@"%s", uuid] primary:primary];
    }
    
    void _iOSBluetoothLERemoveService (char *uuid) {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE removeService:[NSString stringWithFormat:@"%s", uuid]];
    }
    
    void _iOSBluetoothLERemoveServices () {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE removeServices];
    }
    
    void _iOSBluetoothLECreateCharacteristic (char *uuid, int properties, int permissions, unsigned char *data, int length) {
        
        if (_unityBluetoothLE != nil) {
            
            NSData *value = nil;
            if (data != nil)
                value = [[NSData alloc] initWithBytes:data length:length];
            
            [_unityBluetoothLE createCharacteristic:[NSString stringWithFormat:@"%s", uuid] properties:properties permissions:permissions value:value];
        }
    }
    
    void _iOSBluetoothLERemoveCharacteristic (char *uuid) {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE removeCharacteristic:[NSString stringWithFormat:@"%s", uuid]];
    }
    
    void _iOSBluetoothLERemoveCharacteristics () {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE removeCharacteristics];
    }
    
    void _iOSBluetoothLEStartAdvertising () {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE startAdvertising];
    }
    
    void _iOSBluetoothLEStopAdvertising () {
        
        if (_unityBluetoothLE != nil)
            [_unityBluetoothLE stopAdvertising];
    }
    
    void _iOSBluetoothLEUpdateCharacteristicValue (char *uuid, unsigned char *data, int length) {
        
        if (_unityBluetoothLE != nil) {
            
            NSData *value = nil;
            if (data != nil)
                value = [[NSData alloc] initWithBytes:data length:length];
            
            [_unityBluetoothLE updateCharacteristicValue:[NSString stringWithFormat:@"%s", uuid] value:value];
        }
    }
#endif
}

@implementation UnityBluetoothLE

@synthesize _peripherals;
@synthesize _rssiOnly;

- (void)initialize:(BOOL)asCentral asPeripheral:(BOOL)asPeripheral
{
    _mtu = 20;
    
    _isPaused = FALSE;
    _isInitializing = TRUE;
    
    _centralManager = nil;
#if !TARGET_OS_TV
    _peripheralManager = nil;
    _services = nil;
    _characteristics = nil;
    _allCharacteristics = nil;
#endif
    if (asCentral)
        _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
    
#if !TARGET_OS_TV
    if (asPeripheral)
        _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];
    
    _services = [[NSMutableDictionary alloc] init];
    _characteristics = [[NSMutableDictionary alloc] init];
    _allCharacteristics = [[NSMutableDictionary alloc] init];
#endif
    
    _peripherals = [[NSMutableDictionary alloc] init];
}

- (void)deInitialize
{
    if (_backgroundMessages != nil)
    {
        for (UnityMessage *message in _backgroundMessages)
        {
            if (message != nil)
            {
                [message deInitialize];
                [message release];
            }
        }
        
        [_backgroundMessages release];
        _backgroundMessages = nil;
    }
    
#if !TARGET_OS_TV
    if (_peripheralManager != nil)
        [self stopAdvertising];
    
    [self removeCharacteristics];
    [self removeServices];
#endif
    
    if (_centralManager != nil)
        [self stopScan];
    
    [_peripherals removeAllObjects];
}

- (void)pauseMessages:(BOOL)isPaused
{
    if (isPaused != _isPaused) {
        
        if (_backgroundMessages == nil)
            _backgroundMessages = [[NSMutableArray alloc] init];
        
        _isPaused = isPaused;
        
        // if we are not paused now since we know we changed state
        // that means we were paused so we need to pump the saved
        // messages to Unity
        if (isPaused) {
            
            if (_backgroundMessages != nil) {
                
                for (UnityMessage *message in _backgroundMessages) {
                    
                    if (message != nil) {
                        
                        [message sendUnityMessage];
                        [message deInitialize];
                        [message release];
                    }
                }
                
                [_backgroundMessages removeAllObjects];
            }
        }
    }
}

#if !TARGET_OS_TV
- (void)createService:(NSString *)uuid primary:(BOOL)primary
{
    CBUUID *cbuuid = [CBUUID UUIDWithString:uuid];
    CBMutableService *service = [[CBMutableService alloc] initWithType:cbuuid primary:primary];
    
    NSMutableArray *characteristics = [[NSMutableArray alloc] init];
    
    NSEnumerator *enumerator = [_characteristics keyEnumerator];
    id key;
    while ((key = [enumerator nextObject]))
    {
        CBCharacteristic *characteristic = [_characteristics objectForKey:key];
        [_characteristics removeObjectForKey:key];
        [characteristics addObject:characteristic];
        [_allCharacteristics setObject:characteristic forKey:[characteristic UUID]];
    }
    [_characteristics removeAllObjects];
    
    service.characteristics = characteristics;
    
    [_services setObject:service forKey:cbuuid];
    
    if (_peripheralManager != nil)
    {
        [_peripheralManager addService:service];
    }
}

- (void)removeService:(NSString *)uuid
{
    if (_services != nil)
    {
        if (_peripheralManager != nil)
        {
            CBMutableService *service = [_services objectForKey:uuid];
            if (service != nil)
                [_peripheralManager removeService:service];
        }
        
        [_services removeObjectForKey:uuid];
    }
}

- (void)removeServices
{
    if (_services != nil)
    {
        [_services removeAllObjects];
        
        if (_peripheralManager != nil)
            [_peripheralManager removeAllServices];
    }

    if (_allCharacteristics != nil)
        [_allCharacteristics removeAllObjects];
}

- (void)peripheralName:(NSString *)newName
{
    _peripheralName = newName;
}

- (void)createCharacteristic:(NSString *)uuid properties:(CBCharacteristicProperties)properties permissions:(CBAttributePermissions)permissions value:(NSData *)value
{
    CBUUID *cbuuid = [CBUUID UUIDWithString:uuid];
    CBCharacteristic *characteristic = [[CBMutableCharacteristic alloc] initWithType:cbuuid properties:properties value:value permissions:permissions];
    
    [_characteristics setObject:characteristic forKey:cbuuid];
}

- (void)removeCharacteristic:(NSString *)uuid
{
    if (_characteristics != nil)
        [_characteristics removeObjectForKey:uuid];
}

- (void)removeCharacteristics
{
    if (_characteristics != nil)
        [_characteristics removeAllObjects];
}

- (void)startAdvertising
{
    if (_peripheralManager != nil && _services != nil)
    {
        NSMutableArray *services = [[NSMutableArray alloc] init];
        
        NSEnumerator *enumerator = [_services keyEnumerator];
        id key;
        while ((key = [enumerator nextObject]))
        {
            CBMutableService *service = [_services objectForKey:key];
            [services addObject:service.UUID];
        }
        
        if (_peripheralName == nil)
            _peripheralName = @"";
        
        [_peripheralManager startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey : services, CBAdvertisementDataLocalNameKey : _peripheralName }];
    }
}

- (void)stopAdvertising
{
    if (_peripheralManager != nil)
    {
        [_peripheralManager stopAdvertising];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", "StoppedAdvertising");
    }
}

- (void)updateCharacteristicValue:(NSString *)uuid value:(NSData *)value
{
    if (_allCharacteristics != nil)
    {
        CBUUID *cbuuid = [CBUUID UUIDWithString:uuid];
        CBMutableCharacteristic *characteristic = [_allCharacteristics objectForKey:cbuuid];
        if (characteristic != nil)
        {
            characteristic.value = value;
            if (_peripheralManager != nil)
                [_peripheralManager updateValue:value forCharacteristic:characteristic onSubscribedCentrals:nil];
        }
    }
}

- (void)requestMtu:(NSString *)name mtu:(int)mtu
{
    _mtu = mtu;

    NSString *message = [NSString stringWithFormat:@"MtuChanged~%@~%d", name, _mtu];
    UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]);
}

- (void)scanForBeacons:(NSArray<CLBeaconRegion *>*)beaconRegions
{
    if ([CLLocationManager isRangingAvailable])
    {
        _locationManager = [CLLocationManager new];
        _locationManager.delegate = self;
        
        [_locationManager requestWhenInUseAuthorization];
        
        for (CLBeaconRegion *region in beaconRegions)
            [_locationManager startRangingBeaconsInRegion:region];
    }
    else
    {
        NSString *message = [NSString stringWithFormat:@"Error~iBeacon Ranging is not available"];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
}

- (void) stopBeaconScan
{
    if (_locationManager != nil)
    {
        for (CLBeaconRegion *region in _locationManager.rangedRegions)
            [_locationManager stopRangingBeaconsInRegion:region];
        
        [_locationManager release];
        _locationManager = nil;
    }
}

// beacon delegate implementation
- (void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray<CLBeacon *> *)beacons inRegion:(CLBeaconRegion *)region
{
    NSString *message;
    for (CLBeacon *beacon in beacons)
    {
        message = [NSString stringWithFormat:@"DiscoveredBeacon~%@~%ld~%ld~%ld~%ld~%ld", [beacon.proximityUUID.UUIDString stringByReplacingOccurrencesOfString:@"-" withString:@""], beacon.major.longValue, beacon.minor.longValue, (long)beacon.rssi, (long)0, (long)beacon.proximity];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]);
    }
}
#endif

// central delegate implementation
- (void)scanForPeripheralsWithServices:(NSArray *)serviceUUIDs options:(NSDictionary *)options clearPeripheralList:(BOOL)clearPeripheralList recordType:(int)recordType
{
    if (_centralManager != nil)
    {
        recordType = recordType;
        if (clearPeripheralList && _peripherals != nil)
            [_peripherals removeAllObjects];
        
        [_centralManager scanForPeripheralsWithServices:serviceUUIDs options:options];
    }
}

- (void) stopScan
{
    if (_centralManager != nil)
        [_centralManager stopScan];
}

- (void)retrieveListOfPeripheralsWithServices:(NSArray *)serviceUUIDs
{
    if (_centralManager != nil)
    {
        if (_peripherals != nil)
            [_peripherals removeAllObjects];
        
        NSArray * list = [_centralManager retrieveConnectedPeripheralsWithServices:serviceUUIDs];
        if (list != nil)
        {
            for (int i = 0; i < list.count; ++i)
            {
                CBPeripheral *peripheral = [list objectAtIndex:i];
                if (peripheral != nil)
                {
                    NSString *identifier = [[peripheral identifier] UUIDString];
                    NSString *name = [peripheral name];
                    
                    NSString *message = [NSString stringWithFormat:@"RetrievedConnectedPeripheral~%@~%@", identifier, name];
                    UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]);
                    
                    [_peripherals setObject:peripheral forKey:identifier];
                }
            }
        }
    }
}

- (void)connectToPeripheral:(NSString *)name
{
    if (_peripherals != nil && name != nil)
    {
        CBPeripheral *peripheral = [_peripherals objectForKey:name];
        if (peripheral != nil)
            [_centralManager connectPeripheral:peripheral options:nil];
    }
}

- (void)disconnectAll
{
    if (_peripherals != nil && [_peripherals count] > 0)
    {
        NSArray* keys = [_peripherals allKeys];
        for(NSString* key in keys)
        {
            CBPeripheral *peripheral = [_peripherals objectForKey:key];
            if (peripheral != nil)
                [_centralManager cancelPeripheralConnection:peripheral];
        }
    }
}

- (void)disconnectPeripheral:(NSString *)name
{
    if (_peripherals != nil && name != nil)
    {
        CBPeripheral *peripheral = [_peripherals objectForKey:name];
        if (peripheral != nil)
        {
            [_centralManager cancelPeripheralConnection:peripheral];
        }
    }
}

- (void)readRSSI:(NSString *)name
{
    if (_peripherals != nil && name != nil)
    {
        CBPeripheral *peripheral = [_peripherals objectForKey:name];
        if (peripheral != nil)
        {
            [peripheral readRSSI];
        }
    }
}

- (CBCharacteristic *)getCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString
{
    CBCharacteristic *returnCharacteristic = nil;
    
    if (name != nil && serviceString != nil && characteristicString != nil && _peripherals != nil)
    {
        CBPeripheral *peripheral = [_peripherals objectForKey:name];
        if (peripheral != nil)
        {
            CBUUID *serviceUUID = [CBUUID UUIDWithString:serviceString];
            CBUUID *characteristicUUID = [CBUUID UUIDWithString:characteristicString];
            
            for (CBService *service in peripheral.services)
            {
                if ([service.UUID isEqual:serviceUUID])
                {
                    for (CBCharacteristic *characteristic in service.characteristics)
                    {
                        if ([characteristic.UUID isEqual:characteristicUUID])
                        {
                            returnCharacteristic = characteristic;
                        }
                    }
                }
            }
        }
    }
    
    return returnCharacteristic;
}

- (void)readCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString
{
    if (name != nil && serviceString != nil && characteristicString != nil && _peripherals != nil)
    {
        CBPeripheral *peripheral = [_peripherals objectForKey:name];
        if (peripheral != nil)
        {
            CBCharacteristic *characteristic = [_unityBluetoothLE getCharacteristic:name service:serviceString characteristic:characteristicString];
            if (characteristic != nil)
                [peripheral readValueForCharacteristic:characteristic];
        }
    }
}

- (void)writeCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString data:(NSData *)data withResponse:(BOOL)withResponse
{
    if (name != nil && serviceString != nil && characteristicString != nil && _peripherals != nil && data != nil)
    {
        CBPeripheral *peripheral = [_peripherals objectForKey:name];
        if (peripheral != nil)
        {
            CBCharacteristic *characteristic = [_unityBluetoothLE getCharacteristic:name service:serviceString characteristic:characteristicString];
            if (characteristic != nil)
            {
                CBCharacteristicWriteType type = CBCharacteristicWriteWithoutResponse;
                if (withResponse)
                    type = CBCharacteristicWriteWithResponse;
                
                [self writeCharactersticBytes:peripheral characteristic:characteristic data:data withResponse:type];
            }
        }
    }
}

- (void)subscribeCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString
{
    if (name != nil && serviceString != nil && characteristicString != nil && _peripherals != nil)
    {
        CBPeripheral *peripheral = [_peripherals objectForKey:name];
        if (peripheral != nil)
        {
            CBCharacteristic *characteristic = [_unityBluetoothLE getCharacteristic:name service:serviceString characteristic:characteristicString];
            if (characteristic != nil)
                [peripheral setNotifyValue:YES forCharacteristic:characteristic];
        }
    }
}

- (void)unsubscribeCharacteristic:(NSString *)name service:(NSString *)serviceString characteristic:(NSString *)characteristicString
{
    if (name != nil && serviceString != nil && characteristicString != nil && _peripherals != nil)
    {
        CBPeripheral *peripheral = [_peripherals objectForKey:name];
        if (peripheral != nil)
        {
            CBCharacteristic *characteristic = [_unityBluetoothLE getCharacteristic:name service:serviceString characteristic:characteristicString];
            if (characteristic != nil)
                [peripheral setNotifyValue:NO forCharacteristic:characteristic];
        }
    }
}

- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    switch (central.state)
    {
        case CBCentralManagerStateUnsupported:
        {
            NSLog(@"Central State: Unsupported");
            
            NSString *message = [NSString stringWithFormat:@"Error~Bluetooth LE Not Supported"];
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
        } break;
            
        case CBCentralManagerStateUnauthorized:
        {
            NSLog(@"Central State: Unauthorized");
            
            NSString *message = [NSString stringWithFormat:@"Error~Bluetooth LE Not Authorized"];
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
        } break;
            
        case CBCentralManagerStatePoweredOff:
        {
            NSLog(@"Central State: Powered Off");
            
            NSString *message = [NSString stringWithFormat:@"Error~Bluetooth LE Powered Off"];
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
        } break;
            
        case CBCentralManagerStatePoweredOn:
        {
            NSLog(@"Central State: Powered On");
            if (_isInitializing)
                UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", "Initialized");
            _isInitializing = FALSE;
        } break;
            
        case CBCentralManagerStateUnknown:
        {
            NSLog(@"Central State: Unknown");
            
            NSString *message = [NSString stringWithFormat:@"Error~Bluetooth LE Unknown"];
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
        } break;
            
        default:
        {
        }
            
    }
}

- (void)centralManager:(CBCentralManager *)central didRetrievePeripherals:(NSArray *)peripherals
{
    
}

- (void)centralManager:(CBCentralManager *)central didRetrieveConnectedPeripherals:(NSArray *)peripherals
{
    
}

- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    if (error)
    {
        NSString *message = [NSString stringWithFormat:@"Error~%@", error.description];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
}

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
    if (_peripherals != nil && peripheral != nil)
    {
        NSString *name = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey];
        if (name == nil)
            name = peripheral.name;
        if (name == nil)
            name = @"No Name";
        
        if (name != nil)
        {
            NSString *identifier = nil;
            
            NSString *foundPeripheral = [self findPeripheralName:peripheral];
            if (foundPeripheral == nil)
                identifier = [[peripheral identifier] UUIDString];
            else
                identifier = foundPeripheral;
            
            NSString *message = nil;
            
            if (advertisementData != nil && [advertisementData objectForKey:CBAdvertisementDataManufacturerDataKey] != nil)
            {
                NSData* bytes = [advertisementData objectForKey:CBAdvertisementDataManufacturerDataKey];
                message = [NSString stringWithFormat:@"DiscoveredPeripheral~%@~%@~%@~%@", identifier, name, RSSI, [UnityBluetoothLE base64StringFromData:bytes length:bytes.length]];
            }
            else if (RSSI != 0 && _rssiOnly)
            {
                message = [NSString stringWithFormat:@"DiscoveredPeripheral~%@~%@~%@~", identifier, name, RSSI];
            }
            else
            {
                message = [NSString stringWithFormat:@"DiscoveredPeripheral~%@~%@", identifier, name];
            }
            
            if (message != nil)
                UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]);
            
            [_peripherals setObject:peripheral forKey:identifier];
        }
    }
}

- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    if (_peripherals != nil)
    {
        NSString *foundPeripheral = [self findPeripheralName:peripheral];
        if (foundPeripheral != nil)
        {
            NSString *message = [NSString stringWithFormat:@"DisconnectedPeripheral~%@", foundPeripheral];
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]);
        }
    }
}

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    NSString *foundPeripheral = [self findPeripheralName:peripheral];
    if (foundPeripheral != nil)
    {
        NSString *message = [NSString stringWithFormat:@"ConnectedPeripheral~%@", foundPeripheral];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]);
        peripheral.delegate = self;
        [peripheral discoverServices:nil];
    }
}

- (CBPeripheral *) findPeripheralInList:(CBPeripheral*)peripheral
{
    CBPeripheral *foundPeripheral = nil;
    
    NSEnumerator *enumerator = [_peripherals keyEnumerator];
    id key;
    while ((key = [enumerator nextObject]))
    {
        CBPeripheral *tempPeripheral = [_peripherals objectForKey:key];
        if ([tempPeripheral isEqual:peripheral])
        {
            foundPeripheral = tempPeripheral;
            break;
        }
    }
    
    return foundPeripheral;
}

- (NSString *) findPeripheralName:(CBPeripheral*)peripheral
{
    NSString *foundPeripheral = nil;
    
    NSEnumerator *enumerator = [_peripherals keyEnumerator];
    id key;
    while ((key = [enumerator nextObject]))
    {
        CBPeripheral *tempPeripheral = [_peripherals objectForKey:key];
        if ([tempPeripheral isEqual:peripheral])
        {
            foundPeripheral = key;
            break;
        }
    }
    
    return foundPeripheral;
}

// central peripheral delegate implementation
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
    if (error)
    {
        NSString *message = [NSString stringWithFormat:@"Error~%@", error.description];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
    else
    {
        NSString *foundPeripheral = [self findPeripheralName:peripheral];
        if (foundPeripheral != nil)
        {
            for (CBService *service in peripheral.services)
            {
                NSString *message = [NSString stringWithFormat:@"DiscoveredService~%@~%@", foundPeripheral, [service UUID]];
                UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]);
                
                [peripheral discoverCharacteristics:nil forService:service];
            }
        }
    }
}

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
    if (error)
    {
        NSString *message = [NSString stringWithFormat:@"Error~%@", error.description];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
    else
    {
        NSString *foundPeripheral = [self findPeripheralName:peripheral];
        if (foundPeripheral != nil)
        {
            for (CBCharacteristic *characteristic in service.characteristics)
            {
                NSString *message = [NSString stringWithFormat:@"DiscoveredCharacteristic~%@~%@~%@", foundPeripheral, [service UUID], [characteristic UUID]];
                UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]);
            }
        }
    }
}

- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    if (error)
    {
        NSString *message = [NSString stringWithFormat:@"Error~%@", error.description];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
    else
    {
        NSString *foundPeripheral = [self findPeripheralName:peripheral];
        if (foundPeripheral != nil)
        {
            if (characteristic.value != nil)
            {
                NSString *message = [NSString stringWithFormat:@"DidUpdateValueForCharacteristic~%@~%@~%@", foundPeripheral, [characteristic UUID], [UnityBluetoothLE base64StringFromData:characteristic.value length:characteristic.value.length]];
                UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]);
                //NSString *message = [UnityBluetoothLE base64StringFromData:characteristic.value length:characteristic.value.length];
                //UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothData", [message UTF8String] );
            }
        }
    }
}

- (void)writeCharactersticBytesReset
{
    _writeCharacteristicBytes = nil;
    _writeCharacteristicLength = 0;
    _writeCharacteristicPosition = 0;
    _writeCharacteristicBytesToWrite = 0;
    _writeCharacteristicWithResponse = CBCharacteristicWriteWithResponse;
    _writeCharacteristicRetries = 3;
}

- (void)writeCharactersticBytes:(CBPeripheral *)peripheral characteristic:(CBCharacteristic *)characteristic data:(NSData *)data withResponse:(CBCharacteristicWriteType)withResponse
{
    if (_writeCharacteristicBytes == nil && (_mtu == 0 || data.length > _mtu))
    {
        _writeCharacteristicLength = data.length;
        _writeCharacteristicBytes = (unsigned char*)malloc(_writeCharacteristicLength);
        memcpy(_writeCharacteristicBytes, [data bytes], _writeCharacteristicLength);
        _writeCharacteristicPosition = 0;
        _writeCharacteristicWithResponse = withResponse;
        if (_mtu == 0)
            _writeCharacteristicBytesToWrite = _writeCharacteristicLength;
        else
            _writeCharacteristicBytesToWrite = _mtu;
    }

    NSLog(@"write characteristic block");

    if (_writeCharacteristicBytes != nil)
    {
        NSMutableData *newData = [NSMutableData dataWithCapacity:_writeCharacteristicBytesToWrite];
        [newData appendBytes:&_writeCharacteristicBytes[_writeCharacteristicPosition] length:_writeCharacteristicBytesToWrite];
        data = newData;
        NSLog(@"data: %@", data);
    }
    
    NSLog(@"writing %ld bytes, %ld with response", data.length, withResponse);
    [peripheral writeValue:data forCharacteristic:characteristic type:withResponse];
    
    if (withResponse == CBCharacteristicWriteWithoutResponse)
    {
        double delayInSeconds = 0.01;
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
            [self writeNextPacket:peripheral characteristic:characteristic];
        });
    }
}

- (void)writeNextPacket:(CBPeripheral *)peripheral characteristic:(CBCharacteristic *)characteristic
{
    if (_writeCharacteristicLength > _writeCharacteristicPosition + _writeCharacteristicBytesToWrite)
    {
        _writeCharacteristicPosition += _writeCharacteristicBytesToWrite;
        if (_writeCharacteristicPosition + _writeCharacteristicBytesToWrite > _writeCharacteristicLength)
            _writeCharacteristicBytesToWrite = _writeCharacteristicLength - _writeCharacteristicPosition;

        NSMutableData *data = [NSMutableData dataWithCapacity:_writeCharacteristicLength];
        [data appendBytes:_writeCharacteristicBytes length:_writeCharacteristicLength];
        [self writeCharactersticBytes:peripheral characteristic:characteristic data:data withResponse:_writeCharacteristicWithResponse];
    }
    else
    {
        [self writeCharactersticBytesReset];
        NSString *message = [NSString stringWithFormat:@"DidWriteCharacteristic~%@", characteristic.UUID];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
}

- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    if (error)
    {
        NSString *message = [NSString stringWithFormat:@"Error~%@", error.description];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
    else
    {
        NSLog(@"%ld bytes written, %ld position, %ld length, %ld with response", _writeCharacteristicBytesToWrite, _writeCharacteristicPosition, _writeCharacteristicLength, _writeCharacteristicWithResponse);
        
        if (_writeCharacteristicBytesToWrite > 0)
        {
            [self writeNextPacket:peripheral characteristic:characteristic];
        }
        else
        {
            NSString *message = [NSString stringWithFormat:@"DidWriteCharacteristic~%@", characteristic.UUID];
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
        }
    }
}

- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    if (error)
    {
        NSString *message = [NSString stringWithFormat:@"Error~%@", error.description];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
    else
    {
        NSString *foundPeripheral = [self findPeripheralName:peripheral];
        if (foundPeripheral != nil)
        {
            NSString *message = [NSString stringWithFormat:@"DidUpdateNotificationStateForCharacteristic~%@~%@", foundPeripheral, characteristic.UUID];
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
        }
    }
}

- (void)peripheral:(CBPeripheral *) peripheral didReadRSSI:(NSNumber *)RSSI error:(NSError *)error
{
    if (error)
    {
        NSString *message = [NSString stringWithFormat:@"Error~%@", error.description];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
    else
    {
        NSString *foundPeripheral = [self findPeripheralName:peripheral];
        if (foundPeripheral != nil)
        {
            NSString *message = [NSString stringWithFormat:@"DidReadRSSI~%@~%@", foundPeripheral, RSSI];
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
        }
    }
}

#if !TARGET_OS_TV
// peripheral manager delegate implementation
- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
{
    _iOSBluetoothLELogString ([NSString stringWithFormat:@"Peripheral State Update: %d", (int)peripheral.state]);
    if (_isInitializing && peripheral.state == CBPeripheralManagerStatePoweredOn)
    {
        _isInitializing = FALSE;
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", "Initialized");
    }
}

- (void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error
{
    if (error)
    {
        NSString *message = [NSString stringWithFormat:@"Error~%@", error.description];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
    else
    {
        NSString *message = [NSString stringWithFormat:@"ServiceAdded~%@", service.UUID];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
}

- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error
{
    if (error)
    {
        NSString *message = [NSString stringWithFormat:@"Error~%@", error.description];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
    else
    {
        NSString *message = [NSString stringWithFormat:@"StartedAdvertising"];
        UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
    }
}

- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic
{
    
}

- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic
{
    
}

- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveReadRequest:(CBATTRequest *)request
{
    BOOL success = FALSE;
    
    if (_peripheralManager != nil)
    {
        CBMutableCharacteristic *characteristic = [_allCharacteristics objectForKey:request.characteristic.UUID];
        
        if (characteristic != nil)
        {
            request.value = [characteristic.value subdataWithRange:NSMakeRange(request.offset, characteristic.value.length - request.offset)];
            [_peripheralManager respondToRequest:request withResult:CBATTErrorSuccess];
            
            success = TRUE;
        }
    }
    
    if (!success)
        [_peripheralManager respondToRequest:request withResult:CBATTErrorAttributeNotFound];
}

- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray *)requests
{
    BOOL success = FALSE;
    
    if (_peripheralManager != nil)
    {
        for (int i = 0; i < requests.count; ++i)
        {
            CBATTRequest *request = [requests objectAtIndex:i];
            if (request != nil)
            {
                CBMutableCharacteristic *characteristic = [_allCharacteristics objectForKey:request.characteristic.UUID];
                
                if (characteristic != nil)
                {
                    characteristic.value = request.value;
                    
                    NSString *message = [NSString stringWithFormat:@"PeripheralReceivedWriteData~%@~%@", [characteristic UUID], [UnityBluetoothLE base64StringFromData:characteristic.value length:characteristic.value.length]];
                    UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String]);
                    
                    success = TRUE;
                }
                else
                {
                    success = FALSE;
                    break;
                }
            }
            else
            {
                success = FALSE;
                break;
            }
        }
    }
    
    if (success)
        [_peripheralManager respondToRequest:[requests objectAtIndex:0] withResult:CBATTErrorSuccess];
    else
        [_peripheralManager respondToRequest:[requests objectAtIndex:0] withResult:CBATTErrorAttributeNotFound];
}
#endif

- (void)sendUnityMessage:(BOOL)isString message:(NSString *)message
{
    if (_isPaused) {
        
        if (_backgroundMessages != nil) {
            
            UnityMessage *unitymessage = [[UnityMessage alloc] init];
            if (unitymessage != nil) {
                
                [unitymessage initialize:isString message:message];
                [_backgroundMessages addObject:unitymessage];
            }
        }
    }
    else {
        
        if (isString)
        {
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [message UTF8String] );
        }
        else
        {
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothData", [message UTF8String] );
        }
    }
}

static char base64EncodingTable[64] =
{
    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
    'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
    'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
    'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
};

+ (NSString *) base64StringFromData: (NSData *)data length: (int)length
{
    unsigned long ixtext, lentext;
    long ctremaining;
    unsigned char input[3], output[4];
    short i, charsonline = 0, ctcopy;
    const unsigned char *raw;
    NSMutableString *result;
    
    lentext = [data length];
    if (lentext < 1)
        return @"";
    result = [NSMutableString stringWithCapacity: lentext];
    raw = (const unsigned char *)[data bytes];
    ixtext = 0;
    
    while (true) {
        ctremaining = lentext - ixtext;
        if (ctremaining <= 0)
            break;
        for (i = 0; i < 3; i++) {
            unsigned long ix = ixtext + i;
            if (ix < lentext)
                input[i] = raw[ix];
            else
                input[i] = 0;
        }
        output[0] = (input[0] & 0xFC) >> 2;
        output[1] = ((input[0] & 0x03) << 4) | ((input[1] & 0xF0) >> 4);
        output[2] = ((input[1] & 0x0F) << 2) | ((input[2] & 0xC0) >> 6);
        output[3] = input[2] & 0x3F;
        ctcopy = 4;
        switch (ctremaining) {
            case 1:
                ctcopy = 2;
                break;
            case 2:
                ctcopy = 3;
                break;
        }
        
        for (i = 0; i < ctcopy; i++)
            [result appendString: [NSString stringWithFormat: @"%c", base64EncodingTable[output[i]]]];
        
        for (i = ctcopy; i < 4; i++)
            [result appendString: @"="];
        
        ixtext += 3;
        charsonline += 4;
        
        if ((length > 0) && (charsonline >= length))
            charsonline = 0;
    }
    return result;
}

#pragma mark Internal

@end

@implementation UnityMessage

- (void)initialize:(BOOL)isString message:(NSString *)message
{
    _isString = isString;
    _message = [message copy];
}

- (void)deInitialize
{
    if (_message != nil)
        [_message release];
    _message = nil;
}

- (void)sendUnityMessage
{
    if (_message != nil) {
        
        if (_isString)
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothMessage", [_message UTF8String] );
        else
            UnitySendMessage ("BluetoothLEReceiver", "OnBluetoothData", [_message UTF8String] );
    }
}

@end
