<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\ValueObject;

/**
 * Immutable value object containing video file information.
 */
final class VideoInfo {

  /**
   * Constructs a VideoInfo object.
   *
   * @param string $path
   *   Path to the video file.
   * @param int $width
   *   Video width in pixels.
   * @param int $height
   *   Video height in pixels.
   * @param float $duration
   *   Duration in seconds.
   * @param string $codec
   *   Video codec name.
   * @param int $bitrate
   *   Video bitrate in bits per second.
   * @param string $format
   *   Container format.
   * @param float $frameRate
   *   Frames per second.
   * @param string|null $audioCodec
   *   Audio codec name, if present.
   * @param int|null $audioBitrate
   *   Audio bitrate in bits per second, if present.
   * @param int|null $audioChannels
   *   Number of audio channels, if present.
   * @param int|null $rotation
   *   Video rotation in degrees (0, 90, 180, 270).
   * @param string|null $pixelFormat
   *   Pixel format (e.g., 'yuv420p', 'yuv420p10le').
   * @param string|null $colorTransfer
   *   Color transfer characteristics (e.g., 'smpte2084', 'arib-std-b67').
   * @param string|null $colorPrimaries
   *   Color primaries (e.g., 'bt2020').
   *
   * @throws \InvalidArgumentException
   *   If width or height are not positive.
   */
  public function __construct(
    public readonly string $path,
    public readonly int $width,
    public readonly int $height,
    public readonly float $duration,
    public readonly string $codec,
    public readonly int $bitrate,
    public readonly string $format,
    public readonly float $frameRate = 0.0,
    public readonly ?string $audioCodec = NULL,
    public readonly ?int $audioBitrate = NULL,
    public readonly ?int $audioChannels = NULL,
    public readonly ?int $rotation = NULL,
    public readonly ?string $pixelFormat = NULL,
    public readonly ?string $colorTransfer = NULL,
    public readonly ?string $colorPrimaries = NULL,
  ) {
    if ($width < 1) {
      throw new \InvalidArgumentException('Width must be positive.');
    }
    if ($height < 1) {
      throw new \InvalidArgumentException('Height must be positive.');
    }
    if ($duration < 0) {
      throw new \InvalidArgumentException('Duration cannot be negative.');
    }
  }

  // phpcs:disable -- PHP 8.4 property hooks not yet supported by PHPCS sniffs.
  /**
   * Whether the video is HD (720p or higher).
   */
  public bool $isHd {
    get => $this->height >= 720 || $this->width >= 1280;
  }

  /**
   * Whether the video is Full HD (1080p or higher).
   */
  public bool $isFullHd {
    get => $this->height >= 1080 || $this->width >= 1920;
  }

  /**
   * Whether the video is 4K (2160p or higher).
   */
  public bool $is4K {
    get => $this->height >= 2160 || $this->width >= 3840;
  }

  /**
   * Whether the video has an audio stream.
   */
  public bool $hasAudio {
    get => $this->audioCodec !== NULL;
  }

  /**
   * The aspect ratio string (e.g., "16:9", "4:3").
   */
  public string $aspectRatio {
    get => $this->calculateAspectRatio();
  }

  /**
   * The resolution tier name (e.g., 'SD', 'HD', 'Full HD', '4K', '8K').
   */
  public string $resolutionTier {
    get => $this->calculateResolutionTier();
  }

  /**
   * Whether the video is portrait orientation.
   */
  public bool $isPortrait {
    get => $this->height > $this->width;
  }
  // phpcs:enable

