<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_advanced\Plugin\FFmpegProcessor;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\ffmpeg_media\Attribute\FFmpegProcessor;
use Drupal\ffmpeg_media\Plugin\FFmpegProcessor\FFmpegProcessorBase;
use Drupal\ffmpeg_media\Service\FFmpegExecutorInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * Processor plugin for burning subtitles into video.
 *
 * Features:
 * - Hard-burn subtitles (SRT, VTT, ASS/SSA formats).
 * - Configurable font, size, color, and position.
 * - Styling support for ASS/SSA subtitles.
 * - Character encoding handling (UTF-8, Latin-1, etc.).
 * - Background box for better readability.
 *
 * Note: For soft subtitles (separate track), use FFmpeg muxing directly.
 * This processor creates hard-burned subtitles that become part of the video.
 *
 * Requires FFmpeg compiled with libass/subtitles filter support.
 */
#[FFmpegProcessor(
  id: 'subtitles',
  label: new TranslatableMarkup('Subtitles Burner'),
  description: new TranslatableMarkup('Burn subtitles permanently into video (hard subs).'),
  outputType: 'video',
  outputExtension: 'mp4',
  requiredFilters: ['subtitles'],
  weight: 22,
)]
final class SubtitlesProcessor extends FFmpegProcessorBase implements ContainerFactoryPluginInterface {

  /**
   * Supported subtitle formats.
   */
  public const array SUPPORTED_FORMATS = [
    'srt' => 'SubRip (.srt)',
    'vtt' => 'WebVTT (.vtt)',
    'ass' => 'Advanced SubStation Alpha (.ass)',
    'ssa' => 'SubStation Alpha (.ssa)',
  ];

  /**
   * Available subtitle positions.
   */
  public const array POSITIONS = [
    'bottom' => 'Bottom Center (Default)',
    'top' => 'Top Center',
    'middle' => 'Middle Center',
  ];

  /**
   * Supported subtitle character encodings.
   */
  public const array SUPPORTED_ENCODINGS = [
    'UTF-8',
    'ISO-8859-1',
    'ASCII',
    'UTF-16',
  ];

  /**
   * Constructs a SubtitlesProcessor object.
   *
   * @param array $configuration
   *   Plugin configuration.
   * @param string $plugin_id
   *   Plugin ID.
   * @param mixed $plugin_definition
   *   Plugin definition.
   * @param \Drupal\ffmpeg_media\Service\FFmpegExecutorInterface $ffmpegExecutor
   *   FFmpeg executor service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config factory.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   File system service.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    #[Autowire(service: FFmpegExecutorInterface::class)]
    FFmpegExecutorInterface $ffmpegExecutor,
    protected readonly ConfigFactoryInterface $configFactory,
    protected readonly FileSystemInterface $fileSystem,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $ffmpegExecutor);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function process(string $inputPath, string $outputPath, array $options = []): bool {
    $options = $this->mergeOptions($options);

    // Validate subtitle file exists.
    $subtitlePath = $options['subtitle_path'] ?? '';
    if (empty($subtitlePath)) {
      return FALSE;
    }

    $realSubtitlePath = $this->fileSystem->realpath($subtitlePath);
    if ($realSubtitlePath === FALSE || !file_exists($realSubtitlePath)) {
      return FALSE;
    }

    // Determine subtitle format from extension.
    $extension = $realSubtitlePath
      |> (fn(string $p): string => pathinfo($p, PATHINFO_EXTENSION))
      |> strtolower(...);

    // Use ASS filter for ASS/SSA (preserves styling).
    // Use subtitles filter for SRT/VTT formats.
    if (in_array($extension, ['ass', 'ssa'], TRUE)) {
      return $this->processAssSubtitles($inputPath, $outputPath, $realSubtitlePath, $options);
    }

    return $this->processSrtVttSubtitles($inputPath, $outputPath, $realSubtitlePath, $options);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getDefaultOptions(): array {
    $config = $this->configFactory->get('ffmpeg_media_advanced.settings');

    return [
      'subtitle_path' => '',
      'font_name' => $config->get('subtitles_font_name') ?? 'Arial',
      'font_size' => $config->get('subtitles_font_size') ?? 24,
      'primary_color' => $config->get('subtitles_primary_color') ?? 'FFFFFF',
      'outline_color' => $config->get('subtitles_outline_color') ?? '000000',
      'outline_width' => $config->get('subtitles_outline_width') ?? 2,
      'position' => $config->get('subtitles_position') ?? 'bottom',
      'margin_v' => $config->get('subtitles_margin_v') ?? 30,
      'encoding' => $config->get('subtitles_encoding') ?? 'UTF-8',
      'force_style' => $config->get('subtitles_force_style') ?? TRUE,
      'video_codec' => 'libsvtav1',
      'crf' => 23,
      'audio_codec' => 'libopus',
      'audio_bitrate' => '128k',
      'timeout' => 3600,
    ];
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function generateOutputPath(string $inputPath, array $options = []): string {
    $pathInfo = pathinfo($inputPath);
    $baseName = $pathInfo['filename'];
    $directory = $pathInfo['dirname'] ?? dirname($inputPath);

    return $directory . DIRECTORY_SEPARATOR . $baseName . '_subtitled.mp4';
  }

  /**
   * Processes video with ASS/SSA subtitles (preserves advanced styling).
   *
   * @param string $input
   *   Input video path.
   * @param string $output
   *   Output video path.
   * @param string $subtitlePath
   *   Path to ASS/SSA subtitle file.
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return bool
   *   TRUE if processing succeeded.
   */
  protected function processAssSubtitles(
    string $input,
    string $output,
    string $subtitlePath,
    array $options,
  ): bool {
    $ffmpegPath = $this->ffmpegExecutor->getFfmpegPath();

    // ASS filter - preserves styling from file.
    // Escape special characters in path for filter.
    $escapedPath = $this->escapeFilterPath($subtitlePath);
    $assFilter = sprintf("ass=%s", $escapedPath);

    // Build command array.
    $command = [
      $ffmpegPath,
      '-y',
      '-i', $input,
      '-vf', $assFilter,
      '-c:v', $options['video_codec'] ?? 'libsvtav1',
      '-crf', (string) ($options['crf'] ?? 23),
      '-c:a', $options['audio_codec'] ?? 'libopus',
      '-b:a', $options['audio_bitrate'] ?? '128k',
      '-movflags', '+faststart',
      $output,
    ];

    return $this->ffmpegExecutor->execute($command, [
      'timeout' => $options['timeout'] ?? 3600,
    ]);
  }

