<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\ValueObject;

/**
 * Immutable value object representing a single rung in a bitrate ladder.
 *
 * A bitrate ladder consists of multiple rungs, each defining a specific
 * resolution and bitrate combination for adaptive streaming.
 */
final readonly class BitrateRung {

  /**
   * Constructs a BitrateRung object.
   *
   * @param int $width
   *   Video width in pixels.
   * @param int $height
   *   Video height in pixels.
   * @param int $videoBitrate
   *   Target video bitrate in kbps.
   * @param int|null $audioBitrate
   *   Audio bitrate in kbps. NULL = use default.
   * @param int|null $maxBitrate
   *   Maximum bitrate for VBV in kbps. NULL = auto (1.5x video).
   * @param int|null $bufferSize
   *   VBV buffer size in kbits. NULL = auto (2x max).
   * @param int|null $crf
   *   CRF value for quality-based encoding. NULL = use bitrate mode.
   * @param string|null $label
   *   Human-readable label (e.g., "1080p", "720p").
   * @param bool $enabled
   *   Whether this rung is enabled.
   *
   * @throws \InvalidArgumentException
   *   If parameters are invalid.
   */
  public function __construct(
    public int $width,
    public int $height,
    public int $videoBitrate,
    public ?int $audioBitrate = NULL,
    public ?int $maxBitrate = NULL,
    public ?int $bufferSize = NULL,
    public ?int $crf = NULL,
    public ?string $label = NULL,
    public bool $enabled = TRUE,
  ) {
    if ($width < 1 || $width > 8192) {
      throw new \InvalidArgumentException('Width must be between 1 and 8192.');
    }
    if ($height < 1 || $height > 4320) {
      throw new \InvalidArgumentException('Height must be between 1 and 4320.');
    }
    if ($videoBitrate < 1) {
      throw new \InvalidArgumentException('Video bitrate must be positive.');
    }
    if ($crf !== NULL && ($crf < 0 || $crf > 63)) {
      throw new \InvalidArgumentException('CRF must be between 0 and 63.');
    }
  }

  /**
   * Creates BitrateRung from an array.
   *
   * @param array<string, mixed> $data
   *   Associative array of settings.
   *
   * @return self
   *   New BitrateRung instance.
   */
  #[\NoDiscard]
  public static function fromArray(array $data): self {
    return new self(
      width: (int) ($data['width'] ?? 1920),
      height: (int) ($data['height'] ?? 1080),
      videoBitrate: (int) ($data['video_bitrate'] ?? 5000),
      audioBitrate: isset($data['audio_bitrate']) ? (int) $data['audio_bitrate'] : NULL,
      maxBitrate: isset($data['max_bitrate']) ? (int) $data['max_bitrate'] : NULL,
      bufferSize: isset($data['buffer_size']) ? (int) $data['buffer_size'] : NULL,
      crf: isset($data['crf']) ? (int) $data['crf'] : NULL,
      label: isset($data['label']) ? (string) $data['label'] : NULL,
      enabled: (bool) ($data['enabled'] ?? TRUE),
    );
  }

  /**
   * Creates a standard 4K rung.
   *
   * @param int $bitrate
   *   Video bitrate in kbps.
   *
   * @return self
   *   New BitrateRung instance.
   */
  #[\NoDiscard]
  public static function for4K(int $bitrate = 15000): self {
    return new self(
      width: 3840,
      height: 2160,
      videoBitrate: $bitrate,
      audioBitrate: 192,
      label: '4K',
    );
  }

  /**
   * Creates a standard 1080p rung.
   *
   * @param int $bitrate
   *   Video bitrate in kbps.
   *
   * @return self
   *   New BitrateRung instance.
   */
  #[\NoDiscard]
  public static function for1080p(int $bitrate = 6000): self {
    return new self(
      width: 1920,
      height: 1080,
      videoBitrate: $bitrate,
      audioBitrate: 128,
      label: '1080p',
    );
  }

  /**
   * Creates a standard 720p rung.
   *
   * @param int $bitrate
   *   Video bitrate in kbps.
   *
   * @return self
   *   New BitrateRung instance.
   */
  #[\NoDiscard]
  public static function for720p(int $bitrate = 3000): self {
    return new self(
      width: 1280,
      height: 720,
      videoBitrate: $bitrate,
      audioBitrate: 128,
      label: '720p',
    );
  }

  /**
   * Creates a standard 480p rung.
   *
   * @param int $bitrate
   *   Video bitrate in kbps.
   *
   * @return self
   *   New BitrateRung instance.
   */
  #[\NoDiscard]
  public static function for480p(int $bitrate = 1500): self {
    return new self(
      width: 854,
      height: 480,
      videoBitrate: $bitrate,
      audioBitrate: 96,
      label: '480p',
    );
  }

  /**
   * Creates a standard 360p rung.
   *
   * @param int $bitrate
   *   Video bitrate in kbps.
   *
   * @return self
   *   New BitrateRung instance.
   */
  #[\NoDiscard]
  public static function for360p(int $bitrate = 800): self {
    return new self(
      width: 640,
      height: 360,
      videoBitrate: $bitrate,
      audioBitrate: 64,
      label: '360p',
    );
  }

  /**
   * Gets the effective max bitrate.
   *
   * @return int
   *   Max bitrate in kbps.
   */
  #[\NoDiscard]
  public function getEffectiveMaxBitrate(): int {
    return $this->maxBitrate ?? (int) ($this->videoBitrate * 1.5);
  }

  /**
   * Gets the effective buffer size.
   *
   * @return int
   *   Buffer size in kbits.
   */
  #[\NoDiscard]
  public function getEffectiveBufferSize(): int {
    return $this->bufferSize ?? ($this->getEffectiveMaxBitrate() * 2);
  }

  /**
   * Gets the resolution label.
   *
   * @return string
   *   Resolution label (e.g., "1080p", "720p").
   */
  #[\NoDiscard]
  public function getLabel(): string {
    if ($this->label !== NULL) {
      return $this->label;
    }

    return match (TRUE) {
      $this->height >= 2160 => '4K',
      $this->height >= 1440 => '1440p',
      $this->height >= 1080 => '1080p',
      $this->height >= 720 => '720p',
      $this->height >= 480 => '480p',
      $this->height >= 360 => '360p',
      default => sprintf('%dp', $this->height),
    };
  }

  /**
   * Gets the total bitrate (video + audio).
   *
   * @return int
   *   Total bitrate in kbps.
   */
  #[\NoDiscard]
  public function getTotalBitrate(): int {
    return $this->videoBitrate + ($this->audioBitrate ?? 128);
  }

  /**
   * Creates a new rung with adjusted bitrate.
   *
   * @param float $factor
   *   Multiplier for bitrate adjustment.
   *
   * @return self
   *   New rung with adjusted bitrate.
   */
  #[\NoDiscard]
  public function withBitrateAdjustment(float $factor): self {
    return clone($this, [
      'videoBitrate' => (int) ($this->videoBitrate * $factor),
    ]);
  }

  /**
   * Creates a new rung with specific CRF.
   *
   * @param int $crf
   *   CRF value.
   *
   * @return self
   *   New rung with CRF set.
   */
  #[\NoDiscard]
  public function withCrf(int $crf): self {
    return clone($this, ['crf' => $crf]);
  }

  /**
   * Checks if this rung exceeds source resolution.
   *
   * @param int $sourceWidth
   *   Source video width.
   * @param int $sourceHeight
   *   Source video height.
   *
   * @return bool
   *   TRUE if rung resolution exceeds source.
   */
  #[\NoDiscard]
  public function exceedsSource(int $sourceWidth, int $sourceHeight): bool {
    return $this->width > $sourceWidth || $this->height > $sourceHeight;
  }

  /**
   * Converts to ConversionOptions.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\GopSettings|null $gopSettings
   *   GOP settings to apply.
   *
   * @return \Drupal\ffmpeg_media\ValueObject\ConversionOptions
   *   Conversion options for this rung.
   */
  #[\NoDiscard]
  public function toConversionOptions(?GopSettings $gopSettings = NULL): ConversionOptions {
    $rateControl = RateControlSettings::forAbrRendition(
      $this->videoBitrate,
      $this->crf,
    );

    return new ConversionOptions(
      crf: $this->crf ?? 23,
      maxWidth: $this->width,
      maxHeight: $this->height,
      audioBitrate: ($this->audioBitrate ?? 128) . 'k',
      gopSettings: $gopSettings,
      rateControl: $rateControl,
    );
  }

  /**
   * Converts to array format.
   *
   * @return array<string, bool|int|string|null>
   *   Associative array of settings.
   */
  #[\NoDiscard]
  public function toArray(): array {
    return [
      'width' => $this->width,
      'height' => $this->height,
      'video_bitrate' => $this->videoBitrate,
      'audio_bitrate' => $this->audioBitrate,
      'max_bitrate' => $this->maxBitrate,
      'buffer_size' => $this->bufferSize,
      'crf' => $this->crf,
      'label' => $this->label,
      'enabled' => $this->enabled,
    ];
  }

}
