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

#import "IoTPlayer+CategoriesHeader.h"
#import "IoTFriendlyNameMapper.h"
#import "IoTMultichannelState.h"

@interface IoTInputOutputSelector ()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *identifier;
@end

@implementation IoTInputOutputSelector

- (instancetype)init
{
    self = [super init];
    if (self) {
        self->_name = @"";
        self->_identifier = @"";
        self->_type = IoTInputOutputSelectorTypeNone;
    }
    return self;
}

- (instancetype)initWithAttr:(IoTInputOutputInfoAttr *)attr
{
    self = [self init];
    if (self) {
        self->_name = [attr.friendlyName copy] ?: @"";
        self->_identifier = [attr.identifier copy] ?: @"";
        if ([attr.identifier isEqualToString:@"none"]) {
            self->_type = IoTInputOutputSelectorTypeNone;
        } else if ([attr.identifier isEqualToString:@"hdmi-in"]) {
            self->_type = IoTInputOutputSelectorTypeHdmiIn;
        } else if ([attr.identifier isEqualToString:@"spdif-in"]) {
            self->_type = IoTInputOutputSelectorTypeSpdifIn;
        } else if ([attr.identifier isEqualToString:@"linein"]) {
            self->_type = IoTInputOutputSelectorTypeLineIn;
        } else if ([attr.identifier isEqualToString:@"bluetooth"]) {
            self->_type = IoTInputOutputSelectorTypeBluetooth;
        } else if ([attr.identifier isEqualToString:@"speaker"]) {
            self->_type = IoTInputOutputSelectorTypeSpeaker;
        } else {
            self->_type = IoTInputOutputSelectorTypeNone;
        }
    }
    return self;
}

- (IoTInputOutputInfoAttr *)attrWithSyncStateQueue:(dispatch_queue_t)queue
{
    IoTInputOutputInfoAttr *attr = [[IoTInputOutputInfoAttr alloc] initWithStateSyncQueue:queue];
    attr.friendlyName = self.name;
    attr.identifier = self.identifier;
    return attr;
}

+ (NSArray<IoTInputOutputInfoAttr *> *)mapToAttrs:(NSArray<IoTInputOutputSelector *> *)selectors
                                   syncStateQueue:(dispatch_queue_t)queue
{
    NSMutableArray *attrs = [NSMutableArray arrayWithCapacity:selectors.count];
    for (IoTInputOutputSelector *selector in selectors) {
        [attrs addObject:[selector attrWithSyncStateQueue:queue]];
    }
    return [attrs copy];
}

+ (NSArray<IoTInputOutputSelector *> *)mapToSelectors:(NSArray<IoTInputOutputInfoAttr *> *)attrs
{
    NSMutableArray *selectors = [NSMutableArray arrayWithCapacity:attrs.count];
    for (IoTInputOutputInfoAttr *attr in attrs) {
        IoTInputOutputSelector *selector = [[IoTInputOutputSelector alloc] initWithAttr:attr];
        [selectors addObject:selector];
    }
    return [selectors copy];
}

- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    }

    if (![object isKindOfClass:[IoTInputOutputSelector class]]) {
        return NO;
    }

    return [self hash] == [object hash];
}

- (NSUInteger)hash {
    return [self.identifier hash];
}

@end

@implementation IoTPlayer

- (instancetype)init
{
    self = [super init];
    if (self) {
        self->_stateSyncQueue = dispatch_queue_create("com.qti.qualcomm.IoTPlayer.stateSyncQueue", DISPATCH_QUEUE_SERIAL);
        self->_backgroundQueue = dispatch_queue_create("com.qti.qualcomm.IoTPlayer.backgroundQueue", DISPATCH_QUEUE_SERIAL);
        self->_state = [[IoTPlayerState alloc] init];
        self->_timeStartedPlayback = -1;
    }
    return self;
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"Device: %@ PlayerID: %@", self.displayName, self.playerID];
}

- (NSString *)displayName;
{
    NSString *ret = self.state.mediaPlayerAttr.displayName ?: @"";
    return [IoTFriendlyNameMapper localizedUIRepresentationForName:ret];
}

- (NSString *)playerID
{
    return self.allPlayController.serviceInfo.identifier;
}

- (double)minVolume
{
    return 0;
}

- (double)maxVolume
{
    return 1.0;
}

