<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_image\Plugin\FFmpegProcessor;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
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 generating segment GIFs at fixed intervals.
 *
 * Generates multiple short animated GIFs throughout a video at configurable
 * intervals. Each GIF covers a short segment of the video with optional
 * palette optimization for quality.
 */
#[FFmpegProcessor(
  id: 'segment_gif',
  label: new TranslatableMarkup('Segment GIFs'),
  description: new TranslatableMarkup('Generates multiple short GIFs at fixed intervals throughout the video.'),
  outputType: 'image',
  outputExtension: 'gif',
  requiredFilters: ['palettegen', 'paletteuse', 'scale', 'fps'],
  weight: 10,
)]
final class SegmentGifProcessor extends FFmpegProcessorBase {

  /**
   * Output filename pattern for segment GIFs.
   */
  private const string OUTPUT_PATTERN = '_gif_%03d.gif';

  /**
   * Available dithering algorithms for palette use.
   */
  public const array DITHER_MODES = [
    'bayer' => 'Bayer (ordered, fast)',
    'floyd_steinberg' => 'Floyd-Steinberg (error diffusion)',
    'sierra2' => 'Sierra-2 (balanced)',
    'none' => 'None (no dithering)',
  ];

  /**
   * Generated file paths from the last process() call.
   *
   * @var array<string>
   */
  private array $generatedFiles = [];

