<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\ValueObject;

/**
 * Immutable value object for DASH options.
 *
 * DASH stands for Dynamic Adaptive Streaming over HTTP.
 * Supports standard DASH and CMAF packaging for unified HLS/DASH delivery.
 */
final readonly class DashOptions {

  /**
   * Segment format constants.
   */
  public const string FORMAT_MP4 = 'mp4';
  public const string FORMAT_WEBM = 'webm';

  /**
   * Profile constants.
   */
  public const string PROFILE_ONDEMAND = 'ondemand';
  public const string PROFILE_LIVE = 'live';

  /**
   * Constructs a DashOptions object.
   *
   * @param int $segmentDuration
   *   Target segment duration in seconds.
   * @param string $profile
   *   DASH profile: 'ondemand' or 'live'.
   * @param string $segmentFormat
   *   Segment format: 'mp4' or 'webm'.
   * @param bool $singleFile
   *   Use single file (byte range) mode for on-demand.
   * @param int $windowSize
   *   Number of segments in manifest for live (0 = all).
   * @param int $extraWindowSize
   *   Extra segments to keep beyond window.
   * @param bool $usePts
   *   Use PTS for segment boundaries.
   * @param bool $useTimeline
   *   Use SegmentTimeline instead of SegmentTemplate.
   * @param string|null $initSegmentName
   *   Init segment filename pattern.
   * @param string $mediaSegmentName
   *   Media segment filename pattern.
   * @param bool $lowLatencyEnabled
   *   Enable low-latency DASH features.
   * @param float|null $targetLatency
   *   Target latency in seconds for low-latency.
   * @param bool $utcTiming
   *   Add UTCTiming element.
   * @param string|null $utcTimingUrl
   *   URL for UTC timing server.
   * @param int $minBufferTime
   *   Minimum buffer time in seconds.
   * @param bool $removeAtExit
   *   Remove temp files on completion.
   *
   * @throws \InvalidArgumentException
   *   If any parameter is out of valid range.
   */
  public function __construct(
    public int $segmentDuration = 4,
    public string $profile = self::PROFILE_ONDEMAND,
    public string $segmentFormat = self::FORMAT_MP4,
    public bool $singleFile = FALSE,
    public int $windowSize = 0,
    public int $extraWindowSize = 0,
    public bool $usePts = TRUE,
    public bool $useTimeline = TRUE,
    public ?string $initSegmentName = NULL,
    public string $mediaSegmentName = 'chunk-$RepresentationID$-$Number%05d$',
    public bool $lowLatencyEnabled = FALSE,
    public ?float $targetLatency = NULL,
    public bool $utcTiming = FALSE,
    public ?string $utcTimingUrl = NULL,
    public int $minBufferTime = 2,
    public bool $removeAtExit = FALSE,
  ) {
    if ($segmentDuration < 1 || $segmentDuration > 60) {
      throw new \InvalidArgumentException('Segment duration must be between 1 and 60 seconds.');
    }
    if (!in_array($profile, [self::PROFILE_ONDEMAND, self::PROFILE_LIVE], TRUE)) {
      throw new \InvalidArgumentException('Profile must be "ondemand" or "live".');
    }
    if (!in_array($segmentFormat, [self::FORMAT_MP4, self::FORMAT_WEBM], TRUE)) {
      throw new \InvalidArgumentException('Segment format must be "mp4" or "webm".');
    }
    if ($windowSize < 0) {
      throw new \InvalidArgumentException('Window size must be 0 or positive.');
    }
    if ($targetLatency !== NULL && $targetLatency < 0.5) {
      throw new \InvalidArgumentException('Target latency must be at least 0.5 seconds.');
    }
    if ($minBufferTime < 1 || $minBufferTime > 30) {
      throw new \InvalidArgumentException('Min buffer time must be between 1 and 30 seconds.');
    }
  }

  /**
   * Creates DashOptions from an array.
   *
   * @param array<string, mixed> $data
   *   Associative array of settings.
   *
   * @return self
   *   New DashOptions instance.
   */
  #[\NoDiscard]
  public static function fromArray(array $data): self {
    return new self(
      segmentDuration: (int) ($data['segment_duration'] ?? 4),
      profile: (string) ($data['profile'] ?? self::PROFILE_ONDEMAND),
      segmentFormat: (string) ($data['segment_format'] ?? self::FORMAT_MP4),
      singleFile: (bool) ($data['single_file'] ?? FALSE),
      windowSize: (int) ($data['window_size'] ?? 0),
      extraWindowSize: (int) ($data['extra_window_size'] ?? 0),
      usePts: (bool) ($data['use_pts'] ?? TRUE),
      useTimeline: (bool) ($data['use_timeline'] ?? TRUE),
      initSegmentName: isset($data['init_segment_name']) ? (string) $data['init_segment_name'] : NULL,
      mediaSegmentName: (string) ($data['media_segment_name'] ?? 'chunk-$RepresentationID$-$Number%05d$'),
      lowLatencyEnabled: (bool) ($data['low_latency_enabled'] ?? FALSE),
      targetLatency: isset($data['target_latency']) ? (float) $data['target_latency'] : NULL,
      utcTiming: (bool) ($data['utc_timing'] ?? FALSE),
      utcTimingUrl: isset($data['utc_timing_url']) ? (string) $data['utc_timing_url'] : NULL,
      minBufferTime: (int) ($data['min_buffer_time'] ?? 2),
      removeAtExit: (bool) ($data['remove_at_exit'] ?? FALSE),
    );
  }

  /**
   * Creates options for on-demand (VOD) DASH.
   *
   * @param int $segmentDuration
   *   Segment duration in seconds.
   *
   * @return self
   *   Options configured for VOD.
   */
  #[\NoDiscard]
  public static function forVod(int $segmentDuration = 4): self {
    return new self(
      segmentDuration: $segmentDuration,
      profile: self::PROFILE_ONDEMAND,
      singleFile: FALSE,
      useTimeline: TRUE,
    );
  }

  /**
   * Creates options for live DASH streaming.
   *
   * @param int $segmentDuration
   *   Segment duration in seconds.
   * @param int $windowSize
   *   Number of segments in manifest.
   *
   * @return self
   *   Options configured for live streaming.
   */
  #[\NoDiscard]
  public static function forLive(int $segmentDuration = 4, int $windowSize = 5): self {
    return new self(
      segmentDuration: $segmentDuration,
      profile: self::PROFILE_LIVE,
      windowSize: $windowSize,
      extraWindowSize: 2,
      useTimeline: TRUE,
      utcTiming: TRUE,
    );
  }

  /**
   * Creates options for low-latency DASH (LL-DASH).
   *
   * @param float $targetLatency
   *   Target end-to-end latency in seconds.
   * @param int $segmentDuration
   *   Segment duration in seconds.
   *
   * @return self
   *   Options configured for low-latency.
   */
  #[\NoDiscard]
  public static function forLowLatency(float $targetLatency = 3.0, int $segmentDuration = 2): self {
    return new self(
      segmentDuration: $segmentDuration,
      profile: self::PROFILE_LIVE,
      windowSize: 5,
      lowLatencyEnabled: TRUE,
      targetLatency: $targetLatency,
      useTimeline: FALSE,
      utcTiming: TRUE,
      minBufferTime: 1,
    );
  }

  /**
   * Creates options for CMAF-compatible DASH.
   *
   * CMAF enables unified packaging for HLS and DASH.
   *
   * @param int $segmentDuration
   *   Segment duration in seconds.
   *
   * @return self
   *   Options configured for CMAF.
   */
  #[\NoDiscard]
  public static function forCmaf(int $segmentDuration = 4): self {
    return new self(
      segmentDuration: $segmentDuration,
      profile: self::PROFILE_ONDEMAND,
      segmentFormat: self::FORMAT_MP4,
      useTimeline: TRUE,
    );
  }

  /**
   * Creates a new instance with different segment duration.
   *
   * @param int $duration
   *   Segment duration in seconds.
   *
   * @return self
   *   New instance with updated setting.
   */
  #[\NoDiscard]
  public function withSegmentDuration(int $duration): self {
    return clone($this, ['segmentDuration' => $duration]);
  }

  /**
   * Creates a new instance with low-latency enabled.
   *
   * @param float $targetLatency
   *   Target latency in seconds.
   *
   * @return self
   *   New instance with low-latency enabled.
   */
  #[\NoDiscard]
  public function withLowLatency(float $targetLatency = 3.0): self {
    return clone($this, [
      'lowLatencyEnabled' => TRUE,
      'targetLatency' => $targetLatency,
      'profile' => self::PROFILE_LIVE,
    ]);
  }

  /**
   * Creates a new instance with single-file mode.
   *
   * @return self
   *   New instance with single-file mode.
   */
  #[\NoDiscard]
  public function withSingleFile(): self {
    return clone($this, ['singleFile' => TRUE]);
  }

  /**
   * Builds FFmpeg DASH muxer arguments.
   *
   * @return array<int, string>
   *   Array of FFmpeg arguments.
   */
  #[\NoDiscard]
  public function buildArguments(): array {
    $args = [
      '-f', 'dash',
      '-seg_duration', (string) $this->segmentDuration,
      '-min_seg_duration', (string) ($this->segmentDuration * 1000000),
    ];

    // Single file mode.
    if ($this->singleFile) {
      $args[] = '-single_file';
      $args[] = '1';
    }

    // Window size for live.
    if ($this->windowSize > 0) {
      $args[] = '-window_size';
      $args[] = (string) $this->windowSize;
    }

    if ($this->extraWindowSize > 0) {
      $args[] = '-extra_window_size';
      $args[] = (string) $this->extraWindowSize;
    }

    // Segment naming.
    if ($this->initSegmentName !== NULL) {
      $args[] = '-init_seg_name';
      $args[] = $this->initSegmentName;
    }

    $args[] = '-media_seg_name';
    $args[] = $this->mediaSegmentName . $this->getSegmentExtension();

    // Timeline usage.
    if ($this->useTimeline) {
      $args[] = '-use_timeline';
      $args[] = '1';
    }

    // Use PTS.
    if ($this->usePts) {
      $args[] = '-use_template';
      $args[] = '1';
    }

    // Low-latency options.
    if ($this->lowLatencyEnabled) {
      $args[] = '-ldash';
      $args[] = '1';
      $args[] = '-streaming';
      $args[] = '1';

      if ($this->targetLatency !== NULL) {
        $args[] = '-target_latency';
        $args[] = sprintf('%.1f', $this->targetLatency);
      }
    }

    // UTC timing.
    if ($this->utcTiming) {
      $args[] = '-utc_timing_url';
      $args[] = $this->utcTimingUrl ?? 'https://time.akamai.com/?iso';
    }

    // Min buffer time.
    $args[] = '-min_buffer_time';
    $args[] = (string) $this->minBufferTime;

    // Remove at exit.
    if ($this->removeAtExit) {
      $args[] = '-remove_at_exit';
      $args[] = '1';
    }

    // Format-specific movflags for fMP4 (FFmpeg 5.0+ shorthand).
    if ($this->segmentFormat === self::FORMAT_MP4) {
      $args[] = '-movflags';
      $args[] = '+cmaf';
    }

    // Producer Reference Time for live profiles (sync accuracy).
    if ($this->profile === self::PROFILE_LIVE) {
      $args[] = '-write_prft';
      $args[] = '1';
    }

    return $args;
  }

  /**
   * Gets the segment file extension based on format.
   *
   * @return string
   *   File extension including dot.
   */
  #[\NoDiscard]
  public function getSegmentExtension(): string {
    return $this->segmentFormat === self::FORMAT_WEBM ? '.webm' : '.m4s';
  }

  /**
   * Checks if this configuration is low-latency compatible.
   *
   * @return bool
   *   TRUE if low-latency compatible.
   */
  #[\NoDiscard]
  public function isLowLatencyCompatible(): bool {
    return $this->lowLatencyEnabled
      && $this->profile === self::PROFILE_LIVE
      && $this->segmentDuration <= 4;
  }

  /**
   * Calculates recommended GOP size for this DASH configuration.
   *
   * @param float $frameRate
   *   Video frame rate.
   *
   * @return int
   *   Recommended GOP size in frames.
   */
  #[\NoDiscard]
  public function getRecommendedGopSize(float $frameRate): int {
    return (int) round($this->segmentDuration * $frameRate);
  }

  /**
   * Converts settings to array format.
   *
   * @return array<string, bool|float|int|string|null>
   *   Associative array of settings.
   */
  #[\NoDiscard]
  public function toArray(): array {
    return [
      'segment_duration' => $this->segmentDuration,
      'profile' => $this->profile,
      'segment_format' => $this->segmentFormat,
      'single_file' => $this->singleFile,
      'window_size' => $this->windowSize,
      'extra_window_size' => $this->extraWindowSize,
      'use_pts' => $this->usePts,
      'use_timeline' => $this->useTimeline,
      'init_segment_name' => $this->initSegmentName,
      'media_segment_name' => $this->mediaSegmentName,
      'low_latency_enabled' => $this->lowLatencyEnabled,
      'target_latency' => $this->targetLatency,
      'utc_timing' => $this->utcTiming,
      'utc_timing_url' => $this->utcTimingUrl,
      'min_buffer_time' => $this->minBufferTime,
      'remove_at_exit' => $this->removeAtExit,
    ];
  }

}
