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

#import "IoTPlayerGroupManager.h"
#import "IoTPlayerGroup+Private.h"
#import "IoTPlayer+Private.h"
#import "IoTPlayerGroupsInfo.h"
#import "IoTPlayerGroupMember+Private.h"
#import "IoTClient+Private.h"
#import "IoTAppListenerProtocol.h"

@interface IoTPlayerGroupManager ()

@property (nonatomic, readonly) dispatch_queue_t stateSyncQueue;
@property (nonatomic, retain) NSMutableDictionary<NSString *, IoTPlayerGroup *> *playerGroups;

@end

@implementation IoTPlayerGroupManager

- (instancetype)init
{
    self = [super init];
    if (self) {
        self->_stateSyncQueue = dispatch_queue_create("com.qti.qualcomm.IoTPlayerGroupManager.stateSyncQueue", DISPATCH_QUEUE_SERIAL);
    	self->_playerGroups = [[NSMutableDictionary alloc] init];
    }
    return self;
}

- (void)forceReset
{
    dispatch_sync(self.stateSyncQueue, ^{
        [self.playerGroups removeAllObjects];
    });
}

- (NSArray<IoTPlayerGroup *> *)getPlayerGroups
{
    dispatch_assert_queue_not(self.stateSyncQueue);

    __block NSArray<IoTPlayerGroup *> *playerGroups = @[];
    dispatch_sync(self.stateSyncQueue, ^{
        playerGroups = [[self.playerGroups allValues] copy];
    });
    return playerGroups;
}

- (nullable IoTPlayerGroup *)playerGroupForPlayer:(IoTPlayer *)player
{
    NSString *groupID = [self groupIDForPlayer:player];
    return [self playerGroupWithID:groupID];
}

- (nullable IoTPlayerGroup *)playerGroupWithID:(NSString *)groupID
{
	dispatch_assert_queue_not(self.stateSyncQueue);

    if (!groupID) {
        return nil;
    }

    __block IoTPlayerGroup *playerGroup = nil;
    dispatch_sync(self.stateSyncQueue, ^{
        playerGroup = self.playerGroups[groupID];
    });
    return playerGroup;
}

- (void)processUpdatedPlayer:(IoTPlayer *)player
                      client:(IoTClient *)client
                 appListener:(id<IoTAppListenerProtocol>)appListener
{
    [self processNewPlayer:player client:client appListener:appListener];
}

