/**
 * ADTS parser helper
 * @link https://wiki.multimedia.cx/index.php?title=ADTS
 */
import { ErrorDetails, ErrorTypes } from '../../errors';
import { Events } from '../../events';
import { logger } from '../../utils/logger';
import type { HlsEventEmitter } from '../../events';
import type {
  AudioFrame,
  AudioSample,
  DemuxedAudioTrack,
} from '../../types/demuxer';

type AudioConfig = {
  config: [number, number] | [number, number, number, number];
  samplerate: number;
  channelCount: number;
  codec: string;
  parsedCodec: string;
  manifestCodec: string | undefined;
};

type FrameHeader = {
  headerLength: number;
  frameLength: number;
};

export function getAudioConfig(
  observer: HlsEventEmitter,
  data: Uint8Array,
  offset: number,
  manifestCodec: string | undefined,
): AudioConfig | void {
  const adtsSamplingRates = [
    96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025,
    8000, 7350,
  ];
  const byte2 = data[offset + 2];
  const adtsSamplingIndex = (byte2 >> 2) & 0xf;
  if (adtsSamplingIndex > 12) {
    const error = new Error(`invalid ADTS sampling index:${adtsSamplingIndex}`);
    observer.emit(Events.ERROR, Events.ERROR, {
      type: ErrorTypes.MEDIA_ERROR,
      details: ErrorDetails.FRAG_PARSING_ERROR,
      fatal: true,
      error,
      reason: error.message,
    });
    return;
  }
  // MPEG-4 Audio Object Type (profile_ObjectType+1)
  const adtsObjectType = ((byte2 >> 6) & 0x3) + 1;
  const channelCount = ((data[offset + 3] >> 6) & 0x3) | ((byte2 & 1) << 2);
  /* refer to http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config
      ISO/IEC 14496-3 - Table 1.13 — Syntax of AudioSpecificConfig()
    Audio Profile / Audio Object Type
    0: Null
    1: AAC Main
    2: AAC LC (Low Complexity)
    3: AAC SSR (Scalable Sample Rate)
    4: AAC LTP (Long Term Prediction)
    5: SBR (Spectral Band Replication)
    6: AAC Scalable
   sampling freq
    0: 96000 Hz
    1: 88200 Hz
    2: 64000 Hz
    3: 48000 Hz
    4: 44100 Hz
    5: 32000 Hz
    6: 24000 Hz
    7: 22050 Hz
    8: 16000 Hz
    9: 12000 Hz
    10: 11025 Hz
    11: 8000 Hz
    12: 7350 Hz
    13: Reserved
    14: Reserved
    15: frequency is written explictly
    Channel Configurations
    These are the channel configurations:
    0: Defined in AOT Specifc Config
    1: 1 channel: front-center
    2: 2 channels: front-left, front-right
  */
  // audioObjectType = profile => profile, the MPEG-4 Audio Object Type minus 1
  const samplerate = adtsSamplingRates[adtsSamplingIndex];
  let aacSampleIndex = adtsSamplingIndex;
  if (adtsObjectType === 5 || adtsObjectType === 29) {
    // HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
    // there is a factor 2 between frame sample rate and output sample rate
    // multiply frequency by 2 (see table above, equivalent to substract 3)
    aacSampleIndex -= 3;
  }
  let config: [number, number] | [number, number, number, number];
  const samplingIndexHigh = (aacSampleIndex & 0x0e) >> 1;
  const samplingIndexLowAndChannels =
    ((aacSampleIndex & 0x01) << 7) | (channelCount << 3);
  let adtsOverride = 0;
  if (adtsObjectType === 1 && adtsSamplingIndex >= 6) {
    // Handle implicit HE-AAC by adding explicit SBR signaling
    adtsOverride = 5;
    const adtsExtensionSamplingIndex = adtsSamplingIndex - 3;
    config = [
      (adtsOverride << 3) | samplingIndexHigh,
      samplingIndexLowAndChannels | ((adtsExtensionSamplingIndex & 0x0e) >> 1),
      ((adtsExtensionSamplingIndex & 0x01) << 7) | (2 << 2),
      0,
    ];
  } else {
    config = [
      (adtsObjectType << 3) | samplingIndexHigh,
      samplingIndexLowAndChannels,
    ];
  }
  const codec = 'mp4a.40.' + (adtsOverride || adtsObjectType);

  logger.log(
    `manifest codec:${manifestCodec}, parsed codec:${codec}, channels:${channelCount}, rate:${samplerate} (ADTS object type:${adtsObjectType} sampling index:${adtsSamplingIndex} esds config:[${config.join(',')}])`,
  );
  return {
    config,
    samplerate,
    channelCount,
    codec,
    parsedCodec: codec,
    manifestCodec,
  };
}

