//
// © 2018-2020 Qualcomm Technologies, Inc. and/or its subsidiaries. All rights reserved.
//

#import "IoTClient.h"
#import "IoTClient+Private.h"
#import "IoTClientDiscoveryState.h"
#import "IoTAppListenerProtocol.h"
#import "IoTRepresentationUtils.h"
#import "IoTPlayer+Private.h"
#import "IoTPlayerGroup+Private.h"
#import "IoTPlayerGroupsInfo.h"
#import "IoTDevice+Private.h"
#import "IoTPlayerGroupManager.h"
#import "IoTAllPlayController.h"
#import "IoTSysController.h"
#import "IoTServiceDiscoveryResult.h"
#import "IoTGlobals.h"
#import "IoTTransport.h"
#import "IoTIotivity.h"
#import "NSError+IoTErrors.h"
#import "IoTFriendlyNameMapper.h"

#define SERVICE_EXPIRY_AGE 65 // Time until garbage collection.
#define SERVICE_FAST_WINDOW 30 // In the last 30s until garbage collection we go into fast mode to rediscover.

@interface IoTClient () <IoTPlayerUpdates, IoTDeviceUpdates, IoTClientDiscoveryStateDelegate>

@property (nonatomic, readonly) id<IoTTransport> transport;
@property (nonatomic, readonly) dispatch_queue_t stateSyncQueue;
@property (nonatomic, readonly) dispatch_queue_t serviceDiscoverySyncQueue;
@property (nonatomic, readonly) NSMutableDictionary<NSString *, IoTAllPlayController *> *allPlayControllers;
@property (nonatomic, readonly) NSMutableDictionary<NSString *, IoTSysController *> *iotSysControllers;
@property (nonatomic, readonly) NSMutableDictionary<NSString *, IoTPlayer *> *players;
@property (nonatomic, readonly) NSMutableDictionary<NSString *, IoTDevice *> *devices;
@property (nonatomic, readonly) NSMutableDictionary<NSString *, IoTServiceDiscoveryResult *> *discoveredServices;
@property (nonatomic, readonly)	NSSet<NSString *> *deviceTypes;
@property (nonatomic, copy) NSString *applicationName;
@property (nonatomic, readonly) IoTPlayerGroupManager *groupManager;

@property (nonatomic, strong) IoTClientDiscoveryState *discoveryState;

@end

@implementation IoTClient

- (instancetype)init
{
    return [self initWithApplicationName:@""];
}

- (instancetype)initWithApplicationName:(NSString *)appName
{
    return [self initWithApplicationName:appName
                               transport:nil];
}

- (instancetype)initWithApplicationName:(NSString *)appName
                              transport:(id<IoTTransport>)transport
{
    self = [super init];
    if (self) {
        self->_stateSyncQueue = dispatch_queue_create("com.qualcomm.qti.IoTBindings.stateSyncQueue", DISPATCH_QUEUE_CONCURRENT);
        self->_serviceDiscoverySyncQueue = dispatch_queue_create("com.qualcomm.qti.IoTBindings.serviceDiscoverySyncQueue", DISPATCH_QUEUE_SERIAL);
        self->_allPlayControllers = [[NSMutableDictionary alloc] init];
        self->_iotSysControllers = [[NSMutableDictionary alloc] init];
        self->_discoveredServices = [[NSMutableDictionary alloc] init];
        self->_players = [[NSMutableDictionary alloc] init];
        self->_devices = [[NSMutableDictionary alloc] init];
        self->_groupManager = [[IoTPlayerGroupManager alloc] init];
        self->_deviceTypes = [NSSet setWithObjects:ALLPLAY_DEVICE_TYPE, IOTSYS_DEVICE_TYPE, nil];
        self->_applicationName = [appName copy];
        self->_transport = transport ?: [[IoTIotivity alloc] init];
    }
    return self;
}

- (void)removeAllServices
{
    dispatch_sync(self.serviceDiscoverySyncQueue, ^{
        for (IoTServiceDiscoveryResult *service in [self.discoveredServices allValues]) {
            [service markAsExpired];
        }
        [self removeMissingDevicesSync];
        dispatch_async(self.serviceDiscoverySyncQueue, ^{
            // We want to do this *after* removeMissingDevices.
        	[self.groupManager forceReset];
        });
    });
}

- (void)start
{
    if (!self.discoveryState) {
        self.discoveryState = [[IoTClientDiscoveryState alloc] initWithDelegate:self];
    }
    [self.discoveryState start];
}

- (void)stop
{
    [self.discoveryState stop];
}

- (BOOL)isStarted
{
    return self.discoveryState.currentInterval != IoTClientDiscoveryIntervalStopped;
}

- (void)clientDiscoverStateDidStart {
    [self.transport start];
}

- (void)clientDiscoverStateDidStop {
    [self removeAllServices];
    [self.transport stop];
}

- (void)clientDiscoverStateRequestScan {
    [self discover];
}

