<?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;
use Drupal\ffmpeg_media\ValueObject\GopSettings;

/**
 * HEVC/H.265 codec plugin with alpha layer support (FFmpeg 8.0+).
 *
 * This codec provides HEVC encoding via libx265 with support for:
 * - Standard 8-bit and 10-bit encoding
 * - Alpha channel encoding (FFmpeg 8.0+ feature)
 * - Various presets for quality/speed tradeoffs.
 *
 * Alpha layer encoding requirements (FFmpeg 8.0+):
 * - libx265 must be compiled with alpha support
 * - Pixel format must support alpha: yuva420p, yuva444p10le
 * - Container must support alpha: MOV, MKV (not MP4)
 * - Enable alpha explicitly via ConversionOptions::alphaEnabled
 *
 * Example usage for alpha encoding:
 * @code
 * $options = new ConversionOptions(
 *   pixelFormat: 'yuva420p',
 *   alphaEnabled: TRUE,
 * );
 * @endcode
 *
 * @see https://trac.ffmpeg.org/wiki/Encode/H.265
 */
#[FFmpegCodec(
  id: 'hevc',
  label: new TranslatableMarkup('HEVC (H.265)'),
  description: new TranslatableMarkup('High efficiency codec with better compression than H.264. Supports alpha channel encoding in FFmpeg 8.0+.'),
  encoder: 'libx265',
  supportedContainers: ['mp4', 'mkv', 'mov', 'flv', 'hls', 'dash', 'rtp'],
  hardwareAccelerated: FALSE,
  twoPassSupported: TRUE,
  weight: 15,
)]
final class HEVCCodec extends FFmpegCodecBase {

  /**
   * Pixel formats that support alpha channel.
   *
   * @var array<string>
   */
  private const array ALPHA_PIXEL_FORMATS = [
    'yuva420p',
    'yuva422p',
    'yuva444p',
    'yuva420p10le',
    'yuva422p10le',
    'yuva444p10le',
  ];

  /**
   * Containers that support HEVC with alpha channel.
   *
   * @var array<string>
   */
  private const array ALPHA_SUPPORTED_CONTAINERS = [
    'mov',
    'mkv',
  ];

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function buildArguments(ConversionOptions $options): array {
    $args = [
      '-c:v', 'libx265',
      '-preset', $this->getPresetName($options->preset),
      '-tag:v', 'hvc1',
    ];

    // Add rate control settings.
    if ($options->rateControl !== NULL) {
      $args = array_merge($args, $options->rateControl->buildArguments('hevc'));
    }
    else {
      // Default to CRF mode.
      $args[] = '-crf';
      $args[] = (string) $options->crf;
    }

    // Add GOP/keyframe settings.
    if ($options->gopSettings !== NULL) {
      $args = array_merge($args, $options->gopSettings->buildArguments('hevc'));
      // Add x265-specific closed GOP params if needed.
      if ($options->gopSettings->closedGop) {
        $args = $this->addX265GopParams($args, $options->gopSettings);
      }
    }

    // Add alpha layer arguments if enabled (FFmpeg 8.0+ feature).
    if ($this->isAlphaEnabled($options)) {
      $args = array_merge($args, $this->buildAlphaArguments($options));
    }

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

    // Add audio arguments.
    $args = array_merge($args, $this->buildAudioArguments($options));

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

    return $args;
  }

