<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Plugin\FFmpegCodec;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\ffmpeg_media\Attribute\FFmpegCodec;
use Drupal\ffmpeg_media\ValueObject\ConversionOptions;

/**
 * FFV1 lossless codec plugin for archival-grade video preservation.
 *
 * FFV1 is a mathematically lossless intra-frame video codec designed for
 * long-term digital preservation. It is an official IETF standard (RFC 9043)
 * and is widely used by cultural heritage institutions and archives.
 *
 * Key characteristics:
 * - Truly lossless: bit-for-bit identical reconstruction
 * - Open standard with no licensing fees
 * - Supports embedded CRC for error detection per slice
 * - Version 3 supports multithreaded encoding via slices
 * - FFmpeg 8.0+ supports Vulkan compute acceleration
 */
#[FFmpegCodec(
  id: 'ffv1',
  label: new TranslatableMarkup('FFV1'),
  description: new TranslatableMarkup('Lossless archival codec ideal for digital preservation. IETF standard (RFC 9043) with error detection. Supports Vulkan compute in FFmpeg 8+.'),
  encoder: 'ffv1',
  supportedContainers: ['mkv', 'avi', 'nut'],
  hardwareAccelerated: FALSE,
  weight: 5,
)]
final class FFV1Codec extends FFmpegCodecBase {

  /**
   * FFV1 compression level range.
   */
  private const int COMPRESSION_LEVEL_MIN = 0;
  private const int COMPRESSION_LEVEL_MAX = 12;
  private const int COMPRESSION_LEVEL_DEFAULT = 3;

  /**
   * Default number of slices for multithreaded encoding.
   *
   * 24 slices provides good parallelism while maintaining reasonable overhead.
   * Should be divisible by common thread counts (2, 4, 6, 8, 12).
   */
  private const int DEFAULT_SLICES = 24;

  /**
   * Default GOP size for seeking granularity.
   *
   * GOP of 1 means every frame is a keyframe (best for editing/seeking).
   * Higher values reduce file size slightly but reduce seek granularity.
   */
  private const int DEFAULT_GOP_SIZE = 1;

