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

#import "IoTPlayerGroup.h"
#import "IoTPlayer+Private.h"
#import "IoTPlaylist+Private.h"
#import "IoTMediaItem+Private.h"
#import "NSError+IoTErrors.h"
#import "IoTClient.h"
#import "IoTFriendlyNameMapper.h"

#define GETTER(player, type, name, default) \
- (type)name { \
	if ([self player]) { \
		return [ [self player] name ]; \
	} else { \
		return default; \
	} \
} \

#define ACTION(player, method) \
- (void)method##WithCompletion:(void(^)(BOOL, NSError *))completion \
completionQueue:(dispatch_queue_t)queue \
{ \
	[[self player] method##WithCompletion:completion \
							  completionQueue:queue]; \
} \

#define ACTION_WITH_PARAM(player, method, type, param) \
- (void)method:(type)param \
	completion:(void(^)(BOOL, NSError *))completion \
completionQueue:(dispatch_queue_t)queue \
{ \
	if (![self player]) { \
		if (completion && queue) { \
    		dispatch_async(queue, ^{ \
        		completion(NO, [NSError errorWithIoTError:IoTErrorInvalidObject \
                                      description:nil]); \
    		}); \
		} \
	} else { \
    	[[self player] method:param \
                     completion:completion \
               	completionQueue:queue]; \
	} \
} \

@implementation IoTPlayerGroup {
    __weak IoTClient *_client;
    BOOL _isConvenienceGroup;
    NSString *_groupID;
    NSArray<IoTPlayerGroupMember *> *_members;
    NSString *_displayName;
    IoTPlaylist *_playlist;
}

- (instancetype)initWithClient:(IoTClient *)client
{
    self = [super init];
    if (self) {
        self->_client = client;
        self->_isConvenienceGroup = NO;
        self->_members = @[];
        self->_displayName = @"";
        self->_playlist = [[IoTPlaylist alloc] initWithPlayerGroup:self];
    }
    return self;
}

- (NSString *)groupID
{
    return _groupID;
}

- (void)setGroupID:(NSString *)groupID
{
    _groupID = [groupID copy];
}

- (BOOL)isConvenienceGroup
{
    return _isConvenienceGroup;
}

- (void)setIsConvenienceGroup:(BOOL)isConvenienceGroup
{
    _isConvenienceGroup = isConvenienceGroup;
}

- (void)setMembers:(NSArray<IoTPlayerGroupMember *> *)members
{
    _members = [members copy];
}

- (NSArray<IoTPlayerGroupMember *> *)members
{
    return _members;
}

- (IoTPlaylist *)playlist
{
    IoTPlayer *playlistPlayer = [self playlistPlayer];
    if (playlistPlayer) {
        return _playlist;
    } else {
        return nil;
    }
}

- (nullable IoTInputOutputSelector *)activeInputSelector
{
    return [[self sourcePlayer] activeInputSelector];
}

- (NSArray<IoTInputOutputSelector *> *)activeOutputSelectors
{
    NSMutableArray *activeSinks = [NSMutableArray array];
    [_members enumerateObjectsUsingBlock:^(IoTPlayerGroupMember * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj.available) {
            IoTPlayer *player = [self->_client playerForID:obj.playerID];
            if (player) {
                [activeSinks addObjectsFromArray:[player activeOutputSelectors]];
            }
        }
    }];
    return activeSinks;
}

- (nullable IoTPlayer *)sourcePlayerIfPresent
{
    if (self.isConvenienceGroup) {
        IoTPlayer *player = [self->_client playerForID:[self.members firstObject].playerID];
        return player;
    }

    // We need to determine lead player from an available players

    __block IoTPlayer *sourcePlayer = nil;
    [self.members enumerateObjectsUsingBlock:^(IoTPlayerGroupMember * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj.available) {
            IoTPlayer *player = [self->_client playerForID:obj.playerID];
            if (player) {
                NSString *possibleSourcePlayerID = [player leadPlayerInGroupID];
  //              NSLog(@"Source id is: %@", possibleSourcePlayerID);
                sourcePlayer = [self->_client playerForID:possibleSourcePlayerID];
                if (sourcePlayer) {
                    // It exists we have our lead
                    *stop = YES;
                }
            }
        }
    }];

//    NSLog(@"Source Player is %@", sourcePlayer.playerID);
    return sourcePlayer;
}

- (NSString *)sourcePlayerID
{
    return [[self sourcePlayer] playerID];
}

