import type { ExportPreset } from './convert';
import { buildConvertPlan } from './convert';
import type { SourceReference } from './command-utils';
import type { FFmpegCommandPlan } from './command-plan';
import {
  audioFadeFilters,
  videoFadeFilters,
  type FadeCurve,
  type AudioFilterStage,
  type FilterNode,
  type VideoFilterStage,
} from './filtergraph-builder';

export {
  audioFadeFilters,
  videoFadeFilters,
  type AudioFilterStage,
  type FilterNode,
  type VideoFilterStage,
};

export interface FadePlanOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  readonly preset: ExportPreset;
  readonly durationSeconds: number;
  readonly audioFadeInSeconds?: number;
  readonly audioFadeOutSeconds?: number;
  readonly videoFadeInSeconds?: number;
  readonly videoFadeOutSeconds?: number;
  readonly audioCurve?: FadeCurve;
  readonly videoCurve?: FadeCurve;
}

export function buildFadePlan(options: FadePlanOptions): FFmpegCommandPlan {
  if (
    !Number.isFinite(options.durationSeconds) ||
    options.durationSeconds <= 0
  ) {
    throw new RangeError('Fade clip duration must be positive');
  }
  validatePair(
    options.durationSeconds,
    options.audioFadeInSeconds,
    options.audioFadeOutSeconds,
    'Audio'
  );
  validatePair(
    options.durationSeconds,
    options.videoFadeInSeconds,
    options.videoFadeOutSeconds,
    'Video'
  );
  const audioNodes = audioFadeFilters(
    options.durationSeconds,
    options.audioFadeInSeconds,
    options.audioFadeOutSeconds,
    options.audioCurve
  );
  const videoNodes = videoFadeFilters(
    options.durationSeconds,
    options.videoFadeInSeconds,
    options.videoFadeOutSeconds,
    options.videoCurve
  );
  if (audioNodes.length > 0 && !options.preset.audio) {
    throw new TypeError('Audio fades require a preset with audio');
  }
  if (videoNodes.length > 0 && !options.preset.video) {
    throw new TypeError('Video fades require a preset with video');
  }
  if (audioNodes.length === 0 && videoNodes.length === 0) {
    throw new TypeError('At least one fade duration is required');
  }
  return buildConvertPlan({
    jobId: options.jobId,
    source: options.source,
    preset: options.preset,
    operation: 'fades',
    expectedDurationSeconds: options.durationSeconds,
    ...(audioNodes.length > 0
      ? {
          audioFiltergraph: audioNodes.map((node) => node.expression).join(','),
        }
      : {}),
    ...(videoNodes.length > 0
      ? {
          videoFiltergraph: videoNodes.map((node) => node.expression).join(','),
        }
      : {}),
    additionalRequirements: {
      filters: [
        ...new Set(
          [...audioNodes, ...videoNodes].flatMap(
            (node) => node.requiredCapabilities?.filters ?? []
          )
        ),
      ],
    },
  });
}

function validatePair(
  durationSeconds: number,
  fadeInSeconds: number | undefined,
  fadeOutSeconds: number | undefined,
  label: string
): void {
  const fadeIn = fadeInSeconds ?? 0;
  const fadeOut = fadeOutSeconds ?? 0;
  if (
    !Number.isFinite(fadeIn) ||
    !Number.isFinite(fadeOut) ||
    fadeIn < 0 ||
    fadeOut < 0 ||
    fadeIn + fadeOut > durationSeconds
  ) {
    throw new RangeError(
      `${label} fade durations must be nonnegative and cannot overlap`
    );
  }
}
