<?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\Enum\HardwareType;
use Drupal\ffmpeg_media\ValueObject\ConversionOptions;

/**
 * Apple ProRes codec plugin.
 *
 * ProRes is a professional video codec developed by Apple, widely used in
 * video production and post-production workflows. It provides excellent
 * quality with reasonable file sizes for editing purposes.
 *
 * FFmpeg encoders:
 * - prores_ks: Highest quality software encoder (recommended)
 * - prores_videotoolbox: Hardware-accelerated encoder (macOS only)
 *
 * FFmpeg 8.0 adds ProRes RAW decode support with Vulkan acceleration.
 *
 * Available profiles:
 * - proxy (0): Offline editing, smallest files
 * - lt (1): Light editing, good for proxies
 * - standard (2): Standard quality (default)
 * - hq (3): High quality, broadcast ready
 * - 4444 (4): High quality with alpha channel support
 * - 4444xq (5): Highest quality with alpha, maximum bit depth
 *
 * Supported containers: MOV (native), MKV
 *
 * @see https://support.apple.com/en-us/102587
 */
#[FFmpegCodec(
  id: 'prores',
  label: new TranslatableMarkup('Apple ProRes'),
  description: new TranslatableMarkup('Professional video codec for post-production workflows. Supports alpha channel in 4444 profiles.'),
  encoder: 'prores_ks',
  supportedContainers: ['mov', 'mkv'],
  hardwareAccelerated: FALSE,
  weight: 30,
)]
final class ProResCodec extends FFmpegCodecBase {

  /**
   * ProRes profile identifiers.
   */
  private const int PROFILE_PROXY = 0;
  private const int PROFILE_LT = 1;
  private const int PROFILE_STANDARD = 2;
  private const int PROFILE_HQ = 3;
  private const int PROFILE_4444 = 4;
  private const int PROFILE_4444XQ = 5;

  /**
   * Profile names for FFmpeg.
   *
   * @var array<int, string>
   */
  private const array PROFILE_NAMES = [
    self::PROFILE_PROXY => 'proxy',
    self::PROFILE_LT => 'lt',
    self::PROFILE_STANDARD => 'standard',
    self::PROFILE_HQ => 'hq',
    self::PROFILE_4444 => '4444',
    self::PROFILE_4444XQ => '4444xq',
  ];