- (nullable IoTPlayer *)sourcePlayer
{
    return [self sourcePlayerIfPresent] ?: [self playlistPlayer];
}

- (IoTPlayer *)playlistPlayer
{
    if (self.isConvenienceGroup) {
        IoTPlayer *player = [self->_client playerForID:[self.members firstObject].playerID];
        return player;
    }

    // We need to determine the best player from the available players

    NSMutableArray<IoTPlayer *> *availablePlayers = [NSMutableArray array];
    NSMutableArray<IoTPlayer *> *playingPlayers = [NSMutableArray array];
    NSMutableArray<IoTPlayer *> *soundbars = [NSMutableArray array];
    NSMutableArray<IoTPlayer *> *playersWithPlaylists = [NSMutableArray array];

    IoTPlayer *sourcePlayerIfPresent = [self sourcePlayerIfPresent];


    [_members enumerateObjectsUsingBlock:^(IoTPlayerGroupMember * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (obj.available) {
            IoTPlayer *player = [self->_client playerForID:obj.playerID];
            if (player) {
                [availablePlayers addObject:player];

                // Do we have players actually playing/paused from a playlist?
                if ((player.playState != IoTPlayStateStopped) &&
                    (player.indexPlaying >= 0)) {
                    [playingPlayers addObject:player];
                }

                if (player.isSoundBar) {
                    [soundbars addObject:player];
                }
                if (player.playlistSize > 0) {
                    [playersWithPlaylists addObject:player];
                }
            }
        }
    }];

    if (playingPlayers.count > 0) {
        if (sourcePlayerIfPresent &&
            [playingPlayers containsObject:sourcePlayerIfPresent]) {
//            NSLog(@"Playlist Player is source player as not stopped and playing from playlist");
            return sourcePlayerIfPresent;
        } else {
//            NSLog(@"Playlist Player is first playing player");
        	return [playingPlayers firstObject];
        }
    }

    if (playersWithPlaylists.count > 0) {
        if (sourcePlayerIfPresent &&
            (sourcePlayerIfPresent.indexPlaying >= 0) &&
            [playersWithPlaylists containsObject:sourcePlayerIfPresent]) {
//            NSLog(@"Playlist Player is source player as has playlist");
            return sourcePlayerIfPresent;
        } else {
//            NSLog(@"Playlist Player is first player with playlist");
            return [playersWithPlaylists firstObject];
        }
    }

    if (soundbars.count > 0) {
        if (sourcePlayerIfPresent &&
            [soundbars containsObject:sourcePlayerIfPresent]) {
//            NSLog(@"Playlist Player is source player player as soundbar");
            return sourcePlayerIfPresent;
        } else {
//            NSLog(@"Playlist Player is first soundbar");
        	return [soundbars firstObject];
        }
    }

//    NSLog(@"Playlist Player is fallback");
    return sourcePlayerIfPresent ?: [availablePlayers firstObject];
}

- (BOOL)available
{
    for (IoTPlayerGroupMember *member in self.members) {
        if (member.available) {
            return YES;
        }
    }
    return NO;
}

- (NSString *)displayName
{
    return [IoTFriendlyNameMapper localizedUIRepresentationForName:_displayName];
}

- (void)setDisplayName:(NSString * _Nonnull)displayName
{
    _displayName = [displayName copy];
}

GETTER(sourcePlayer, BOOL, isSeekEnabled, NO)
GETTER(sourcePlayer, BOOL, isNextEnabled, NO)
GETTER(sourcePlayer, BOOL, isPreviousEnabled, NO)
GETTER(sourcePlayer, BOOL, isPauseEnabled, NO)
//GETTER(BOOL,isInputSelectorModeSupported,NO)
GETTER(sourcePlayer, IoTPlayState, playState, IoTPlayStateStopped)
GETTER(sourcePlayer, int, playerPosition, 0)
GETTER(sourcePlayer, BOOL, isInterruptible, NO)
//GETTER(sourcePlayer, BOOL, isPartyModeEnabled, NO)

ACTION_WITH_PARAM(sourcePlayer, setPlayerPosition, int, position)
ACTION(sourcePlayer, play)
ACTION(sourcePlayer, pause)
ACTION(sourcePlayer, stop)
ACTION(sourcePlayer, next)
ACTION(sourcePlayer, previous)

ACTION_WITH_PARAM(playlistPlayer, playMediaItem, IoTMediaItem *, mediaItem)

#pragma mark - Private