- (double)volume
{
    RETURN_GETTER(double, volumeControlAttr, volume, 0)
}

- (BOOL)groupingSupported
{
    RETURN_SUPPORTED(groupAttr)
}

- (BOOL)isInterruptible
{
    return ((self.activeInputSelector.type == IoTInputOutputSelectorTypeNone && self.playlistSize > 0) ||
            (self.activeInputSelector.type == IoTInputOutputSelectorTypeBluetooth));
}

- (BOOL)isVolumeEnabled
{
    RETURN_SUPPORTED(volumeControlAttr)
}

- (BOOL)isMuted
{
    RETURN_GETTER(BOOL, volumeControlAttr, mute, NO)
}

- (BOOL)isInputSelectorModeSupported
{
    RETURN_SUPPORTED(inputSelectorAttr)
}

- (BOOL)isOutputSelectorModeSupported
{
    RETURN_SUPPORTED(outputSelectorAttr)
}

- (NSArray<IoTInputOutputSelector *> *)inputSelectors
{
    NSArray<IoTInputOutputInfoAttr *> *attrs = self.state.inputSelectorAttr.inputList ?: @[];
    return [IoTInputOutputSelector mapToSelectors:attrs];
}

- (IoTInputOutputSelector *)activeInputSelector
{
    IoTInputOutputInfoAttr *attr = self.state.inputSelectorAttr.activeInput;
    return [[IoTInputOutputSelector alloc] initWithAttr:attr];
}

- (NSArray<IoTInputOutputSelector *> *)outputSelectors
{
    NSArray<IoTInputOutputInfoAttr *> *attrs = self.state.outputSelectorAttr.outputList ?: @[];
    return [IoTInputOutputSelector mapToSelectors:attrs];
}

- (NSArray<IoTInputOutputSelector *> *)activeOutputSelectors
{
    NSArray<IoTInputOutputInfoAttr *> *attrs = self.state.outputSelectorAttr.activeOutputs ?: @[];
    return [IoTInputOutputSelector mapToSelectors:attrs];
}

- (void)setVolume:(double)volume
       completion:(void(^)(BOOL, NSError *))completion
  completionQueue:(dispatch_queue_t)queue
{
	SETTER(volumeControlAttr,volume,volume,postVolumeControlWith)
    if (self.isSoundBar) {
        [self updateHomeTheaterRatioVolumesForPlayerVolume:volume
                                                completion:nil
                                           completionQueue:nil];
    }
}

- (void)setInputSelector:(IoTInputOutputSelector *)input
              completion:(void(^)(BOOL, NSError *))completion
         completionQueue:(dispatch_queue_t)queue
{
    IoTInputOutputInfoAttr *attr = [input attrWithSyncStateQueue:self.stateSyncQueue];
    SETTER(inputSelectorAttr,activeInput,attr,postInputSelectorWith)
}

- (void)setOutputSelectors:(NSArray<IoTInputOutputSelector *> *)outputs
                completion:(void(^)(BOOL, NSError *))completion
           completionQueue:(dispatch_queue_t)queue
{
    NSArray<IoTInputOutputInfoAttr *> *attrs = [IoTInputOutputSelector mapToAttrs:outputs syncStateQueue:self.stateSyncQueue];
	SETTER(outputSelectorAttr,activeOutputs,attrs,postOutputSelectorWith)
}

- (void)setMuted:(BOOL)mute
      completion:(void(^)(BOOL, NSError *))completion
 completionQueue:(dispatch_queue_t)queue
{
    SETTER(volumeControlAttr,mute,mute,postVolumeControlWith)
}

#pragma mark - Observer

/*
 - (void)player:(IoTPlayer *)player didHavePlaybackError:(NSError *)error atIndex:(NSUInteger)index;
 - (void)player:(IoTPlayer *)player didChangeInterruptible:(BOOL)interruptible;
 */

