<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\ValueObject;

/**
 * Immutable value object for GOP (Group of Pictures) settings.
 *
 * Configures keyframe intervals for consistent ABR streaming.
 * All renditions must have identical GOP structure for seamless
 * quality switching.
 */
final readonly class GopSettings {

  /**
   * Constructs a GopSettings object.
   *
   * @param int $keyframeInterval
   *   Keyframe interval in frames (GOP length). 0 = auto.
   *   Common values: 48 (2s at 24fps), 60 (2s at 30fps), 120 (2s at 60fps).
   * @param float $keyframeIntervalSeconds
   *   Keyframe interval in seconds. Takes precedence over keyframeInterval.
   *   Recommended: 2.0 for most streaming scenarios.
   * @param int $minKeyframeInterval
   *   Minimum keyframe interval in frames. Prevents too frequent keyframes.
   * @param int $sceneChangeThreshold
   *   Scene change detection threshold (0-100). 0 = disabled, 40 = default.
   *   Lower values = more scene-change keyframes.
   * @param bool $forceKeyframes
   *   Force keyframes at exact intervals (no scene detection override).
   *   Essential for ABR streaming compatibility.
   * @param bool $closedGop
   *   Use closed GOPs (no B-frame references across GOP boundaries).
   *   Required for segment-based streaming (HLS/DASH).
   * @param int $bFrames
   *   Number of B-frames between reference frames (0-16). 0 = disabled.
   *   More B-frames = better compression but higher latency.
   * @param int $refFrames
   *   Number of reference frames (1-16). Higher = better compression.
   *
   * @throws \InvalidArgumentException
   *   If any parameter is out of valid range.
   */
  public function __construct(
    public int $keyframeInterval = 0,
    public float $keyframeIntervalSeconds = 2.0,
    public int $minKeyframeInterval = 0,
    public int $sceneChangeThreshold = 0,
    public bool $forceKeyframes = TRUE,
    public bool $closedGop = TRUE,
    public int $bFrames = 3,
    public int $refFrames = 4,
  ) {
    if ($keyframeInterval < 0) {
      throw new \InvalidArgumentException('Keyframe interval must be 0 (auto) or positive.');
    }
    if ($keyframeIntervalSeconds < 0.0) {
      throw new \InvalidArgumentException('Keyframe interval seconds must be 0 (auto) or positive.');
    }
    if ($minKeyframeInterval < 0) {
      throw new \InvalidArgumentException('Min keyframe interval must be 0 or positive.');
    }
    if ($sceneChangeThreshold < 0 || $sceneChangeThreshold > 100) {
      throw new \InvalidArgumentException('Scene change threshold must be between 0 and 100.');
    }
    if ($bFrames < 0 || $bFrames > 16) {
      throw new \InvalidArgumentException('B-frames must be between 0 and 16.');
    }
    if ($refFrames < 1 || $refFrames > 16) {
      throw new \InvalidArgumentException('Reference frames must be between 1 and 16.');
    }
  }

  /**
   * Creates GopSettings from an array.
   *
   * @param array<string, mixed> $data
   *   Associative array of settings.
   *
   * @return self
   *   New GopSettings instance.
   */
  #[\NoDiscard]
  public static function fromArray(array $data): self {
    return new self(
      keyframeInterval: (int) ($data['keyframe_interval'] ?? 0),
      keyframeIntervalSeconds: (float) ($data['keyframe_interval_seconds'] ?? 2.0),
      minKeyframeInterval: (int) ($data['min_keyframe_interval'] ?? 0),
      sceneChangeThreshold: (int) ($data['scene_change_threshold'] ?? 0),
      forceKeyframes: (bool) ($data['force_keyframes'] ?? TRUE),
      closedGop: (bool) ($data['closed_gop'] ?? TRUE),
      bFrames: (int) ($data['b_frames'] ?? 3),
      refFrames: (int) ($data['ref_frames'] ?? 4),
    );
  }

  /**
   * Creates settings optimized for ABR streaming.
   *
   * @param float $segmentDuration
   *   Target segment duration in seconds (typically 2, 4, or 6).
   *
   * @return self
   *   New instance with ABR streaming settings.
   */
  #[\NoDiscard]
  public static function forAbrStreaming(float $segmentDuration = 2.0): self {
    return new self(
      keyframeInterval: 0,
      keyframeIntervalSeconds: $segmentDuration,
      minKeyframeInterval: 0,
      sceneChangeThreshold: 0,
      forceKeyframes: TRUE,
      closedGop: TRUE,
      bFrames: 3,
      refFrames: 4,
    );
  }

  /**
   * Creates settings optimized for low-latency streaming.
   *
   * @return self
   *   New instance with low-latency settings.
   */
  #[\NoDiscard]
  public static function forLowLatency(): self {
    return new self(
      keyframeInterval: 0,
      keyframeIntervalSeconds: 1.0,
      minKeyframeInterval: 0,
      sceneChangeThreshold: 0,
      forceKeyframes: TRUE,
      closedGop: TRUE,
      bFrames: 0,
      refFrames: 1,
    );
  }

  /**
   * Creates a new instance with different keyframe interval seconds.
   *
   * @param float $seconds
   *   Keyframe interval in seconds.
   *
   * @return self
   *   New instance with updated setting.
   */
  #[\NoDiscard]
  public function withKeyframeIntervalSeconds(float $seconds): self {
    return clone($this, ['keyframeIntervalSeconds' => $seconds]);
  }

  /**
   * Creates a new instance with force keyframes setting.
   *
   * @param bool $force
   *   Whether to force keyframes.
   *
   * @return self
   *   New instance with updated setting.
   */
  #[\NoDiscard]
  public function withForceKeyframes(bool $force): self {
    return clone($this, ['forceKeyframes' => $force]);
  }

  /**
   * Creates a new instance with closed GOP setting.
   *
   * @param bool $closed
   *   Whether to use closed GOPs.
   *
   * @return self
   *   New instance with updated setting.
   */
  #[\NoDiscard]
  public function withClosedGop(bool $closed): self {
    return clone($this, ['closedGop' => $closed]);
  }

  /**
   * Calculates keyframe interval in frames for a given frame rate.
   *
   * @param float $frameRate
   *   Video frame rate (e.g., 23.976, 25.0, 29.97, 30.0, 60.0).
   *
   * @return int
   *   Keyframe interval in frames.
   */
  #[\NoDiscard]
  public function getKeyframeIntervalForFrameRate(float $frameRate): int {
    if ($this->keyframeInterval > 0) {
      return $this->keyframeInterval;
    }

    return (int) round($this->keyframeIntervalSeconds * $frameRate);
  }

  /**
   * Builds FFmpeg arguments for GOP settings.
   *
   * @param string $codec
   *   Codec ID ('h264', 'hevc', 'vp9', 'av1').
   * @param float|null $frameRate
   *   Video frame rate for calculating keyframe interval.
   *
   * @return array<int, string>
   *   Array of FFmpeg arguments.
   */
  #[\NoDiscard]
  public function buildArguments(string $codec, ?float $frameRate = NULL): array {
    $args = [];

    // Calculate keyframe interval.
    $gopSize = $this->keyframeInterval;
    if ($gopSize === 0 && $frameRate !== NULL) {
      $gopSize = $this->getKeyframeIntervalForFrameRate($frameRate);
    }

    // Codec-specific arguments.
    switch ($codec) {
      case 'h264':
      case 'hevc':
        if ($gopSize > 0) {
          $args[] = '-g';
          $args[] = (string) $gopSize;
        }

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

        // Scene change detection.
        if ($this->sceneChangeThreshold === 0 || $this->forceKeyframes) {
          $args[] = '-sc_threshold';
          $args[] = '0';
        }
        else {
          $args[] = '-sc_threshold';
          $args[] = (string) $this->sceneChangeThreshold;
        }

        // B-frames.
        $args[] = '-bf';
        $args[] = (string) $this->bFrames;

        // Reference frames.
        $args[] = '-refs';
        $args[] = (string) $this->refFrames;

        // Closed GOP for x264/x265.
        if ($this->closedGop) {
          if ($codec === 'h264') {
            $args[] = '-flags';
            $args[] = '+cgop';
          }
          // HEVC uses -x265-params.
        }
        break;

      case 'vp9':
        if ($gopSize > 0) {
          $args[] = '-g';
          $args[] = (string) $gopSize;
        }
        // VP9 doesn't support B-frames in the same way.
        break;

      case 'av1':
        if ($gopSize > 0) {
          $args[] = '-g';
          $args[] = (string) $gopSize;
        }
        break;
    }

    // Force keyframe expression for precise timing.
    if ($this->forceKeyframes && $this->keyframeIntervalSeconds > 0) {
      $args[] = '-force_key_frames';
      $args[] = sprintf('expr:gte(t,n_forced*%.1f)', $this->keyframeIntervalSeconds);
    }

    return $args;
  }

  /**
   * Converts settings to array format.
   *
   * @return array<string, bool|float|int>
   *   Associative array of settings.
   */
  #[\NoDiscard]
  public function toArray(): array {
    return [
      'keyframe_interval' => $this->keyframeInterval,
      'keyframe_interval_seconds' => $this->keyframeIntervalSeconds,
      'min_keyframe_interval' => $this->minKeyframeInterval,
      'scene_change_threshold' => $this->sceneChangeThreshold,
      'force_keyframes' => $this->forceKeyframes,
      'closed_gop' => $this->closedGop,
      'b_frames' => $this->bFrames,
      'ref_frames' => $this->refFrames,
    ];
  }

}
