<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_streaming\Plugin\FFmpegOutput;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\ffmpeg_media\Attribute\FFmpegOutput;
use Drupal\ffmpeg_media\ValueObject\ConversionOptions;
use Drupal\ffmpeg_media\ValueObject\DashOptions;
use Drupal\ffmpeg_media\ValueObject\GopSettings;
use Drupal\ffmpeg_media\ValueObject\VideoInfo;

/**
 * DASH (Dynamic Adaptive Streaming over HTTP) output format plugin.
 */
#[FFmpegOutput(
  id: 'dash',
  label: new TranslatableMarkup('DASH (MPEG-DASH)'),
  description: new TranslatableMarkup('MPEG-DASH adaptive streaming for cross-platform delivery.'),
  extension: 'mpd',
  mimeType: 'application/dash+xml',
  supportedCodecs: ['h264', 'hevc', 'vp9', 'av1'],
  defaultCodec: 'h264',
  supportedAudioCodecs: ['aac', 'libopus', 'libvorbis'],
  weight: 35,
)]
final class DashOutput extends AdaptiveBitrateOutputBase {

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function buildArguments(ConversionOptions $options): array {
    $dashOptions = $this->getDashOptions();

    $args = [
      '-f', 'dash',
      '-seg_duration', (string) $dashOptions->segmentDuration,
      '-min_seg_duration', (string) ($dashOptions->segmentDuration * 1000000),
    ];

    if ($dashOptions->useTimeline) {
      $args[] = '-use_timeline';
      $args[] = '1';
    }

    $args[] = '-use_template';
    $args[] = '1';
    $args[] = '-init_seg_name';
    $args[] = 'init-$RepresentationID$.m4s';
    $args[] = '-media_seg_name';
    $args[] = 'chunk-$RepresentationID$-$Number%05d$.m4s';

    // LL-DASH options.
    if ($dashOptions->lowLatencyEnabled) {
      $args[] = '-ldash';
      $args[] = '1';
      $args[] = '-streaming';
      $args[] = '1';
      if ($dashOptions->targetLatency !== NULL) {
        $args[] = '-target_latency';
        $args[] = (string) $dashOptions->targetLatency;
      }
      if ($dashOptions->utcTiming) {
        $args[] = '-utc_timing_url';
        $args[] = 'https://time.akamai.com/?iso';
      }
    }

    // Add GOP settings for consistent keyframe alignment.
    if ($options->gopSettings !== NULL) {
      $args = array_merge($args, $options->gopSettings->buildArguments('h264'));
    }
    else {
      $gopSettings = GopSettings::forAbrStreaming((float) $dashOptions->segmentDuration);
      $args = array_merge($args, $gopSettings->buildArguments('h264'));
    }

    $args[] = '-adaptation_sets';
    $args[] = 'id=0,streams=v id=1,streams=a';

    return $args;
  }

