import type {
  MediaProbe,
  MediaStreamProbe,
  MediaStreamType,
} from './media.types';

export interface StreamSelection {
  videoStreamIndex?: number;
  audioStreamIndexes: number[];
  subtitleStreamIndexes: number[];
}

export interface DefaultStreamSelectionOptions {
  preferredAudioLanguages?: readonly string[];
  preferredSubtitleLanguages?: readonly string[];
  includeSubtitles?: boolean;
  includeAllAudioStreams?: boolean;
}

export type StreamSelectionIssueCode =
  'stream-not-found' | 'wrong-stream-type' | 'duplicate-stream';

export interface StreamSelectionIssue {
  code: StreamSelectionIssueCode;
  field: 'videoStreamIndex' | 'audioStreamIndexes' | 'subtitleStreamIndexes';
  streamIndex: number;
  message: string;
}

export interface StreamSelectionValidation {
  valid: boolean;
  issues: StreamSelectionIssue[];
}

export function streamsOfType(
  probe: MediaProbe,
  type: MediaStreamType
): MediaStreamProbe[] {
  return probe.streams.filter((stream) => stream.type === type);
}

export function findStream(
  probe: MediaProbe,
  index: number
): MediaStreamProbe | undefined {
  return probe.streams.find((stream) => stream.index === index);
}

export function selectDefaultStreams(
  probe: MediaProbe,
  options: DefaultStreamSelectionOptions = {}
): StreamSelection {
  const videoCandidates = streamsOfType(probe, 'video').filter(
    (stream) => stream.disposition.attached_pic !== true
  );
  const audioCandidates = streamsOfType(probe, 'audio');
  const subtitleCandidates = streamsOfType(probe, 'subtitle');
  const defaultVideo = choosePreferredStream(videoCandidates);
  const preferredAudio = choosePreferredStream(
    audioCandidates,
    options.preferredAudioLanguages
  );
  const preferredSubtitle = choosePreferredStream(
    subtitleCandidates,
    options.preferredSubtitleLanguages
  );

  return {
    ...(defaultVideo === undefined
      ? {}
      : { videoStreamIndex: defaultVideo.index }),
    audioStreamIndexes:
      options.includeAllAudioStreams === true
        ? orderByPreference(
            audioCandidates,
            options.preferredAudioLanguages
          ).map((stream) => stream.index)
        : preferredAudio === undefined
          ? []
          : [preferredAudio.index],
    subtitleStreamIndexes:
      options.includeSubtitles === true && preferredSubtitle !== undefined
        ? [preferredSubtitle.index]
        : [],
  };
}

export function validateStreamSelection(
  probe: MediaProbe,
  selection: StreamSelection
): StreamSelectionValidation {
  const issues: StreamSelectionIssue[] = [];
  const seen = new Set<number>();

  if (selection.videoStreamIndex !== undefined) {
    validateSelectedIndex(
      probe,
      selection.videoStreamIndex,
      'video',
      'videoStreamIndex',
      seen,
      issues
    );
  }
  for (const streamIndex of selection.audioStreamIndexes) {
    validateSelectedIndex(
      probe,
      streamIndex,
      'audio',
      'audioStreamIndexes',
      seen,
      issues
    );
  }
  for (const streamIndex of selection.subtitleStreamIndexes) {
    validateSelectedIndex(
      probe,
      streamIndex,
      'subtitle',
      'subtitleStreamIndexes',
      seen,
      issues
    );
  }

  return { valid: issues.length === 0, issues };
}

export function streamSelectionMapArgs(
  selection: StreamSelection,
  inputIndex = 0
): string[] {
  if (!Number.isSafeInteger(inputIndex) || inputIndex < 0) {
    throw new RangeError('Input index must be a non-negative safe integer.');
  }

  const args: string[] = [];
  const append = (streamIndex: number): void => {
    if (!Number.isSafeInteger(streamIndex) || streamIndex < 0) {
      throw new RangeError(
        'Stream indexes must be non-negative safe integers.'
      );
    }
    args.push('-map', `${inputIndex}:${streamIndex}`);
  };

  if (selection.videoStreamIndex !== undefined) {
    append(selection.videoStreamIndex);
  }
  selection.audioStreamIndexes.forEach(append);
  selection.subtitleStreamIndexes.forEach(append);
  return args;
}

function choosePreferredStream(
  streams: readonly MediaStreamProbe[],
  preferredLanguages?: readonly string[]
): MediaStreamProbe | undefined {
  return orderByPreference(streams, preferredLanguages)[0];
}

function orderByPreference(
  streams: readonly MediaStreamProbe[],
  preferredLanguages?: readonly string[]
): MediaStreamProbe[] {
  const languageRank = new Map(
    (preferredLanguages ?? []).map((language, index) => [
      normalizeLanguage(language),
      index,
    ])
  );

  return [...streams].sort((left, right) => {
    const leftLanguage = left.language
      ? languageRank.get(normalizeLanguage(left.language))
      : undefined;
    const rightLanguage = right.language
      ? languageRank.get(normalizeLanguage(right.language))
      : undefined;
    const leftRank = leftLanguage ?? Number.POSITIVE_INFINITY;
    const rightRank = rightLanguage ?? Number.POSITIVE_INFINITY;
    if (leftRank !== rightRank) {
      return leftRank - rightRank;
    }
    const defaultDifference =
      Number(right.disposition.default === true) -
      Number(left.disposition.default === true);
    if (defaultDifference !== 0) {
      return defaultDifference;
    }
    return left.index - right.index;
  });
}

function validateSelectedIndex(
  probe: MediaProbe,
  streamIndex: number,
  expectedType: MediaStreamType,
  field: StreamSelectionIssue['field'],
  seen: Set<number>,
  issues: StreamSelectionIssue[]
): void {
  if (seen.has(streamIndex)) {
    issues.push({
      code: 'duplicate-stream',
      field,
      streamIndex,
      message: `Stream ${streamIndex} is selected more than once.`,
    });
  }
  seen.add(streamIndex);

  const stream = findStream(probe, streamIndex);
  if (stream === undefined) {
    issues.push({
      code: 'stream-not-found',
      field,
      streamIndex,
      message: `Stream ${streamIndex} does not exist in the source.`,
    });
  } else if (stream.type !== expectedType) {
    issues.push({
      code: 'wrong-stream-type',
      field,
      streamIndex,
      message: `Stream ${streamIndex} is ${stream.type}, not ${expectedType}.`,
    });
  }
}

function normalizeLanguage(language: string): string {
  return language.trim().toLowerCase().split(/[-_]/)[0] ?? '';
}