  /**
   * Adds x265-specific GOP parameters.
   *
   * @param array<int, string> $args
   *   Current FFmpeg arguments.
   * @param \Drupal\ffmpeg_media\ValueObject\GopSettings $gopSettings
   *   GOP settings.
   *
   * @return array<int, string>
   *   Updated FFmpeg arguments.
   */
  protected function addX265GopParams(array $args, GopSettings $gopSettings): array {
    $x265Params = [];

    // Closed GOP for segment-based streaming.
    if ($gopSettings->closedGop) {
      $x265Params[] = 'open-gop=0';
    }

    // B-frames pyramid for better compression.
    if ($gopSettings->bFrames > 0) {
      $x265Params[] = 'bframes=' . $gopSettings->bFrames;
    }

    // Reference frames.
    $x265Params[] = 'ref=' . $gopSettings->refFrames;

    // Check if -x265-params already exists.
    $existingIdx = array_search('-x265-params', $args, TRUE);
    if ($existingIdx !== FALSE) {
      $idx = (int) $existingIdx;
      if (isset($args[$idx + 1])) {
        $args[$idx + 1] .= ':' . implode(':', $x265Params);
      }
    }
    else {
      $args[] = '-x265-params';
      $args[] = implode(':', $x265Params);
    }

    return $args;
  }

  /**
   * Builds FFmpeg arguments for alpha channel encoding.
   *
   * Alpha layer encoding in HEVC (FFmpeg 8.0+) requires:
   * - A pixel format with alpha support (yuva420p, yuva444p10le, etc.)
   * - x265 params configured for alpha handling
   * - Appropriate container format (MOV or MKV recommended)
   *
   * The alpha channel is encoded as a separate layer that compositing
   * software can use for transparency effects.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options containing pixel format and other settings.
   *
   * @return array<string>
   *   FFmpeg arguments for alpha channel encoding.
   */
  #[\NoDiscard]
  public function buildAlphaArguments(ConversionOptions $options): array {
    $args = [];

    // Override pixel format if not already alpha-capable.
    $pixelFormat = $this->getAlphaPixelFormat($options);
    if ($pixelFormat !== $options->pixelFormat) {
      $args[] = '-pix_fmt';
      $args[] = $pixelFormat;
    }

    // Configure x265 params for alpha encoding.
    // The alpha layer requires specific x265 settings for proper encoding.
    $x265Params = $this->buildX265AlphaParams($options);
    if ($x265Params !== '') {
      $args[] = '-x265-params';
      $args[] = $x265Params;
    }

    return $args;
  }

  /**
   * Determines the appropriate pixel format for alpha encoding.
   *
   * If the current pixel format already supports alpha, it is returned.
   * Otherwise, an appropriate alpha format is selected based on bit depth.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options with current pixel format.
   *
   * @return string
   *   Pixel format with alpha support.
   */
  #[\NoDiscard]
  protected function getAlphaPixelFormat(ConversionOptions $options): string {
    // If already an alpha format, use it.
    if (in_array($options->pixelFormat, self::ALPHA_PIXEL_FORMATS, TRUE)) {
      return $options->pixelFormat;
    }

    // Select alpha format based on current format's bit depth.
    return match ($options->pixelFormat) {
      'yuv420p10le', 'yuv422p10le', 'yuv444p10le' => 'yuva444p10le',
      default => 'yuva420p',
    };
  }

  /**
   * Builds x265 parameters for alpha channel encoding.
   *
   * These parameters configure libx265 for proper alpha layer handling:
   * - lossless mode can be enabled for alpha preservation
   * - SAO (Sample Adaptive Offset) settings
   * - Other encoder optimizations for alpha content.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options for context.
   *
   * @return string
   *   Colon-separated x265 parameters string.
   */
  #[\NoDiscard]
  protected function buildX265AlphaParams(ConversionOptions $options): string {
    $params = [];

    // Enable alpha layer encoding mode.
    // This ensures the alpha channel is properly multiplexed in the output.
    $params[] = 'alpha=1';

    // For high quality alpha, consider lower CRF on alpha plane.
    if ($options->crf <= 23) {
      // High quality mode: preserve alpha fidelity.
      $params[] = 'sao=0';
    }

    return implode(':', $params);
  }

