import {
  DEFAULT_MAX_PCM_SAMPLES,
  MAX_WAVEFORM_BUCKET_COUNT,
  WAVEFORM_PEAKS_VERSION,
  type DecodedPcm,
  type PcmByteSource,
  type PcmEncoding,
  type PeakAggregationOptions,
  type WaveformPcmInput,
  type WaveformPeakData,
} from './waveform.types';
import {
  WaveformCancellationError,
  WaveformDataError,
} from './waveform-errors';

const RIFF_HEADER_BYTES = 12;
const WAVE_FORMAT_PCM = 0x0001;
const WAVE_FORMAT_IEEE_FLOAT = 0x0003;
const WAVE_FORMAT_EXTENSIBLE = 0xfffe;
const PROGRESS_INTERVAL_SAMPLES = 16_384;
const MIN_SAMPLE_RATE = 1;
const MAX_SAMPLE_RATE = 384_000;

interface InspectedPcm {
  readonly view: DataView;
  readonly dataOffset: number;
  readonly dataByteLength: number;
  readonly encoding: PcmEncoding;
  readonly sampleRate: number;
  readonly sampleCount: number;
  readonly bytesPerSample: number;
}

export function decodePcmSamples(
  input: WaveformPcmInput,
  maxSamples = DEFAULT_MAX_PCM_SAMPLES
): DecodedPcm {
  const inspected = inspectPcmInput(input, maxSamples);
  const samples = new Float32Array(inspected.sampleCount);
  for (let index = 0; index < inspected.sampleCount; index += 1) {
    samples[index] = readNormalizedSample(inspected, index);
  }
  return Object.freeze({
    encoding: inspected.encoding,
    sampleRate: inspected.sampleRate,
    samples,
  });
}

/**
 * Aggregates directly from the PCM bytes, avoiding a full-size decoded sample
 * allocation. Bucket boundaries are deterministic and every input sample is
 * assigned to exactly one bucket.
 */
export function aggregatePcmPeaks(
  input: WaveformPcmInput,
  options: PeakAggregationOptions
): WaveformPeakData {
  const requestedBucketCount = validateBucketCount(options.bucketCount);
  const maxSamples = validateMaxSamples(options.maxSamples);
  const inspected = inspectPcmInput(input, maxSamples);
  throwIfCancelled(options.isCancelled);

  if (inspected.sampleCount === 0) {
    options.onProgress?.(1);
    return createPeakData(inspected, 0);
  }

  const bucketCount = Math.min(requestedBucketCount, inspected.sampleCount);
  const result = createPeakData(inspected, bucketCount);
  let lastProgress = -1;

  for (let bucketIndex = 0; bucketIndex < bucketCount; bucketIndex += 1) {
    const start = Math.floor(
      (bucketIndex * inspected.sampleCount) / bucketCount
    );
    const end = Math.floor(
      ((bucketIndex + 1) * inspected.sampleCount) / bucketCount
    );
    let minimum = Number.POSITIVE_INFINITY;
    let maximum = Number.NEGATIVE_INFINITY;
    let squareScale = 0;
    let scaledSquareSum = 1;

    for (let sampleIndex = start; sampleIndex < end; sampleIndex += 1) {
      if (sampleIndex % PROGRESS_INTERVAL_SAMPLES === 0) {
        throwIfCancelled(options.isCancelled);
        const progress = sampleIndex / inspected.sampleCount;
        if (progress !== lastProgress) {
          options.onProgress?.(progress);
          lastProgress = progress;
        }
      }

      const sample = readNormalizedSample(inspected, sampleIndex);
      minimum = Math.min(minimum, sample);
      maximum = Math.max(maximum, sample);
      const magnitude = Math.abs(sample);
      if (magnitude !== 0) {
        if (squareScale < magnitude) {
          const ratio = squareScale / magnitude;
          scaledSquareSum = 1 + scaledSquareSum * ratio * ratio;
          squareScale = magnitude;
        } else {
          const ratio = magnitude / squareScale;
          scaledSquareSum += ratio * ratio;
        }
      }
    }

    result.minimum[bucketIndex] = normalizeZero(minimum);
    result.maximum[bucketIndex] = normalizeZero(maximum);
    result.rms[bucketIndex] =
      squareScale === 0
        ? 0
        : squareScale * Math.sqrt(scaledSquareSum / (end - start));
  }

  throwIfCancelled(options.isCancelled);
  options.onProgress?.(1);
  return result;
}