  /**
   * Constructs a SegmentGifProcessor object.
   *
   * @param array<string, mixed> $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 service.
   * @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}
   *
   * @param string $inputPath
   *   Input file path.
   * @param string $outputPath
   *   Output file path or pattern.
   * @param array<string, mixed> $options
   *   Processing options.
   */
  #[\Override]
  #[\NoDiscard]
  public function process(string $inputPath, string $outputPath, array $options = []): bool {
    $this->generatedFiles = [];
    $options = $this->mergeOptions($options);

    $interval = $options['interval'] ?? 30;
    $duration = $options['duration'] ?? 3;
    $maxGifs = $options['max_gifs'] ?? 20;

    // Get video duration if not provided.
    $videoDuration = $options['video_duration'] ?? $this->getVideoDuration($inputPath);
    if ($videoDuration <= 0) {
      return FALSE;
    }

    // Ensure output directory exists.
    $outputDir = dirname($outputPath);
    if (!$this->fileSystem->prepareDirectory($outputDir, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
      return FALSE;
    }

    // Calculate segment start times.
    $startTimes = $this->calculateSegmentStartTimes($videoDuration, $interval, $duration, $maxGifs);

    if (empty($startTimes)) {
      return FALSE;
    }

    // Generate GIFs for each segment.
    $basePath = $this->getBaseOutputPath($outputPath);
    $index = 1;

    foreach ($startTimes as $startTime) {
      $segmentPath = sprintf('%s_%03d.gif', $basePath, $index);
      $segmentOptions = $options;
      $segmentOptions['start_time'] = $startTime;
      $segmentOptions['duration'] = min($duration, $videoDuration - $startTime);

      if ($this->processSegment($inputPath, $segmentPath, $segmentOptions)) {
        $this->generatedFiles[] = $segmentPath;
      }

      $index++;
    }

    // Return true if at least one GIF was generated.
    return !empty($this->generatedFiles);
  }

  /**
   * {@inheritdoc}
   *
   * @return array<string, bool|int|float|string>
   *   Default option values.
   */
  #[\Override]
  #[\NoDiscard]
  public function getDefaultOptions(): array {
    $imageConfig = $this->configFactory->get('ffmpeg_media_image.settings');
    $coreConfig = $this->configFactory->get('ffmpeg_media.settings');

    return [
      'interval' => $imageConfig->get('segment_gif_interval') ?? $coreConfig->get('segment_gif_interval') ?? 30,
      'duration' => $imageConfig->get('segment_gif_duration') ?? $coreConfig->get('segment_gif_duration') ?? 3,
      'fps' => $imageConfig->get('segment_gif_fps') ?? $coreConfig->get('segment_gif_fps') ?? 10,
      'width' => $imageConfig->get('segment_gif_width') ?? $coreConfig->get('segment_gif_width') ?? 320,
      'height' => -1,
      'max_gifs' => $imageConfig->get('segment_gif_max') ?? $coreConfig->get('segment_gif_max') ?? 20,
      'optimize_palette' => $imageConfig->get('segment_gif_optimize') ?? $coreConfig->get('segment_gif_optimize') ?? TRUE,
      'dither' => $imageConfig->get('gif_dither_mode') ?? 'bayer',
      'bayer_scale' => 3,
      'stats_mode' => 'diff',
      'loop' => 0,
      'timeout' => 120,
    ];
  }

  /**
   * {@inheritdoc}
   *
   * @param string $inputPath
   *   Input file path.
   * @param array<string, mixed> $options
   *   Processing options.
   */
  #[\Override]
  #[\NoDiscard]
  public function generateOutputPath(string $inputPath, array $options = []): string {
    $baseName = pathinfo($inputPath, PATHINFO_FILENAME) ?: 'video';
    $directory = pathinfo($inputPath, PATHINFO_DIRNAME) ?: '';

    // Create subdirectory for segment GIFs.
    $gifDir = $directory . DIRECTORY_SEPARATOR . $baseName . '_gifs';

    return $gifDir . DIRECTORY_SEPARATOR . $baseName . self::OUTPUT_PATTERN;
  }

  /**
   * Gets the list of generated GIF paths.
   *
   * Call this after process() to get actual generated file paths.
   *
   * @return array<string>
   *   Array of generated GIF file paths.
   */
  #[\NoDiscard]
  public function getGeneratedFiles(): array {
    return $this->generatedFiles;
  }

  /**
   * Processes a single video segment to GIF.
   *
   * @param string $inputPath
   *   Input video path.
   * @param string $outputPath
   *   Output GIF path.
   * @param array<string, mixed> $options
   *   Processing options including start_time and duration.
   *
   * @return bool
   *   TRUE if processing succeeded.
   */
  protected function processSegment(string $inputPath, string $outputPath, array $options): bool {
    if ($options['optimize_palette'] ?? TRUE) {
      return $this->processWithPaletteOptimization($inputPath, $outputPath, $options);
    }

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

  /**
   * Processes segment with palette optimization.
   *
   * Uses a single-pass complex filter for palette generation and application.
   *
   * @param string $input
   *   Input video path.
   * @param string $output
   *   Output GIF path.
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return bool
   *   TRUE if processing succeeded.
   */
  protected function processWithPaletteOptimization(string $input, string $output, array $options): bool {
    $ffmpegPath = $this->ffmpegExecutor->getFfmpegPath();

    // Build filter components.
    $filters = $this->buildBaseFilters($options);
    $filterString = implode(',', $filters);

    // Dithering options.
    $ditherOption = $this->buildDitherOption($options);
    $statsMode = $options['stats_mode'] ?? 'diff';
    $loop = (int) ($options['loop'] ?? 0);

    // Complex filter for palette optimization.
    $filterComplex = sprintf(
      '%s,split[s0][s1];[s0]palettegen=stats_mode=%s[p];[s1][p]paletteuse%s',
      $filterString,
      $statsMode,
      $ditherOption,
    );

    // Build command array.
    $command = [
      $ffmpegPath,
      '-y',
      ...$this->buildSeekArgs($options),
      '-i', $input,
      '-filter_complex', $filterComplex,
      '-loop', (string) $loop,
      $output,
    ];

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

  /**
   * Processes segment with simple conversion (no palette optimization).
   *
   * @param string $input
   *   Input video path.
   * @param string $output
   *   Output GIF path.
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return bool
   *   TRUE if processing succeeded.
   */
  protected function processSimple(string $input, string $output, array $options): bool {
    $ffmpegPath = $this->ffmpegExecutor->getFfmpegPath();

    $filters = $this->buildBaseFilters($options);
    $filterString = implode(',', $filters);
    $loop = (int) ($options['loop'] ?? 0);

    // Build command array.
    $command = [
      $ffmpegPath,
      '-y',
      ...$this->buildSeekArgs($options),
      '-i', $input,
      '-vf', $filterString,
      '-loop', (string) $loop,
      $output,
    ];

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

  /**
   * Builds base filter chain for GIF generation.
   *
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return array<string>
   *   Array of filter strings.
   */
  protected function buildBaseFilters(array $options): array {
    $filters = [];

    // FPS filter.
    $fps = (int) ($options['fps'] ?? 10);
    $filters[] = sprintf('fps=%d', max(1, min(30, $fps)));

    // Scale filter with aspect ratio preservation.
    $width = max(1, (int) ($options['width'] ?? 320));
    $height = (int) ($options['height'] ?? -1);

    $scaleFilter = sprintf(
      "scale='min(%d,iw)':%d:flags=lanczos",
      $width,
      $height,
    );
    $filters[] = $scaleFilter;

    return $filters;
  }

  /**
   * Builds seek and duration arguments as array.
   *
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return array<int, string>
   *   Seek arguments array.
   */
  protected function buildSeekArgs(array $options): array {
    $args = [];

    $startTime = (float) ($options['start_time'] ?? 0.0);
    if ($startTime > 0) {
      $args[] = '-ss';
      $args[] = number_format($startTime, 3, '.', '');
    }

    $duration = (float) ($options['duration'] ?? 3.0);
    if ($duration > 0) {
      $args[] = '-t';
      $args[] = number_format($duration, 3, '.', '');
    }

    return $args;
  }

  /**
   * Builds dithering option for paletteuse filter.
   *
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return string
   *   Dithering option string.
   */
  protected function buildDitherOption(array $options): string {
    $dither = $options['dither'] ?? 'bayer';

    if ($dither === 'none') {
      return ':dither=none';
    }

    if ($dither === 'bayer') {
      $bayerScale = (int) ($options['bayer_scale'] ?? 3);
      return sprintf(':dither=bayer:bayer_scale=%d', max(0, min(5, $bayerScale)));
    }

    return sprintf(':dither=%s', $dither);
  }

  /**
   * Gets video duration using FFprobe.
   *
   * @param string $inputPath
   *   Path to video file.
   *
   * @return float
   *   Duration in seconds, or 0 if unable to determine.
   */
  protected function getVideoDuration(string $inputPath): float {
    try {
      $videoInfo = $this->ffmpegExecutor->getVideoInfo($inputPath);
      return $videoInfo->duration ?? 0.0;
    }
    catch (\Throwable) {
      return 0.0;
    }
  }

  /**
   * Calculates segment start times.
   *
   * @param float $videoDuration
   *   Total video duration in seconds.
   * @param int $interval
   *   Interval between segments in seconds.
   * @param int $gifDuration
   *   Duration of each GIF in seconds.
   * @param int $maxGifs
   *   Maximum number of GIFs to generate.
   *
   * @return array<float>
   *   Array of start times in seconds.
   */
  protected function calculateSegmentStartTimes(
    float $videoDuration,
    int $interval,
    int $gifDuration,
    int $maxGifs,
  ): array {
    if ($interval <= 0 || $maxGifs <= 0 || $videoDuration <= 0) {
      return [];
    }

    $startTimes = [];
    $currentTime = 0.0;

    while ($currentTime < $videoDuration && count($startTimes) < $maxGifs) {
      // Ensure GIF fits within video duration.
      if ($currentTime + $gifDuration <= $videoDuration) {
        $startTimes[] = $currentTime;
      }
      $currentTime += $interval;
    }

    return $startTimes;
  }

  /**
   * Gets the base output path without pattern specifier.
   *
   * @param string $outputPath
   *   Full output path pattern.
   *
   * @return string
   *   Base path without _NNN.gif suffix.
   */
  protected function getBaseOutputPath(string $outputPath): string {
    // Remove pattern suffix like _gif_%03d.gif or _001.gif.
    return preg_replace('/_(?:gif_)?%?\d*d?\.gif$/i', '', $outputPath) ?? $outputPath;
  }

  /**
   * Gets available dithering modes.
   *
   * @return array<string, string>
   *   Dither mode ID => description.
   */
  #[\NoDiscard]
  public static function getAvailableDitherModes(): array {
    return self::DITHER_MODES;
  }

}
