#import "AviationEngineRegistry.h"

#import "aviation_types.h"

// The guard vocabulary is public API; the C command enum is not. Keep them
// in step at compile time.
_Static_assert((uint32_t)AviationGuardedCommandLoad == (uint32_t)AVIATION_CMD_DECODER_LOAD, "guard/command drift: load");
_Static_assert((uint32_t)AviationGuardedCommandPlay == (uint32_t)AVIATION_CMD_DECODER_PLAY, "guard/command drift: play");
_Static_assert((uint32_t)AviationGuardedCommandPause == (uint32_t)AVIATION_CMD_DECODER_PAUSE, "guard/command drift: pause");
_Static_assert((uint32_t)AviationGuardedCommandSeek == (uint32_t)AVIATION_CMD_DECODER_SEEK, "guard/command drift: seek");
_Static_assert((uint32_t)AviationGuardedCommandStop == (uint32_t)AVIATION_CMD_DECODER_STOP, "guard/command drift: stop");

// Same for the state vocabulary an output reports back.
_Static_assert((int32_t)AviationOutputStateIdle == (int32_t)AVIATION_DECODER_IDLE, "output/decoder state drift: idle");
_Static_assert((int32_t)AviationOutputStateLoading == (int32_t)AVIATION_DECODER_LOADING, "output/decoder state drift: loading");
_Static_assert((int32_t)AviationOutputStateReady == (int32_t)AVIATION_DECODER_READY, "output/decoder state drift: ready");
_Static_assert((int32_t)AviationOutputStateBuffering == (int32_t)AVIATION_DECODER_BUFFERING, "output/decoder state drift: buffering");
_Static_assert((int32_t)AviationOutputStateEnded == (int32_t)AVIATION_DECODER_ENDED, "output/decoder state drift: ended");
_Static_assert((int32_t)AviationOutputStateError == (int32_t)AVIATION_DECODER_ERROR, "output/decoder state drift: error");
_Static_assert((int32_t)AviationOutputStatePaused == (int32_t)AVIATION_DECODER_PAUSED, "output/decoder state drift: paused");
_Static_assert((int32_t)AviationOutputStatePlaying == (int32_t)AVIATION_DECODER_PLAYING, "output/decoder state drift: playing");

static NSHashTable<id<AviationVideoViewPlugin>> *_videoViewPlugins = nil;
static NSMutableDictionary<NSString *, AviationEngineAttachment *> *_engineAttachments = nil;
static BOOL _recorderPauseActive = NO;

@implementation AviationEngineAttachment

- (instancetype)initWithEngineId:(NSString *)engineId {
    self = [super init];
    if (self) {
        _engineId = [engineId copy] ?: @"";
    }
    return self;
}

- (void)installCommandGuardWithOwner:(NSObject *)owner block:(AviationCommandGuardBlock)block {
    if (_installCommandGuardBlock) _installCommandGuardBlock(owner, block);
}

- (void)clearCommandGuardWithOwner:(NSObject *)owner {
    if (_clearCommandGuardBlock) _clearCommandGuardBlock(owner);
}

- (void)installPlaybackOutputWithOwner:(NSObject *)owner output:(id<AviationPlaybackOutput>)output {
    if (_installPlaybackOutputBlock) _installPlaybackOutputBlock(owner, output);
}

- (void)clearPlaybackOutputWithOwner:(NSObject *)owner {
    if (_clearPlaybackOutputBlock) _clearPlaybackOutputBlock(owner);
}

- (void)reportOutputTimeWithCurrentMs:(double)currentMs
                           durationMs:(double)durationMs
                      seekableStartMs:(double)seekableStartMs
                        seekableEndMs:(double)seekableEndMs
                               isLive:(BOOL)isLive {
    if (_reportOutputTimeBlock) {
        _reportOutputTimeBlock(currentMs, durationMs, seekableStartMs, seekableEndMs, isLive);
    }
}

- (void)reportOutputState:(int32_t)decoderState {
    if (_reportOutputStateBlock) _reportOutputStateBlock(decoderState);
}

- (void)reportOutputError:(NSString *)message code:(NSString *)code {
    if (_reportOutputErrorBlock) _reportOutputErrorBlock(message, code);
}

- (uint32_t)subscribeToDomainEvents:(void (^)(int32_t type, int32_t queueIndex))callback {
    if (!_domainEventSubscribeBlock) return 0;
    return _domainEventSubscribeBlock(callback);
}

- (void)unsubscribeDomainEvent:(uint32_t)subscriptionId {
    if (_domainEventUnsubscribeBlock) _domainEventUnsubscribeBlock(subscriptionId);
}

- (AviationStateChangeUnsubscribeBlock)subscribeToStateChanges:(void (^)(int32_t state))callback {
    if (!_stateChangeSubscribeBlock) return nil;
    return _stateChangeSubscribeBlock(callback);
}

@end

@implementation AviationVideoViewAttachment

- (instancetype)initWithPlayer:(AVPlayer *)player
                       engineId:(NSString *)engineId
                       adTagUrl:(NSString *)adTagUrl {
    self = [super initWithEngineId:engineId];
    if (self) {
        _player = player;
        _adTagUrl = [adTagUrl copy] ?: @"";
    }
    return self;
}

- (void)contentPauseRequested {
    if (_contentPauseBlock) _contentPauseBlock();
}

- (void)contentResumeRequested {
    if (_contentResumeBlock) _contentResumeBlock();
}

