<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\ValueObject;

/**
 * Immutable value object for rate control settings.
 *
 * Configures VBV (Video Buffering Verifier), CBR, and VBR modes
 * for consistent bitrate behavior across ABR renditions.
 */
final readonly class RateControlSettings {

  /**
   * Rate control mode constants.
   */
  public const string MODE_CRF = 'crf';
  public const string MODE_CBR = 'cbr';
  public const string MODE_VBR = 'vbr';
  public const string MODE_CQ = 'cq';

  /**
   * Constructs a RateControlSettings object.
   *
   * @param string $mode
   *   Rate control mode: 'crf', 'cbr', 'vbr', or 'cq'.
   * @param int|null $targetBitrate
   *   Target video bitrate in kbps. Required for CBR/VBR modes.
   * @param int|null $maxBitrate
   *   Maximum bitrate in kbps (VBV maxrate). NULL = no limit.
   * @param int|null $minBitrate
   *   Minimum bitrate in kbps. NULL = no limit.
   * @param int|null $bufferSize
   *   VBV buffer size in kbits. Typically 1.5-2x target bitrate.
   *   NULL = auto-calculate from maxBitrate.
   * @param float $bufferInitialFill
   *   Initial buffer fullness (0.0-1.0). 0.9 = 90% full at start.
   * @param int|null $crf
   *   CRF value override for CRF mode (0-51 for x264, 0-63 for VP9/AV1).
   * @param int|null $cq
   *   Constrained quality value for CQ mode (hardware encoders).
   * @param bool $strictVbv
   *   Enforce strict VBV compliance (may reduce quality).
   *
   * @throws \InvalidArgumentException
   *   If any parameter is out of valid range.
   */
  public function __construct(
    public string $mode = self::MODE_CRF,
    public ?int $targetBitrate = NULL,
    public ?int $maxBitrate = NULL,
    public ?int $minBitrate = NULL,
    public ?int $bufferSize = NULL,
    public float $bufferInitialFill = 0.9,
    public ?int $crf = NULL,
    public ?int $cq = NULL,
    public bool $strictVbv = FALSE,
  ) {
    $validModes = [self::MODE_CRF, self::MODE_CBR, self::MODE_VBR, self::MODE_CQ];
    if (!in_array($mode, $validModes, TRUE)) {
      throw new \InvalidArgumentException(
        sprintf('Invalid rate control mode: %s. Valid modes: %s', $mode, implode(', ', $validModes))
      );
    }

    if ($targetBitrate !== NULL && $targetBitrate < 1) {
      throw new \InvalidArgumentException('Target bitrate must be positive.');
    }
    if ($maxBitrate !== NULL && $maxBitrate < 1) {
      throw new \InvalidArgumentException('Max bitrate must be positive.');
    }
    if ($minBitrate !== NULL && $minBitrate < 1) {
      throw new \InvalidArgumentException('Min bitrate must be positive.');
    }
    if ($bufferSize !== NULL && $bufferSize < 1) {
      throw new \InvalidArgumentException('Buffer size must be positive.');
    }
    if ($bufferInitialFill < 0.0 || $bufferInitialFill > 1.0) {
      throw new \InvalidArgumentException('Buffer initial fill must be between 0.0 and 1.0.');
    }
    if ($crf !== NULL && ($crf < 0 || $crf > 63)) {
      throw new \InvalidArgumentException('CRF must be between 0 and 63.');
    }
    if ($cq !== NULL && ($cq < 0 || $cq > 51)) {
      throw new \InvalidArgumentException('CQ must be between 0 and 51.');
    }

    // Validate CBR/VBR modes require target bitrate.
    if (($mode === self::MODE_CBR || $mode === self::MODE_VBR) && $targetBitrate === NULL) {
      throw new \InvalidArgumentException(
        sprintf('%s mode requires a target bitrate.', strtoupper($mode))
      );
    }
  }

  /**
   * Creates RateControlSettings from an array.
   *
   * @param array<string, mixed> $data
   *   Associative array of settings.
   *
   * @return self
   *   New RateControlSettings instance.
   */
  #[\NoDiscard]
  public static function fromArray(array $data): self {
    return new self(
      mode: (string) ($data['mode'] ?? self::MODE_CRF),
      targetBitrate: isset($data['target_bitrate']) ? (int) $data['target_bitrate'] : NULL,
      maxBitrate: isset($data['max_bitrate']) ? (int) $data['max_bitrate'] : NULL,
      minBitrate: isset($data['min_bitrate']) ? (int) $data['min_bitrate'] : NULL,
      bufferSize: isset($data['buffer_size']) ? (int) $data['buffer_size'] : NULL,
      bufferInitialFill: (float) ($data['buffer_initial_fill'] ?? 0.9),
      crf: isset($data['crf']) ? (int) $data['crf'] : NULL,
      cq: isset($data['cq']) ? (int) $data['cq'] : NULL,
      strictVbv: (bool) ($data['strict_vbv'] ?? FALSE),
    );
  }

  /**
   * Creates settings for constant bitrate (CBR) encoding.
   *
   * @param int $bitrate
   *   Target bitrate in kbps.
   * @param int|null $bufferSize
   *   VBV buffer size in kbits. NULL = auto (2x bitrate).
   *
   * @return self
   *   New instance with CBR settings.
   */
  #[\NoDiscard]
  public static function forCbr(int $bitrate, ?int $bufferSize = NULL): self {
    return new self(
      mode: self::MODE_CBR,
      targetBitrate: $bitrate,
      maxBitrate: $bitrate,
      minBitrate: $bitrate,
      bufferSize: $bufferSize ?? ($bitrate * 2),
      strictVbv: TRUE,
    );
  }

  /**
   * Creates settings for variable bitrate (VBR) encoding.
   *
   * @param int $targetBitrate
   *   Target average bitrate in kbps.
   * @param int|null $maxBitrate
   *   Maximum bitrate in kbps. NULL = 1.5x target.
   *
   * @return self
   *   New instance with VBR settings.
   */
  #[\NoDiscard]
  public static function forVbr(int $targetBitrate, ?int $maxBitrate = NULL): self {
    $max = $maxBitrate ?? (int) ($targetBitrate * 1.5);
    return new self(
      mode: self::MODE_VBR,
      targetBitrate: $targetBitrate,
      maxBitrate: $max,
      bufferSize: $max * 2,
    );
  }

  /**
   * Creates settings for CRF with VBV constraints (capped CRF).
   *
   * This is the recommended mode for ABR streaming: quality-based
   * encoding with a maximum bitrate cap for bandwidth predictability.
   *
   * @param int $crf
   *   CRF value (lower = better quality).
   * @param int $maxBitrate
   *   Maximum bitrate cap in kbps.
   *
   * @return self
   *   New instance with capped CRF settings.
   */
  #[\NoDiscard]
  public static function forCappedCrf(int $crf, int $maxBitrate): self {
    return new self(
      mode: self::MODE_CRF,
      crf: $crf,
      maxBitrate: $maxBitrate,
      bufferSize: $maxBitrate * 2,
    );
  }

  /**
   * Creates settings for a specific ABR rendition.
   *
   * @param int $targetBitrate
   *   Target bitrate for this rendition in kbps.
   * @param int|null $crf
   *   Optional CRF value for quality floor.
   *
   * @return self
   *   New instance optimized for ABR rendition.
   */
  #[\NoDiscard]
  public static function forAbrRendition(int $targetBitrate, ?int $crf = NULL): self {
    $maxBitrate = (int) ($targetBitrate * 1.3);
    return new self(
      mode: $crf !== NULL ? self::MODE_CRF : self::MODE_VBR,
      targetBitrate: $targetBitrate,
      maxBitrate: $maxBitrate,
      bufferSize: $maxBitrate * 2,
      crf: $crf,
    );
  }

  /**
   * Creates a new instance with different target bitrate.
   *
   * @param int $bitrate
   *   Target bitrate in kbps.
   *
   * @return self
   *   New instance with updated setting.
   */
  #[\NoDiscard]
  public function withTargetBitrate(int $bitrate): self {
    return clone($this, ['targetBitrate' => $bitrate]);
  }

  /**
   * Creates a new instance with different max bitrate.
   *
   * @param int $maxBitrate
   *   Max bitrate in kbps.
   *
   * @return self
   *   New instance with updated setting.
   */
  #[\NoDiscard]
  public function withMaxBitrate(int $maxBitrate): self {
    return clone($this, ['maxBitrate' => $maxBitrate]);
  }

  /**
   * Creates a new instance with VBV buffer settings.
   *
   * @param int $bufferSize
   *   Buffer size in kbits.
   * @param float $initialFill
   *   Initial fill ratio (0.0-1.0).
   *
   * @return self
   *   New instance with VBV settings.
   */
  #[\NoDiscard]
  public function withVbvBuffer(int $bufferSize, float $initialFill = 0.9): self {
    return clone($this, [
      'bufferSize' => $bufferSize,
      'bufferInitialFill' => $initialFill,
    ]);
  }

  /**
   * Calculates effective buffer size.
   *
   * @return int|null
   *   Buffer size in kbits, or NULL if not applicable.
   */
  #[\NoDiscard]
  public function getEffectiveBufferSize(): ?int {
    if ($this->bufferSize !== NULL) {
      return $this->bufferSize;
    }

    if ($this->maxBitrate !== NULL) {
      return $this->maxBitrate * 2;
    }

    return NULL;
  }

  /**
   * Builds FFmpeg arguments for rate control.
   *
   * @param string $codec
   *   Codec ID ('h264', 'hevc', 'vp9', 'av1').
   *
   * @return array<int, string>
   *   Array of FFmpeg arguments.
   */
  #[\NoDiscard]
  public function buildArguments(string $codec): array {
    $args = [];

    switch ($this->mode) {
      case self::MODE_CRF:
        $args = $this->buildCrfArguments($codec);
        break;

      case self::MODE_CBR:
        $args = $this->buildCbrArguments($codec);
        break;

      case self::MODE_VBR:
        $args = $this->buildVbrArguments($codec);
        break;

      case self::MODE_CQ:
        $args = $this->buildCqArguments($codec);
        break;
    }

    // Add VBV constraints if applicable.
    if ($this->maxBitrate !== NULL || $this->bufferSize !== NULL) {
      $args = array_merge($args, $this->buildVbvArguments($codec));
    }

    return $args;
  }

  /**
   * Builds CRF mode arguments.
   *
   * @param string $codec
   *   Codec ID.
   *
   * @return array<int, string>
   *   FFmpeg arguments.
   */
  protected function buildCrfArguments(string $codec): array {
    $args = [];
    $crf = $this->crf ?? 23;

    switch ($codec) {
      case 'h264':
      case 'hevc':
        $args[] = '-crf';
        $args[] = (string) $crf;
        break;

      case 'vp9':
        $args[] = '-crf';
        $args[] = (string) $crf;
        $args[] = '-b:v';
        $args[] = '0';
        break;

      case 'av1':
        $args[] = '-crf';
        $args[] = (string) $crf;
        break;
    }

    return $args;
  }

  /**
   * Builds CBR mode arguments.
   *
   * @param string $codec
   *   Codec ID.
   *
   * @return array<int, string>
   *   FFmpeg arguments.
   */
  protected function buildCbrArguments(string $codec): array {
    $args = [];
    $bitrate = $this->targetBitrate ?? 2000;

    switch ($codec) {
      case 'h264':
      case 'hevc':
        $args[] = '-b:v';
        $args[] = "{$bitrate}k";
        $args[] = '-minrate';
        $args[] = "{$bitrate}k";
        $args[] = '-maxrate';
        $args[] = "{$bitrate}k";
        break;

      case 'vp9':
        $args[] = '-b:v';
        $args[] = "{$bitrate}k";
        $args[] = '-minrate';
        $args[] = "{$bitrate}k";
        $args[] = '-maxrate';
        $args[] = "{$bitrate}k";
        break;

      case 'av1':
        $args[] = '-b:v';
        $args[] = "{$bitrate}k";
        break;
    }

    return $args;
  }

  /**
   * Builds VBR mode arguments.
   *
   * @param string $codec
   *   Codec ID.
   *
   * @return array<int, string>
   *   FFmpeg arguments.
   */
  protected function buildVbrArguments(string $codec): array {
    $args = [];
    $target = $this->targetBitrate ?? 2000;
    $max = $this->maxBitrate ?? (int) ($target * 1.5);

    switch ($codec) {
      case 'h264':
      case 'hevc':
        $args[] = '-b:v';
        $args[] = "{$target}k";
        $args[] = '-maxrate';
        $args[] = "{$max}k";
        break;

      case 'vp9':
        $args[] = '-b:v';
        $args[] = "{$target}k";
        $args[] = '-maxrate';
        $args[] = "{$max}k";
        break;

      case 'av1':
        $args[] = '-b:v';
        $args[] = "{$target}k";
        break;
    }

    if ($this->minBitrate !== NULL) {
      $args[] = '-minrate';
      $args[] = "{$this->minBitrate}k";
    }

    return $args;
  }

  /**
   * Builds CQ mode arguments (for hardware encoders).
   *
   * @param string $codec
   *   Codec ID.
   *
   * @return array<int, string>
   *   FFmpeg arguments.
   */
  protected function buildCqArguments(string $codec): array {
    $args = [];
    $cq = $this->cq ?? 23;

    // CQ mode is primarily for NVENC/QSV/VAAPI.
    switch ($codec) {
      case 'h264':
      case 'hevc':
        $args[] = '-rc';
        $args[] = 'vbr';
        $args[] = '-cq';
        $args[] = (string) $cq;
        break;

      default:
        // Fall back to CRF for software encoders.
        $args[] = '-crf';
        $args[] = (string) $cq;
        break;
    }

    return $args;
  }

  /**
   * Builds VBV buffer arguments.
   *
   * @param string $codec
   *   Codec ID.
   *
   * @return array<int, string>
   *   FFmpeg arguments.
   */
  protected function buildVbvArguments(string $codec): array {
    $args = [];
    $bufferSize = $this->getEffectiveBufferSize();

    if ($this->maxBitrate !== NULL) {
      $args[] = '-maxrate';
      $args[] = "{$this->maxBitrate}k";
    }

    if ($bufferSize !== NULL) {
      $args[] = '-bufsize';
      $args[] = "{$bufferSize}k";
    }

    // Initial buffer occupancy.
    if ($bufferSize !== NULL && $this->bufferInitialFill > 0) {
      // x264-specific option.
      if ($codec === 'h264') {
        $args[] = '-x264-params';
        $args[] = "vbv-init=$this->bufferInitialFill";
      }
    }

    return $args;
  }

  /**
   * Converts settings to array format.
   *
   * @return array<string, bool|float|int|null|string>
   *   Associative array of settings.
   */
  #[\NoDiscard]
  public function toArray(): array {
    return [
      'mode' => $this->mode,
      'target_bitrate' => $this->targetBitrate,
      'max_bitrate' => $this->maxBitrate,
      'min_bitrate' => $this->minBitrate,
      'buffer_size' => $this->bufferSize,
      'buffer_initial_fill' => $this->bufferInitialFill,
      'crf' => $this->crf,
      'cq' => $this->cq,
      'strict_vbv' => $this->strictVbv,
    ];
  }

}
