import { validateWaveformPeakData } from './peak-aggregation';
import type { WaveformPeakData } from './waveform.types';

const MAX_CANVAS_DIMENSION = 8_192;
const MAX_CANVAS_PIXELS = 16_777_216;

export interface WaveformCanvasRenderOptions {
  readonly width?: number;
  readonly height?: number;
  readonly devicePixelRatio?: number;
  readonly startBucket?: number;
  readonly endBucket?: number;
  readonly waveColor?: string;
  readonly backgroundColor?: string;
  readonly centerLineColor?: string;
  readonly selection?: {
    readonly startFraction: number;
    readonly endFraction: number;
    readonly color?: string;
  };
  /**
   * Full-scale amplitude. Defaults to 1, or the largest observed magnitude
   * when float PCM exceeds full scale.
   */
  readonly amplitudeScale?: number;
  readonly accessibleLabel?: string;
}

export interface WaveformCanvasRenderResult {
  readonly width: number;
  readonly height: number;
  readonly startBucket: number;
  readonly endBucket: number;
  readonly amplitudeScale: number;
  readonly accessibleLabel: string;
}

/**
 * Canvas is efficient but has no intrinsic text alternative. This renderer
 * applies an image role and a concise label while returning the same label for
 * callers that prefer a separate visible or screen-reader-only description.
 */
export function renderWaveformToCanvas(
  canvas: HTMLCanvasElement,
  peaks: WaveformPeakData,
  options: WaveformCanvasRenderOptions = {}
): WaveformCanvasRenderResult {
  validateWaveformPeakData(peaks);
  const width = validateCssDimension(
    options.width ?? canvas.clientWidth ?? canvas.width,
    'width'
  );
  const height = validateCssDimension(
    options.height ?? canvas.clientHeight ?? canvas.height,
    'height'
  );
  const pixelRatio = validatePixelRatio(options.devicePixelRatio ?? 1);
  const physicalWidth = Math.round(width * pixelRatio);
  const physicalHeight = Math.round(height * pixelRatio);
  if (
    physicalWidth > MAX_CANVAS_DIMENSION ||
    physicalHeight > MAX_CANVAS_DIMENSION ||
    physicalWidth * physicalHeight > MAX_CANVAS_PIXELS
  ) {
    throw new RangeError('Requested waveform canvas exceeds safe size limits');
  }

  const context = canvas.getContext('2d');
  if (context === null) {
    throw new Error('A 2D canvas context is required to render a waveform');
  }
  canvas.width = physicalWidth;
  canvas.height = physicalHeight;
  context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
  context.clearRect(0, 0, width, height);
  if (options.backgroundColor !== undefined) {
    context.fillStyle = options.backgroundColor;
    context.fillRect(0, 0, width, height);
  }

  const [startBucket, endBucket] = validateBucketRange(
    peaks,
    options.startBucket,
    options.endBucket
  );
  const accessibleLabel =
    options.accessibleLabel ?? describeWaveform(peaks, startBucket, endBucket);
  canvas.setAttribute('role', 'img');
  canvas.setAttribute('aria-label', accessibleLabel);

  drawSelection(context, width, height, options.selection);
  drawCenterLine(
    context,
    width,
    height,
    options.centerLineColor ?? 'rgba(127, 127, 127, 0.35)'
  );

  const amplitudeScale =
    options.amplitudeScale === undefined
      ? findAmplitudeScale(peaks, startBucket, endBucket)
      : validateAmplitudeScale(options.amplitudeScale);
  drawPeaks(
    context,
    peaks,
    startBucket,
    endBucket,
    width,
    height,
    amplitudeScale,
    options.waveColor ?? 'currentColor'
  );

  return Object.freeze({
    width,
    height,
    startBucket,
    endBucket,
    amplitudeScale,
    accessibleLabel,
  });
}

export function describeWaveform(
  peaks: WaveformPeakData,
  startBucket = 0,
  endBucket = peaks.bucketCount
): string {
  validateWaveformPeakData(peaks);
  const range = validateBucketRange(peaks, startBucket, endBucket);
  if (peaks.bucketCount === 0) {
    return 'Empty audio waveform.';
  }
  let peakAmplitude = 0;
  for (let index = range[0]; index < range[1]; index += 1) {
    peakAmplitude = Math.max(
      peakAmplitude,
      Math.abs(peaks.minimum[index] ?? 0),
      Math.abs(peaks.maximum[index] ?? 0)
    );
  }
  const duration = peaks.durationSeconds;
  const shownBuckets = range[1] - range[0];
  return `Audio waveform, ${formatDuration(
    duration
  )}, ${String(shownBuckets)} peak buckets, peak amplitude ${peakAmplitude.toFixed(
    2
  )}.`;
}