- (void)setNowPlayingAdModeWithTitle:(NSString *)title durationSeconds:(double)durationSeconds {
    if (_setAdModeBlock) _setAdModeBlock(title, durationSeconds);
}

- (void)clearNowPlayingAdMode {
    if (_clearAdModeBlock) _clearAdModeBlock();
}

- (void)emitAdDomainEventWithType:(int32_t)type
                       breakIndex:(int32_t)breakIndex
                       breakTimeMs:(double)breakTimeMs
                   totalAdsInBreak:(int32_t)totalAdsInBreak
                              adId:(NSString *)adId
                             title:(NSString *)title
                        durationMs:(double)durationMs
                         currentMs:(double)currentMs
                    adIndexInBreak:(int32_t)adIndexInBreak
                 adTotalAdsInBreak:(int32_t)adTotalAdsInBreak
                       isSkippable:(BOOL)isSkippable
                      skipOffsetMs:(double)skipOffsetMs
                      errorMessage:(NSString *)errorMessage {
    if (!_emitAdDomainEventBlock) return;
    _emitAdDomainEventBlock(
        type,
        breakIndex,
        breakTimeMs,
        totalAdsInBreak,
        adId,
        title,
        durationMs,
        currentMs,
        adIndexInBreak,
        adTotalAdsInBreak,
        isSkippable,
        skipOffsetMs,
        errorMessage
    );
}

@end

@implementation AviationPauseReceipt
- (instancetype)initWithEngineId:(NSString *)engineId shouldResume:(BOOL)shouldResume {
    self = [super init];
    if (self) {
        _engineId = [engineId copy];
        _shouldResume = shouldResume;
    }
    return self;
}
@end

@implementation AviationEngineRegistry

+ (void)registerVideoViewPlugin:(id<AviationVideoViewPlugin>)plugin {
    @synchronized(self) {
        if (!_videoViewPlugins) {
            _videoViewPlugins = [NSHashTable weakObjectsHashTable];
        }
        [_videoViewPlugins addObject:plugin];
    }
}

+ (void)unregisterVideoViewPlugin:(id<AviationVideoViewPlugin>)plugin {
    @synchronized(self) {
        [_videoViewPlugins removeObject:plugin];
    }
}

+ (void)notifyVideoViewAttachedWithAttachment:(AviationVideoViewAttachment *)attachment
                                    container:(UIView *)container
                               viewController:(UIViewController *)viewController {
    NSArray<id<AviationVideoViewPlugin>> *plugins = nil;
    @synchronized(self) {
        plugins = [_videoViewPlugins allObjects];
    }
    for (id<AviationVideoViewPlugin> plugin in plugins) {
        [plugin onVideoViewAttachedWithAttachment:attachment container:container viewController:viewController];
    }
}

+ (void)registerEngineAttachment:(AviationEngineAttachment *)attachment {
    if (attachment.engineId.length == 0) return;
    @synchronized(self) {
        if (!_engineAttachments) {
            _engineAttachments = [NSMutableDictionary dictionary];
        }
        _engineAttachments[attachment.engineId] = attachment;
    }
}

+ (void)unregisterEngineAttachmentForEngineId:(NSString *)engineId {
    if (engineId.length == 0) return;
    @synchronized(self) {
        [_engineAttachments removeObjectForKey:engineId];
    }
}

+ (NSArray<AviationPauseReceipt *> *)pausePlayingEngines {
    NSMutableArray<AviationPauseReceipt *> *receipts = [NSMutableArray array];
    _recorderPauseActive = YES;
    @synchronized(self) {
        for (AviationEngineAttachment *attachment in _engineAttachments.allValues) {
            NSNumber *shouldResume = attachment.recorderPauseBlock
                ? attachment.recorderPauseBlock()
                : nil;
            if (!shouldResume) continue; // transport wasn't playing
            [receipts addObject:[[AviationPauseReceipt alloc]
                initWithEngineId:attachment.engineId
                    shouldResume:shouldResume.boolValue]];
        }
    }
    return receipts;
}

+ (void)resumeEngines:(NSArray<AviationPauseReceipt *> *)receipts {
    for (AviationPauseReceipt *receipt in receipts) {
        // Policy was snapshotted at pause time; a 'pause'/'ignore' engine
        // must not be resumed here regardless of its current mode.
        if (!receipt.shouldResume) continue;
        AviationEngineAttachment *attachment = nil;
        @synchronized(self) {
            attachment = _engineAttachments[receipt.engineId];
        }
        if (!attachment || !attachment.recorderResumeBlock) continue;
        attachment.recorderResumeBlock();
    }
    _recorderPauseActive = NO;
}

+ (BOOL)isRecorderPauseActive { return _recorderPauseActive; }
+ (void)setRecorderPauseActive:(BOOL)active { _recorderPauseActive = active; }

+ (AviationEngineAttachment *)attachmentForEngineId:(NSString *)engineId {
    if (engineId.length == 0) return nil;
    @synchronized(self) {
        return _engineAttachments[engineId];
    }
}

+ (void)notifyVideoViewDetached {
    NSArray<id<AviationVideoViewPlugin>> *plugins = nil;
    @synchronized(self) {
        plugins = [_videoViewPlugins allObjects];
    }
    for (id<AviationVideoViewPlugin> plugin in plugins) {
        [plugin onVideoViewDetached];
    }
}

@end