export function isHeaderPattern(data: Uint8Array, offset: number): boolean {
  return data[offset] === 0xff && (data[offset + 1] & 0xf6) === 0xf0;
}

export function getHeaderLength(data: Uint8Array, offset: number): number {
  return data[offset + 1] & 0x01 ? 7 : 9;
}

export function getFullFrameLength(data: Uint8Array, offset: number): number {
  return (
    ((data[offset + 3] & 0x03) << 11) |
    (data[offset + 4] << 3) |
    ((data[offset + 5] & 0xe0) >>> 5)
  );
}

export function canGetFrameLength(data: Uint8Array, offset: number): boolean {
  return offset + 5 < data.length;
}

export function isHeader(data: Uint8Array, offset: number): boolean {
  // Look for ADTS header | 1111 1111 | 1111 X00X | where X can be either 0 or 1
  // Layer bits (position 14 and 15) in header should be always 0 for ADTS
  // More info https://wiki.multimedia.cx/index.php?title=ADTS
  return offset + 1 < data.length && isHeaderPattern(data, offset);
}

export function canParse(data: Uint8Array, offset: number): boolean {
  return (
    canGetFrameLength(data, offset) &&
    isHeaderPattern(data, offset) &&
    getFullFrameLength(data, offset) <= data.length - offset
  );
}

export function probe(data: Uint8Array, offset: number): boolean {
  // same as isHeader but we also check that ADTS frame follows last ADTS frame
  // or end of data is reached
  if (isHeader(data, offset)) {
    // ADTS header Length
    const headerLength = getHeaderLength(data, offset);
    if (offset + headerLength >= data.length) {
      return false;
    }
    // ADTS frame Length
    const frameLength = getFullFrameLength(data, offset);
    if (frameLength <= headerLength) {
      return false;
    }

    const newOffset = offset + frameLength;
    return newOffset === data.length || isHeader(data, newOffset);
  }
  return false;
}

export function initTrackConfig(
  track: DemuxedAudioTrack,
  observer: HlsEventEmitter,
  data: Uint8Array,
  offset: number,
  audioCodec: string | undefined,
) {
  if (!track.samplerate) {
    const config = getAudioConfig(observer, data, offset, audioCodec);
    if (!config) {
      return;
    }
    Object.assign(track, config);
  }
}

export function getFrameDuration(samplerate: number): number {
  return (1024 * 90000) / samplerate;
}

export function parseFrameHeader(
  data: Uint8Array,
  offset: number,
): FrameHeader | void {
  // The protection skip bit tells us if we have 2 bytes of CRC data at the end of the ADTS header
  const headerLength = getHeaderLength(data, offset);
  if (offset + headerLength <= data.length) {
    // retrieve frame size
    const frameLength = getFullFrameLength(data, offset) - headerLength;
    if (frameLength > 0) {
      // logger.log(`AAC frame, offset/length/total/pts:${offset+headerLength}/${frameLength}/${data.byteLength}`);
      return { headerLength, frameLength };
    }
  }
}

export function appendFrame(
  track: DemuxedAudioTrack,
  data: Uint8Array,
  offset: number,
  pts: number,
  frameIndex: number,
): AudioFrame {
  const frameDuration = getFrameDuration(track.samplerate as number);
  const stamp = pts + frameIndex * frameDuration;
  const header = parseFrameHeader(data, offset);
  let unit: Uint8Array;
  if (header) {
    const { frameLength, headerLength } = header;
    const length = headerLength + frameLength;
    const missing = Math.max(0, offset + length - data.length);
    // logger.log(`AAC frame ${frameIndex}, pts:${stamp} length@offset/total: ${frameLength}@${offset+headerLength}/${data.byteLength} missing: ${missing}`);
    if (missing) {
      unit = new Uint8Array(length - headerLength);
      unit.set(data.subarray(offset + headerLength, data.length), 0);
    } else {
      unit = data.subarray(offset + headerLength, offset + length);
    }

    const sample: AudioSample = {
      unit,
      pts: stamp,
    };
    if (!missing) {
      track.samples.push(sample as AudioSample);
    }

    return { sample, length, missing };
  }
  // overflow incomplete header
  const length = data.length - offset;
  unit = new Uint8Array(length);
  unit.set(data.subarray(offset, data.length), 0);
  const sample: AudioSample = {
    unit,
    pts: stamp,
  };
  return { sample, length, missing: -1 };
}
