#import <React/RCTLog.h>

#import "ReactNativeAudio.h"
#import "RNAudioException.h"
#import "RNAInputAudioStream.h"
#import "RNASamplePlayer.h"

@implementation ReactNativeAudio {
  NSMutableDictionary<NSNumber*,RNAInputAudioStream*> *inputStreams;
  NSMutableDictionary<NSNumber*,RNASamplePlayer*> *samplePlayers;
}

- (id) init {
  if (self = [super init]) {
    inputStreams = [NSMutableDictionary new];
    samplePlayers = [NSMutableDictionary new];
  }
  return self;
}

- (facebook::react::ModuleConstants<JS::NativeReactNativeAudio::Constants>) constantsToExport {
  return facebook::react::typedConstants<JS::NativeReactNativeAudio::Constants>({
    .AUDIO_FORMAT_PCM_8BIT = PCM_8BIT,
    .AUDIO_FORMAT_PCM_16BIT = PCM_16BIT,
    .AUDIO_FORMAT_PCM_FLOAT = PCM_FLOAT,
    .CHANNEL_IN_MONO = MONO,
    .CHANNEL_IN_STEREO = STEREO,
    .IS_MAC_CATALYST = TARGET_OS_MACCATALYST,
    .AUDIO_SOURCE_DEFAULT = DEFAULT,
    .AUDIO_SOURCE_MIC = MIC,
    .AUDIO_SOURCE_UNPROCESSED = UNPROCESSED
  });
}

- (facebook::react::ModuleConstants<JS::NativeReactNativeAudio::Constants>) getConstants {
  return [self constantsToExport];
}

- (void) getInputAvailable:(RCTPromiseResolveBlock)resolve
  reject:(RCTPromiseRejectBlock)reject
{
  resolve([NSNumber numberWithBool: AVAudioSession.sharedInstance.inputAvailable]);
}

/**
 *  Creates a dedicated queue for module operations.
 */
- (dispatch_queue_t)methodQueue
{
  return dispatch_queue_create("studio.pogodin.react_native_audio", DISPATCH_QUEUE_SERIAL);
}

// TODO: Should we somehow plug-in this audio system configuration into
// AudioStream initialization, and base it on the "audioSource" parameter,
// which is now ignored on iOS?
- (void) configAudioSystem:(RCTPromiseResolveBlock)resolve
  reject:(RCTPromiseRejectBlock)reject
{
  RCTLogInfo(@"Audio session configuration...");

  AVAudioSession *audioSession = AVAudioSession.sharedInstance;
  NSArray<AVAudioSessionCategory> *cats = audioSession.availableCategories;

  AVAudioSessionCategory category;
  if ([cats containsObject:AVAudioSessionCategoryPlayAndRecord]) {
    category = AVAudioSessionCategoryPlayAndRecord;
  } else if ([cats containsObject:AVAudioSessionCategoryPlayback]) {
    category = AVAudioSessionCategoryPlayback;
  } else {
    reject(@"incompatible_audio_session",
           @"neither play-and-record, nor playback category is supported",
           nil);
    return;
  }

  NSError *error = nil;

  AVAudioSessionCategoryOptions options =
    AVAudioSessionCategoryOptionAllowBluetooth |
    AVAudioSessionCategoryOptionAllowBluetoothA2DP |
    AVAudioSessionCategoryOptionDefaultToSpeaker;

  // NOTE: The AVAudioSessionCategoryOptionOverrideMutedMicrophoneInterruption
  // option triggers an error if one attempts to set it for a category that does
  // not support audio input. For categories that do support it, it allows for
  // simultaneous playback and audio input.
  if (category == AVAudioSessionCategoryPlayAndRecord) {
    if (@available(iOS 14.5, *)) {
      options |= AVAudioSessionCategoryOptionOverrideMutedMicrophoneInterruption;
    }
  }

  BOOL res = [audioSession setCategory:category
                           withOptions:options
                                 error:&error];

  // TODO: Currently here, and in the next rejection, although we include
  // error object, the details of error we get to sentry are minimal, should
  // be investigated, and checked how do we pass all available details to
  // Sentry?

  // TODO: Probably, need to provide additional details here. At least
  // add some error ID, to determine where exactly do errors are thrown.
  if (res != YES || error != nil) {
    reject(error.domain, error.localizedDescription, error);
    return;
  }

  res = [audioSession setActive:YES error:&error];
  if (res != YES || error != nil) {
    reject(error.domain, error.localizedDescription, error);
    return;
  }

  resolve(nil);
}