  /**
   * Creates VideoInfo from ffprobe JSON output.
   *
   * @param string $path
   *   Path to the video file.
   * @param array<string, mixed> $data
   *   Parsed ffprobe JSON output.
   *
   * @return self
   *   New VideoInfo instance.
   */
  #[\NoDiscard]
  public static function fromFfprobe(string $path, array $data): self {
    $streams = $data['streams'] ?? [];
    $videoStream = array_find($streams, static fn(array $stream): bool => ($stream['codec_type'] ?? '') === 'video');
    $audioStream = array_find($streams, static fn(array $stream): bool => ($stream['codec_type'] ?? '') === 'audio');

    if ($videoStream === NULL) {
      throw new \InvalidArgumentException('No video stream found in ffprobe data.');
    }

    $format = $data['format'] ?? [];
    $rotation = NULL;

    // Check for rotation in side_data_list or tags.
    if (isset($videoStream['side_data_list'])) {
      foreach ($videoStream['side_data_list'] as $sideData) {
        if (isset($sideData['rotation'])) {
          $rotation = (int) $sideData['rotation'];
          break;
        }
      }
    }
    if ($rotation === NULL && isset($videoStream['tags']['rotate'])) {
      $rotation = (int) $videoStream['tags']['rotate'];
    }

    // Parse frame rate from string like "30/1" or "29.97".
    $frameRate = 0.0;
    if (isset($videoStream['r_frame_rate'])) {
      $parts = explode('/', $videoStream['r_frame_rate']);
      if (count($parts) === 2 && (int) $parts[1] > 0) {
        $frameRate = (float) $parts[0] / (float) $parts[1];
      }
      else {
        $frameRate = (float) $parts[0];
      }
    }

    return new self(
      path: $path,
      width: (int) ($videoStream['width'] ?? 0),
      height: (int) ($videoStream['height'] ?? 0),
      duration: (float) ($format['duration'] ?? $videoStream['duration'] ?? 0),
      codec: (string) ($videoStream['codec_name'] ?? 'unknown'),
      bitrate: (int) ($format['bit_rate'] ?? $videoStream['bit_rate'] ?? 0),
      format: (string) ($format['format_name'] ?? 'unknown'),
      frameRate: $frameRate,
      audioCodec: $audioStream ? (string) $audioStream['codec_name'] : NULL,
      audioBitrate: $audioStream ? (int) ($audioStream['bit_rate'] ?? 0) : NULL,
      audioChannels: $audioStream ? (int) ($audioStream['channels'] ?? 0) : NULL,
      rotation: $rotation,
      pixelFormat: isset($videoStream['pix_fmt']) ? (string) $videoStream['pix_fmt'] : NULL,
      colorTransfer: isset($videoStream['color_transfer']) ? (string) $videoStream['color_transfer'] : NULL,
      colorPrimaries: isset($videoStream['color_primaries']) ? (string) $videoStream['color_primaries'] : NULL,
    );
  }

  /**
   * Calculates the aspect ratio as a string (e.g., "16:9", "4:3").
   */
  private function calculateAspectRatio(): string {
    $gcd = $this->gcd($this->width, $this->height);
    if ($gcd === 0) {
      return '0:0';
    }
    return ($this->width / $gcd) . ':' . ($this->height / $gcd);
  }

  /**
   * Calculates the resolution tier name.
   */
  private function calculateResolutionTier(): string {
    if ($this->height >= 4320 || $this->width >= 7680) {
      return '8K';
    }
    if ($this->height >= 2160 || $this->width >= 3840) {
      return '4K';
    }
    if ($this->height >= 1080 || $this->width >= 1920) {
      return 'Full HD';
    }
    if ($this->height >= 720 || $this->width >= 1280) {
      return 'HD';
    }
    return 'SD';
  }

  /**
   * Gets the effective dimensions accounting for rotation.
   *
   * @return array{width: int, height: int}
   *   Array with width and height keys.
   */
  #[\NoDiscard]
  public function getEffectiveDimensions(): array {
    if ($this->rotation === 90 || $this->rotation === 270 || $this->rotation === -90) {
      return ['width' => $this->height, 'height' => $this->width];
    }
    return ['width' => $this->width, 'height' => $this->height];
  }

  /**
   * Calculates the greatest common divisor.
   *
   * @param int $a
   *   First number.
   * @param int $b
   *   Second number.
   *
   * @return int
   *   Greatest common divisor.
   */
  private function gcd(int $a, int $b): int {
    while ($b !== 0) {
      $temp = $b;
      $b = $a % $b;
      $a = $temp;
    }
    return $a;
  }

}
