import { EventEmitter, Subscription, Platform } from '@unimodules/core';
import { PermissionResponse, PermissionStatus } from 'unimodules-permissions-interface';

import {
  _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS,
  AVPlaybackStatus,
  AVPlaybackStatusToSet,
} from '../AV';
import ExponentAV from '../ExponentAV';
import { isAudioEnabled, throwIfAudioIsDisabled } from './AudioAvailability';
import { Sound } from './Sound';

export type RecordingOptions = {
  android: {
    extension: string;
    outputFormat: number;
    audioEncoder: number;
    sampleRate?: number;
    numberOfChannels?: number;
    bitRate?: number;
    maxFileSize?: number;
  };
  ios: {
    extension: string;
    outputFormat?: string | number;
    audioQuality: number;
    sampleRate: number;
    numberOfChannels: number;
    bitRate: number;
    bitRateStrategy?: number;
    bitDepthHint?: number;
    linearPCMBitDepth?: number;
    linearPCMIsBigEndian?: boolean;
    linearPCMIsFloat?: boolean;
  };
};

// TODO: consider changing these to enums
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_DEFAULT = 0;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_THREE_GPP = 1;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4 = 2;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AMR_NB = 3;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AMR_WB = 4;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADIF = 5;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADTS = 6;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_RTP_AVP = 7;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG2TS = 8;
export const RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_WEBM = 9;

export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_DEFAULT = 0;
export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_NB = 1;
export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_WB = 2;
export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC = 3;
export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_HE_AAC = 4;
export const RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC_ELD = 5;

export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_LINEARPCM = 'lpcm';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AC3 = 'ac-3';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_60958AC3 = 'cac3';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_APPLEIMA4 = 'ima4';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC = 'aac ';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4CELP = 'celp';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4HVXC = 'hvxc';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4TWINVQ = 'twvq';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MACE3 = 'MAC3';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MACE6 = 'MAC6';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ULAW = 'ulaw';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ALAW = 'alaw';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QDESIGN = 'QDMC';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QDESIGN2 = 'QDM2';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_QUALCOMM = 'Qclp';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER1 = '.mp1';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER2 = '.mp2';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEGLAYER3 = '.mp3';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_APPLELOSSLESS = 'alac';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_HE = 'aach';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_LD = 'aacl';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD = 'aace';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD_SBR = 'aacf';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_ELD_V2 = 'aacg';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_HE_V2 = 'aacp';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC_SPATIAL = 'aacs';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AMR = 'samr';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AMR_WB = 'sawb';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AUDIBLE = 'AUDB';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ILBC = 'ilbc';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_DVIINTELIMA = 0x6d730011;
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_MICROSOFTGSM = 0x6d730031;
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_AES3 = 'aes3';
export const RECORDING_OPTION_IOS_OUTPUT_FORMAT_ENHANCEDAC3 = 'ec-3';

export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MIN = 0;
export const RECORDING_OPTION_IOS_AUDIO_QUALITY_LOW = 0x20;
export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MEDIUM = 0x40;
export const RECORDING_OPTION_IOS_AUDIO_QUALITY_HIGH = 0x60;
export const RECORDING_OPTION_IOS_AUDIO_QUALITY_MAX = 0x7f;

export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_CONSTANT = 0;
export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_LONG_TERM_AVERAGE = 1;
export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_VARIABLE_CONSTRAINED = 2;
export const RECORDING_OPTION_IOS_BIT_RATE_STRATEGY_VARIABLE = 3;

// TODO : maybe make presets for music and speech, or lossy / lossless.

export const RECORDING_OPTIONS_PRESET_HIGH_QUALITY: RecordingOptions = {
  android: {
    extension: '.m4a',
    outputFormat: RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_MPEG_4,
    audioEncoder: RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC,
    sampleRate: 44100,
    numberOfChannels: 2,
    bitRate: 128000,
  },
  ios: {
    extension: '.caf',
    audioQuality: RECORDING_OPTION_IOS_AUDIO_QUALITY_MAX,
    sampleRate: 44100,
    numberOfChannels: 2,
    bitRate: 128000,
    linearPCMBitDepth: 16,
    linearPCMIsBigEndian: false,
    linearPCMIsFloat: false,
  },
};

