<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\ValueObject;

/**
 * Immutable value object for HLS (HTTP Live Streaming) options.
 *
 * Supports standard HLS, Low-Latency HLS (LL-HLS), and CMAF packaging.
 * LL-HLS requires FFmpeg 4.3+ with fmp4 segment support.
 */
final readonly class HlsOptions {

  /**
   * Segment type constants.
   */
  public const string SEGMENT_MPEGTS = 'mpegts';
  public const string SEGMENT_FMP4 = 'fmp4';

  /**
   * Playlist type constants.
   */
  public const string PLAYLIST_VOD = 'vod';
  public const string PLAYLIST_EVENT = 'event';
  public const string PLAYLIST_LIVE = '';

  /**
   * Constructs an HlsOptions object.
   *
   * @param int $segmentDuration
   *   Target segment duration in seconds. 2-6 for standard, 1-2 for LL-HLS.
   * @param string $playlistType
   *   Playlist type: 'vod', 'event', or '' (live).
   * @param int $listSize
   *   Number of segments to keep in playlist. 0 = keep all (VOD).
   * @param string $segmentType
   *   Segment container: 'mpegts' or 'fmp4' (CMAF).
   * @param bool $lowLatencyEnabled
   *   Enable Low-Latency HLS features.
   * @param float $partDuration
   *   Partial segment duration for LL-HLS (0.2-0.5 seconds recommended).
   * @param bool $independentSegments
   *   Add EXT-X-INDEPENDENT-SEGMENTS tag.
   * @param int $deleteThreshold
   *   Segments to keep after removal from playlist (live only).
   * @param bool $splitByTime
   *   Split segments by time rather than keyframes.
   * @param string|null $baseUrl
   *   Base URL for segment paths in manifest.
   * @param string $segmentFilename
   *   Segment filename pattern. Use %d or %03d for sequence number.
   * @param string|null $masterPlaylistName
   *   Name for master playlist (multi-bitrate).
   * @param bool $programDateTime
   *   Add EXT-X-PROGRAM-DATE-TIME tags.
   * @param int|null $startNumber
   *   Starting sequence number for segments.
   * @param array<int, string> $extraFlags
   *   Additional HLS flags.
   *
   * @throws \InvalidArgumentException
   *   If any parameter is out of valid range.
   */
  public function __construct(
    public int $segmentDuration = 6,
    public string $playlistType = self::PLAYLIST_VOD,
    public int $listSize = 0,
    public string $segmentType = self::SEGMENT_MPEGTS,
    public bool $lowLatencyEnabled = FALSE,
    public float $partDuration = 0.333,
    public bool $independentSegments = TRUE,
    public int $deleteThreshold = 1,
    public bool $splitByTime = FALSE,
    public ?string $baseUrl = NULL,
    public string $segmentFilename = 'segment_%03d',
    public ?string $masterPlaylistName = NULL,
    public bool $programDateTime = FALSE,
    public ?int $startNumber = NULL,
    public array $extraFlags = [],
  ) {
    if ($segmentDuration < 1 || $segmentDuration > 60) {
      throw new \InvalidArgumentException('Segment duration must be between 1 and 60 seconds.');
    }
    if ($listSize < 0) {
      throw new \InvalidArgumentException('List size must be 0 or positive.');
    }
    if (!in_array($segmentType, [self::SEGMENT_MPEGTS, self::SEGMENT_FMP4], TRUE)) {
      throw new \InvalidArgumentException('Segment type must be "mpegts" or "fmp4".');
    }
    if ($partDuration < 0.1 || $partDuration > 2.0) {
      throw new \InvalidArgumentException('Part duration must be between 0.1 and 2.0 seconds.');
    }
    if ($deleteThreshold < 0) {
      throw new \InvalidArgumentException('Delete threshold must be 0 or positive.');
    }
  }

  /**
   * Creates HlsOptions from an array.
   *
   * @param array<string, mixed> $data
   *   Associative array of settings.
   *
   * @return self
   *   New HlsOptions instance.
   */
  #[\NoDiscard]
  public static function fromArray(array $data): self {
    return new self(
      segmentDuration: (int) ($data['segment_duration'] ?? 6),
      playlistType: (string) ($data['playlist_type'] ?? self::PLAYLIST_VOD),
      listSize: (int) ($data['list_size'] ?? 0),
      segmentType: (string) ($data['segment_type'] ?? self::SEGMENT_MPEGTS),
      lowLatencyEnabled: (bool) ($data['low_latency_enabled'] ?? FALSE),
      partDuration: (float) ($data['part_duration'] ?? 0.333),
      independentSegments: (bool) ($data['independent_segments'] ?? TRUE),
      deleteThreshold: (int) ($data['delete_threshold'] ?? 1),
      splitByTime: (bool) ($data['split_by_time'] ?? FALSE),
      baseUrl: isset($data['base_url']) ? (string) $data['base_url'] : NULL,
      segmentFilename: (string) ($data['segment_filename'] ?? 'segment_%03d'),
      masterPlaylistName: isset($data['master_playlist_name']) ? (string) $data['master_playlist_name'] : NULL,
      programDateTime: (bool) ($data['program_date_time'] ?? FALSE),
      startNumber: isset($data['start_number']) ? (int) $data['start_number'] : NULL,
      extraFlags: (array) ($data['extra_flags'] ?? []),
    );
  }

  /**
   * Creates options for standard VOD HLS.
   *
   * @param int $segmentDuration
   *   Segment duration in seconds.
   *
   * @return self
   *   Options configured for VOD.
   */
  #[\NoDiscard]
  public static function forVod(int $segmentDuration = 6): self {
    return new self(
      segmentDuration: $segmentDuration,
      playlistType: self::PLAYLIST_VOD,
      listSize: 0,
      segmentType: self::SEGMENT_MPEGTS,
      independentSegments: TRUE,
    );
  }

  /**
   * Creates options for Low-Latency HLS (LL-HLS).
   *
   * @param float $partDuration
   *   Partial segment duration (0.2-0.5 recommended).
   * @param int $segmentDuration
   *   Full segment duration.
   *
   * @return self
   *   Options configured for LL-HLS.
   */
  #[\NoDiscard]
  public static function forLowLatency(float $partDuration = 0.333, int $segmentDuration = 2): self {
    return new self(
      segmentDuration: $segmentDuration,
      playlistType: self::PLAYLIST_LIVE,
      listSize: 6,
      segmentType: self::SEGMENT_FMP4,
      lowLatencyEnabled: TRUE,
      partDuration: $partDuration,
      independentSegments: TRUE,
      deleteThreshold: 3,
      programDateTime: TRUE,
    );
  }

  /**
   * Creates options for CMAF-compatible HLS (fmp4 segments).
   *
   * CMAF enables single-file encoding for both 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,
      playlistType: self::PLAYLIST_VOD,
      listSize: 0,
      segmentType: self::SEGMENT_FMP4,
      independentSegments: TRUE,
    );
  }

  /**
   * Creates options for live streaming.
   *
   * @param int $segmentDuration
   *   Segment duration in seconds.
   * @param int $listSize
   *   Number of segments in playlist.
   *
   * @return self
   *   Options configured for live streaming.
   */
  #[\NoDiscard]
  public static function forLive(int $segmentDuration = 4, int $listSize = 5): self {
    return new self(
      segmentDuration: $segmentDuration,
      playlistType: self::PLAYLIST_LIVE,
      listSize: $listSize,
      segmentType: self::SEGMENT_MPEGTS,
      independentSegments: TRUE,
      deleteThreshold: 1,
      programDateTime: 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 $partDuration
   *   Partial segment duration.
   *
   * @return self
   *   New instance with LL-HLS enabled.
   */
  #[\NoDiscard]
  public function withLowLatency(float $partDuration = 0.333): self {
    return clone($this, [
      'lowLatencyEnabled' => TRUE,
      'partDuration' => $partDuration,
      'segmentType' => self::SEGMENT_FMP4,
    ]);
  }

  /**
   * Creates a new instance with CMAF (fmp4) segments.
   *
   * @return self
   *   New instance with CMAF enabled.
   */
  #[\NoDiscard]
  public function withCmaf(): self {
    return clone($this, ['segmentType' => self::SEGMENT_FMP4]);
  }

  /**
   * Creates a new instance with a base URL.
   *
   * @param string $baseUrl
   *   Base URL for segments.
   *
   * @return self
   *   New instance with base URL set.
   */
  #[\NoDiscard]
  public function withBaseUrl(string $baseUrl): self {
    return clone($this, ['baseUrl' => $baseUrl]);
  }

  /**
   * Builds FFmpeg HLS muxer arguments.
   *
   * @return array<int, string>
   *   Array of FFmpeg arguments.
   */
  #[\NoDiscard]
  public function buildArguments(): array {
    $args = [
      '-f', 'hls',
      '-hls_time', (string) $this->segmentDuration,
      '-hls_list_size', (string) $this->listSize,
      '-hls_segment_type', $this->segmentType,
    ];

    // Playlist type.
    if ($this->playlistType !== '') {
      $args[] = '-hls_playlist_type';
      $args[] = $this->playlistType;
    }

    // Build flags.
    $flags = $this->buildFlags();
    if (!empty($flags)) {
      $args[] = '-hls_flags';
      $args[] = implode('+', $flags);
    }

    // Low-Latency HLS options.
    if ($this->lowLatencyEnabled) {
      $args[] = '-hls_part_size';
      $args[] = sprintf('%.3f', $this->partDuration);
    }

    // Segment filename pattern.
    $args[] = '-hls_segment_filename';
    $args[] = $this->segmentFilename . $this->getSegmentExtension();

    // Optional settings.
    if ($this->baseUrl !== NULL) {
      $args[] = '-hls_base_url';
      $args[] = $this->baseUrl;
    }

    if ($this->startNumber !== NULL) {
      $args[] = '-start_number';
      $args[] = (string) $this->startNumber;
    }

    if ($this->deleteThreshold > 0 && $this->playlistType === '') {
      $args[] = '-hls_delete_threshold';
      $args[] = (string) $this->deleteThreshold;
    }

    // CMAF-specific: add movflags for fmp4 (FFmpeg 5.0+ shorthand).
    if ($this->segmentType === self::SEGMENT_FMP4) {
      $args[] = '-movflags';
      $args[] = '+cmaf';
    }

    return $args;
  }

  /**
   * Builds HLS flags array.
   *
   * @return array<int, string>
   *   Array of flag strings.
   */
  protected function buildFlags(): array {
    $flags = [];

    if ($this->independentSegments) {
      $flags[] = 'independent_segments';
    }

    if ($this->lowLatencyEnabled) {
      $flags[] = 'low_latency';
    }

    if ($this->splitByTime) {
      $flags[] = 'split_by_time';
    }

    if ($this->programDateTime) {
      $flags[] = 'program_date_time';
    }

    if ($this->playlistType === '' && $this->deleteThreshold > 0) {
      $flags[] = 'delete_segments';
    }

    // Add extra flags.
    foreach ($this->extraFlags as $flag) {
      if (!in_array($flag, $flags, TRUE)) {
        $flags[] = $flag;
      }
    }

    return $flags;
  }

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

  /**
   * Checks if this configuration is LL-HLS compatible.
   *
   * @return bool
   *   TRUE if LL-HLS compatible.
   */
  #[\NoDiscard]
  public function isLowLatencyCompatible(): bool {
    return $this->lowLatencyEnabled
      && $this->segmentType === self::SEGMENT_FMP4
      && $this->partDuration > 0
      && $this->segmentDuration <= 4;
  }

  /**
   * Checks if this configuration uses CMAF.
   *
   * @return bool
   *   TRUE if using CMAF (fmp4 segments).
   */
  #[\NoDiscard]
  public function isCmaf(): bool {
    return $this->segmentType === self::SEGMENT_FMP4;
  }

  /**
   * Calculates recommended GOP size for this HLS configuration.
   *
   * For seamless ABR switching, GOP should align with segment boundaries.
   *
   * @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|array<int, string>>
   *   Associative array of settings.
   */
  #[\NoDiscard]
  public function toArray(): array {
    return [
      'segment_duration' => $this->segmentDuration,
      'playlist_type' => $this->playlistType,
      'list_size' => $this->listSize,
      'segment_type' => $this->segmentType,
      'low_latency_enabled' => $this->lowLatencyEnabled,
      'part_duration' => $this->partDuration,
      'independent_segments' => $this->independentSegments,
      'delete_threshold' => $this->deleteThreshold,
      'split_by_time' => $this->splitByTime,
      'base_url' => $this->baseUrl,
      'segment_filename' => $this->segmentFilename,
      'master_playlist_name' => $this->masterPlaylistName,
      'program_date_time' => $this->programDateTime,
      'start_number' => $this->startNumber,
      'extra_flags' => $this->extraFlags,
    ];
  }

}