GETTER(sourcePlayer, int, indexPlaying, -1)
GETTER(sourcePlayer, IoTMediaItem *, currentItem,[[IoTMediaItem alloc] init])
GETTER(sourcePlayer, BOOL,isLoopModeEnabled,NO)
GETTER(sourcePlayer, BOOL,isShuffleModeEnabled,NO)
GETTER(sourcePlayer, IoTLoopMode,loopMode,IoTLoopModeNone)
GETTER(sourcePlayer, IoTShuffleMode,shuffleMode,IoTShuffleModeLinear)

ACTION_WITH_PARAM(sourcePlayer, setLoopMode, IoTLoopMode, loopMode)
ACTION_WITH_PARAM(sourcePlayer, setShuffleMode, IoTShuffleMode, shuffleMode)
ACTION_WITH_PARAM(playlistPlayer, playItemAtIndex, int, index)
ACTION_WITH_PARAM(playlistPlayer, removeMediaItemsInRange, NSRange, range)
ACTION(playlistPlayer, clear)

GETTER(playlistPlayer, NSString *,playlistUserData,@"")
GETTER(playlistPlayer, uint, playlistSize,0)

- (void)moveMediaItemsInRange:(NSRange)range
                     position:(int)position
                   completion:(void(^)(BOOL, NSError *))completion
              completionQueue:(dispatch_queue_t)queue;
{
    if (!self.playlistPlayer) {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorInvalidObject
                                              description:nil]);
            });
        }
    } else {
        [self.playlistPlayer moveMediaItemsInRange:range
                                      position:position
                                    completion:completion
                               completionQueue:queue];
    }
}

- (void)addMediaItem:(IoTMediaItem *)mediaItem
             atIndex:(int)index
                play:(BOOL)play
            userData:(NSString *)userData
          completion:(void(^)(BOOL, NSError *))completion
     completionQueue:(dispatch_queue_t)queue
{
    if (!self.playlistPlayer) {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorInvalidObject
                                              description:nil]);
            });
        }
    } else {
        [self.playlistPlayer addMediaItem:mediaItem
                              atIndex:index
                                 play:play
                             userData:userData
                           completion:completion
                      completionQueue:queue];
    }
}

- (void)addMediaItems:(NSArray *)mediaItems
              atIndex:(int)index
                 play:(BOOL)play
             userData:(NSString *)userData
           completion:(void(^)(BOOL, NSError *))completion
      completionQueue:(dispatch_queue_t)queue
{
    if (!self.playlistPlayer) {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorInvalidObject
                                              description:nil]);
            });
        }
    } else {
        [self.playlistPlayer addMediaItems:mediaItems
                               atIndex:index
                                  play:play
                              userData:userData
                            completion:completion
                       completionQueue:queue];
    }
}

- (void)getItemsInRange:(NSRange)range
             completion:(void(^)(BOOL, NSArray<IoTMediaItem *> *,NSError *))completion
        completionQueue:(dispatch_queue_t)queue
{
    if (!self.playlistPlayer) {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(NO, @[], [NSError errorWithIoTError:IoTErrorInvalidObject
                                                   description:nil]);
            });
        }
    } else {
        [self.playlistPlayer getItemsInRange:range
                                  completion:completion
                             completionQueue:queue];
    }
}

// For volumes we handle the case that master volume may or may not be available

- (double)maxVolume
{
    if (self.sourcePlayer.isMasterVolumeSupported) {
        return self.sourcePlayer.masterMaxVolume;
    } else {
        return [self averageMaxVolume];
    }
}

- (double)averageMaxVolume
{
    __block double totalMaxVolume = 0.0;
    __block uint totalPlayers = 0.0;
    [self.members enumerateObjectsUsingBlock:^(IoTPlayerGroupMember * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        IoTPlayer *player = [self->_client playerForID:obj.playerID];
        if (player) {
            if (player.isVolumeEnabled) {
                totalMaxVolume += player.maxVolume;
                totalPlayers ++;
            }
        }
    }];
    if (totalPlayers > 0) {
        return totalMaxVolume / totalPlayers;
    } else {
        return 0.0;
    }
}

- (double)volume {
    if (self.sourcePlayer.isMasterVolumeSupported) {
        return self.sourcePlayer.masterVolume;
    } else {
        return [self averageVolumes];
    }
}