  /**
   * Checks if alpha encoding is enabled in options.
   *
   * Alpha encoding is enabled when either:
   * - The explicit alphaEnabled option is set
   * - The pixel format is an alpha format (yuva420p, yuva444p, etc.)
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options to check.
   *
   * @return bool
   *   TRUE if alpha encoding should be enabled.
   */
  #[\NoDiscard]
  public function isAlphaEnabled(ConversionOptions $options): bool {
    if ($options->alphaEnabled) {
      return TRUE;
    }
    // Alpha enabled based on pixel format.
    return in_array($options->pixelFormat, self::ALPHA_PIXEL_FORMATS, TRUE);
  }

  /**
   * Validates alpha encoding configuration.
   *
   * Checks that all requirements for alpha encoding are met:
   * - FFmpeg 8.0+ (assumed available)
   * - Compatible pixel format
   * - Compatible container format.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options to validate.
   * @param string $containerFormat
   *   Target container format (e.g., 'mov', 'mkv', 'mp4').
   *
   * @return array{valid: bool, errors: array<string>}
   *   Validation result with any error messages.
   */
  #[\NoDiscard]
  public function validateAlphaConfiguration(
    ConversionOptions $options,
    string $containerFormat,
  ): array {
    $errors = [];

    // Check container support.
    if (!in_array($containerFormat, self::ALPHA_SUPPORTED_CONTAINERS, TRUE)) {
      $errors[] = sprintf(
        'Container "%s" does not support HEVC alpha. Use MOV or MKV.',
        $containerFormat,
      );
    }

    // Warn about pixel format.
    if (!in_array($options->pixelFormat, self::ALPHA_PIXEL_FORMATS, TRUE)) {
      $errors[] = sprintf(
        'Pixel format "%s" will be converted to "%s" for alpha support.',
        $options->pixelFormat,
        $this->getAlphaPixelFormat($options),
      );
    }

    return [
      'valid' => empty($errors),
      'errors' => $errors,
    ];
  }

  /**
   * Gets supported alpha pixel formats.
   *
   * @return array<string>
   *   List of pixel formats that support alpha channel.
   */
  #[\NoDiscard]
  public static function getAlphaPixelFormats(): array {
    return self::ALPHA_PIXEL_FORMATS;
  }

  /**
   * Gets containers that support alpha channel with HEVC.
   *
   * @return array<string>
   *   List of container formats supporting HEVC alpha.
   */
  #[\NoDiscard]
  public static function getAlphaSupportedContainers(): array {
    return self::ALPHA_SUPPORTED_CONTAINERS;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getDefaultOptions(): ConversionOptions {
    return new ConversionOptions(
      crf: 28,
      preset: 6,
      threads: 1,
      audioBitrate: '128k',
      audioCodec: 'libopus',
      pixelFormat: 'yuv420p',
    );
  }

  /**
   * Gets default options optimized for alpha channel encoding.
   *
   * Returns options pre-configured for alpha encoding:
   * - Alpha-capable pixel format (yuva420p)
   * - Slightly lower CRF for quality preservation.
   *
   * @return \Drupal\ffmpeg_media\ValueObject\ConversionOptions
   *   Options configured for alpha encoding.
   */
  #[\NoDiscard]
  public function getAlphaEncodingOptions(): ConversionOptions {
    return new ConversionOptions(
      crf: 23,
      preset: 6,
      threads: 1,
      audioBitrate: '128k',
      audioCodec: 'libopus',
      pixelFormat: 'yuva420p',
    );
  }

  /**
   * Converts numeric preset to x265 preset name.
   *
   * @param int $preset
   *   Numeric preset (0-9).
   *
   * @return string
   *   x265 preset name.
   */
  protected function getPresetName(int $preset): string {
    $presets = [
      0 => 'placebo',
      1 => 'veryslow',
      2 => 'slower',
      3 => 'slow',
      4 => 'medium',
      5 => 'medium',
      6 => 'fast',
      7 => 'faster',
      8 => 'veryfast',
      9 => 'ultrafast',
    ];

    return $presets[$preset] ?? 'medium';
  }

}