  /**
   * Processes video with SRT/VTT subtitles.
   *
   * @param string $input
   *   Input video path.
   * @param string $output
   *   Output video path.
   * @param string $subtitlePath
   *   Path to SRT/VTT subtitle file.
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return bool
   *   TRUE if processing succeeded.
   */
  protected function processSrtVttSubtitles(
    string $input,
    string $output,
    string $subtitlePath,
    array $options,
  ): bool {
    $ffmpegPath = $this->ffmpegExecutor->getFfmpegPath();

    // Escape special characters in path for filter.
    $escapedPath = $this->escapeFilterPath($subtitlePath);

    // Build subtitles filter with styling.
    $filter = sprintf("subtitles=%s", $escapedPath);

    // Add force_style if enabled.
    if ($options['force_style'] ?? TRUE) {
      $style = $this->buildForceStyle($options);
      $filter .= sprintf(":force_style='%s'", $style);
    }

    // Add character encoding if specified.
    $encoding = $this->normalizeEncoding($options['encoding'] ?? 'UTF-8');
    if ($encoding !== 'UTF-8') {
      $filter .= sprintf(":charenc=%s", $encoding);
    }

    // Build command array.
    $command = [
      $ffmpegPath,
      '-y',
      '-i', $input,
      '-vf', $filter,
      '-c:v', $options['video_codec'] ?? 'libsvtav1',
      '-crf', (string) ($options['crf'] ?? 23),
      '-c:a', $options['audio_codec'] ?? 'libopus',
      '-b:a', $options['audio_bitrate'] ?? '128k',
      '-movflags', '+faststart',
      $output,
    ];

    return $this->ffmpegExecutor->execute($command, [
      'timeout' => $options['timeout'] ?? 3600,
    ]);
  }

  /**
   * Normalizes subtitle encoding to the supported FFmpeg allowlist.
   *
   * @param mixed $encoding
   *   Raw encoding option.
   *
   * @return string
   *   A supported encoding. Invalid values fall back to UTF-8.
   */
  private function normalizeEncoding(mixed $encoding): string {
    $encoding = strtoupper(trim((string) $encoding));
    if ($encoding === '') {
      return 'UTF-8';
    }

    foreach (self::SUPPORTED_ENCODINGS as $supportedEncoding) {
      if (strtoupper($supportedEncoding) === $encoding) {
        return $supportedEncoding;
      }
    }

    return 'UTF-8';
  }

