<?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 animated GIF generation from video.
 *
 * Features:
 * - Configurable start time, duration, FPS, and dimensions
 * - Palette optimization using palettegen/paletteuse filters for best quality
 * - Multiple dithering algorithms for different visual styles
 * - Output to a configurable stream-wrapper directory
 *   (default: public://ffmpeg_gifs/)
 */
#[FFmpegProcessor(
  id: 'gif',
  label: new TranslatableMarkup('GIF Generator'),
  description: new TranslatableMarkup('Generate animated GIFs from video with palette optimization.'),
  outputType: 'image',
  outputExtension: 'gif',
  requiredFilters: ['palettegen', 'paletteuse', 'scale', 'fps'],
  weight: 25,
)]
final class GifProcessor extends FFmpegProcessorBase {

  /**
   * 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)',
    'sierra2_4a' => 'Sierra-2-4A (faster Sierra)',
    'none' => 'None (no dithering)',
  ];

  /**
   * Default output directory for generated GIFs.
   */
  private const string DEFAULT_OUTPUT_DIRECTORY = 'public://ffmpeg_gifs';

  /**
   * Constructs a GifProcessor 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.
   * @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);

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

    // Use palette optimization for best quality.
    if ($options['optimize_palette'] ?? TRUE) {
      return $this->processWithPaletteOptimization($inputPath, $outputPath, $options);
    }

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

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

    return [
      'start_time' => $imageConfig->get('gif_start_time') ?? 0.0,
      'duration' => $imageConfig->get('gif_duration') ?? 5.0,
      'fps' => $imageConfig->get('gif_fps') ?? 10,
      'width' => $imageConfig->get('gif_width') ?? 480,
      'height' => -1,
      'optimize_palette' => $imageConfig->get('gif_palette_optimization') ?? $advancedConfig->get('gif_palette_optimization') ?? TRUE,
      'dither' => $imageConfig->get('gif_dither_mode') ?? $advancedConfig->get('gif_dither_mode') ?? 'bayer',
      'bayer_scale' => 3,
      'stats_mode' => 'diff',
      'loop' => 0,
      'timeout' => 300,
    ];
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function generateOutputPath(string $inputPath, array $options = []): string {
    $imageConfig = $this->configFactory->get('ffmpeg_media_image.settings');
    $advancedConfig = $this->configFactory->get('ffmpeg_media_advanced.settings');
    $outputDir = $imageConfig->get('gif_output_directory') ?? $advancedConfig->get('gif_output_directory') ?? self::DEFAULT_OUTPUT_DIRECTORY;

    if (!is_string($outputDir) || $outputDir === '' || !str_contains($outputDir, '://')) {
      throw new \InvalidArgumentException(sprintf(
        'GIF output directory must be a Drupal stream wrapper URI. Got "%s".',
        is_scalar($outputDir) ? (string) $outputDir : get_debug_type($outputDir),
      ));
    }

    // Ensure the directory URI is resolved.
    $realPath = $this->fileSystem->realpath($outputDir);
    if ($realPath === FALSE) {
      // Create directory if it doesn't exist.
      $this->fileSystem->prepareDirectory($outputDir, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
      $realPath = $this->fileSystem->realpath($outputDir);
    }

    if ($realPath === FALSE) {
      throw new \RuntimeException(sprintf(
        'Cannot resolve GIF output directory "%s" to a local filesystem path.',
        $outputDir,
      ));
    }

    $pathInfo = pathinfo($inputPath);
    $baseName = $pathInfo['filename'];
    $timestamp = time();

    return $realPath . DIRECTORY_SEPARATOR . $baseName . '_' . $timestamp . '.gif';
  }

  /**
   * Processes video to GIF with palette optimization.
   *
   * Uses a two-pass approach:
   * 1. Generate optimized palette from video frames
   * 2. Create GIF using the optimized palette.
   *
   * @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'] ?? 300,
    ]);
  }

  /**
   * Processes video to GIF with simple conversion (no palette optimization).
   *
   * Faster but lower quality than palette-optimized conversion.
   *
   * @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'] ?? 300,
    ]);
  }

  /**
   * Builds base filter chain for GIF generation.
   *
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return array<int, 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);

    // Ensure even dimensions and use lanczos for high-quality scaling.
    $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 available dithering modes.
   *
   * @return array<string, string>
   *   Dither mode ID => description.
   */
  #[\NoDiscard]
  public static function getAvailableDitherModes(): array {
    return self::DITHER_MODES;
  }

  /**
   * Creates options for a preview GIF (short, low quality, fast).
   *
   * @param float $startTime
   *   Start time in seconds.
   *
   * @return array<string, bool|float|int|string>
   *   Options array for preview GIF.
   */
  #[\NoDiscard]
  public static function previewOptions(float $startTime = 0.0): array {
    return [
      'start_time' => $startTime,
      'duration' => 2.0,
      'fps' => 8,
      'width' => 200,
      'optimize_palette' => FALSE,
      'dither' => 'none',
    ];
  }

  /**
   * Creates options for a high-quality GIF.
   *
   * @param float $startTime
   *   Start time in seconds.
   * @param float $duration
   *   Duration in seconds.
   * @param int $width
   *   Output width.
   *
   * @return array<string, bool|float|int|string>
   *   Options array for high-quality GIF.
   */
  #[\NoDiscard]
  public static function highQualityOptions(
    float $startTime = 0.0,
    float $duration = 5.0,
    int $width = 480,
  ): array {
    return [
      'start_time' => $startTime,
      'duration' => $duration,
      'fps' => 15,
      'width' => $width,
      'optimize_palette' => TRUE,
      'dither' => 'floyd_steinberg',
      'stats_mode' => 'full',
    ];
  }

  /**
   * Creates options for a social media-optimized GIF.
   *
   * Balanced settings for file size and quality.
   *
   * @param float $startTime
   *   Start time in seconds.
   *
   * @return array<string, bool|float|int|string>
   *   Options array for social media GIF.
   */
  #[\NoDiscard]
  public static function socialMediaOptions(float $startTime = 0.0): array {
    return [
      'start_time' => $startTime,
      'duration' => 3.0,
      'fps' => 12,
      'width' => 320,
      'optimize_palette' => TRUE,
      'dither' => 'bayer',
      'bayer_scale' => 2,
      'stats_mode' => 'diff',
    ];
  }

}