- (double)averageVolumes
{
    __block double totalVolume = 0.0;
    __block uint totalPlayers = 0.0;
    [self.members enumerateObjectsUsingBlock:^(IoTPlayerGroupMember * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        IoTPlayer *player = [self->_client playerForID:obj.playerID];
        if (player) {
            if (player.isVolumeEnabled) {
                totalVolume += player.volume;
                totalPlayers ++;
            }
        }
    }];
    if (totalPlayers > 0) {
        return totalVolume / totalPlayers;
    } else {
        return 0.0;
    }
}

- (BOOL)isMuted
{
    __block BOOL allMuted = YES;
    [self.members enumerateObjectsUsingBlock:^(IoTPlayerGroupMember * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        IoTPlayer *player = [self->_client playerForID:obj.playerID];
        if (player) {
            if (player.isVolumeEnabled) {
                allMuted = allMuted && player.isMuted;
            }
        }
    }];
    return allMuted;
}

- (double)targetVolumeForPlayer:(IoTPlayer *)player
                 groupNewVolume:(double)groupNewVolume
                 groupOldVolume:(double)groupOldVolume
                 groupMaxVolume:(double)groupMaxVolume
{
    double adjustment;

    if (groupNewVolume < 0.01) {
        return 0.0;
    }
    if (groupNewVolume > groupOldVolume) {
        adjustment = (float)(groupNewVolume - groupOldVolume) / (groupMaxVolume - groupOldVolume);
    } else {
        adjustment = (float)(groupNewVolume - groupOldVolume) / groupOldVolume;
    }

    if ((adjustment < 0.01) && (adjustment > -0.01)) {
        return [player volume];
    } else {
        double playerVolume = [player volume];
        double playerMaxVolume = [player maxVolume];
        if (adjustment <= -1.0) {
            return 0.0;
        } else if (adjustment >= 1.0) {
            return playerMaxVolume;
        } else {
            if (adjustment > 0) {
                playerVolume += ((playerMaxVolume - playerVolume) * adjustment);
            } else {
                playerVolume += (playerVolume * adjustment);
            }
        }
        return (playerVolume < 0) ? 0 : ((playerVolume > playerMaxVolume) ? playerMaxVolume : playerVolume);
    }
}

- (void)setVolume:(double)volume
       completion:(void(^)(BOOL, NSError *))completion
  completionQueue:(dispatch_queue_t)queue
{
    double groupMaxVolume = [self maxVolume];
    double groupOldVolume = [self volume];
    double normalizedVolume = (volume < 0) ? 0.0 : (volume > groupMaxVolume) ? groupMaxVolume : volume;

    if (self.sourcePlayer.isMasterVolumeSupported) {
        if (fabs(normalizedVolume - [self.sourcePlayer masterVolume]) > 0.01) {
            [self.sourcePlayer setMasterVolume:normalizedVolume
                                    completion:completion
                               completionQueue:queue];
        }
        return;
    }

    // Fallback by setting individual speakers.

    [self.members enumerateObjectsUsingBlock:^(IoTPlayerGroupMember * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        IoTPlayer *player = [self->_client playerForID:obj.playerID];
        if (player) {
            if (player.isVolumeEnabled) {
                double newPlayerVolume = [self targetVolumeForPlayer:player
                                                      groupNewVolume:normalizedVolume
                                                      groupOldVolume:groupOldVolume
                                                      groupMaxVolume:groupMaxVolume];

                if (fabs(newPlayerVolume - [player volume]) > 0.01) {
                    // Double equality comparison...
                    [player setVolume:newPlayerVolume
                           completion:nil
                      completionQueue:nil];
                }
            }
        }
    }];

    if (completion && queue) {
        dispatch_async(queue, ^{
            completion(YES, nil);
        });
    }
}

- (void)setMuted:(BOOL)muted
      completion:(void(^)(BOOL, NSError *))completion
 completionQueue:(dispatch_queue_t)queue
{
    if (!self.sourcePlayer) {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorInvalidObject
                                              description:nil]);
            });
        }
    } else if (self.sourcePlayer.isMasterVolumeSupported) {
        [self.sourcePlayer setMasterMuted:muted
                               completion:completion
                          completionQueue:queue];
    } else if (self.sourcePlayer.isVolumeEnabled) {
        [self.members enumerateObjectsUsingBlock:^(IoTPlayerGroupMember * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            IoTPlayer *player = [self->_client playerForID:obj.playerID];
            if (player) {
                [player setMuted:muted
                      completion:nil
                 completionQueue:nil];
            }
        }];
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(YES, nil);
            });
        }
    } else {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest
                                              description:nil]);
            });
        }
    }
}

@end
