<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_advanced\Plugin\FFmpegProcessor;

use Drupal\Core\Config\ConfigFactoryInterface;
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 audio normalization.
 *
 * Features:
 * - EBU R128 loudness normalization (broadcast standard, single-pass)
 * - Peak normalization for maximum headroom
 * - RMS normalization for consistent perceived loudness
 * - Configurable target levels (I, TP, LRA)
 *
 * Requires FFmpeg compiled with loudnorm filter support.
 */
#[FFmpegProcessor(
  id: 'normalize',
  label: new TranslatableMarkup('Audio Normalizer'),
  description: new TranslatableMarkup('Normalize audio levels using EBU R128 or peak normalization.'),
  outputType: 'video',
  outputExtension: 'mp4',
  requiredFilters: ['loudnorm'],
  weight: 18,
)]
final class NormalizeProcessor extends FFmpegProcessorBase implements ContainerFactoryPluginInterface {

  /**
   * Available normalization modes.
   */
  public const array MODES = [
    'ebu_r128' => 'EBU R128 (Broadcast Standard)',
    'peak' => 'Peak Normalization',
    'rms' => 'RMS Normalization',
  ];

  /**
   * EBU R128 target levels for different content types.
   */
  public const array TARGET_PRESETS = [
    'broadcast' => [
      'I' => -24.0,
      'TP' => -2.0,
      'LRA' => 7.0,
    ],
    'streaming' => [
      'I' => -16.0,
      'TP' => -1.5,
      'LRA' => 11.0,
    ],
    'podcast' => [
      'I' => -19.0,
      'TP' => -1.0,
      'LRA' => 8.0,
    ],
    'music' => [
      'I' => -14.0,
      'TP' => -1.0,
      'LRA' => 11.0,
    ],
  ];

