<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Form;

use Drupal\Core\Config\ConfigFactoryInterface;
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;
use Drupal\ffmpeg_media\Service\HardwareDetectorInterface;

/**
 * Builds and validates profile output configuration form elements.
 */
final class ProfileFormOutputHelper {

  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'];

  /**
   * Constructs the helper.
   */
  public function __construct(
    private readonly ConfigFactoryInterface $configFactory,
    private readonly FFmpegCodecPluginManager $codecManager,
    private readonly FFmpegOutputPluginManager $outputManager,
    private readonly HardwareDetectorInterface $hardwareDetector,
  ) {}

  /**
   * Builds form elements for a single output configuration.
   *
   * @param array<string, mixed> $output
   *   Output configuration.
   * @param int $index
   *   Zero-based output index in the form state.
   *
   * @return array<string, mixed>
   *   Form element definition.
   */
  #[\NoDiscard]
  public function buildOutputForm(array $output, int $index): array {
    $element = [
      '#type' => 'details',
      '#title' => $this->t('Output @index: @container', [
        '@index' => $index + 1,
        '@container' => strtoupper($output['container'] ?? 'NEW'),
      ]),
      '#open' => $output['enabled'] ?? TRUE,
    ];

    $ajaxValidation = [
      'callback' => '::validateOutputAjaxCallback',
      'wrapper' => 'output-validation-' . $index,
    ];

    $element['enabled'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Enabled'),
      '#default_value' => $output['enabled'] ?? TRUE,
    ];

    try {
      $containerOptions = $this->outputManager->getOutputOptions();
    }
    catch (\Throwable) {
      $containerOptions = [];
    }

    $element['container'] = [
      '#type' => 'select',
      '#title' => $this->t('Container format'),
      '#options' => $containerOptions,
      '#default_value' => $output['container'] ?? '',
      '#required' => TRUE,
      '#empty_option' => $this->t('- Select -'),
      '#ajax' => $ajaxValidation,
    ];

    $element['codec'] = [
      '#type' => 'select',
      '#title' => $this->t('Codec'),
      '#options' => $this->getGroupedCodecOptions(),
      '#default_value' => $output['codec'] ?? '',
      '#required' => TRUE,
      '#empty_option' => $this->t('- Select -'),
      '#description' => $this->t('Codecs in the <em>Patent-Encumbered</em> group may require licensing depending on jurisdiction. The server operator is responsible for codec patent compliance. See <a href=":url">FFmpeg Legal</a>.', [
        ':url' => 'https://ffmpeg.org/legal.html',
      ]),
      '#ajax' => $ajaxValidation,
    ];

    $element['options'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Encoding Options'),
    ];

    $options = $output['options'] ?? [];

    $element['options']['crf'] = [
      '#type' => 'number',
      '#title' => $this->t('<abbr title="Constant Rate Factor">CRF</abbr> (Quality)'),
      '#description' => $this->t('Lower = better quality, larger file. Recommended ranges: H.264/HEVC 18–28, VP9 25–35, AV1 20–35.'),
      '#default_value' => $options['crf'] ?? 31,
      '#min' => 0,
      '#max' => 63,
    ];

    $element['options']['preset'] = [
      '#type' => 'number',
      '#title' => $this->t('Preset'),
      '#description' => $this->t('0 = slowest/best quality, 9 = fastest.'),
      '#default_value' => $options['preset'] ?? 6,
      '#min' => 0,
      '#max' => 9,
    ];

    $element['options']['audio_bitrate'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Audio bitrate'),
      '#default_value' => $options['audio_bitrate'] ?? '128k',
      '#size' => 10,
      '#states' => [
        'invisible' => [
          ":input[name=\"outputs[$index][container]\"]" => ['value' => 'jxl'],
        ],
      ],
    ];

    $element['options']['audio_codec'] = [
      '#type' => 'select',
      '#title' => $this->t('Audio codec'),
      '#options' => $this->getGroupedAudioCodecOptions(),
      '#default_value' => $options['audio_codec'] ?? 'libopus',
      '#description' => $this->t('Codecs in the <em>Patent-Encumbered</em> group may require licensing. The server operator is responsible for codec patent compliance.'),
      '#ajax' => $ajaxValidation,
      '#states' => [
        'invisible' => [
          ":input[name=\"outputs[$index][container]\"]" => ['value' => 'jxl'],
        ],
      ],
    ];

    $element['options']['max_width'] = [
      '#type' => 'number',
      '#title' => $this->t('Max width'),
      '#description' => $this->t('Leave empty for no limit.'),
      '#default_value' => $options['max_width'] ?? NULL,
      '#min' => 0,
    ];

    $element['options']['max_height'] = [
      '#type' => 'number',
      '#title' => $this->t('Max height'),
      '#description' => $this->t('Leave empty for no limit.'),
      '#default_value' => $options['max_height'] ?? NULL,
      '#min' => 0,
    ];

    $element['options']['two_pass'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Two-pass encoding'),
      '#description' => $this->t('Better quality but slower.'),
      '#default_value' => $options['two_pass'] ?? FALSE,
      '#ajax' => $ajaxValidation,
    ];

    $element['validation_messages'] = [
      '#type' => 'container',
      '#attributes' => ['id' => 'output-validation-' . $index],
    ];

    $element['remove'] = [
      '#type' => 'submit',
      '#value' => $this->t('Remove output'),
      '#name' => 'remove_output_' . $index,
      '#submit' => ['::removeOutputSubmit'],
      '#ajax' => [
        'callback' => '::outputsAjaxCallback',
        'wrapper' => 'outputs-wrapper',
      ],
      '#limit_validation_errors' => [],
    ];

    return $element;
  }