  /**
   * Builds force_style string for subtitle customization.
   *
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return string
   *   ASS style override string.
   */
  protected function buildForceStyle(array $options): string {
    $styles = [];

    // Font name.
    if (!empty($options['font_name'])) {
      $fontName = $this->sanitizeFontName((string) $options['font_name']);
      if ($fontName !== '') {
        $styles[] = sprintf('FontName=%s', $fontName);
      }
    }

    // Font size.
    if (!empty($options['font_size'])) {
      $styles[] = sprintf('FontSize=%d', (int) $options['font_size']);
    }

    // Primary color (ASS uses &HAABBGGRR format, but we accept hex RGB).
    if (!empty($options['primary_color'])) {
      $assColor = $this->rgbToAssColor($options['primary_color']);
      $styles[] = sprintf('PrimaryColour=%s', $assColor);
    }

    // Outline color.
    if (!empty($options['outline_color'])) {
      $assColor = $this->rgbToAssColor($options['outline_color']);
      $styles[] = sprintf('OutlineColour=%s', $assColor);
    }

    // Outline width.
    if (isset($options['outline_width'])) {
      $styles[] = sprintf('Outline=%d', (int) $options['outline_width']);
    }

    // Vertical margin based on position.
    $marginV = (int) ($options['margin_v'] ?? 30);
    $position = $options['position'] ?? 'bottom';

    switch ($position) {
      case 'top':
        $styles[] = sprintf('MarginV=%d', $marginV);
        $styles[] = 'Alignment=8';
        break;

      case 'middle':
        $styles[] = 'Alignment=5';
        break;

      default:
        $styles[] = sprintf('MarginV=%d', $marginV);
        $styles[] = 'Alignment=2';
        break;
    }

    return implode(',', $styles);
  }

  /**
   * Sanitizes a font name for ASS style overrides.
   *
   * @param string $fontName
   *   Raw font name option.
   *
   * @return string
   *   Sanitized font name limited to common printable characters.
   */
  protected function sanitizeFontName(string $fontName): string {
    $fontName = trim($fontName);
    if ($fontName === '') {
      return '';
    }

    return preg_replace('/[^A-Za-z0-9 _.-]/', '', $fontName) ?? '';
  }

  /**
   * Converts RGB hex color to ASS color format.
   *
   * ASS uses &HAABBGGRR format (alpha, blue, green, red).
   *
   * @param string $rgb
   *   RGB hex color (e.g., "FFFFFF" or "#FFFFFF").
   *
   * @return string
   *   ASS color string (e.g., "&H00FFFFFF").
   */
  protected function rgbToAssColor(string $rgb): string {
    // Remove # if present.
    $rgb = ltrim($rgb, '#');

    if (!preg_match('/^[0-9A-Fa-f]{6}$/', $rgb)) {
      return '&H00FFFFFF';
    }

    // Extract RGB components.
    $r = substr($rgb, 0, 2);
    $g = substr($rgb, 2, 2);
    $b = substr($rgb, 4, 2);

    // Convert to ASS format (BGR, with 00 alpha for opaque).
    return sprintf('&H00%s%s%s', strtoupper($b), strtoupper($g), strtoupper($r));
  }

  /**
   * Escapes file path for use in FFmpeg filter.
   *
   * @param string $path
   *   File path to escape.
   *
   * @return string
   *   Escaped path safe for filter use.
   */
  protected function escapeFilterPath(string $path): string {
    // Escape characters that have special meaning in FFmpeg filtergraphs.
    // Backslash must be escaped first to avoid double-escaping the others.
    // Characters: \ : ' [ ] , ; — all are separator or label delimiters
    // in FFmpeg filter syntax.
    return str_replace(
      ['\\', ':', "'", '[', ']', ',', ';', '%'],
      ['\\\\', '\\:', "\\'", '\\[', '\\]', '\\,', '\\;', '%%'],
      $path,
    );
  }

  /**
   * Gets supported subtitle formats.
   *
   * @return array<string, string>
   *   Format extension => description.
   */
  #[\NoDiscard]
  public static function getSupportedFormats(): array {
    return self::SUPPORTED_FORMATS;
  }

  /**
   * Gets available subtitle positions.
   *
   * @return array<string, string>
   *   Position ID => description.
   */
  #[\NoDiscard]
  public static function getAvailablePositions(): array {
    return self::POSITIONS;
  }

  /**
   * Creates options for standard subtitle burning.
   *
   * @param string $subtitlePath
   *   Path to subtitle file.
   *
   * @return array<string, int|string>
   *   Options array for standard subtitles.
   */
  #[\NoDiscard]
  public static function standardOptions(string $subtitlePath): array {
    return [
      'subtitle_path' => $subtitlePath,
      'font_name' => 'Arial',
      'font_size' => 24,
      'primary_color' => 'FFFFFF',
      'outline_color' => '000000',
      'outline_width' => 2,
      'position' => 'bottom',
      'margin_v' => 30,
    ];
  }

  /**
   * Creates options for large subtitles (accessibility).
   *
   * @param string $subtitlePath
   *   Path to subtitle file.
   *
   * @return array<string, int|string>
   *   Options array for large subtitles.
   */
  #[\NoDiscard]
  public static function accessibleOptions(string $subtitlePath): array {
    return [
      'subtitle_path' => $subtitlePath,
      'font_name' => 'Arial',
      'font_size' => 32,
      'primary_color' => 'FFFF00',
      'outline_color' => '000000',
      'outline_width' => 3,
      'position' => 'bottom',
      'margin_v' => 40,
    ];
  }

}