- (void)discover
{
    [self removeMissingDevices];
    makeWeak(self)
	NSLog(@"Known hosts before scan:\n%@", self.discoveredServices);

    [self.transport scanForServicesOfTypes:self.deviceTypes
                         serviceDiscovered:^(IoTServiceDiscoveryResult * _Nonnull discoveredService) {
                              makeStrongAndReturnIfNil(self)

                             NSString *host = discoveredService.host;

                             IoTServiceDiscoveryResult *result = self.discoveredServices[host];

                             if (result) {
                                 // Was it there before?
                                 [result markAsSeen];
                             } else {
                                 [self.discoveryState didDiscoverService];
                                 result = discoveredService;

                                 NSLog(@"**** Found a device host: %@ name: %@, type: %@", result.host, result.name, result.type);
                                 // We haven't seen this service before.
                                 self.discoveredServices[host] = result;

                                 if ([result.type isEqualToString:ALLPLAY_DEVICE_TYPE]) {
                                     IoTAllPlayController *controller = [[IoTAllPlayController alloc] initWithServiceInfo:result
                                                                                                           stateSyncQueue:self.stateSyncQueue
                                                                                                                transport:self.transport];

                                     // Is this a replacement service on the same host?
                                     IoTAllPlayController *existingService = (IoTAllPlayController *)[self existingServiceForDeviceWithHost:host
                                                                                                                         services:[self.allPlayControllers allValues]];
                                     if (existingService) {
                                         NSLog(@"Existing service detected: %@ name: %@, type: %@", existingService.serviceInfo.host, existingService.serviceInfo.name, existingService.serviceInfo.type);
                                         [existingService.serviceInfo markAsExpired];
                                     }
                                     self.allPlayControllers[host] = controller;

                                     makeWeak(self)

                                     IoTPlayer *existingPlayer = [self existingPlayerForDeviceWithHost:host];
                                     if (existingPlayer) {
                                         self.players[host] = existingPlayer;
                                         self.players[existingPlayer.host] = nil;
                                         NSLog(@"****** REPLACING ALLPLAY CONTROLLER!!!!!!");
                                         [existingPlayer replaceAllPlayController:controller];
                                     }

                                     [controller discoverWithCompletion:^(BOOL success) {
                                         makeStrongAndReturnIfNil(self)

                                         if (!success) {
                                             dispatch_sync(self.serviceDiscoverySyncQueue, ^{
                                                 [controller.serviceInfo markAsExpired];
                                                 [self removeMissingDevicesSync];
                                             });
                                         } else {
                                             IoTPlayer *player = existingPlayer ?:
                                             [[IoTPlayer alloc] initWithAllPlayController:controller
                                                                          applicationName:self.applicationName
                                                                           stateSyncQueue:self.stateSyncQueue];
                                             player.updatesDelegate = self;
                                             [player updateWithCompletion: ^(BOOL success) {
                                                 makeStrongAndReturnIfNil(self)
                                                 dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);
                                                 if (!success) {
                                                     dispatch_sync(self.serviceDiscoverySyncQueue , ^{
                                                         [controller.serviceInfo markAsExpired];
                                                         [self removeMissingDevicesSync];
                                                     });
                                                 } else {
                                                     [controller startObserving];

                                                     if (!existingPlayer) {
                                                         dispatch_sync(self.serviceDiscoverySyncQueue, ^{
                                                             self.players[host] = player;
                                                         });

                                                         [self.groupManager processNewPlayer:player
                                                                                      client:self
                                                                                 appListener:self.delegate];
                                                     }
                                                 }
                                             } completionQueue:dispatch_get_main_queue()];
                                         }
                                     } completionQueue:dispatch_get_main_queue()];
                                 } else {
                                     // IoTSys
                                     IoTSysController *controller = [[IoTSysController alloc] initWithServiceInfo:result
                                                                                                   stateSyncQueue:self.stateSyncQueue
                                                                                                        transport:self.transport];

                                     // Is this a replacement service on the same host?
                                     IoTSysController *existingService = (IoTSysController *)[self existingServiceForDeviceWithHost:host
                                                                                                                         services:[self.iotSysControllers allValues]];
                                     if (existingService) {
                                         NSLog(@"Existing service detected: %@", existingService.serviceInfo.host);
                                         [existingService.serviceInfo markAsExpired];
                                     }
                                     self.iotSysControllers[host] = controller;

                                     makeWeak(self)

                                     IoTDevice *existingDevice = [self existingDeviceForDeviceWithHost:host];
                                     if (existingDevice) {
                                         self.devices[host] = existingDevice;
                                         self.devices[existingDevice.host] = nil;
                                         NSLog(@"****** REPLACING IOTSYS CONTROLLER!!!!!!");
                                         [existingDevice replaceIoTSysController:controller];
                                     }

                                     [controller discoverWithCompletion:^(BOOL success) {
                                         makeStrongAndReturnIfNil(self)

                                         if (!success) {
                                             dispatch_sync(self.serviceDiscoverySyncQueue, ^{
                                                 [controller.serviceInfo markAsExpired];
                                                 [self removeMissingDevicesSync];
                                             });
                                         } else {

                                             IoTDevice *device = existingDevice ?:
                                             [[IoTDevice alloc] initWithIoTSysController:controller
                                                                          stateSyncQueue:self.stateSyncQueue];
                                             device.updatesDelegate = self;
                                             [device updateWithCompletion: ^(BOOL success) {
                                                 makeStrongAndReturnIfNil(self)
                                                 dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);
                                                 if (!success) {
                                                     dispatch_sync(self.serviceDiscoverySyncQueue, ^{
                                                         [controller.serviceInfo markAsExpired];
                                                         [self removeMissingDevicesSync];
                                                     });
                                                 } else {
                                                     [controller startObserving];
                                                     if (!existingDevice) {
                                                         dispatch_sync(self.serviceDiscoverySyncQueue, ^{
                                                             self.devices[host] = device;
                                                         });

                                                         if ([self.delegate respondsToSelector:@selector(deviceAdded:)]) {
                                                             [self.delegate deviceAdded:device];
                                                         }
                                                     }
                                                 }
                                             } completionQueue:dispatch_get_main_queue()];
                                         }
                                     } completionQueue:dispatch_get_main_queue()];
                                 }
                             }

                         } queue:self.serviceDiscoverySyncQueue];
}