  /**
   * {@inheritdoc}
   *
   * @return array<int, string>
   *   FFmpeg arguments.
   */
  #[\Override]
  #[\NoDiscard]
  public function buildArguments(ConversionOptions $options): array {
    // Extract FFV1-specific options from extraArgs or use defaults.
    $slices = $this->extractSliceCount($options);
    $gopSize = $this->extractGopSize($options);
    $useVulkan = $this->extractVulkanOption($options);

    $args = [];

    // Add Vulkan compute acceleration if requested and available.
    if ($useVulkan) {
      $args = array_merge($args, $this->buildVulkanArguments());
    }

    // Video codec configuration.
    $args[] = '-c:v';
    $args[] = 'ffv1';

    // FFV1 version 3 is required for slicing and modern features.
    $args[] = '-level';
    $args[] = '3';

    // Compression level (0-12, higher = slower but smaller).
    $args[] = '-coder';
    $args[] = '1';
    $args[] = '-context';
    $args[] = '1';
    $args[] = '-g';
    $args[] = (string) $gopSize;

    // Slices for multithreaded encoding and error resilience.
    $args[] = '-slices';
    $args[] = (string) $slices;

    // Enable slice CRC for error detection (critical for archival).
    $args[] = '-slicecrc';
    $args[] = '1';

    // Add common video arguments (pixel format, threads, scaling).
    $args = array_merge($args, $this->buildCommonArguments($options));

    // Audio - use FLAC for truly lossless archival,
    // or copy if already lossless.
    $args[] = '-c:a';
    $args[] = $this->getArchivalAudioCodec($options);

    // Add extra arguments (filtered to remove FFV1-specific ones
    // we already handled).
    foreach ($this->filterExtraArgs($options->extraArgs) as $arg) {
      $args[] = $arg;
    }

    return $args;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getDefaultOptions(): ConversionOptions {
    return new ConversionOptions(
      crf: 0,
      preset: self::COMPRESSION_LEVEL_DEFAULT,
      threads: 0,
      audioBitrate: '0',
      audioCodec: 'flac',
      pixelFormat: 'yuv420p10le',
      extraArgs: [
        '-ffv1_compression_level', (string) self::COMPRESSION_LEVEL_DEFAULT,
        '-ffv1_slices', (string) self::DEFAULT_SLICES,
        '-ffv1_gop', (string) self::DEFAULT_GOP_SIZE,
      ],
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getHighQualityOptions(): ConversionOptions {
    return new ConversionOptions(
      crf: 0,
      preset: 0,
      threads: 0,
      audioBitrate: '0',
      audioCodec: 'flac',
      pixelFormat: 'yuv444p16le',
      extraArgs: [
        '-ffv1_compression_level', '12',
        '-ffv1_slices', (string) self::DEFAULT_SLICES,
        '-ffv1_gop', '1',
        '-ffv1_vulkan', '1',
      ],
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getFastEncodingOptions(): ConversionOptions {
    return new ConversionOptions(
      crf: 0,
      preset: 9,
      threads: 0,
      audioBitrate: '0',
      audioCodec: 'flac',
      pixelFormat: 'yuv420p',
      extraArgs: [
        '-ffv1_compression_level', '0',
        '-ffv1_slices', '48',
        '-ffv1_gop', '12',
        '-ffv1_vulkan', '1',
      ],
    );
  }

  /**
   * Builds Vulkan compute acceleration arguments.
   *
   * FFmpeg 8.0+ supports Vulkan compute for FFV1 encoding, providing
   * significant speedups on supported GPUs.
   *
   * @return array<int, string>
   *   Vulkan initialization arguments.
   */
  protected function buildVulkanArguments(): array {
    return [
      '-init_hw_device', 'vulkan=vk:0',
      '-filter_hw_device', 'vk',
    ];
  }

  /**
   * Extracts compression level from options.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options.
   *
   * @return int
   *   Compression level (0-12).
   */
  protected function extractCompressionLevel(ConversionOptions $options): int {
    foreach ($options->extraArgs as $index => $arg) {
      if ($arg === '-ffv1_compression_level' && isset($options->extraArgs[$index + 1])) {
        $level = (int) $options->extraArgs[$index + 1];
        return max(self::COMPRESSION_LEVEL_MIN, min(self::COMPRESSION_LEVEL_MAX, $level));
      }
    }

    return self::COMPRESSION_LEVEL_DEFAULT;
  }

  /**
   * Extracts slice count from options.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options.
   *
   * @return int
   *   Number of slices for parallel encoding.
   */
  protected function extractSliceCount(ConversionOptions $options): int {
    foreach ($options->extraArgs as $index => $arg) {
      if ($arg === '-ffv1_slices' && isset($options->extraArgs[$index + 1])) {
        $slices = (int) $options->extraArgs[$index + 1];
        return max(1, $slices);
      }
    }

    return self::DEFAULT_SLICES;
  }

  /**
   * Extracts GOP size from options.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options.
   *
   * @return int
   *   GOP size (keyframe interval).
   */
  protected function extractGopSize(ConversionOptions $options): int {
    foreach ($options->extraArgs as $index => $arg) {
      if ($arg === '-ffv1_gop' && isset($options->extraArgs[$index + 1])) {
        $gop = (int) $options->extraArgs[$index + 1];
        return max(1, $gop);
      }
    }

    return self::DEFAULT_GOP_SIZE;
  }

  /**
   * Extracts Vulkan acceleration option.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options.
   *
   * @return bool
   *   TRUE if Vulkan compute should be used.
   */
  protected function extractVulkanOption(ConversionOptions $options): bool {
    foreach ($options->extraArgs as $index => $arg) {
      if ($arg === '-ffv1_vulkan' && isset($options->extraArgs[$index + 1])) {
        return (bool) (int) $options->extraArgs[$index + 1];
      }
    }

    return FALSE;
  }

  /**
   * Gets the appropriate lossless audio codec for archival.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options.
   *
   * @return string
   *   Audio codec name.
   */
  protected function getArchivalAudioCodec(ConversionOptions $options): string {
    // FLAC is the standard for lossless audio archival.
    // PCM is acceptable but larger, copy preserves original
    // if already lossless.
    $losslessCodecs = ['flac', 'pcm_s16le', 'pcm_s24le', 'pcm_s32le', 'copy'];

    if (in_array($options->audioCodec, $losslessCodecs, TRUE)) {
      return $options->audioCodec;
    }

    // Default to FLAC for true lossless archival.
    return 'flac';
  }

  /**
   * Filters out FFV1-specific arguments from extraArgs.
   *
   * @param array<int, string> $extraArgs
   *   Original extra arguments.
   *
   * @return array<int, string>
   *   Filtered arguments without FFV1-specific options.
   */
  protected function filterExtraArgs(array $extraArgs): array {
    $ffv1Options = [
      '-ffv1_compression_level',
      '-ffv1_slices',
      '-ffv1_gop',
      '-ffv1_vulkan',
    ];

    $filtered = [];
    $skipNext = FALSE;

    foreach ($extraArgs as $arg) {
      if ($skipNext) {
        $skipNext = FALSE;
        continue;
      }

      if (in_array($arg, $ffv1Options, TRUE)) {
        $skipNext = TRUE;
        continue;
      }

      $filtered[] = $arg;
    }

    return $filtered;
  }

}