- (void)fetchGroupingInfoAfterDelay
{
    // A grouping notification is not sent after group sync therefore maybe incomplete
    makeWeak(self)
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        [self.allPlayController getGroupWithCompletion:^(IoTGroupAttr * attr, BOOL success) {
            makeStrongAndReturnIfNil(self);
            BOOL canSend = self.dispatchedAfterInitialFetch;
            self.state.groupAttr = success ? attr : nil;
            if (canSend) {
                [self.updatesDelegate playerDidChangeGroupInfo:self];
            }
        } completionQueue:self.backgroundQueue];

        // We refetch the group volume as it fails for non-grouped speakers.
        [self.allPlayController getGroupVolumeControlWithCompletion:^(IoTVolumeControlAttr *attr, BOOL success) {
            makeStrongAndReturnIfNil(self);
            NSLog(@"*** GROUP VOLUME FETCH AFTER GROUPING: SUCCESS = %@", success ? @"YES" : @"NO");
            self.state.groupVolumeControlAttr = success ? attr : nil;
        } completionQueue:self.backgroundQueue];
    });
}

- (void)refetchMultichannelVolumeInfo
{
    makeWeak(self)
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        [self.allPlayController getMultichannelVolumeWithCompletion:^(IoTMultichannelVolumeControlAttr *outAttr, BOOL success) {
            makeStrongAndReturnIfNil(self);
            BOOL canSend = self.dispatchedAfterInitialFetch;
            [self.state.multiChannelState updateWithMCVolumeAttr:success ? outAttr : nil
                                                   volumeChanged:^(IoTHomeTheaterChannel channel) {
                makeStrongAndReturnIfNil(self);
                if (canSend) {
                    NSLog(@"Updating Channel Volume after multichannel change");
                     [self.updatesDelegate player:self homeTheaterChannelVolumeChanged:channel];
                }
            } changeQueue:self.backgroundQueue];
        } completionQueue:self.backgroundQueue];
    });
}

- (void)refetchMediaPlayerInfo
{
    makeWeak(self)
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        makeStrongAndReturnIfNil(self)
        [self.allPlayController getMediaPlayerWithCompletion:^(IoTMediaPlayerAttr *outAttr, BOOL success) {
            makeStrongAndReturnIfNil(self);
            BOOL canSend = self.dispatchedAfterInitialFetch;
            if (success && outAttr.playState.playState != IoTPlayStateStopped) {
                self.state.mediaPlayerAttr = outAttr;
                if (canSend) {
                    NSLog(@"Refetch media player...posting update");
                    [self handlePlayStateUpdate:YES];
                    [self.updatesDelegate player:self didChangePlayState:self.playState];
                }
            }
        } completionQueue:self.backgroundQueue];
    });
}