- (id<IoTIotivityService>)existingServiceForDeviceWithHost:(NSString *)host
                                                  services:(NSArray<id<IoTIotivityService>> *)services
{
    // Is this a replacement service on the same host?
    NSURL *newServiceURL = [NSURL URLWithString:host];
    NSString *newServiceBaseURL = [newServiceURL host];
    for (id<IoTIotivityService> service in services) {
        NSURL *serviceURL = [NSURL URLWithString:service.serviceInfo.host];
        NSString *serviceBaseURL = [serviceURL host];
        if ([serviceBaseURL isEqualToString:newServiceBaseURL]) {
            // We already have a service of that type running on that host.
            return service;
        }
    }
    return nil;
}

- (IoTPlayer *)existingPlayerForDeviceWithHost:(NSString *)host
{
    NSURL *newHostURL = [NSURL URLWithString:host];
    NSString *newHostBaseURL = [newHostURL host];
    for (IoTPlayer *player in [self.players allValues]) {
        NSURL *hostURL = [NSURL URLWithString:player.host];
        NSString *hostBaseURL = [hostURL host];
        if ([hostBaseURL isEqualToString:newHostBaseURL]) {
            // We already have a player running on that host.
            return player;
        }
    }
    return nil;
}

- (IoTDevice *)existingDeviceForDeviceWithHost:(NSString *)host
{
    NSURL *newHostURL = [NSURL URLWithString:host];
    NSString *newHostBaseURL = [newHostURL host];
    for (IoTDevice *device in [self.devices allValues]) {
        NSURL *hostURL = [NSURL URLWithString:device.host];
        NSString *hostBaseURL = [hostURL host];
        if ([hostBaseURL isEqualToString:newHostBaseURL]) {
            // We already have a device running on that host.
            return device;
        }
    }
    return nil;
}

- (void)removeMissingDevices
{
    makeWeak(self)
    dispatch_async(self.serviceDiscoverySyncQueue, ^{
		makeStrongAndReturnIfNil(self)
        [self removeMissingDevicesSync];
    });
}

- (void)trimMLANMembersSync
{
	dispatch_assert_queue(self.serviceDiscoverySyncQueue);

    NSMutableSet<NSString *> *allRemovablePlayerIDsInHomeTheater = [NSMutableSet set];
    for (NSString *host in [self.discoveredServices allKeys]) {
        IoTServiceDiscoveryResult *service = self.discoveredServices[host];
        if ([service.type isEqualToString:ALLPLAY_DEVICE_TYPE]) {
            IoTPlayer *player = self.players[host];
            if (player && player.isSoundBar) {
                NSArray<NSString *> *removableDevices = [player allRemovableHomeTheaterPlayerIDs];
                [allRemovablePlayerIDsInHomeTheater addObjectsFromArray:removableDevices];
            }
        }
    }

    // Mark those in multichannel as gone.
    for (IoTPlayer *player in self.players.allValues) {
        if ([allRemovablePlayerIDsInHomeTheater member:player.playerID]) {
            NSLog(@"****** Removing Player as in MLAN: %@", player.displayName);
            [self.discoveredServices[player.host] markAsExpired];
        }
    }
}