export const RECORDING_OPTIONS_PRESET_LOW_QUALITY: RecordingOptions = {
  android: {
    extension: '.3gp',
    outputFormat: RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_THREE_GPP,
    audioEncoder: RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AMR_NB,
    sampleRate: 44100,
    numberOfChannels: 2,
    bitRate: 128000,
  },
  ios: {
    extension: '.caf',
    audioQuality: RECORDING_OPTION_IOS_AUDIO_QUALITY_MIN,
    sampleRate: 44100,
    numberOfChannels: 2,
    bitRate: 128000,
    linearPCMBitDepth: 16,
    linearPCMIsBigEndian: false,
    linearPCMIsFloat: false,
  },
};

// TODO: For consistency with PlaybackStatus, should we include progressUpdateIntervalMillis here as
// well?
export type RecordingStatus = {
  canRecord: boolean;
  isRecording: boolean;
  isDoneRecording: boolean;
  durationMillis: number;
};

export { PermissionResponse, PermissionStatus };

let _recorderExists: boolean = false;
const eventEmitter = Platform.OS === 'android' ? new EventEmitter(ExponentAV) : null;

export async function getPermissionsAsync(): Promise<PermissionResponse> {
  return ExponentAV.getPermissionsAsync();
}

export async function requestPermissionsAsync(): Promise<PermissionResponse> {
  return ExponentAV.requestPermissionsAsync();
}

export class Recording {
  _subscription: Subscription | null = null;
  _canRecord: boolean = false;
  _isDoneRecording: boolean = false;
  _finalDurationMillis: number = 0;
  _uri: string | null = null;
  _onRecordingStatusUpdate: ((status: RecordingStatus) => void) | null = null;
  _progressUpdateTimeoutVariable: number | null = null;
  _progressUpdateIntervalMillis: number = _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS;
  _options: RecordingOptions | null = null;

  // Internal methods

  _cleanupForUnloadedRecorder = async (finalStatus: RecordingStatus) => {
    this._canRecord = false;
    this._isDoneRecording = true;
    // $FlowFixMe(greg): durationMillis is not always defined
    this._finalDurationMillis = finalStatus.durationMillis;
    _recorderExists = false;
    if (this._subscription) {
      this._subscription.remove();
      this._subscription = null;
    }
    this._disablePolling();
    return await this.getStatusAsync(); // Automatically calls onRecordingStatusUpdate for the final state.
  };

  _pollingLoop = async () => {
    if (isAudioEnabled() && this._canRecord && this._onRecordingStatusUpdate != null) {
      this._progressUpdateTimeoutVariable = setTimeout(
        this._pollingLoop,
        this._progressUpdateIntervalMillis
      ) as any;
      try {
        await this.getStatusAsync();
      } catch (error) {
        this._disablePolling();
      }
    }
  };

  _disablePolling() {
    if (this._progressUpdateTimeoutVariable != null) {
      clearTimeout(this._progressUpdateTimeoutVariable);
      this._progressUpdateTimeoutVariable = null;
    }
  }

  _enablePollingIfNecessaryAndPossible() {
    if (isAudioEnabled() && this._canRecord && this._onRecordingStatusUpdate != null) {
      this._disablePolling();
      this._pollingLoop();
    }
  }

  _callOnRecordingStatusUpdateForNewStatus(status: RecordingStatus) {
    if (this._onRecordingStatusUpdate != null) {
      this._onRecordingStatusUpdate(status);
    }
  }

  async _performOperationAndHandleStatusAsync(
    operation: () => Promise<RecordingStatus>
  ): Promise<RecordingStatus> {
    throwIfAudioIsDisabled();
    if (this._canRecord) {
      const status = await operation();
      this._callOnRecordingStatusUpdateForNewStatus(status);
      return status;
    } else {
      throw new Error('Cannot complete operation because this recorder is not ready to record.');
    }
  }

  // Note that all calls automatically call onRecordingStatusUpdate as a side effect.

  // Get status API

  getStatusAsync = async (): Promise<RecordingStatus> => {
    // Automatically calls onRecordingStatusUpdate.
    if (this._canRecord) {
      return this._performOperationAndHandleStatusAsync(() => ExponentAV.getAudioRecordingStatus());
    }
    const status = {
      canRecord: false,
      isRecording: false,
      isDoneRecording: this._isDoneRecording,
      durationMillis: this._finalDurationMillis,
    };
    this._callOnRecordingStatusUpdateForNewStatus(status);
    return status;
  };

