<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\ffmpeg_media\Enum\HardwareType;
use Drupal\ffmpeg_media\FFmpegCodecPluginManager;
use Drupal\ffmpeg_media\FFmpegOutputPluginManager;
use Drupal\ffmpeg_media\Plugin\FFmpegCodec\FFmpegCodecInterface;
use Drupal\ffmpeg_media\Plugin\FFmpegOutput\FFmpegOutputInterface;

/**
 * Validates ProfileForm outputs and builds codec option lists.
 *
 * @internal
 */
final class ProfileFormValidator implements ProfileFormValidatorInterface {

  use StringTranslationTrait;

  /**
   * Video codecs known to have patent encumbrances.
   *
   * @var array<int, string>
   */
  private const array PATENT_ENCUMBERED_CODECS = ['h264', 'hevc', 'vvc', 'prores'];

  /**
   * Audio codecs known to have patent encumbrances.
   *
   * @var array<int, string>
   */
  private const array PATENT_ENCUMBERED_AUDIO_CODECS = ['aac'];

  /**
   * Maximum number of outputs per profile.
   */
  public const int MAX_OUTPUTS = 20;

  /**
   * Constructs a ProfileFormValidator.
   *
   * @param \Drupal\ffmpeg_media\FFmpegCodecPluginManager $codecManager
   *   Codec plugin manager.
   * @param \Drupal\ffmpeg_media\FFmpegOutputPluginManager $outputManager
   *   Output plugin manager.
   * @param \Drupal\ffmpeg_media\Service\HardwareDetectorInterface $hardwareDetector
   *   Hardware detector for GPU compatibility validation.
   */
  public function __construct(
    private readonly FFmpegCodecPluginManager $codecManager,
    private readonly FFmpegOutputPluginManager $outputManager,
    private readonly HardwareDetectorInterface $hardwareDetector,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function validateOutputs(FormStateInterface $form_state, string $hwAccel): void {
    $outputs = $form_state->getValue('outputs') ?? [];

    if (count(array_filter(array_keys($outputs), 'is_int')) > self::MAX_OUTPUTS) {
      $form_state->setErrorByName('outputs', $this->t('A profile may have at most @max outputs.', ['@max' => self::MAX_OUTPUTS]));
      return;
    }

    foreach ($outputs as $index => $output) {
      if (!is_numeric($index)) {
        continue;
      }

      $codecId = $output['codec'] ?? '';
      $containerId = $output['container'] ?? '';
      $enabled = (bool) ($output['enabled'] ?? FALSE);

      if ($codecId === '' || $containerId === '') {
        continue;
      }

      try {
        $codec = $this->codecManager->createInstance($codecId);
      }
      catch (\Throwable) {
        continue;
      }

      if (!$codec instanceof FFmpegCodecInterface) {
        continue;
      }

      // Validate container/codec compatibility.
      $supportedContainers = $codec->getSupportedContainers();
      if (!in_array($containerId, $supportedContainers, TRUE)) {
        $form_state->setErrorByName(
          "outputs][$index][container",
          $this->t('The codec %codec does not support the %container container format. Supported containers: %supported.', [
            '%codec' => $codec->getLabel(),
            '%container' => strtoupper($containerId),
            '%supported' => implode(', ', array_map('strtoupper', $supportedContainers)),
          ]),
        );
      }

      // Validate audio codec ↔ container compatibility.
      $audioCodecId = $output['options']['audio_codec'] ?? '';
      if ($audioCodecId !== '') {
        $this->validateAudioCodecCompatibility($form_state, $containerId, $audioCodecId, (int) $index);
      }

      // Validate two-pass ↔ codec compatibility.
      $twoPass = (bool) ($output['options']['two_pass'] ?? FALSE);
      if ($twoPass) {
        $this->validateTwoPassCompatibility($form_state, $codecId, (int) $index);
      }

      // Validate hardware encoder availability when GPU is required.
      if ($hwAccel === 'required' && $enabled) {
        $this->validateHardwareCodecCompatibility($form_state, $codecId, $containerId, (int) $index);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getGroupedCodecOptions(): array {
    try {
      $codecs = $this->codecManager->getAvailableCodecs();
    }
    catch (\Throwable) {
      return [];
    }

    $royaltyFree = [];
    $patentEncumbered = [];

    foreach ($codecs as $id => $codec) {
      $label = (string) $codec->getLabel();
      if (in_array($id, self::PATENT_ENCUMBERED_CODECS, TRUE)) {
        $patentEncumbered[$id] = $label;
      }
      else {
        $royaltyFree[$id] = $label;
      }
    }

    $options = [];
    if ($royaltyFree !== []) {
      $options[(string) $this->t('Royalty-Free')] = $royaltyFree;
    }
    if ($patentEncumbered !== []) {
      $options[(string) $this->t('Patent-Encumbered')] = $patentEncumbered;
    }

    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function getGroupedAudioCodecOptions(): array {
    $allCodecs = [
      'aac' => $this->t('<abbr title="Advanced Audio Coding">AAC</abbr>'),
      'mp3' => $this->t('MP3'),
      'libopus' => $this->t('Opus'),
      'libvorbis' => $this->t('Vorbis'),
      'flac' => $this->t('<abbr title="Free Lossless Audio Codec">FLAC</abbr>'),
    ];

    $royaltyFree = [];
    $patentEncumbered = [];

    foreach ($allCodecs as $id => $label) {
      if (in_array($id, self::PATENT_ENCUMBERED_AUDIO_CODECS, TRUE)) {
        $patentEncumbered[$id] = $label;
      }
      else {
        $royaltyFree[$id] = $label;
      }
    }

    $options = [];
    if ($royaltyFree !== []) {
      $options[(string) $this->t('Royalty-Free')] = $royaltyFree;
    }
    if ($patentEncumbered !== []) {
      $options[(string) $this->t('Patent-Encumbered')] = $patentEncumbered;
    }
    $options[(string) $this->t('Other')] = [
      'copy' => $this->t('Copy (no re-encoding)'),
    ];

    return $options;
  }

  /**
   * Validates audio codec compatibility with the container format.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state for setting errors.
   * @param string $containerId
   *   Container format plugin ID.
   * @param string $audioCodecId
   *   Audio codec ID (e.g. 'aac', 'libopus').
   * @param int $index
   *   Output index for error placement.
   */
  private function validateAudioCodecCompatibility(
    FormStateInterface $form_state,
    string $containerId,
    string $audioCodecId,
    int $index,
  ): void {
    try {
      $outputPlugin = $this->outputManager->createInstance($containerId);
    }
    catch (\Throwable) {
      return;
    }

    if (!$outputPlugin instanceof FFmpegOutputInterface) {
      return;
    }

    if (!$outputPlugin->supportsAudio()) {
      $form_state->setErrorByName(
        "outputs][$index][options][audio_codec",
        $this->t('The %container format does not support audio. Remove the audio codec setting or choose a different container.', [
          '%container' => strtoupper($containerId),
        ]),
      );
      return;
    }

    if (!$outputPlugin->supportsAudioCodec($audioCodecId)) {
      $supported = $outputPlugin->getSupportedAudioCodecs();
      $form_state->setErrorByName(
        "outputs][$index][options][audio_codec",
        $this->t('The audio codec %audio_codec is not compatible with the %container container. Supported audio codecs: %supported.', [
          '%audio_codec' => $audioCodecId,
          '%container' => strtoupper($containerId),
          '%supported' => implode(', ', $supported),
        ]),
      );
    }
  }

  /**
   * Validates two-pass encoding compatibility with the video codec.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state for setting errors.
   * @param string $codecId
   *   Video codec plugin ID.
   * @param int $index
   *   Output index for error placement.
   */
  private function validateTwoPassCompatibility(
    FormStateInterface $form_state,
    string $codecId,
    int $index,
  ): void {
    try {
      $codec = $this->codecManager->createInstance($codecId);
    }
    catch (\Throwable) {
      return;
    }

    if (!$codec instanceof FFmpegCodecInterface) {
      return;
    }

    if (!$codec->supportsTwoPass()) {
      $form_state->setErrorByName(
        "outputs][$index][options][two_pass",
        $this->t('Two-pass encoding is not supported with the %codec codec.', [
          '%codec' => $codec->getLabel(),
        ]),
      );
    }
  }

  /**
   * Validates that a codec has a hardware encoder on the detected GPU.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state for setting errors.
   * @param string $codecId
   *   Codec plugin ID (e.g. 'vp9', 'h264').
   * @param string $containerId
   *   Container format (e.g. 'webm', 'mp4').
   * @param int $index
   *   Output index for error placement.
   */
  private function validateHardwareCodecCompatibility(
    FormStateInterface $form_state,
    string $codecId,
    string $containerId,
    int $index,
  ): void {
    $availableHw = $this->hardwareDetector->detectAvailableHardware();

    if ($availableHw === []) {
      $form_state->setErrorByName(
        'hardware_acceleration',
        $this->t('Hardware acceleration is set to <em>Required</em> but no GPU hardware was detected on this system.'),
      );
      return;
    }

    // Check if any detected hardware type has an encoder for this codec.
    $compatibleHwTypes = [];
    foreach (HardwareType::preferenceOrder() as $hwType) {
      if (in_array($hwType->value, $availableHw, TRUE) && $hwType->encoderForCodec($codecId) !== NULL) {
        $compatibleHwTypes[] = strtoupper($hwType->value);
      }
    }

    if ($compatibleHwTypes !== []) {
      return;
    }

    // No hardware encoder found — build a list of compatible codecs.
    $detectedTypes = [];
    foreach ($availableHw as $hw) {
      $hwType = HardwareType::tryFrom($hw);
      if ($hwType !== NULL) {
        $detectedTypes[] = $hwType;
      }
    }

    $compatibleCodecs = $this->getHardwareCompatibleCodecs($detectedTypes);

    $form_state->setErrorByName(
      "outputs][$index][codec",
      $this->t('The codec %codec (%container) has no GPU hardware encoder on your detected hardware (%hw). Compatible codecs for your GPU: %compatible.', [
        '%codec' => strtoupper($codecId),
        '%container' => strtoupper($containerId),
        '%hw' => implode(', ', array_map('strtoupper', $availableHw)),
        '%compatible' => $compatibleCodecs !== [] ? implode(', ', $compatibleCodecs) : (string) $this->t('none'),
      ]),
    );
  }

  /**
   * Gets codecs that have hardware encoders on the given hardware types.
   *
   * @param array<int, \Drupal\ffmpeg_media\Enum\HardwareType> $hwTypes
   *   Detected hardware types.
   *
   * @return array<int, string>
   *   Compatible codec labels (e.g. ['H264', 'HEVC', 'AV1']).
   */
  private function getHardwareCompatibleCodecs(array $hwTypes): array {
    $knownCodecs = ['h264', 'hevc', 'av1', 'vp9'];
    $compatible = [];

    foreach ($knownCodecs as $codec) {
      foreach ($hwTypes as $hwType) {
        if ($hwType->encoderForCodec($codec) !== NULL) {
          $compatible[] = strtoupper($codec);
          break;
        }
      }
    }

    return $compatible;
  }

}