// NOTE: Can't use enum as the argument type here, as RN won't understand that.
- (void) listen:(double)streamId
  audioSource:(double)audioSource
  sampleRate:(double)sampleRate
  channelConfig:(double)channelConfig
  audioFormat:(double)audioFormat
  samplingSize:(double)samplingSize
  resolve:(RCTPromiseResolveBlock) resolve
  reject:(RCTPromiseRejectBlock) reject
{
  NSNumber *sid = [NSNumber numberWithDouble:streamId];

  OnChunk onChunk = ^void(int chunkId, unsigned char *chunk, int size) {
    RCTLogInfo(@"[Stream %@] Audio data chunk %d received", sid, chunkId);
    NSData* data = [NSData dataWithBytesNoCopy:chunk
                                        length:size
                                  freeWhenDone:NO];
    [self emitOnAudioChunk: @{@"streamId":sid,
                              @"chunkId":@(chunkId),
                              @"data":[data base64EncodedStringWithOptions:0]}];
  };
  
  OnError onError = ^void(NSString* error) {
    [self emitOnInputAudioStreamError:@{@"streamId":sid, @"error":error}];
  };
  
  RNAInputAudioStream *stream =
  [RNAInputAudioStream streamAudioSource:(AUDIO_SOURCES)audioSource
                              sampleRate:sampleRate
                           channelConfig:(CHANNEL_CONFIGS)channelConfig
                             audioFormat:(AUDIO_FORMATS)audioFormat
                            samplingSize:samplingSize
                                 onChunk:onChunk
                                 onError:onError];
  
  inputStreams[sid] = stream;
  
  resolve(nil);
}

- (void) unlisten:(double)streamId
  resolve:(RCTPromiseResolveBlock) resolve
  reject:(RCTPromiseRejectBlock) reject
{
  NSNumber *id = [NSNumber numberWithDouble:streamId];
  [inputStreams[id] stop];
  [inputStreams removeObjectForKey:id];
  RCTLogInfo(@"[Stream %@] Is unlistened", id);
  resolve(nil);
}

- (void) muteInputStream:(double)streamId muted:(BOOL)muted {
  inputStreams[[NSNumber numberWithDouble:streamId]].muted = muted;
}

- (void) destroySamplePlayer:(double)playerId
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject
{
  NSNumber *id = [NSNumber numberWithDouble:playerId];
  if (samplePlayers[id] == nil) {
    [RNAudioException UNKNOWN_PLAYER_ID:reject];
    return;
  }

  [samplePlayers removeObjectForKey:id];
  resolve(nil);
}

- (void) initSamplePlayer:(double)playerId
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject
{
  NSNumber *id = [NSNumber numberWithDouble:playerId];
  if (samplePlayers[id] != nil) {
    [[RNAudioException INTERNAL_ERROR:0
                              details:@"Sample player ID is occupied"]
     reject:reject];
    return;
  }

  OnError onError = ^void(NSString *error) {
    [self emitOnSamplePlayerError:@{@"playerId":id, @"error":error}];
  };

  samplePlayers[id] = [RNASamplePlayer new:onError];
  resolve(nil);
}

- (void) loadSample:(double)playerId
                  sampleName:(NSString *)sampleName
                  samplePath:(NSString *)samplePath
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject
{
  NSNumber *id = [NSNumber numberWithDouble:playerId];
  RNASamplePlayer *player = samplePlayers[id];
  if (player == nil) {
    [RNAudioException UNKNOWN_PLAYER_ID:reject];
    return;
  }
  [player load:sampleName fromPath:samplePath resolve:resolve reject:reject];
}

- (void) playSample:(double)playerId
                  sampleName:(NSString *)sampleName
                  loop:(BOOL)loop
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject
{
  NSNumber *id = [NSNumber numberWithDouble:playerId];
  RNASamplePlayer *player = samplePlayers[id];
  if (player == nil) {
    [RNAudioException UNKNOWN_PLAYER_ID:reject];
    return;
  }
  [player play:sampleName loop:loop resolve:resolve reject:reject];
}

- (void) stopSample:(double)playerId
                  sampleName:(NSString *)sampleName
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject
{
  NSNumber *id = [NSNumber numberWithDouble:playerId];
  RNASamplePlayer *player = samplePlayers[id];
  if (player == nil) {
    [RNAudioException UNKNOWN_PLAYER_ID:reject];
    return;
  }
  [player stop:sampleName resolve:resolve reject:reject];
}

- (void) unloadSample:(double)playerId
                  sampleName:(NSString *)sampleName
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject
{
  NSNumber *id = [NSNumber numberWithDouble:playerId];
  RNASamplePlayer *player = samplePlayers[id];
  if (player == nil) {
    [RNAudioException UNKNOWN_PLAYER_ID:reject];
    return;
  }
  [player unload:sampleName resolve:resolve reject:reject];
}

+ (BOOL) requiresMainQueueSetup {
    return NO;
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
    (const facebook::react::ObjCTurboModule::InitParams &)params
{
    return std::make_shared<facebook::react::NativeReactNativeAudioSpecJSI>(params);
}

+ (NSString *)moduleName
{
  return @"ReactNativeAudio";
}

@end