function drawPeaks(
  context: CanvasRenderingContext2D,
  peaks: WaveformPeakData,
  startBucket: number,
  endBucket: number,
  width: number,
  height: number,
  amplitudeScale: number,
  color: string
): void {
  const rangeLength = endBucket - startBucket;
  if (rangeLength === 0) {
    return;
  }
  const columnCount = Math.max(1, Math.min(Math.ceil(width), rangeLength));
  const center = height / 2;
  const halfHeight = Math.max(0, height / 2 - 1);
  context.strokeStyle = color;
  context.lineWidth = Math.max(1, width / columnCount);
  context.beginPath();

  for (let column = 0; column < columnCount; column += 1) {
    const first =
      startBucket + Math.floor((column * rangeLength) / columnCount);
    const last =
      startBucket + Math.floor(((column + 1) * rangeLength) / columnCount);
    let minimum = Number.POSITIVE_INFINITY;
    let maximum = Number.NEGATIVE_INFINITY;
    for (let bucket = first; bucket < last; bucket += 1) {
      minimum = Math.min(minimum, peaks.minimum[bucket] ?? 0);
      maximum = Math.max(maximum, peaks.maximum[bucket] ?? 0);
    }
    const x = ((column + 0.5) * width) / columnCount;
    const yTop = center - clampAmplitude(maximum / amplitudeScale) * halfHeight;
    const yBottom =
      center - clampAmplitude(minimum / amplitudeScale) * halfHeight;
    context.moveTo(x, yTop);
    context.lineTo(x, yBottom);
  }
  context.stroke();
}

function drawCenterLine(
  context: CanvasRenderingContext2D,
  width: number,
  height: number,
  color: string
): void {
  context.strokeStyle = color;
  context.lineWidth = 1;
  context.beginPath();
  context.moveTo(0, height / 2);
  context.lineTo(width, height / 2);
  context.stroke();
}

function drawSelection(
  context: CanvasRenderingContext2D,
  width: number,
  height: number,
  selection?: WaveformCanvasRenderOptions['selection']
): void {
  if (selection === undefined) {
    return;
  }
  if (
    !Number.isFinite(selection.startFraction) ||
    !Number.isFinite(selection.endFraction) ||
    selection.startFraction < 0 ||
    selection.endFraction > 1 ||
    selection.startFraction > selection.endFraction
  ) {
    throw new RangeError('Waveform selection must be within 0 and 1');
  }
  context.fillStyle = selection.color ?? 'rgba(75, 120, 255, 0.2)';
  context.fillRect(
    selection.startFraction * width,
    0,
    (selection.endFraction - selection.startFraction) * width,
    height
  );
}

function findAmplitudeScale(
  peaks: WaveformPeakData,
  startBucket: number,
  endBucket: number
): number {
  let maximum = 1;
  for (let index = startBucket; index < endBucket; index += 1) {
    maximum = Math.max(
      maximum,
      Math.abs(peaks.minimum[index] ?? 0),
      Math.abs(peaks.maximum[index] ?? 0)
    );
  }
  return maximum;
}

function validateBucketRange(
  peaks: WaveformPeakData,
  start = 0,
  end = peaks.bucketCount
): readonly [number, number] {
  if (
    !Number.isSafeInteger(start) ||
    !Number.isSafeInteger(end) ||
    start < 0 ||
    end < start ||
    end > peaks.bucketCount
  ) {
    throw new RangeError('Waveform bucket range is invalid');
  }
  return [start, end];
}

function validateCssDimension(value: number, name: string): number {
  if (!Number.isFinite(value) || value <= 0 || value > MAX_CANVAS_DIMENSION) {
    throw new RangeError(`Waveform canvas ${name} is outside safe limits`);
  }
  return value;
}

function validatePixelRatio(value: number): number {
  if (!Number.isFinite(value) || value <= 0 || value > 4) {
    throw new RangeError('Waveform canvas pixel ratio must be between 0 and 4');
  }
  return value;
}

function validateAmplitudeScale(value: number): number {
  if (!Number.isFinite(value) || value <= 0) {
    throw new RangeError('Waveform amplitude scale must be positive');
  }
  return value;
}

function clampAmplitude(value: number): number {
  return Math.max(-1, Math.min(1, value));
}

function formatDuration(seconds: number): string {
  if (seconds < 60) {
    return `${seconds.toFixed(1)} seconds`;
  }
  const minutes = Math.floor(seconds / 60);
  const remainder = Math.round(seconds % 60);
  return `${String(minutes)} minutes ${String(remainder)} seconds`;
}