  /**
   * Gets DASH options from configuration.
   *
   * @return \Drupal\ffmpeg_media\ValueObject\DashOptions
   *   DASH options.
   */
  protected function getDashOptions(): DashOptions {
    $config = $this->configFactory->get('ffmpeg_media_streaming.settings');

    $llDashEnabled = (bool) ($config->get('dash_low_latency') ?? FALSE);

    if ($llDashEnabled) {
      return DashOptions::forLowLatency(
        targetLatency: (float) ($config->get('dash_target_latency') ?? 3.0),
      );
    }

    return new DashOptions(
      segmentDuration: (int) ($config->get('dash_segment_duration') ?? 4),
      profile: (string) ($config->get('dash_profile') ?? 'ondemand'),
      minBufferTime: (int) ($config->get('dash_min_buffer_time') ?? 2),
      useTimeline: TRUE,
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function generateOutputPath(string $inputPath): string {
    // If custom output directory is set via options, use it.
    if (isset($this->configuration['output_directory'])) {
      $directory = $this->configuration['output_directory'];
      return $directory . DIRECTORY_SEPARATOR . 'manifest.mpd';
    }

    // Default behavior - use input path directory.
    $pathInfo = pathinfo($inputPath);
    $baseName = $pathInfo['filename'];
    $directory = $pathInfo['dirname'] ?? dirname($inputPath);

    // DASH outputs to a directory structure.
    return $directory . DIRECTORY_SEPARATOR . $baseName . '_dash' . DIRECTORY_SEPARATOR . 'manifest.mpd';
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function buildAdaptiveArguments(
    ConversionOptions $options,
    ?VideoInfo $videoInfo = NULL,
    array $audioTracks = [],
  ): array {
    $dashOptions = $this->getDashOptions();
    $representations = $this->getRepresentations($videoInfo);

    $args = ['-f', 'dash'];

    // Build filter complex for multiple outputs.
    $filterParts = [];
    foreach ($representations as $index => $rep) {
      $filterParts[] = sprintf(
        '[0:v]scale=%d:%d[v%d]',
        $rep['width'],
        $rep['height'],
        $index,
      );
    }

    $args[] = '-filter_complex';
    $args[] = implode(';', $filterParts);

    // Map video streams.
    foreach ($representations as $index => $rep) {
      $args[] = '-map';
      $args[] = "[v$index]";
    }

    // Map audio streams.
    if (empty($audioTracks)) {
      $args[] = '-map';
      $args[] = '0:a';
    }
    else {
      foreach ($audioTracks as $track) {
        $args[] = '-map';
        $args[] = sprintf('0:a:%d', $track->streamIndex);
      }
    }

    // Use actual frame rate for GOP calculation.
    $frameRate = ($videoInfo !== NULL && $videoInfo->frameRate > 0) ? $videoInfo->frameRate : 30.0;
    $gopSize = $dashOptions->getRecommendedGopSize($frameRate);

    // Set encoding parameters for each video stream.
    foreach ($representations as $index => $rep) {
      $videoBitrate = $rep['bitrate'] ?? '2500k';
      $args[] = "-b:v:$index";
      $args[] = $videoBitrate;

      // Add maxrate and bufsize for consistent ABR.
      $bitrateKbps = (int) filter_var($videoBitrate, FILTER_SANITIZE_NUMBER_INT);
      $args[] = "-maxrate:v:$index";
      $args[] = (int) ($bitrateKbps * 1.5) . 'k';
      $args[] = "-bufsize:v:$index";
      $args[] = ($bitrateKbps * 2) . 'k';

      // GOP settings per stream.
      $args[] = "-g:v:$index";
      $args[] = (string) $gopSize;
      $args[] = "-keyint_min:v:$index";
      $args[] = (string) $gopSize;
      $args[] = "-sc_threshold:v:$index";
      $args[] = '0';
    }

    // Force keyframes at segment boundaries.
    $args[] = '-force_key_frames';
    $args[] = sprintf('expr:gte(t,n_forced*%d)', $dashOptions->segmentDuration);

    // Audio metadata for multi-language.
    foreach ($audioTracks as $trackIndex => $track) {
      $args[] = sprintf('-metadata:s:a:%d', $trackIndex);
      $args[] = sprintf('language=%s', $track->language);
      $args[] = sprintf('-metadata:s:a:%d', $trackIndex);
      $args[] = sprintf('title=%s', $track->label);
    }

    // DASH muxer settings.
    $args[] = '-seg_duration';
    $args[] = (string) $dashOptions->segmentDuration;
    $args[] = '-min_seg_duration';
    $args[] = (string) ($dashOptions->segmentDuration * 1000000);

    if ($dashOptions->useTimeline) {
      $args[] = '-use_timeline';
      $args[] = '1';
    }

    $args[] = '-use_template';
    $args[] = '1';
    $args[] = '-init_seg_name';
    $args[] = 'init-$RepresentationID$.m4s';
    $args[] = '-media_seg_name';
    $args[] = 'chunk-$RepresentationID$-$Number%05d$.m4s';

    // LL-DASH options.
    if ($dashOptions->lowLatencyEnabled) {
      $args[] = '-ldash';
      $args[] = '1';
      $args[] = '-streaming';
      $args[] = '1';
      if ($dashOptions->targetLatency !== NULL) {
        $args[] = '-target_latency';
        $args[] = (string) $dashOptions->targetLatency;
      }
    }

    // Build adaptation sets.
    $adaptationSets = $this->buildAdaptationSetsWithAudio($representations, $audioTracks);
    $args[] = '-adaptation_sets';
    $args[] = $adaptationSets;

    return $args;
  }

  /**
   * Builds adaptation sets string with audio track support.
   *
   * @param array<int, array{name: string, width: int, height: int, bitrate: string, audio_bitrate: string}> $representations
   *   Video representations.
   * @param array<\Drupal\ffmpeg_media\ValueObject\AudioTrack> $audioTracks
   *   Audio tracks.
   *
   * @return string
   *   Adaptation sets value.
   */
  protected function buildAdaptationSetsWithAudio(array $representations, array $audioTracks): string {
    $sets = [];

    // Video adaptation set.
    $videoStreams = implode(',', range(0, count($representations) - 1));
    $sets[] = "id=0,streams={$videoStreams}";

    if (empty($audioTracks)) {
      // Single audio track.
      $audioStreamIndex = count($representations);
      $sets[] = "id=1,streams={$audioStreamIndex}";
    }
    else {
      // Multiple audio tracks - one adaptation set per language.
      $baseAudioIndex = count($representations);
      foreach ($audioTracks as $trackIndex => $track) {
        $streamIndex = $baseAudioIndex + $trackIndex;
        $sets[] = sprintf('id=%d,streams=%d,lang=%s', $trackIndex + 1, $streamIndex, $track->language);
      }
    }

    return implode(' ', $sets);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function buildAdaptiveArgumentsWithSubtitles(
    ConversionOptions $options,
    array $subtitleTracks,
  ): array {
    // Validate that all subtitle files exist before building arguments.
    foreach ($subtitleTracks as $track) {
      if (!file_exists($track->path)) {
        throw new \InvalidArgumentException(sprintf('Subtitle file not found: %s', $track->path));
      }
    }

    $dashOptions = $this->getDashOptions();
    $representations = $this->getRepresentations(NULL);
    $config = $this->configFactory->get('ffmpeg_media_streaming.settings');
    $initSegment = $config->get('dash_init_segment') ?? 'init-$RepresentationID$.m4s';
    $mediaSegment = $config->get('dash_media_segment') ?? 'chunk-$RepresentationID$-$Number%05d$.m4s';

    $args = ['-f', 'dash'];

    // Build filter complex for multiple outputs.
    $filterParts = [];

    foreach ($representations as $index => $rep) {
      $filterParts[] = sprintf(
        '[0:v]scale=%d:%d[v%d]',
        $rep['width'],
        $rep['height'],
        $index,
      );
    }

    $args[] = '-filter_complex';
    $args[] = implode(';', $filterParts);

    // Map video streams.
    foreach ($representations as $index => $rep) {
      $args[] = '-map';
      $args[] = "[v$index]";
    }

    // Map audio stream.
    $args[] = '-map';
    $args[] = '0:a';

    // Map subtitle streams (inputs 1, 2, 3, etc. for each subtitle file).
    $subtitleCount = count($subtitleTracks);
    for ($i = 0; $i < $subtitleCount; $i++) {
      $args[] = '-map';
      $args[] = sprintf('%d:s', $i + 1);
    }

    // Set subtitle codec to WebVTT for DASH compatibility.
    if ($subtitleCount > 0) {
      $args[] = '-c:s';
      $args[] = 'webvtt';
    }

    // Set bitrates for each video stream.
    foreach ($representations as $index => $rep) {
      $args[] = "-b:v:$index";
      $args[] = $rep['bitrate'] ?? '2500k';
    }

    // DASH muxer settings from value object.
    $args[] = '-seg_duration';
    $args[] = (string) $dashOptions->segmentDuration;
    $args[] = '-min_seg_duration';
    $args[] = (string) ($dashOptions->segmentDuration * 1000000);

    if ($dashOptions->useTimeline) {
      $args[] = '-use_timeline';
      $args[] = '1';
    }

    $args[] = '-use_template';
    $args[] = '1';
    $args[] = '-init_seg_name';
    $args[] = $initSegment;
    $args[] = '-media_seg_name';
    $args[] = $mediaSegment;

    // LL-DASH options for subtitle streams.
    if ($dashOptions->lowLatencyEnabled) {
      $args[] = '-ldash';
      $args[] = '1';
      $args[] = '-streaming';
      $args[] = '1';
      if ($dashOptions->targetLatency !== NULL) {
        $args[] = '-target_latency';
        $args[] = (string) $dashOptions->targetLatency;
      }
    }

    // Build adaptation sets including subtitles.
    $adaptationSets = $this->buildAdaptationSetsWithSubtitles($representations, $subtitleTracks);
    $args[] = '-adaptation_sets';
    $args[] = $adaptationSets;

    return $args;
  }

  /**
   * Builds the adaptation_sets string including subtitle tracks.
   *
   * @param array<int, array{name: string, width: int, height: int, bitrate: string, audio_bitrate: string}> $representations
   *   Video representations configuration.
   * @param array<\Drupal\ffmpeg_media\ValueObject\SubtitleTrack> $subtitleTracks
   *   Subtitle tracks.
   *
   * @return string
   *   The adaptation_sets value for FFmpeg DASH muxer.
   */
  protected function buildAdaptationSetsWithSubtitles(array $representations, array $subtitleTracks): string {
    $sets = [];

    // Video adaptation set (all video streams).
    $videoStreams = implode(',', range(0, count($representations) - 1));
    $sets[] = "id=0,streams={$videoStreams}";

    // Audio adaptation set (stream after videos).
    $audioStreamIndex = count($representations);
    $sets[] = "id=1,streams={$audioStreamIndex}";

    // Subtitle adaptation sets (one per language).
    if (!empty($subtitleTracks)) {
      $subtitleStartIndex = count($representations) + 1;
      foreach ($subtitleTracks as $subIndex => $track) {
        $streamIndex = $subtitleStartIndex + $subIndex;
        $sets[] = sprintf(
          'id=%d,streams=%d,lang=%s',
          $subIndex + 2,
          $streamIndex,
          $track->language,
        );
      }
    }

    return implode(' ', $sets);
  }

}