- (void)removeMissingDevicesSync
{
    dispatch_assert_queue(self.serviceDiscoverySyncQueue);

    // Build a set of removable devices in multichannel

    [self trimMLANMembersSync];

    BOOL anyServicesNearlyDead = false;

    for (NSString *host in [self.discoveredServices allKeys]) {
        IoTServiceDiscoveryResult *service = self.discoveredServices[host];
        NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
        NSTimeInterval age = fabs(now - service.lastSeen);
        BOOL expired = age > SERVICE_EXPIRY_AGE;
        if (expired) {
            NSLog(@"**** Dead service: host: %@ name: %@, type: %@", service.host, service.name, service.type);
            self.discoveredServices[host] = nil;
            [self.discoveryState didRemoveService:self.discoveredServices.count];
            if ([service.type isEqualToString:ALLPLAY_DEVICE_TYPE]) {
                IoTAllPlayController *controller = self.allPlayControllers[host];
                [controller prepareForDisposal];
                self.allPlayControllers[host] = nil;

                IoTPlayer *deletedPlayer = self.players[host];
                self.players[host] = nil;

                if (deletedPlayer) {
                    makeWeak(self)
                    dispatch_async(dispatch_get_main_queue(), ^{
                        makeStrongAndReturnIfNil(self)
                        [self.groupManager processDeadPlayer:deletedPlayer appListener:self.delegate];
                    });
                }
            } else {
                // IoTSys
                IoTSysController *controller = self.iotSysControllers[host];
                [controller prepareForDisposal];
                self.iotSysControllers[host] = nil;

                IoTDevice *deletedDevice = self.devices[host];
                self.devices[host] = nil;

                if (deletedDevice) {
                    makeWeak(self)
                    dispatch_async(dispatch_get_main_queue(), ^{
                        makeStrongAndReturnIfNil(self)
                        if ([self.delegate respondsToSelector:@selector(deviceRemoved:)]) {
                            [self.delegate deviceRemoved:deletedDevice];
                        }
                    });
                }
            }

        } else {
            anyServicesNearlyDead = anyServicesNearlyDead || age > SERVICE_EXPIRY_AGE - SERVICE_FAST_WINDOW;
        }
    }

    if (anyServicesNearlyDead) {
        NSLog(@"We have some nearly dead services");
    	[self.discoveryState scanToRefindService];
    }

//    NSLog(@"Currently we have:\nServices: %@\nAllPlayCoontrollers: %@\nPlayers: %@\nIoTSysControllers: %@\nDevices: %@", self.discoveredServices, self.allPlayControllers, self.players, self.iotSysControllers, self.devices);
}

- (NSArray<IoTServiceDiscoveryResult *> *)getDiscoveredServices
{
    dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);

    __block NSArray<IoTServiceDiscoveryResult *> *services = @[];
    dispatch_sync(self.serviceDiscoverySyncQueue, ^{
        services = [[self.discoveredServices allValues] copy];
    });
    return services;
}

- (NSArray<IoTDeviceController *> *)getDeviceControllers
{
    dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);

    __block NSArray<IoTDeviceController *> *controllers = @[];
    dispatch_sync(self.serviceDiscoverySyncQueue, ^{
        controllers = [[self.allPlayControllers allValues] copy];
    });
    return controllers;
}

- (NSArray<IoTPlayer *> *)getPlayers
{
    dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);

    __block NSArray<IoTPlayer *> *players = @[];
    dispatch_sync(self.serviceDiscoverySyncQueue, ^{
        players = [[self.players allValues] copy];
    });
    return players;
}

- (NSArray<IoTPlayerGroup *> *)getPlayerGroups
{
    return [self.groupManager getPlayerGroups];
}

- (NSArray<IoTDevice *> *)getDevices
{
    dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);

    __block NSArray<IoTDevice *> *devices = @[];
    dispatch_sync(self.serviceDiscoverySyncQueue, ^{
        devices = [[self.devices allValues] copy];
    });
    return devices;
}

- (nullable IoTServiceDiscoveryResult *)discoveredServiceForHost:(NSString *)host
{
    dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);

    if (!host) {
        return nil;
    }

    __block IoTServiceDiscoveryResult *service = nil;
    dispatch_sync(self.serviceDiscoverySyncQueue, ^{
        service = self.discoveredServices[host];
    });
    return service;
}

- (nullable IoTAllPlayController *)allPlayControllerForHost:(NSString *)host
{
    dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);

    if (!host) {
        return nil;
    }

    __block IoTAllPlayController *controller = nil;
    dispatch_sync(self.serviceDiscoverySyncQueue, ^{
        controller = self.allPlayControllers[host];
    });
    return controller;
}

- (nullable IoTPlayer *)playerForHost:(NSString *)host
{
    dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);

    if (!host) {
        return nil;
    }

    __block IoTPlayer *player = nil;
    dispatch_sync(self.serviceDiscoverySyncQueue, ^{
        player = self.players[host];
    });
    return player;
}

- (nullable IoTPlayerGroup *)playerGroupForPlayer:(IoTPlayer *)player
{
    return [self.groupManager playerGroupForPlayer:player];
}

- (nullable IoTPlayer *)playerForID:(NSString *)playerID
{
    dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);

    if (!playerID) {
        return nil;
    }

    __block IoTPlayer *foundPlayer = nil;
    dispatch_sync(self.serviceDiscoverySyncQueue, ^{
        for (IoTPlayer *player in [self.players allValues]) {
            if ([playerID isEqualToString:player.playerID]) {
                foundPlayer = player;
                break;
            }
        }
    });
    return foundPlayer;
}

- (nullable IoTDevice *)deviceForID:(NSString *)deviceID
{
    dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);

    if (!deviceID) {
        return nil;
    }

    __block IoTDevice *foundDevice = nil;
    dispatch_sync(self.serviceDiscoverySyncQueue, ^{
        for (IoTDevice *device in [self.devices allValues]) {
            if ([deviceID isEqualToString:device.deviceID]) {
                foundDevice = device;
                break;
            }
        }
    });
/*  TEST CODE
    if (!foundDevice) {
        IoTSysController *cont = [[IoTSysController alloc] initWithServiceInfo:nil
                                                                stateSyncQueue:self.stateSyncQueue
                                                                     transport:nil];
        return [[IoTDevice alloc] initWithIoTSysController:cont
                                            stateSyncQueue:self.stateSyncQueue];
    }
*/
    return foundDevice;
}