  /**
   * Constructs a NormalizeProcessor 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.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    #[Autowire(service: FFmpegExecutorInterface::class)]
    FFmpegExecutorInterface $ffmpegExecutor,
    protected readonly ConfigFactoryInterface $configFactory,
  ) {
    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);
    $mode = $options['mode'] ?? 'ebu_r128';

    return match ($mode) {
      'ebu_r128' => $this->processEbuR128($inputPath, $outputPath, $options),
      'peak' => $this->processPeakNormalization($inputPath, $outputPath, $options),
      'rms' => $this->processRmsNormalization($inputPath, $outputPath, $options),
      default => $this->processEbuR128($inputPath, $outputPath, $options),
    };
  }

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

    return [
      'mode' => $config->get('normalize_mode') ?? 'ebu_r128',
      'preset' => $preset,
      'target_i' => $config->get('normalize_target_i') ?? $presetValues['I'],
      'target_tp' => $config->get('normalize_target_tp') ?? $presetValues['TP'],
      'target_lra' => $config->get('normalize_target_lra') ?? $presetValues['LRA'],
      'peak_level' => $config->get('normalize_peak_level') ?? -1.0,
      'rms_level' => $config->get('normalize_rms_level') ?? -18.0,
      'video_codec' => 'copy',
      'audio_codec' => 'libopus',
      'audio_bitrate' => '192k',
      '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 . '_normalized.mp4';
  }

  /**
   * Processes audio with EBU R128 loudness normalization.
   *
   * @param string $input
   *   Input video path.
   * @param string $output
   *   Output video path.
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return bool
   *   TRUE if processing succeeded.
   */
  protected function processEbuR128(string $input, string $output, array $options): bool {
    $ffmpegPath = $this->ffmpegExecutor->getFfmpegPath();

    $targetI = (float) ($options['target_i'] ?? -16.0);
    $targetTp = (float) ($options['target_tp'] ?? -1.5);
    $targetLra = (float) ($options['target_lra'] ?? 11.0);

    // Build loudnorm filter.
    $loudnorm = sprintf(
      'loudnorm=I=%s:TP=%s:LRA=%s:print_format=summary',
      number_format($targetI, 1),
      number_format($targetTp, 1),
      number_format($targetLra, 1),
    );

    // Build command array.
    $command = [
      $ffmpegPath,
      '-y',
      '-i', $input,
      '-c:v', $options['video_codec'] ?? 'copy',
      '-af', $loudnorm,
      '-c:a', $options['audio_codec'] ?? 'libopus',
      '-b:a', $options['audio_bitrate'] ?? '192k',
      '-movflags', '+faststart',
      $output,
    ];

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

  /**
   * Processes audio with peak normalization.
   *
   * @param string $input
   *   Input video path.
   * @param string $output
   *   Output video path.
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return bool
   *   TRUE if processing succeeded.
   */
  protected function processPeakNormalization(string $input, string $output, array $options): bool {
    $ffmpegPath = $this->ffmpegExecutor->getFfmpegPath();
    $peakLevel = (float) ($options['peak_level'] ?? -1.0);

    // Use dynaudnorm for peak-based normalization.
    // This filter normalizes audio based on dynamic range compression.
    $dynaudnorm = sprintf(
      'dynaudnorm=p=%s:m=100:s=12',
      number_format(10 ** ($peakLevel / 20), 4),
    );

    // Build command array.
    $command = [
      $ffmpegPath,
      '-y',
      '-i', $input,
      '-c:v', $options['video_codec'] ?? 'copy',
      '-af', $dynaudnorm,
      '-c:a', $options['audio_codec'] ?? 'libopus',
      '-b:a', $options['audio_bitrate'] ?? '192k',
      '-movflags', '+faststart',
      $output,
    ];

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

  /**
   * Processes audio with RMS normalization.
   *
   * @param string $input
   *   Input video path.
   * @param string $output
   *   Output video path.
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return bool
   *   TRUE if processing succeeded.
   */
  protected function processRmsNormalization(string $input, string $output, array $options): bool {
    $ffmpegPath = $this->ffmpegExecutor->getFfmpegPath();
    $rmsLevel = (float) ($options['rms_level'] ?? -18.0);

    // Build compand filter for RMS-based normalization.
    $compand = sprintf(
      'compand=attacks=0:decays=1:points=-80/-80|-%.1f/0:soft-knee=6:gain=0:volume=0:delay=0',
      abs($rmsLevel),
    );

    // Build command array.
    $command = [
      $ffmpegPath,
      '-y',
      '-i', $input,
      '-c:v', $options['video_codec'] ?? 'copy',
      '-af', $compand,
      '-c:a', $options['audio_codec'] ?? 'libopus',
      '-b:a', $options['audio_bitrate'] ?? '192k',
      '-movflags', '+faststart',
      $output,
    ];

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

  /**
   * Gets available normalization modes.
   *
   * @return array<string, string>
   *   Mode ID => description.
   */
  #[\NoDiscard]
  public static function getAvailableModes(): array {
    return self::MODES;
  }

  /**
   * Gets available target presets.
   *
   * @return array<string, array{I: float, TP: float, LRA: float}>
   *   Preset ID => target values.
   */
  #[\NoDiscard]
  public static function getTargetPresets(): array {
    return self::TARGET_PRESETS;
  }

  /**
   * Creates options for broadcast-compliant normalization.
   *
   * @return array<string, float|string>
   *   Options array for broadcast normalization.
   */
  #[\NoDiscard]
  public static function broadcastOptions(): array {
    return [
      'mode' => 'ebu_r128',
      'preset' => 'broadcast',
      'target_i' => -24.0,
      'target_tp' => -2.0,
      'target_lra' => 7.0,
    ];
  }

  /**
   * Creates options for streaming-optimized normalization.
   *
   * @return array<string, float|string>
   *   Options array for streaming normalization.
   */
  #[\NoDiscard]
  public static function streamingOptions(): array {
    return [
      'mode' => 'ebu_r128',
      'preset' => 'streaming',
      'target_i' => -16.0,
      'target_tp' => -1.5,
      'target_lra' => 11.0,
    ];
  }

  /**
   * Creates options for podcast-optimized normalization.
   *
   * @return array<string, float|string>
   *   Options array for podcast normalization.
   */
  #[\NoDiscard]
  public static function podcastOptions(): array {
    return [
      'mode' => 'ebu_r128',
      'preset' => 'podcast',
      'target_i' => -19.0,
      'target_tp' => -1.0,
      'target_lra' => 8.0,
    ];
  }

}