  /**
   * Recommended pixel formats per profile.
   *
   * @var array<int, string>
   */
  private const array PROFILE_PIXEL_FORMATS = [
    self::PROFILE_PROXY => 'yuv422p10le',
    self::PROFILE_LT => 'yuv422p10le',
    self::PROFILE_STANDARD => 'yuv422p10le',
    self::PROFILE_HQ => 'yuv422p10le',
    self::PROFILE_4444 => 'yuva444p10le',
    self::PROFILE_4444XQ => 'yuva444p10le',
  ];

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function buildArguments(ConversionOptions $options): array {
    $profile = $this->extractProfile($options);
    $encoder = $this->selectEncoder($options);
    $pixelFormat = $this->selectPixelFormat($options, $profile);

    $args = [
      '-c:v', $encoder,
      '-profile:v', (string) $profile,
      '-pix_fmt', $pixelFormat,
    ];

    // Add vendor tag for Apple compatibility.
    $args[] = '-vendor';
    $args[] = 'apl0';

    // Add bits_per_mb for quality control (prores_ks specific).
    if ($encoder === 'prores_ks') {
      $bitsPerMb = $this->calculateBitsPerMb($profile, $options);
      if ($bitsPerMb !== NULL) {
        $args[] = '-bits_per_mb';
        $args[] = (string) $bitsPerMb;
      }

      // Quant matrix for quality tuning.
      $args[] = '-quant_mat';
      $args[] = $this->getQuantMatrix($profile);
    }

    // Alpha handling for 4444 profiles.
    if ($this->supportsAlpha($profile)) {
      $args[] = '-alpha_bits';
      $args[] = '16';
    }

    // Thread count for parallel encoding.
    if ($options->threads > 0) {
      $args[] = '-threads';
      $args[] = (string) $options->threads;
    }

    // Add scale filter if dimensions are specified.
    if ($options->maxWidth !== NULL || $options->maxHeight !== NULL) {
      $args[] = '-vf';
      $args[] = $this->buildScaleFilter($options->maxWidth, $options->maxHeight);
    }

    // Audio - use PCM for professional workflows.
    $args[] = '-c:a';
    $args[] = 'pcm_s24le';

    if ($options->audioSampleRate !== NULL) {
      $args[] = '-ar';
      $args[] = (string) $options->audioSampleRate;
    }

    if ($options->audioChannels !== NULL) {
      $args[] = '-ac';
      $args[] = (string) $options->audioChannels;
    }

    // Add extra arguments.
    foreach ($options->extraArgs as $arg) {
      $args[] = $arg;
    }

    return $args;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getDefaultOptions(): ConversionOptions {
    return new ConversionOptions(
      crf: 0,
      preset: 4,
      threads: 0,
      audioBitrate: '0',
      audioCodec: 'pcm_s24le',
      pixelFormat: 'yuv422p10le',
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getHighQualityOptions(): ConversionOptions {
    return new ConversionOptions(
      crf: 0,
      preset: 5,
      threads: 0,
      audioBitrate: '0',
      audioCodec: 'pcm_s24le',
      pixelFormat: 'yuva444p10le',
      extraArgs: ['-profile:v', '5'],
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getFastEncodingOptions(): ConversionOptions {
    return new ConversionOptions(
      crf: 0,
      preset: 0,
      threads: 0,
      audioBitrate: '0',
      audioCodec: 'pcm_s24le',
      pixelFormat: 'yuv422p10le',
      extraArgs: ['-profile:v', '0'],
    );
  }

  /**
   * Extracts ProRes profile from conversion options.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options.
   *
   * @return int
   *   Profile identifier (0-5).
   */
  #[\NoDiscard]
  private function extractProfile(ConversionOptions $options): int {
    // Check for explicit profile in extra args.
    foreach ($options->extraArgs as $index => $arg) {
      if ($arg === '-profile:v' && isset($options->extraArgs[$index + 1])) {
        $profile = $options->extraArgs[$index + 1];

        // Handle numeric profiles.
        if (is_numeric($profile)) {
          $profileInt = (int) $profile;
          if ($profileInt >= self::PROFILE_PROXY && $profileInt <= self::PROFILE_4444XQ) {
            return $profileInt;
          }
        }

        // Handle named profiles.
        $profileMap = array_flip(self::PROFILE_NAMES);
        if (isset($profileMap[$profile])) {
          return $profileMap[$profile];
        }
      }
    }

    // Map preset to profile for workflow compatibility.
    // Preset 0-1 -> proxy, 2-3 -> lt, 4-5 -> standard,
    // 6-7 -> hq, 8 -> 4444, 9 -> 4444xq.
    return match (TRUE) {
      $options->preset <= 1 => self::PROFILE_PROXY,
      $options->preset <= 3 => self::PROFILE_LT,
      $options->preset <= 5 => self::PROFILE_STANDARD,
      $options->preset <= 7 => self::PROFILE_HQ,
      $options->preset === 8 => self::PROFILE_4444,
      default => self::PROFILE_4444XQ,
    };
  }

  /**
   * Selects the appropriate ProRes encoder.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options.
   *
   * @return string
   *   Encoder name.
   */
  #[\NoDiscard]
  private function selectEncoder(ConversionOptions $options): string {
    // Check for explicit encoder in extra args.
    foreach ($options->extraArgs as $index => $arg) {
      if ($arg === '-c:v' && isset($options->extraArgs[$index + 1])) {
        $encoder = $options->extraArgs[$index + 1];
        if (in_array($encoder, ['prores_ks', 'prores_videotoolbox', 'prores_aw'], TRUE)) {
          return $encoder;
        }
      }
    }

    // Check if hardware encoder is available and preferred.
    if ($this->ffmpegExecutor->hasEncoder('prores_videotoolbox')) {
      foreach ($options->extraArgs as $arg) {
        if ($arg === '-hwaccel' || $arg === HardwareType::VideoToolbox->value) {
          return 'prores_videotoolbox';
        }
      }
    }

    // Default to prores_ks for best quality.
    return 'prores_ks';
  }

  /**
   * Selects appropriate pixel format based on profile and options.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options.
   * @param int $profile
   *   ProRes profile.
   *
   * @return string
   *   Pixel format string.
   */
  #[\NoDiscard]
  private function selectPixelFormat(ConversionOptions $options, int $profile): string {
    // Check for explicit pixel format in options.
    if ($options->pixelFormat !== 'yuv420p') {
      return $options->pixelFormat;
    }

    // Check extra args for explicit format.
    foreach ($options->extraArgs as $index => $arg) {
      if ($arg === '-pix_fmt' && isset($options->extraArgs[$index + 1])) {
        return $options->extraArgs[$index + 1];
      }
    }

    // Use profile-appropriate format.
    return array_key_exists($profile, self::PROFILE_PIXEL_FORMATS)
      ? self::PROFILE_PIXEL_FORMATS[$profile]
      : 'yuv422p10le';
  }

  /**
   * Determines if the profile supports alpha channel.
   *
   * @param int $profile
   *   ProRes profile.
   *
   * @return bool
   *   TRUE if alpha is supported.
   */
  #[\NoDiscard]
  private function supportsAlpha(int $profile): bool {
    return $profile >= self::PROFILE_4444;
  }

  /**
   * Calculates bits per macroblock based on profile and quality settings.
   *
   * @param int $profile
   *   ProRes profile.
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options.
   *
   * @return int|null
   *   Bits per MB, or NULL to use encoder default.
   */
  #[\NoDiscard]
  private function calculateBitsPerMb(int $profile, ConversionOptions $options): ?int {
    // Check for explicit value in extra args.
    foreach ($options->extraArgs as $index => $arg) {
      if ($arg === '-bits_per_mb' && isset($options->extraArgs[$index + 1])) {
        return (int) $options->extraArgs[$index + 1];
      }
    }

    // Use encoder defaults based on profile - these are
    // approximate target values.
    return match ($profile) {
      self::PROFILE_PROXY => 1000,
      self::PROFILE_LT => 2400,
      self::PROFILE_STANDARD => 4000,
      self::PROFILE_HQ => 6200,
      self::PROFILE_4444 => 8000,
      self::PROFILE_4444XQ => 10000,
      default => NULL,
    };
  }

  /**
   * Gets the quantization matrix for quality tuning.
   *
   * @param int $profile
   *   ProRes profile.
   *
   * @return string
   *   Quantization matrix identifier.
   */
  #[\NoDiscard]
  private function getQuantMatrix(int $profile): string {
    // Higher quality profiles use finer quantization.
    return match ($profile) {
      self::PROFILE_PROXY, self::PROFILE_LT => 'proxy',
      self::PROFILE_STANDARD => 'lt',
      self::PROFILE_HQ => 'standard',
      self::PROFILE_4444, self::PROFILE_4444XQ => 'hq',
      default => 'auto',
    };
  }

  /**
   * Gets the profile name for display.
   *
   * @param int $profile
   *   Profile identifier.
   *
   * @return string
   *   Human-readable profile name.
   */
  #[\NoDiscard]
  public function getProfileName(int $profile): string {
    return self::PROFILE_NAMES[$profile] ?? 'unknown';
  }

  /**
   * Gets all available ProRes profiles.
   *
   * @return array<int, string>
   *   Array of profile ID => name.
   */
  #[\NoDiscard]
  public function getAvailableProfiles(): array {
    return self::PROFILE_NAMES;
  }

  /**
   * Checks if hardware acceleration is available.
   *
   * @return bool
   *   TRUE if prores_videotoolbox encoder is available.
   */
  #[\NoDiscard]
  public function hasHardwareAcceleration(): bool {
    return $this->ffmpegExecutor->hasEncoder('prores_videotoolbox');
  }

}