- (nullable IoTDevice *)deviceForPlayer:(IoTPlayer *)player
{
	dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);

    NSURL *playerURL = [NSURL URLWithString:player.host];
    NSString *playerHost = [playerURL host];

    __block IoTDevice *foundDevice = nil;
    dispatch_sync(self.serviceDiscoverySyncQueue, ^{
        for (IoTDevice *device in [self.devices allValues]) {
            NSURL *deviceURL = [NSURL URLWithString:device.host];
            NSString *deviceHost = [deviceURL host];
            if ([deviceHost isEqualToString:playerHost]) {
                foundDevice = device;
                break;
            }
        }
    });
/*  TEST CODE
    if (!foundDevice) {
        IoTSysController *cont = [[IoTSysController alloc] initWithServiceInfo:nil
                                                                stateSyncQueue:self.stateSyncQueue
                                                                     transport:nil];
        return [[IoTDevice alloc] initWithIoTSysController:cont
                                            stateSyncQueue:self.stateSyncQueue];
    }
*/
    return foundDevice;
}

- (nullable IoTPlayer *)playerForDevice:(IoTDevice *)device
{
    dispatch_assert_queue_not(self.serviceDiscoverySyncQueue);

    NSURL *deviceURL = [NSURL URLWithString:device.host];
    NSString *deviceHost = [deviceURL host];

    __block IoTPlayer *foundPlayer = nil;
    dispatch_sync(self.serviceDiscoverySyncQueue, ^{
        for (IoTPlayer *player in [self.players allValues]) {
            NSURL *playerURL = [NSURL URLWithString:player.host];
            NSString *playerHost = [playerURL host];
            if ([deviceHost isEqualToString:playerHost]) {
                foundPlayer = player;
                break;
            }
        }
    });

/*  TEST CODE
 	if (!foundPlayer) {
        return [[self.players allValues] firstObject];
    }
*/
    return foundPlayer;
}

- (void)createPlayerGroupWithName:(NSString *)name
                       completion:(void(^)(BOOL success, NSString * _Nullable groupID, NSError * _Nullable error))completion
                  completionQueue:(nullable dispatch_queue_t)queue
{
    // Get a player and tell it to create a group.
    IoTPlayer *player = [[self getPlayers] firstObject];
    if (player) {
        [player createPlayerGroupWithName:[IoTFriendlyNameMapper dutRepresentationForLocalizedName:name]
                               completion:completion
                          completionQueue:queue];
    } else {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(NO, nil, [NSError errorWithIoTError:IoTErrorInvalidObject description:nil]);
            });
        }
    }
}

- (IoTPlayer *)notifiablePlayerForGroupWithID:(NSString *)groupID
{
    IoTPlayerGroup *group = [self.groupManager playerGroupWithID:groupID];
    IoTPlayer *notifiablePlayer = [group sourcePlayer];
    if (notifiablePlayer) {
        return notifiablePlayer;
    }

    // Are there any other present players in the group

    __block IoTPlayer *foundPlayer = nil;
    [group.members enumerateObjectsUsingBlock:^(IoTPlayerGroupMember * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj.available) {
            IoTPlayer *player = [self playerForID:obj.playerID];
            if (player) {
                foundPlayer = player;
                *stop = YES;
            }
        }
    }];
    if (foundPlayer) {
        return foundPlayer;
    }

    // If there are no players in the group we need to find a player that "knows" about the group

    NSArray<IoTPlayer *> *players = [[self players] allValues];
    [players enumerateObjectsUsingBlock:^(IoTPlayer * _Nonnull player, NSUInteger idx, BOOL * _Nonnull stop) {
        IoTPlayerGroupsInfo *groupsInfo = [player groupsInfo];
        for (IoTPlayerGroupInfo *group in groupsInfo.groups) {
            if ([group.groupID isEqualToString:groupID]) {
                foundPlayer = player;
                *stop = YES;
                break;
            }
        }
    }];

    return foundPlayer;
}