export function validateWaveformPeakData(
  value: WaveformPeakData
): WaveformPeakData {
  if (value.version !== WAVEFORM_PEAKS_VERSION) {
    throw new WaveformDataError(
      `Unsupported waveform peak version: ${String(value.version)}`
    );
  }
  validateSampleRate(value.sampleRate);
  if (
    !Number.isSafeInteger(value.sampleCount) ||
    value.sampleCount < 0 ||
    value.sampleCount > DEFAULT_MAX_PCM_SAMPLES
  ) {
    throw new WaveformDataError('Waveform sample count is invalid');
  }
  if (
    !Number.isSafeInteger(value.bucketCount) ||
    value.bucketCount < 0 ||
    value.bucketCount > MAX_WAVEFORM_BUCKET_COUNT ||
    value.bucketCount > value.sampleCount
  ) {
    throw new WaveformDataError('Waveform bucket count is invalid');
  }
  if (
    (value.sampleCount === 0) !== (value.bucketCount === 0) ||
    value.minimum.length !== value.bucketCount ||
    value.maximum.length !== value.bucketCount ||
    value.rms.length !== value.bucketCount
  ) {
    throw new WaveformDataError('Waveform peak array lengths are inconsistent');
  }
  if (value.sourceEncoding !== 's16le' && value.sourceEncoding !== 'f32le') {
    throw new WaveformDataError('Waveform source encoding is invalid');
  }
  const expectedDuration = value.sampleCount / value.sampleRate;
  if (
    !Number.isFinite(value.durationSeconds) ||
    value.durationSeconds < 0 ||
    Math.abs(value.durationSeconds - expectedDuration) >
      Math.max(1e-9, expectedDuration * 1e-9)
  ) {
    throw new WaveformDataError('Waveform duration is inconsistent');
  }
  for (let index = 0; index < value.bucketCount; index += 1) {
    const minimum = value.minimum[index];
    const maximum = value.maximum[index];
    const rms = value.rms[index];
    if (
      minimum === undefined ||
      maximum === undefined ||
      rms === undefined ||
      !Number.isFinite(minimum) ||
      !Number.isFinite(maximum) ||
      !Number.isFinite(rms) ||
      minimum > maximum ||
      rms < 0
    ) {
      throw new WaveformDataError(
        `Waveform bucket ${String(index)} contains invalid values`
      );
    }
  }
  return value;
}

function inspectPcmInput(
  input: WaveformPcmInput,
  maxSamples: number
): InspectedPcm {
  const view = toDataView(input.data);
  if (input.container === 'raw') {
    validateSampleRate(input.sampleRate);
    const bytesPerSample = bytesPerSampleFor(input.encoding);
    return inspectPayload(
      view,
      0,
      view.byteLength,
      input.encoding,
      input.sampleRate,
      bytesPerSample,
      maxSamples
    );
  }
  return inspectWaveFile(view, maxSamples);
}