- (void)allPlayController:(IoTAllPlayController *)controller
         didObserveChange:(IoTAllPlayObserveOrigin)type
    wrappedRepresentation:(IoTWrappedRepresentation *)wrapped
{
    NSArray<NSString *> *keys = [wrapped keys];
    BOOL canSend = self.dispatchedAfterInitialFetch; // We only notify if we have finished all fetches
    if (canSend) {
        NSLog(@"**** Got observe callback: %ld in player: %@\nkeys: %@", (long)type, self.playerID, keys);
    } else {
		NSLog(@"**** Ignoring observe callback: %ld in player: %@\nkeys: %@", (long)type, self.playerID, keys);
    }
    switch (type) {
        case IoTAllPlayObserveOriginGroup:
        {
            if (!self.state.groupAttr) {
                // Previously returned unsupported but now we have a notification for it?
                self.state.groupAttr = [[IoTGroupAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.groupAttr unpack:wrapped];
            if (canSend) {
				[self.updatesDelegate playerDidChangeGroupInfo:self];
            }
            [self fetchGroupingInfoAfterDelay];
        }
            break;
        case IoTAllPlayObserveOriginInputSelector:
        {
            NSString *oldSelector = [self.state.inputSelectorAttr.activeInput.identifier copy];
            if (!self.state.inputSelectorAttr) {
                // Previously returned unsupported but now we have a notification for it?
                self.state.inputSelectorAttr = [[IoTInputSelectorAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.inputSelectorAttr unpack:wrapped];
            NSString *newSelector = [self.state.inputSelectorAttr.activeInput.identifier copy];
            if (canSend && ![oldSelector isEqualToString:newSelector]) {
                [self.updatesDelegate player:self didChangeInputSelector:self.activeInputSelector];
                [self refetchMediaPlayerInfo];
            }
        }
            break;
        case IoTAllPlayObserveOriginOutputSelector:
        {
            NSArray<IoTInputOutputSelector *> *oldSelectors = [self.state.outputSelectorAttr.activeOutputs copy];
            if (!self.state.outputSelectorAttr) {
                // Previously returned unsupported but now we have a notification for it?
                self.state.outputSelectorAttr = [[IoTOutputSelectorAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.outputSelectorAttr unpack:wrapped];
            NSArray<NSString *> *newSelectors = [self.state.outputSelectorAttr.activeOutputs copy];
            if (canSend && ![oldSelectors isEqual:newSelectors]) {
                [self.updatesDelegate player:self didChangeOutputSelectors:self.activeOutputSelectors];
            }
        }
            break;
        case IoTAllPlayObserveOriginMediaPlayer:
        {
            NSString *oldLeadPlayer = self.leadPlayerInGroupID;
            [self.state.mediaPlayerAttr unpack:wrapped];
            if (canSend) {
                if ([keys containsObject:[NSString stringWithUTF8String:kDisplayName]]) {
                    [self.updatesDelegate player:self didChangeDisplayName:self.displayName];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kEnabledControls]]) {
                    [self.updatesDelegate playerDidChangeEnabledControls:self];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kLoopMode]]) {
                    [self.updatesDelegate player:self didChangeLoopMode:self.loopMode];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kShuffleMode]]) {
                    [self.updatesDelegate player:self didChangeShuffleMode:self.shuffleMode];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kPlayState]]) {
                    NSArray *playStateKeys = [wrapped subKeysForKey:[NSString stringWithUTF8String:kPlayState]];
                    NSLog(@"playState Keys: %@", playStateKeys);
                    BOOL force = NO;
                    if ([playStateKeys containsObject:[NSString stringWithUTF8String:kQueuedItems]]) {
                        // PlayState does not leave "playing" while changing tracks.
                        force = YES;
                    }
                    [self handlePlayStateUpdate:force];
                    if (![oldLeadPlayer isEqualToString:self.leadPlayerInGroupID]) {
                        [self.updatesDelegate playerDidChangeLeadPlayer:self];
                    }
                    [self.updatesDelegate player:self didChangePlayState:self.playState];
                }
            }
        }
            break;
        case IoTAllPlayObserveOriginMultichannelGroupMain:
        {
            makeWeak(self)
            [self.state.multiChannelState updateWithMainAttrDiff:wrapped
                                             availabilityChanged:^(IoTHomeTheaterChannel channel) {
                makeStrongAndReturnIfNil(self);
                [self.updatesDelegate player:self homeTheaterChannelAvailabilityChanged:channel];
            } volumeChanged:^(IoTHomeTheaterChannel channel) {
                makeStrongAndReturnIfNil(self);
                [self.updatesDelegate player:self homeTheaterChannelVolumeChanged:channel];
            } changeQueue:self.backgroundQueue];
        }
            break;
        case IoTAllPlayObserveOriginMultichannelGroupSatellite:
        {
            [self.state.multiChannelState updateWithSatelliteAttrDiff:wrapped];
        }
            break;
        case IoTAllPlayObserveOriginMultichannelVolume:
        {
            [self.state.multiChannelState updateWithMCVolumeAttrDiff:wrapped
                                                       volumeChanged:^(IoTHomeTheaterChannel channel) {
                 [self.updatesDelegate player:self homeTheaterChannelVolumeChanged:channel];
            } changeQueue:self.backgroundQueue];
        }
            break;
        case IoTAllPlayObserveOriginPlaylist:
        {
            makeWeak(self)
            // What actually happened?
            [self.allPlayController getPlaylistWithCompletion:^(IoTPlaylistAttr *attr, BOOL success) {
                makeStrongAndReturnIfNil(self);
                if (!success) {
                    self.state.playlistState.playlistSupported = NO;
                } else {
                    self.state.playlistState.playlistSupported = YES;
                    self.state.playlistState.ownerInfo = attr.ownerInfo;
                    self.state.playlistState.userData = attr.userData;
                    self.state.playlistState.snapshotID = attr.snapshotId;

                    IoTPlaylistGetRangeInAttr *inAttr = [[IoTPlaylistGetRangeInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
                    inAttr.snapshotId = self.state.playlistState.snapshotID;
                    inAttr.start = 0;
                    inAttr.count = 1;
                    [self.allPlayController postPlaylistGetRangeWith:inAttr
                                                         completion:^(IoTPlaylistGetRangeOutAttr *attr, BOOL success) {
                                                             makeStrongAndReturnIfNil(self);
                                                             if (success) {
                                                                 self.state.playlistState.size = attr.totalSize;
                                                                 self.state.playlistState.snapshotID = attr.latestSnapshotId;
                                                             }
                                                             if (self.dispatchedAfterInitialFetch) {
                                                             	[self.updatesDelegate playerDidChangePlaylist:self];
                                                             }
                                                             [self refetchMediaPlayerInfo];
                                                         } completionQueue:self.backgroundQueue];
                }
            } completionQueue:self.backgroundQueue];
        }
            break;
        case IoTAllPlayObserveOriginVolumeControl:
        {
            if (!self.state.volumeControlAttr) {
                // Previously returned unsupported but now we have a notification for it?
                self.state.volumeControlAttr = [[IoTVolumeControlAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.volumeControlAttr unpack:wrapped];
            double volume = self.state.volumeControlAttr.volume;
            BOOL muted = self.state.volumeControlAttr.mute;
            if (canSend) {
                if ([keys containsObject:[NSString stringWithUTF8String:kVolume]]) {
                    [self.updatesDelegate player:self didChangeVolume:volume];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kMute]]) {
                    [self.updatesDelegate player:self didChangeMuteState:muted];
                }
            }
            // Workaround - We don't get a multichannel notification when the device volume changes and we're the only in-room device.
            if ([self isSoundBar]) {
                NSArray<NSString *> * allPlayerIDs = [self allHomeTheaterPlayerIDs];
                if ((allPlayerIDs.count == 1) &&
                    ([allPlayerIDs.firstObject isEqualToString:self.playerID])) {
                	[self refetchMultichannelVolumeInfo];
                }
            }
        }
            break;
        case IoTAllPlayObserveOriginGroupVolumeControl:
        {
            NSLog(@"*** GROUP VOLUME OBSERVER!!!");
            if (!self.state.groupVolumeControlAttr) {
                // If the player was not in a group we may have stored nil for this attribute.
                self.state.groupVolumeControlAttr = [[IoTVolumeControlAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.groupVolumeControlAttr unpack:wrapped];
            double volume = self.state.groupVolumeControlAttr.volume;
            BOOL muted = self.state.groupVolumeControlAttr.mute;
            if (canSend) {
                if ([keys containsObject:[NSString stringWithUTF8String:kVolume]]) {
                    [self.updatesDelegate player:self didChangeGroupVolume:volume];
                }
                if ([keys containsObject:[NSString stringWithUTF8String:kMute]]) {
                    [self.updatesDelegate player:self didChangeGroupMuteState:muted];
                }
            }
        }
            break;
        case IoTAllPlayObserveOriginAudioEffects:
        {
            if (!self.state.audioAdjustmentState.audioEffectsAttr) {
                // Previously returned unsupported but now we have a notification for it?
                self.state.audioAdjustmentState.audioEffectsAttr = [[IoTAudioEffectsAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.audioAdjustmentState.audioEffectsAttr unpack:wrapped];
            if (canSend) {
            	[self.updatesDelegate playerDidChangeAudioConfiguration:self];
            }
        }
            break;
        case IoTAllPlayObserveOriginBassboost:
        {
            if (!self.state.audioAdjustmentState.bassboostAttr) {
                // Previously returned unsupported but now we have a notification for it?
                self.state.audioAdjustmentState.bassboostAttr = [[IoTBassboostAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.audioAdjustmentState.bassboostAttr unpack:wrapped];
            if (canSend) {
            	[self.updatesDelegate playerDidChangeAudioConfiguration:self];
            }
            break;
        }
        case IoTAllPlayObserveOriginEqualizer:
        {
            if (!self.state.audioAdjustmentState.equalizerAttr) {
                // Previously returned unsupported but now we have a notification for it?
                self.state.audioAdjustmentState.equalizerAttr = [[IoTEqualizerAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.audioAdjustmentState.equalizerAttr unpack:wrapped];
            if (canSend) {
            	[self.updatesDelegate playerDidChangeAudioConfiguration:self];
            }
            break;
        }
        case IoTAllPlayObserveOriginPresetReverb:
        {
            if (!self.state.audioAdjustmentState.reverbAttr) {
                // Previously returned unsupported but now we have a notification for it?
                self.state.audioAdjustmentState.reverbAttr = [[IoTPresetReverbAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.audioAdjustmentState.reverbAttr unpack:wrapped];
            if (canSend) {
            	[self.updatesDelegate playerDidChangeAudioConfiguration:self];
            }
            break;
        }
        case IoTAllPlayObserveOriginDolby: {
            if (!self.state.audioAdjustmentState.dolbyAttr) {
                // Previously returned unsupported but now we have a notification for it?
                self.state.audioAdjustmentState.dolbyAttr = [[IoTDolbyAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.audioAdjustmentState.dolbyAttr unpack:wrapped];
            if (canSend) {
                [self.updatesDelegate playerDidChangeAudioConfiguration:self];
            }
            break;
        }
        case IoTAllPlayObserveOriginVirtualXSoundX: {
            if (!self.state.audioAdjustmentState.virtualXAttr) {
                // Previously returned unsupported but now we have a notification for it?
                self.state.audioAdjustmentState.virtualXAttr = [[IoTVirtualXSoundXAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.audioAdjustmentState.virtualXAttr unpack:wrapped];
            if (canSend) {
                [self.updatesDelegate playerDidChangeAudioConfiguration:self];
            }
            break;
        }
        case IoTAllPlayObserveOriginEffectsTrumpet: {
            if (!self.state.audioAdjustmentState.trumpetAttr) {
                // Previously returned unsupported but now we have a notification for it?
                self.state.audioAdjustmentState.trumpetAttr = [[IoTEffectsTrumpetAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.audioAdjustmentState.trumpetAttr unpack:wrapped];
            if (canSend) {
                [self.updatesDelegate playerDidChangeAudioConfiguration:self];
            }
            break;
        }
        case IoTAllPlayObserveOriginMeloD:
            if (!self.state.audioAdjustmentState.meloDAttr) {
                // Previously returned unsupported but now we have a notification for it?
                self.state.audioAdjustmentState.meloDAttr = [[IoTMeloDAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
            }
            [self.state.audioAdjustmentState.meloDAttr unpack:wrapped];
            if (canSend) {
                [self.updatesDelegate playerDidChangeAudioConfiguration:self];
            }
            break;
        case IoTAllPlayObserveOriginCapabilities:
            break;
    }
}

#pragma mark - Home Theater

- (BOOL)homeTheaterSupported
{
    return self.state.multiChannelState.isSoundbar || self.state.multiChannelState.isSatellite;
}

- (BOOL)isSoundBar
{
    return self.state.multiChannelState.isSoundbar;
}

- (BOOL)isSatellite
{
    return self.state.multiChannelState.isSatellite;
}

- (BOOL)haveHomeTheaterChannel:(IoTHomeTheaterChannel)channel
{
    return [[self.state.multiChannelState devicesForChannel:channel] count] > 0;
}

- (BOOL)isHomeTheaterChannelAvailable:(IoTHomeTheaterChannel)channel
{
    for (IoTMultichannelChannelDevice *device in [self.state.multiChannelState devicesForChannel:channel]) {
        if (device.connected) {
            return YES;
        }
    }
    return NO;
}

- (BOOL)haveRemovableHomeTheaterChannel:(IoTHomeTheaterChannel)channel
{
    for (IoTMultichannelChannelDevice *device in [self.state.multiChannelState devicesForChannel:channel]) {
        if (device.removable) {
            return YES;
        }
    }
    return NO;
}

/**
 @brief The player IDs for all speakers involved in the MLAN.
 */
- (NSArray<NSString *> *)allHomeTheaterPlayerIDs
{
    return [self.state.multiChannelState playerIDsForAllChannels];
}

- (NSArray<IoTMultichannelChannelDevice *> *)homeTheaterChannelDevices:(IoTHomeTheaterChannel)channel
{
    return [self.state.multiChannelState devicesForChannel:channel];
}

- (void)setHomeTheaterChannel:(IoTHomeTheaterChannel)channel
                     playerID:(NSString *)playerId
                       volume:(double)volume
                   completion:(nullable IoTCompletionHandler)completion
              completionQueue:(nullable dispatch_queue_t)queue
{
    if (!playerId) {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
        return;
    }
    
    IoTMultichannelMembersSetAttr *changedMember = [[IoTMultichannelMembersSetAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    changedMember.deviceId = playerId;
    changedMember.volume = volume;
    changedMember.mute = NO;
    
    IoTMultichannelVolumeControlInAttr *inAttr = [[IoTMultichannelVolumeControlInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    inAttr.changedMembers = @[changedMember];
    
    
    BOOL success = [self.allPlayController postMultichannelVolumeWith:inAttr
                                                           completion:^(BOOL success) {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
    } completionQueue:self.backgroundQueue];
    
    if (!success && completion && queue) {
        dispatch_async(queue, ^{
            completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
        });
    }
}

- (void)addHomeTheaterChannels:(NSArray<NSNumber *> *)channels
                        player:(IoTPlayer *)player
                    completion:(nullable IoTCompletionHandler)completion
               completionQueue:(nullable dispatch_queue_t)queue
{
    NSMutableSet<NSNumber *> *mappedSet = [NSMutableSet set];
    for (NSNumber *channelNum in channels) {
        IoTHomeTheaterChannel channel = [channelNum integerValue];
        AttrChannelMapNumber attrChannel = [self.state.multiChannelState attrChannelMapForHomeTheaterChannel:channel];

        if (attrChannel == AttrChannelMapNumberNone) {
            // No such supported channel
            if (completion && queue) {
                dispatch_async(queue, ^{
                    completion(NO, [NSError errorWithIoTError:IoTErrorNotSupported description:nil]);
                });
            }
            return;
        }

        [mappedSet addObject:@(attrChannel)];
    }

    NSArray<NSNumber *> *mapped = [mappedSet allObjects]; // get uniques

    if (mapped.count == 0) {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
        return;
    }

    IoTAddConfiguredDevicesPlayersAttr *playerAttr = [[IoTAddConfiguredDevicesPlayersAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    playerAttr.channelMap = mapped;
    playerAttr.deviceId = player.playerID;

    IoTAddConfiguredDevicesInAttr *inAttr = [[IoTAddConfiguredDevicesInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    inAttr.players = @[playerAttr];

    BOOL success = [self.allPlayController postAddConfiguredDevicesWith:inAttr
                                                             completion:^(IoTAddConfiguredDevicesOutAttr *outAttr, BOOL success) {
                                                                 if (completion && queue) {
                                                                     dispatch_async(queue, ^{
                                                                         completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                                     });
                                                                 }
                                                             } completionQueue:self.backgroundQueue];
    if (!success && completion && queue) {
        dispatch_async(queue, ^{
            completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
        });
    }
}

- (void)removeHomeTheaterChannels:(NSArray<NSNumber *> *)channels
                       completion:(nullable IoTCompletionHandler)completion
                  completionQueue:(nullable dispatch_queue_t)queue
{
    NSMutableSet<NSString *> *mappedSet = [NSMutableSet set];
    for (NSNumber *channelNum in channels) {
        IoTHomeTheaterChannel channel = [channelNum integerValue];
        NSArray<IoTMultichannelChannelDevice *> *deviceArray = [self.state.multiChannelState devicesForChannel:channel];
        for (IoTMultichannelChannelDevice *device in deviceArray) {
            if (device.removable && device.playerID) {
                [mappedSet addObject:device.playerID];
            }
        }

    }

    NSArray<NSString *> *mapped = [mappedSet allObjects]; // get uniques

    if (mapped.count == 0) {
        if (completion && queue) {
            dispatch_async(queue, ^{
                completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
            });
        }
        return;
    }

    IoTRemoveDevicesInAttr *inAttr = [[IoTRemoveDevicesInAttr alloc] initWithStateSyncQueue:self.stateSyncQueue];
    inAttr.deviceIdList = mapped;

    BOOL success = [self.allPlayController postRemoveDevicesWith:inAttr
                                                      completion:^(BOOL success) {
                                                          if (completion && queue) {
                                                              dispatch_async(queue, ^{
                                                                  completion(success, success ? nil : [NSError errorWithIoTError:IoTErrorRequest description:nil]);
                                                              });
                                                          }
                                                      } completionQueue:self.backgroundQueue];

    if (!success && completion && queue) {
        dispatch_async(queue, ^{
            completion(NO, [NSError errorWithIoTError:IoTErrorRequest description:nil]);
        });
    }
}

@end