- (void)renamePlayerGroupWithID:(NSString *)groupID
                        newName:(NSString *)newName
                     completion:(nullable IoTCompletionHandler)completion
                completionQueue:(nullable dispatch_queue_t)queue
{
    // Get a player and tell it to create a group.
    IoTPlayer *player = [self notifiablePlayerForGroupWithID:groupID];
    IoTPlayerGroup *playerGroup = [self.groupManager playerGroupWithID:groupID];

    if (player && playerGroup && !playerGroup.isConvenienceGroup) {
        [player renamePlayerGroupWithID:groupID
                                newName:[IoTFriendlyNameMapper dutRepresentationForLocalizedName:newName]
                             completion:completion
                        completionQueue:queue];
    } else {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

- (void)deletePlayerGroupWithID:(NSString *)groupID
                     completion:(nullable IoTCompletionHandler)completion
                completionQueue:(nullable dispatch_queue_t)queue
{
    IoTPlayer *player = [self notifiablePlayerForGroupWithID:groupID];
    IoTPlayerGroup *playerGroup = [self.groupManager playerGroupWithID:groupID];

    if (player && playerGroup && !playerGroup.isConvenienceGroup) {
        [player deletePlayerGroupWithID:groupID
                             completion:completion
                        completionQueue:queue];
    } else {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    }
}

- (void)addPlayersWithIDs:(NSSet<NSString *> *)playerIDs
      toPlayerGroupWithID:(NSString *)groupID
               completion:(nullable IoTCompletionHandler)completion
          completionQueue:(nullable dispatch_queue_t)queue
{
    __block BOOL overallSuccess = YES;
    BOOL dispatched = NO;

    IoTPlayerGroup *playerGroup = [self.groupManager playerGroupWithID:groupID];
    if (!playerGroup || playerGroup.isConvenienceGroup) {
        dispatch_async(queue, ^{
            completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
        });
        return;
    }

    dispatch_group_t playerDispatchGroup = dispatch_group_create();
    for (NSString *playerID in playerIDs) {
        IoTPlayer *player = [self playerForID:playerID];
        if (player) {
            dispatched = YES;
            dispatch_group_enter(playerDispatchGroup);
            [player addToPlayerGroupWithID:groupID
                                completion:^(BOOL success, NSError * _Nullable error) {
                                    overallSuccess = overallSuccess && success;
                                    dispatch_group_leave(playerDispatchGroup);
                                } completionQueue:self.serviceDiscoverySyncQueue];
        }
    }

    dispatch_group_notify(playerDispatchGroup, self.serviceDiscoverySyncQueue, ^{
        if (completion && queue) {
            if (dispatched) {
                dispatch_async(queue, ^{
                    completion(overallSuccess, overallSuccess ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                });
            } else {
                if (playerIDs.count) {
                    // We should've sent some
                    dispatch_async(queue, ^{
                        completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                    });
                } else {
                    // No-op
                    dispatch_async(queue, ^{
                        completion(YES, nil);
                    });
                }
            }
        }
    });
}

- (void)removePlayersWithIDs:(NSSet<NSString *> *)playerIDs
fromPlayerGroupWithCompletion:(nullable IoTCompletionHandler)completion
             completionQueue:(nullable dispatch_queue_t)queue
{
    __block BOOL overallSuccess = YES;
    BOOL dispatched = NO;

    dispatch_group_t playerDispatchGroup = dispatch_group_create();
    for (NSString *playerID in playerIDs) {
        IoTPlayer *player = [self playerForID:playerID];
        if (player) {
            IoTPlayerGroup *playerGroup = [self playerGroupForPlayer:player];
            if (playerGroup && !playerGroup.isConvenienceGroup) {
				// If it's a convenience group it's not really in a group.
                dispatched = YES;
                dispatch_group_enter(playerDispatchGroup);
                [player removeFromPlayerGroupWithCompletion:^(BOOL success, NSError * _Nullable error) {
                    overallSuccess = overallSuccess && success;
                    dispatch_group_leave(playerDispatchGroup);
                } completionQueue:self.serviceDiscoverySyncQueue];
            }
        }
    }

    if (completion && queue) {
        if (dispatched) {
            dispatch_group_notify(playerDispatchGroup, self.serviceDiscoverySyncQueue, ^{
                dispatch_async(queue, ^{
                    completion(overallSuccess, overallSuccess ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                });
            });
        } else {
            if (playerIDs.count) {
                // We should've sent some
                dispatch_async(queue, ^{
                    completion(NO, [NSError errorWithIoTError:IoTErrorInvalidObject description:nil]);
                });
            } else {
                // No-op
                dispatch_async(queue, ^{
                    completion(YES, nil);
                });
            }
        }
    }
}


#pragma mark - IoTPlayerUpdates

- (BOOL)isPlayerSourcePlayer:(IoTPlayer *)player
{
    IoTPlayerGroup *group = [self.groupManager playerGroupForPlayer:player];
    if (group.isConvenienceGroup) {
        return YES;
    }
    NSString *sourcePlayerID = [group sourcePlayerID];
    return [player.playerID isEqualToString:sourcePlayerID];
}

- (void)player:(IoTPlayer *)player didChangeDisplayName:(NSString *)name
{
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(player:didChangeDisplayName:)]) {
            [self.delegate player:player didChangeDisplayName:name];
        }
    });

    IoTPlayerGroup *group = [self playerGroupForPlayer:player];
    if (group.isConvenienceGroup && ![group.displayName isEqualToString:name]) {
        [group setDisplayName:name];
        dispatch_async(dispatch_get_main_queue(), ^{
            makeStrongAndReturnIfNil(self)
            if ([self.delegate respondsToSelector:@selector(playerGroup:didChangeName:)]) {
                [self.delegate playerGroup:group didChangeName:name];
            }
        });
    }
}

- (void)player:(IoTPlayer *)player didChangeInputSelector:(IoTInputOutputSelector *)inputSelector
{
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(playerGroup:didChangeInputSelector:)]) {
            IoTPlayerGroup *group = [self.groupManager playerGroupForPlayer:player];
            [self.delegate playerGroup:group didChangeInputSelector:inputSelector];
        }
        if ([self.delegate respondsToSelector:@selector(player:didChangeInputSelector:)]) {
            [self.delegate player:player didChangeInputSelector:inputSelector];
        }
    });
}