  /**
   * Gets default output configurations.
   *
   * @return array<int, array<string, mixed>>
   *   Default outputs.
   */
  #[\NoDiscard]
  public function getDefaultOutputs(): array {
    $globalBitrate = $this->configFactory->get('ffmpeg_media.settings')
      ->get('audio.format.bitrate') ?? '128k';

    return [
      [
        'container' => 'webm',
        'codec' => 'av1',
        'enabled' => TRUE,
        'options' => [
          'crf' => 30,
          'preset' => 6,
          'audio_bitrate' => $globalBitrate,
          'audio_codec' => 'libopus',
        ],
      ],
      [
        'container' => 'mp4',
        'codec' => 'av1',
        'enabled' => TRUE,
        'options' => [
          'crf' => 30,
          'preset' => 6,
          'audio_bitrate' => $globalBitrate,
          'audio_codec' => 'libopus',
        ],
      ],
    ];
  }

  /**
   * Gets audio codec options grouped by patent status.
   *
   * @return array<string, array<string, string|\Drupal\Core\StringTranslation\TranslatableMarkup>>
   *   Grouped audio codec options.
   */
  #[\NoDiscard]
  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 configured outputs in the profile form.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state to record validation errors on.
   * @param string $hardwareAcceleration
   *   Selected hardware acceleration requirement setting.
   * @param array<mixed, mixed> $outputs
   *   Output configuration values from form state.
   */
  public function validateOutputs(FormStateInterface $form_state, string $hardwareAcceleration, array $outputs): void {
    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;
      }

      $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)),
          ]),
        );
      }

      $audioCodecId = $output['options']['audio_codec'] ?? '';
      if ($audioCodecId !== '') {
        $this->validateAudioCodecCompatibility($form_state, $containerId, $audioCodecId, (int) $index);
      }

      $twoPass = (bool) ($output['options']['two_pass'] ?? FALSE);
      if ($twoPass) {
        $this->validateTwoPassCompatibility($form_state, $codecId, (int) $index);
      }

      if ($hardwareAcceleration === 'required' && $enabled) {
        $this->validateHardwareCodecCompatibility($form_state, $codecId, $containerId, (int) $index);
      }
    }
  }

  /**
   * Gets codec options grouped by patent status.
   *
   * @return array<string, array<string, string>>
   *   Grouped codec options.
   */
  #[\NoDiscard]
  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;
  }

  /**
   * Validates audio codec compatibility with the container format.
   */
  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 codec.
   */
  private function validateTwoPassCompatibility(FormStateInterface $form_state, string $codecId, int $index): void {
    $twoPassCodecs = ['libx264', 'libx265', 'libvpx-vp9', 'libsvtav1', 'libvvenc'];

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

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

    $encoder = $codec->getEncoder();
    if (!in_array($encoder, $twoPassCodecs, TRUE)) {
      $form_state->setErrorByName(
        "outputs][$index][options][two_pass",
        $this->t('Two-pass encoding is not supported with the %codec codec (encoder: %encoder). Supported encoders: %supported.', [
          '%codec' => $codec->getLabel(),
          '%encoder' => $encoder,
          '%supported' => implode(', ', $twoPassCodecs),
        ]),
      );
    }
  }

  /**
   * Validates that the selected codec has a hardware encoder.
   */
  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;
    }

    $compatibleHwTypes = [];
    foreach (HardwareType::preferenceOrder() as $hwType) {
      if (in_array($hwType->value, $availableHw, TRUE) && $hwType->encoderForCodec($codecId) !== NULL) {
        $compatibleHwTypes[] = strtoupper($hwType->value);
      }
    }

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

    $detectedTypes = [];
    foreach ($availableHw as $hardware) {
      $hwType = HardwareType::tryFrom($hardware);
      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> $hardwareTypes
   *   Detected hardware types.
   *
   * @return array<int, string>
   *   Compatible codec labels.
   */
  #[\NoDiscard]
  private function getHardwareCompatibleCodecs(array $hardwareTypes): array {
    $knownCodecs = ['h264', 'hevc', 'av1', 'vp9'];
    $compatible = [];

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

    return $compatible;
  }

}