function inspectWaveFile(view: DataView, maxSamples: number): InspectedPcm {
  if (
    view.byteLength < RIFF_HEADER_BYTES ||
    readAscii(view, 0, 4) !== 'RIFF' ||
    readAscii(view, 8, 4) !== 'WAVE'
  ) {
    throw new WaveformDataError('Expected a RIFF/WAVE PCM file');
  }

  const declaredRiffLength = view.getUint32(4, true) + 8;
  if (
    declaredRiffLength < RIFF_HEADER_BYTES ||
    declaredRiffLength > view.byteLength
  ) {
    throw new WaveformDataError(
      'WAVE file is truncated or has an invalid size'
    );
  }

  let encoding: PcmEncoding | undefined;
  let sampleRate: number | undefined;
  let bytesPerSample: number | undefined;
  let blockAlign: number | undefined;
  let channelCount: number | undefined;
  let dataOffset: number | undefined;
  let dataByteLength: number | undefined;
  let offset = RIFF_HEADER_BYTES;

  while (offset + 8 <= declaredRiffLength) {
    const chunkId = readAscii(view, offset, 4);
    const chunkLength = view.getUint32(offset + 4, true);
    const payloadOffset = offset + 8;
    const payloadEnd = payloadOffset + chunkLength;
    if (
      !Number.isSafeInteger(payloadEnd) ||
      payloadEnd > declaredRiffLength ||
      payloadEnd > view.byteLength
    ) {
      throw new WaveformDataError(`WAVE ${chunkId} chunk is truncated`);
    }

    if (chunkId === 'fmt ' && encoding === undefined) {
      if (chunkLength < 16) {
        throw new WaveformDataError('WAVE format chunk is too short');
      }
      const formatTag = view.getUint16(payloadOffset, true);
      channelCount = view.getUint16(payloadOffset + 2, true);
      sampleRate = view.getUint32(payloadOffset + 4, true);
      blockAlign = view.getUint16(payloadOffset + 12, true);
      const bitsPerSample = view.getUint16(payloadOffset + 14, true);
      const resolvedFormatTag =
        formatTag === WAVE_FORMAT_EXTENSIBLE
          ? inspectExtensibleFormat(view, payloadOffset, chunkLength)
          : formatTag;

      if (resolvedFormatTag === WAVE_FORMAT_PCM && bitsPerSample === 16) {
        encoding = 's16le';
        bytesPerSample = 2;
      } else if (
        resolvedFormatTag === WAVE_FORMAT_IEEE_FLOAT &&
        bitsPerSample === 32
      ) {
        encoding = 'f32le';
        bytesPerSample = 4;
      } else {
        throw new WaveformDataError(
          `Unsupported WAVE encoding (format ${String(
            resolvedFormatTag
          )}, ${String(bitsPerSample)} bits)`
        );
      }
    } else if (chunkId === 'data' && dataOffset === undefined) {
      dataOffset = payloadOffset;
      dataByteLength = chunkLength;
    }

    offset = payloadEnd + (chunkLength % 2);
  }

  if (
    encoding === undefined ||
    sampleRate === undefined ||
    bytesPerSample === undefined ||
    blockAlign === undefined ||
    channelCount === undefined
  ) {
    throw new WaveformDataError('WAVE file has no supported format chunk');
  }
  if (dataOffset === undefined || dataByteLength === undefined) {
    throw new WaveformDataError('WAVE file has no audio data chunk');
  }
  if (channelCount !== 1) {
    throw new WaveformDataError(
      'Waveform analysis requires mono PCM; downmix with FFmpeg first'
    );
  }
  if (blockAlign !== bytesPerSample) {
    throw new WaveformDataError('WAVE block alignment is inconsistent');
  }
  validateSampleRate(sampleRate);
  return inspectPayload(
    view,
    dataOffset,
    dataByteLength,
    encoding,
    sampleRate,
    bytesPerSample,
    maxSamples
  );
}

function inspectExtensibleFormat(
  view: DataView,
  payloadOffset: number,
  chunkLength: number
): number {
  if (chunkLength < 40 || view.getUint16(payloadOffset + 16, true) < 22) {
    throw new WaveformDataError('WAVE extensible format chunk is incomplete');
  }
  const subFormatOffset = payloadOffset + 24;
  const canonicalGuidTail = [
    0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71,
  ];
  for (let index = 0; index < canonicalGuidTail.length; index += 1) {
    if (
      view.getUint8(subFormatOffset + 4 + index) !== canonicalGuidTail[index]
    ) {
      throw new WaveformDataError('Unsupported WAVE extensible sub-format');
    }
  }
  return view.getUint16(subFormatOffset, true);
}