- (void)player:(IoTPlayer *)player didChangeOutputSelectors:(NSArray<IoTInputOutputSelector *> *)outputSelectors
{
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(player:didChangeOutputSelectors:)]) {
            [self.delegate player:player didChangeOutputSelectors:outputSelectors];
        }
    });
}

//- (void)player:(IoTPlayer *)player didChangeInterruptible:(BOOL)interruptible {
//}

- (void)player:(IoTPlayer *)player didChangeLoopMode:(IoTLoopMode)loopMode {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(playlist:didChangeLoopMode:)]) {
            IoTPlayerGroup *group = [self.groupManager playerGroupForPlayer:player];
            IoTPlaylist *playlist = [group playlist];
            if (playlist && [self isPlayerSourcePlayer:player]) {
                [self.delegate playlist:playlist didChangeLoopMode:loopMode];
            }
        }
    });
}


- (void)player:(IoTPlayer *)player didChangeShuffleMode:(IoTShuffleMode)shuffleMode {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(playlist:didChangeShuffleMode:)]) {
            IoTPlayerGroup *group = [self.groupManager playerGroupForPlayer:player];
            IoTPlaylist *playlist = [group playlist];
            if (playlist && [self isPlayerSourcePlayer:player]) {
                [self.delegate playlist:playlist didChangeShuffleMode:shuffleMode];
            }
        }
    });
}

- (void)player:(IoTPlayer *)player didChangeMuteState:(BOOL)muted {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(playerGroup:didChangeMuteState:)]) {
            IoTPlayerGroup *group = [self.groupManager playerGroupForPlayer:player];
            [self.delegate playerGroup:group didChangeMuteState:muted];
        }
        if ([self.delegate respondsToSelector:@selector(player:didChangeMuteState:)]) {
            [self.delegate player:player didChangeMuteState:muted];
        }
    });
}

- (void)player:(IoTPlayer *)player didChangeGroupMuteState:(BOOL)muted {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(playerGroup:didChangeMuteState:)]) {
            IoTPlayerGroup *group = [self.groupManager playerGroupForPlayer:player];
            [self.delegate playerGroup:group didChangeMuteState:muted];
        }
    });
}

- (void)player:(IoTPlayer *)player didChangePlayState:(IoTPlayState)state {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(playerGroup:didChangePlayState:)]) {
            if ([self isPlayerSourcePlayer:player]) {
                IoTPlayerGroup *group = [self.groupManager playerGroupForPlayer:player];
				[self.delegate playerGroup:group didChangePlayState:state];
            }
        }
    });
}

- (void)playerDidChangePlaylist:(IoTPlayer *)player {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(playlistDidChange:)]) {
            IoTPlayerGroup *group = [self.groupManager playerGroupForPlayer:player];
            IoTPlaylist *playlist = [group playlist];
            if (playlist && [self isPlayerSourcePlayer:player]) {
                [self.delegate playlistDidChange:playlist];
            }
        }
    });
}

- (void)player:(IoTPlayer *)player didChangeVolume:(double)volume {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(playerGroup:didChangeVolume:)]) {
            IoTPlayerGroup *group = [self.groupManager playerGroupForPlayer:player];
            [self.delegate playerGroup:group didChangeVolume:volume];
        }
        if ([self.delegate respondsToSelector:@selector(player:didChangeVolume:)]) {
            [self.delegate player:player didChangeVolume:volume];
        }
    });
}

- (void)player:(IoTPlayer *)player didChangeGroupVolume:(double)volume {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(playerGroup:didChangeVolume:)]) {
            IoTPlayerGroup *group = [self.groupManager playerGroupForPlayer:player];
            [self.delegate playerGroup:group didChangeVolume:volume];
        }
    });
}

- (void)player:(IoTPlayer *)player didHavePlaybackError:(NSError *)error atIndex:(NSUInteger)index
{
}

- (void)playerDidChangeGroupInfo:(IoTPlayer *)player
{
    [self.groupManager processUpdatedPlayer:player client:self appListener:self.delegate];
}

- (void)playerDidChangeLeadPlayer:(IoTPlayer *)player
{
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(playerGroupDidChangeSourcePlayer:)]) {
            IoTPlayerGroup *group = [self.groupManager playerGroupForPlayer:player];
            [self.delegate playerGroupDidChangeSourcePlayer:group];
        }
    });
}

- (void)playerDidChangeAudioConfiguration:(IoTPlayer *)player
{
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(playerDidChangeAudioConfiguration:)]) {
            [self.delegate playerDidChangeAudioConfiguration:player];
        }
    });
}

- (void)playerDidChangeEnabledControls:(IoTPlayer *)player
{
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(playerGroupDidChangeEnabledControls:)]) {
            if ([self isPlayerSourcePlayer:player]) {
                IoTPlayerGroup *group = [self.groupManager playerGroupForPlayer:player];
				[self.delegate playerGroupDidChangeEnabledControls:group];
            }
        }
    });
}

- (void)player:(IoTPlayer *)player homeTheaterChannelAvailabilityChanged:(IoTHomeTheaterChannel)channel
{
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(homeTheaterChannelAvailabilityChanged:player:)]) {
            [self.delegate homeTheaterChannelAvailabilityChanged:channel player:player];
        }
    });
}