- (void)processNewPlayer:(IoTPlayer *)player
                  client:(IoTClient *)client
             appListener:(id<IoTAppListenerProtocol>)appListener
{
    dispatch_assert_queue_not(self.stateSyncQueue);

    dispatch_sync(self.stateSyncQueue, ^{

        NSSet<NSString *> *existingPlayerGroupIDs = [NSSet setWithArray:[self.playerGroups allKeys]];
        NSMutableSet<NSString *> *newPlayerGroupIDs = [NSMutableSet set];
        NSMutableSet<NSString *> *membersChangedPlayerGroupIDs = [NSMutableSet set];
        NSMutableSet<NSString *> *nameChangedPlayerGroupIDs = [NSMutableSet set];

        NSMutableSet<NSString *> *groupedPlayerIDs = [NSMutableSet set];

        NSMutableDictionary<NSString *, IoTPlayerGroup *> *newPlayerGroups = [NSMutableDictionary dictionary];
        NSDictionary<NSString *, IoTPlayerGroup *> *oldPlayerGroups = [self.playerGroups copy];

        if (player.groupingSupported) {
            IoTPlayerGroupsInfo *groupsInfo = [player groupsInfo];

            // Create a group map based on the groups on the devices.
            for (IoTPlayerGroupInfo *groupInfo in groupsInfo.groups) {
                [newPlayerGroupIDs addObject:groupInfo.groupID];

                IoTPlayerGroup* playerGroup = oldPlayerGroups[groupInfo.groupID];
                BOOL groupExists = playerGroup != nil;
                if (!groupExists) {
                    playerGroup = [[IoTPlayerGroup alloc] initWithClient:client];
                    playerGroup.groupID = groupInfo.groupID;
                } else {
                    if (![playerGroup.displayName isEqualToString:groupInfo.groupName]) {
                        [nameChangedPlayerGroupIDs addObject:playerGroup.groupID];
                    }
                }

                playerGroup.displayName = groupInfo.groupName;
                playerGroup.isConvenienceGroup = NO;

                newPlayerGroups[groupInfo.groupID] = playerGroup;

                NSMutableArray *newMembers = [NSMutableArray array];
                for (IoTPlayerGroupMemberInfo *memberInfo in groupInfo.members) {
                    IoTPlayer *memberPlayer = [client playerForID:memberInfo.memberID];
                    BOOL availability = memberPlayer != nil;
                    IoTPlayerGroupMember *playerGroupMember = [[IoTPlayerGroupMember alloc] initWithStoredName:memberInfo.memberName
                                                                                                      playerID:memberInfo.memberID
                                                                                                        client:client];
                    [newMembers addObject:playerGroupMember];
                    if (availability) {
                        [groupedPlayerIDs addObject:memberInfo.memberID];
                    }
                }

                if (groupExists && (![newMembers isEqual:playerGroup.members])) {
                    [membersChangedPlayerGroupIDs addObject:playerGroup.groupID];
                }

                [playerGroup setMembers:newMembers];
            }
        } else {
            // Doesn't support grouping. It will ultimately be added as a convenience group
            // Doesn't change grouped map
            newPlayerGroups = [oldPlayerGroups mutableCopy];
            [newPlayerGroupIDs addObjectsFromArray:[oldPlayerGroups allKeys]];
        }

        // The new map does not include convenience groups (groups created for single speakers).
        // Go through the players and create convenience groups for those players not assigned to groups.

        NSArray<IoTPlayer *> *players = [client getPlayers];
        for (IoTPlayer *player in players) {
            if (![groupedPlayerIDs member:player.playerID]) {
                // We haven't grouped it.
                IoTPlayerGroup* playerGroup = oldPlayerGroups[player.playerID];
                BOOL groupExists = playerGroup != nil;
                if (!groupExists) {
                    playerGroup = [[IoTPlayerGroup alloc] initWithClient:client];
                    playerGroup.groupID = player.playerID;
                } else {
                    if (![playerGroup.displayName isEqualToString:player.displayName]) {
                        [nameChangedPlayerGroupIDs addObject:playerGroup.groupID];
                    }
                }
                playerGroup.isConvenienceGroup = YES;
                playerGroup.members = @[[[IoTPlayerGroupMember alloc] initWithStoredName:player.displayName
                                                                                playerID:player.playerID
                                                                                  client:client]];
                playerGroup.displayName = player.displayName;
                newPlayerGroups[player.playerID] = playerGroup;
                [newPlayerGroupIDs addObject:player.playerID];
            }
        }

        self.playerGroups = newPlayerGroups;

        // At this point:
        //        modifiedPlayerGroupIDs has the IDs of all existing groups where the membership list has changed in some way.
        //        existingPlayerGroupIDs has the IDs of all existing groups.
        //        newPlayerGroupIDs has the IDs of all groups on this pass (including existing).

        NSMutableSet *deletedPlayerGroupIDs = [existingPlayerGroupIDs mutableCopy];
        [deletedPlayerGroupIDs minusSet:newPlayerGroupIDs];
        NSMutableSet *addedPlayerGroupIDs = [newPlayerGroupIDs mutableCopy];
        [addedPlayerGroupIDs minusSet:existingPlayerGroupIDs];

        if (deletedPlayerGroupIDs.count > 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                if ([appListener respondsToSelector:@selector(playerGroupRemoved:)]) {
                    for (NSString *groupID in deletedPlayerGroupIDs) {
                        [appListener playerGroupRemoved:oldPlayerGroups[groupID]];
                    }
                }
            });
        }

        if (addedPlayerGroupIDs.count > 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                if ([appListener respondsToSelector:@selector(playerGroupAdded:)]) {
                    for (NSString *groupID in addedPlayerGroupIDs) {
                        [appListener playerGroupAdded:newPlayerGroups[groupID]];
                    }
                }
            });
        }

        if (membersChangedPlayerGroupIDs.count > 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                if ([appListener respondsToSelector:@selector(playerGroupDidChangePlayerList:)]) {
                    for (NSString *groupID in membersChangedPlayerGroupIDs) {
                        [appListener playerGroupDidChangePlayerList:newPlayerGroups[groupID]];
                    }
                }
            });
        }

        if (nameChangedPlayerGroupIDs.count > 0) {
            dispatch_async(dispatch_get_main_queue(), ^{
                if ([appListener respondsToSelector:@selector(playerGroup:didChangeName:)]) {
                    for (NSString *groupID in nameChangedPlayerGroupIDs) {
                        [appListener playerGroup:newPlayerGroups[groupID] didChangeName:newPlayerGroups[groupID].displayName];
                    }
                }
            });
        }
    });
}

- (void)processDeadPlayer:(IoTPlayer *)player appListener:(id<IoTAppListenerProtocol>)appListener
{
	dispatch_assert_queue_not(self.stateSyncQueue);

    // There are two options:
    // 		1. It's a bona fide group member so it's availability needs to change
    //		2. It's in a convenience group so that group needs removing.

	dispatch_sync(self.stateSyncQueue, ^{
        IoTPlayerGroup *group = self.playerGroups[player.playerID];
        if (group && group.isConvenienceGroup) {
            // Convenience
            self.playerGroups[player.playerID] = nil;
            dispatch_async(dispatch_get_main_queue(), ^{
                if ([appListener respondsToSelector:@selector(playerGroupRemoved:)]) {
                	[appListener playerGroupRemoved:group];
                }
            });
        } else {
			// Proper group
            group = self.playerGroups[player.groupID];
            dispatch_async(dispatch_get_main_queue(), ^{
                if ([appListener respondsToSelector:@selector(playerGroupDidChangePlayerList:)]) {
                	[appListener playerGroupDidChangePlayerList:group];
                }
            });
        }
    });
}

#pragma mark - Private

- (NSString *)groupIDForPlayer:(IoTPlayer *)player
{
    IoTPlayerGroup *group = self.playerGroups[player.playerID];
    if (group && group.isConvenienceGroup) {
        return player.playerID;
    } else {
        return player.groupID;
    }
}

@end