function inspectPayload(
  view: DataView,
  dataOffset: number,
  dataByteLength: number,
  encoding: PcmEncoding,
  sampleRate: number,
  bytesPerSample: number,
  maxSamples: number
): InspectedPcm {
  if (dataByteLength % bytesPerSample !== 0) {
    throw new WaveformDataError(
      `PCM byte length is not aligned for ${encoding}`
    );
  }
  const sampleCount = dataByteLength / bytesPerSample;
  if (!Number.isSafeInteger(sampleCount) || sampleCount > maxSamples) {
    throw new WaveformDataError(
      `PCM input exceeds the ${String(maxSamples)} sample analysis limit`
    );
  }
  return {
    view,
    dataOffset,
    dataByteLength,
    encoding,
    sampleRate,
    sampleCount,
    bytesPerSample,
  };
}

function createPeakData(
  inspected: Pick<InspectedPcm, 'encoding' | 'sampleRate' | 'sampleCount'>,
  bucketCount: number
): WaveformPeakData {
  return Object.freeze({
    version: WAVEFORM_PEAKS_VERSION,
    sourceEncoding: inspected.encoding,
    sampleRate: inspected.sampleRate,
    sampleCount: inspected.sampleCount,
    durationSeconds: inspected.sampleCount / inspected.sampleRate,
    bucketCount,
    minimum: new Float32Array(bucketCount),
    maximum: new Float32Array(bucketCount),
    rms: new Float32Array(bucketCount),
  });
}

function readNormalizedSample(
  inspected: InspectedPcm,
  sampleIndex: number
): number {
  const offset = inspected.dataOffset + sampleIndex * inspected.bytesPerSample;
  if (inspected.encoding === 's16le') {
    return normalizeZero(inspected.view.getInt16(offset, true) / 32_768);
  }
  const sample = inspected.view.getFloat32(offset, true);
  if (!Number.isFinite(sample)) {
    throw new WaveformDataError(
      `Float PCM sample ${String(sampleIndex)} is not finite`
    );
  }
  return normalizeZero(sample);
}

function validateBucketCount(bucketCount: number): number {
  if (
    !Number.isSafeInteger(bucketCount) ||
    bucketCount <= 0 ||
    bucketCount > MAX_WAVEFORM_BUCKET_COUNT
  ) {
    throw new RangeError(
      `Waveform bucketCount must be between 1 and ${String(
        MAX_WAVEFORM_BUCKET_COUNT
      )}`
    );
  }
  return bucketCount;
}

function validateMaxSamples(maxSamples?: number): number {
  const value = maxSamples ?? DEFAULT_MAX_PCM_SAMPLES;
  if (
    !Number.isSafeInteger(value) ||
    value <= 0 ||
    value > DEFAULT_MAX_PCM_SAMPLES
  ) {
    throw new RangeError(
      `maxSamples must be between 1 and ${String(DEFAULT_MAX_PCM_SAMPLES)}`
    );
  }
  return value;
}

function validateSampleRate(sampleRate: number): void {
  if (
    !Number.isSafeInteger(sampleRate) ||
    sampleRate < MIN_SAMPLE_RATE ||
    sampleRate > MAX_SAMPLE_RATE
  ) {
    throw new WaveformDataError(
      `PCM sample rate must be between ${String(MIN_SAMPLE_RATE)} and ${String(
        MAX_SAMPLE_RATE
      )} Hz`
    );
  }
}

function bytesPerSampleFor(encoding: PcmEncoding): number {
  if (encoding === 's16le') {
    return 2;
  }
  if (encoding === 'f32le') {
    return 4;
  }
  throw new WaveformDataError(`Unsupported PCM encoding: ${String(encoding)}`);
}

function throwIfCancelled(isCancelled?: () => boolean): void {
  if (isCancelled?.()) {
    throw new WaveformCancellationError();
  }
}

function normalizeZero(value: number): number {
  return value === 0 ? 0 : value;
}

function toDataView(source: PcmByteSource): DataView {
  if (source instanceof ArrayBuffer) {
    return new DataView(source);
  }
  return new DataView(source.buffer, source.byteOffset, source.byteLength);
}

function readAscii(view: DataView, offset: number, length: number): string {
  let result = '';
  for (let index = 0; index < length; index += 1) {
    result += String.fromCharCode(view.getUint8(offset + index));
  }
  return result;
}