- (void)player:(IoTPlayer *)player homeTheaterChannelVolumeChanged:(IoTHomeTheaterChannel)channel
{
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(homeTheaterChannelVolumeChanged:player:)]) {
            [self.delegate homeTheaterChannelVolumeChanged:channel player:player];
        }
    });
}

#pragma mark - IoTDeviceUpdates

- (void)device:(nonnull IoTDevice *)device btAdapterStateDidChange:(IoTBluetoothAdapterState)adapterState {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:btAdapterStateDidChange:)]) {
            [self.delegate device:device btAdapterStateDidChange:adapterState];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device btDidConnectDevice:(nonnull IoTBluetoothDevice *)btDevice {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:btDidConnectDevice:)]) {
            [self.delegate device:device btDidConnectDevice:btDevice];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device btDidDisconnectDevice:(nonnull IoTBluetoothDevice *)btDevice {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:btDidDisconnectDevice:)]) {
            [self.delegate device:device btDidDisconnectDevice:btDevice];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device btDiscoverabilityDidChange:(IoTDiscoverable)discoverability {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:btDiscoverabilityDidChange:)]) {
            [self.delegate device:device btDiscoverabilityDidChange:discoverability];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device btPairedDevicesDidChange:(nonnull NSArray<IoTBluetoothDevice *> *)pairedDevices {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:btPairedDevicesDidChange:)]) {
            [self.delegate device:device btPairedDevicesDidChange:pairedDevices];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device btScanDidDiscoverBtDevice:(nonnull IoTBluetoothDevice *)btDevice {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:btScanDidDiscoverBtDevice:)]) {
            [self.delegate device:device btScanDidDiscoverBtDevice:btDevice];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device btScanStateDidChange:(BOOL)scanning {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:btScanStateDidChange:)]) {
            [self.delegate device:device btScanStateDidChange:scanning];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device voiceUIClientsDidChange:(nonnull NSArray<IoTVoiceUIClient *> *)voiceUIClients {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:voiceUIClientsDidChange:)]) {
            [self.delegate device:device voiceUIClientsDidChange:voiceUIClients];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device voiceUIDefaultClientDidChange:(nullable IoTVoiceUIClient *)voiceUIClient {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:voiceUIDefaultClientDidChange:)]) {
            [self.delegate device:device voiceUIDefaultClientDidChange:voiceUIClient];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device voiceUIEnabledStateDidChange:(BOOL)enabled {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:voiceUIEnabledStateDidChange:)]) {
            [self.delegate device:device voiceUIEnabledStateDidChange:enabled];
        }
    });
}

- (void)device:(IoTDevice *)device voiceUIDidProvideAVSAuthenticationCode:(NSString *)code url:(nonnull NSString *)url
{
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:voiceUIDidProvideAVSAuthenticationCode:url:)]) {
            [self.delegate device:device
voiceUIDidProvideAVSAuthenticationCode:code
                              url:url];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device voiceUIOnboardingDidErrorWithTimeout:(BOOL)didTimeout reattempt:(int)reattempt {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:voiceUIOnboardingDidErrorWithTimeout:reattempt:)]) {
            [self.delegate device:device
voiceUIOnboardingDidErrorWithTimeout:didTimeout
                        reattempt:reattempt];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device didChangeName:(nonnull NSString *)name {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:didChangeName:)]) {
            [self.delegate device:device didChangeName:name];
        }
    });
}

- (void)deviceDidChangeAccessPointState:(nonnull IoTDevice *)device {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(deviceDidChangeAccessPointState:)]) {
            [self.delegate deviceDidChangeAccessPointState:device];
        }
    });
}


- (void)deviceDidChangeBatteryState:(nonnull IoTDevice *)device {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(deviceDidChangeBatteryState:)]) {
            [self.delegate deviceDidChangeBatteryState:device];
        }
    });
}


- (void)deviceDidChangeEthernetState:(nonnull IoTDevice *)device {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(deviceDidChangeEthernetState:)]) {
            [self.delegate deviceDidChangeEthernetState:device];
        }
    });
}


- (void)deviceDidChangeWiFiState:(nonnull IoTDevice *)device {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(deviceDidChangeWiFiState:)]) {
            [self.delegate deviceDidChangeWiFiState:device];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device zbAdapterStateDidChange:(IoTZigbeeAdapterState)adapterState {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:zbAdapterStateDidChange:)]) {
            [self.delegate device:device zbAdapterStateDidChange:adapterState];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device zbCoordinatorStateDidChange:(IoTCoordinatorState)coordinatorState {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:zbCoordinatorStateDidChange:)]) {
            [self.delegate device:device zbCoordinatorStateDidChange:coordinatorState];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device zbJoinedDevicesDidChange:(nonnull NSArray<IoTZigbeeDevice *> *)joinedDevices {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:zbJoinedDevicesDidChange:)]) {
            [self.delegate device:device zbJoinedDevicesDidChange:joinedDevices];
        }
    });
}

- (void)device:(nonnull IoTDevice *)device zbJoiningStateDidChange:(BOOL)joiningAllowed {
    makeWeak(self)
    dispatch_async(dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        if ([self.delegate respondsToSelector:@selector(device:zbJoiningStateDidChange:)]) {
            [self.delegate device:device zbJoiningStateDidChange:joiningAllowed];
        }
    });
}

@end