  setOnRecordingStatusUpdate(onRecordingStatusUpdate: ((status: RecordingStatus) => void) | null) {
    this._onRecordingStatusUpdate = onRecordingStatusUpdate;
    if (onRecordingStatusUpdate == null) {
      this._disablePolling();
    } else {
      this._enablePollingIfNecessaryAndPossible();
    }
    this.getStatusAsync();
  }

  setProgressUpdateInterval(progressUpdateIntervalMillis: number) {
    this._progressUpdateIntervalMillis = progressUpdateIntervalMillis;
    this.getStatusAsync();
  }

  // Record API

  async prepareToRecordAsync(
    options: RecordingOptions = RECORDING_OPTIONS_PRESET_LOW_QUALITY
  ): Promise<RecordingStatus> {
    throwIfAudioIsDisabled();

    if (_recorderExists) {
      throw new Error('Only one Recording object can be prepared at a given time.');
    }

    if (this._isDoneRecording) {
      throw new Error('This Recording object is done recording; you must make a new one.');
    }

    if (!options || !options.android || !options.ios) {
      throw new Error(
        'You must provide recording options for android and ios in order to prepare to record.'
      );
    }

    const extensionRegex = /^\.\w+$/;
    if (
      !options.android.extension ||
      !options.ios.extension ||
      !extensionRegex.test(options.android.extension) ||
      !extensionRegex.test(options.ios.extension)
    ) {
      throw new Error(`Your file extensions must match ${extensionRegex.toString()}.`);
    }

    if (!this._canRecord) {
      if (eventEmitter) {
        this._subscription = eventEmitter.addListener(
          'Expo.Recording.recorderUnloaded',
          this._cleanupForUnloadedRecorder
        );
      }

      const {
        uri,
        status,
      }: {
        uri: string;
        // status is of type RecordingStatus, but without the canRecord field populated
        status: Pick<RecordingStatus, Exclude<keyof RecordingStatus, 'canRecord'>>;
      } = await ExponentAV.prepareAudioRecorder(options);

      _recorderExists = true;
      this._uri = uri;
      this._options = options;
      this._canRecord = true;

      const currentStatus = { ...status, canRecord: true };
      this._callOnRecordingStatusUpdateForNewStatus(currentStatus);
      this._enablePollingIfNecessaryAndPossible();
      return currentStatus;
    } else {
      throw new Error('This Recording object is already prepared to record.');
    }
  }

  async startAsync(): Promise<RecordingStatus> {
    return this._performOperationAndHandleStatusAsync(() => ExponentAV.startAudioRecording());
  }

  async pauseAsync(): Promise<RecordingStatus> {
    return this._performOperationAndHandleStatusAsync(() => ExponentAV.pauseAudioRecording());
  }

  async stopAndUnloadAsync(): Promise<RecordingStatus> {
    if (!this._canRecord) {
      if (this._isDoneRecording) {
        throw new Error('Cannot unload a Recording that has already been unloaded.');
      } else {
        throw new Error('Cannot unload a Recording that has not been prepared.');
      }
    }
    // We perform a separate native API call so that the state of the Recording can be updated with
    // the final duration of the recording. (We cast stopStatus as Object to appease Flow)
    const finalStatus = await ExponentAV.stopAudioRecording();
    await ExponentAV.unloadAudioRecorder();
    return this._cleanupForUnloadedRecorder(finalStatus);
  }

  // Read API

  getURI(): string | null {
    return this._uri;
  }

  async createNewLoadedSound(
    initialStatus: AVPlaybackStatusToSet = {},
    onPlaybackStatusUpdate: ((status: AVPlaybackStatus) => void) | null = null
  ): Promise<{ sound: Sound; status: AVPlaybackStatus }> {
    console.warn(
      `createNewLoadedSound is deprecated in favor of createNewLoadedSoundAsync, which has the same API aside from the method name`
    );
    return this.createNewLoadedSoundAsync(initialStatus, onPlaybackStatusUpdate);
  }

  async createNewLoadedSoundAsync(
    initialStatus: AVPlaybackStatusToSet = {},
    onPlaybackStatusUpdate: ((status: AVPlaybackStatus) => void) | null = null
  ): Promise<{ sound: Sound; status: AVPlaybackStatus }> {
    if (this._uri == null || !this._isDoneRecording) {
      throw new Error('Cannot create sound when the Recording has not finished!');
    }
    return Sound.createAsync(
      // $FlowFixMe: Flow can't distinguish between this literal and Asset
      { uri: this._uri },
      initialStatus,
      onPlaybackStatusUpdate,
      false
    );
  }
}
