<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\ValueObject;

use Drupal\ffmpeg_media\Enum\HardwareType;

/**
 * Immutable value object for hardware acceleration options.
 *
 * Configures hardware-accelerated encoding/decoding pipeline including
 * device initialization, hwupload filters, and format conversion.
 */
final readonly class HwAccelOptions {

  /**
   * The hardware type as an enum.
   */
  public HardwareType $hardwareType;

  /**
   * Constructs HwAccelOptions.
   *
   * @param string $type
   *   Hardware acceleration type (vaapi, vulkan, nvenc, etc.).
   * @param string|null $device
   *   Device path (e.g., /dev/dri/renderD128 for VAAPI).
   * @param bool $hwDecode
   *   Whether to use hardware decoding.
   * @param bool $hwEncode
   *   Whether to use hardware encoding.
   * @param string|null $hwPixelFormat
   *   Hardware pixel format (e.g., vaapi, cuda, vulkan).
   * @param string|null $swPixelFormat
   *   Software pixel format for upload (e.g., nv12, p010le).
   * @param bool $copyBack
   *   Whether to copy frames back to system memory after processing.
   * @param string|null $deviceType
   *   FFmpeg device type for init_hw_device (e.g., vaapi, vulkan, cuda).
   * @param string|null $deviceName
   *   Logical device name for FFmpeg (e.g., vaapi0, vulkan0).
   * @param bool $deriveDevice
   *   Whether to derive device from decoder context.
   * @param array<string> $extraFilters
   *   Additional filters to apply in hardware pipeline.
   *
   * @throws \InvalidArgumentException
   *   If type is invalid.
   */
  public function __construct(
    public string $type = HardwareType::None->value,
    public ?string $device = NULL,
    public bool $hwDecode = TRUE,
    public bool $hwEncode = TRUE,
    public ?string $hwPixelFormat = NULL,
    public ?string $swPixelFormat = NULL,
    public bool $copyBack = FALSE,
    public ?string $deviceType = NULL,
    public ?string $deviceName = NULL,
    public bool $deriveDevice = FALSE,
    public array $extraFilters = [],
  ) {
    $hwType = HardwareType::tryFrom($type);
    if ($hwType === NULL) {
      throw new \InvalidArgumentException(sprintf(
        'Invalid hardware acceleration type "%s". Valid types: %s',
        $type,
        implode(', ', array_column(HardwareType::cases(), 'value')),
      ));
    }
    $this->hardwareType = $hwType;
  }

  /**
   * Creates VAAPI hardware acceleration options.
   *
   * @param string $device
   *   DRI render device path.
   * @param bool $hwDecode
   *   Whether to use hardware decoding.
   *
   * @return self
   *   Hardware acceleration options instance.
   */
  #[\NoDiscard]
  public static function forVaapi(
    string $device = '/dev/dri/renderD128',
    bool $hwDecode = TRUE,
  ): self {
    return new self(
      type: HardwareType::Vaapi->value,
      device: $device,
      hwDecode: $hwDecode,
      hwEncode: TRUE,
      hwPixelFormat: 'vaapi',
      swPixelFormat: 'nv12',
      deviceType: 'vaapi',
      deviceName: 'vaapi0',
    );
  }

  /**
   * Creates Vulkan hardware acceleration options.
   *
   * @param string|null $device
   *   Vulkan device specifier (NULL for auto-detect).
   * @param bool $hwDecode
   *   Whether to use hardware decoding.
   *
   * @return self
   *   Hardware acceleration options instance.
   */
  #[\NoDiscard]
  public static function forVulkan(
    ?string $device = NULL,
    bool $hwDecode = TRUE,
  ): self {
    return new self(
      type: HardwareType::Vulkan->value,
      device: $device,
      hwDecode: $hwDecode,
      hwEncode: TRUE,
      hwPixelFormat: 'vulkan',
      swPixelFormat: 'nv12',
      deviceType: 'vulkan',
      deviceName: 'vulkan0',
    );
  }

  /**
   * Creates NVIDIA NVENC hardware acceleration options.
   *
   * @param int $gpuIndex
   *   GPU index for multi-GPU systems.
   * @param bool $hwDecode
   *   Whether to use hardware decoding (CUVID/NVDEC).
   *
   * @return self
   *   Hardware acceleration options instance.
   */
  #[\NoDiscard]
  public static function forNvenc(
    int $gpuIndex = 0,
    bool $hwDecode = TRUE,
  ): self {
    return new self(
      type: HardwareType::Nvenc->value,
      device: (string) $gpuIndex,
      hwDecode: $hwDecode,
      hwEncode: TRUE,
      hwPixelFormat: 'cuda',
      swPixelFormat: 'nv12',
      deviceType: 'cuda',
      deviceName: 'cuda0',
    );
  }

  /**
   * Creates Intel Quick Sync Video options.
   *
   * @param string|null $device
   *   Device path or NULL for auto-detect.
   * @param bool $hwDecode
   *   Whether to use hardware decoding.
   *
   * @return self
   *   Hardware acceleration options instance.
   */
  #[\NoDiscard]
  public static function forQsv(
    ?string $device = NULL,
    bool $hwDecode = TRUE,
  ): self {
    return new self(
      type: HardwareType::Qsv->value,
      device: $device,
      hwDecode: $hwDecode,
      hwEncode: TRUE,
      hwPixelFormat: 'qsv',
      swPixelFormat: 'nv12',
      deviceType: 'qsv',
      deviceName: 'qsv0',
      deriveDevice: TRUE,
    );
  }

  /**
   * Creates macOS VideoToolbox options.
   *
   * @param bool $hwDecode
   *   Whether to use hardware decoding.
   *
   * @return self
   *   Hardware acceleration options instance.
   */
  #[\NoDiscard]
  public static function forVideoToolbox(bool $hwDecode = TRUE): self {
    return new self(
      type: HardwareType::VideoToolbox->value,
      hwDecode: $hwDecode,
      hwEncode: TRUE,
      hwPixelFormat: 'videotoolbox_vld',
      swPixelFormat: 'nv12',
      deviceType: 'videotoolbox',
      deviceName: 'vt0',
    );
  }

  /**
   * Creates AMD AMF options.
   *
   * @param bool $hwDecode
   *   Whether to use hardware decoding (D3D11VA).
   *
   * @return self
   *   Hardware acceleration options instance.
   */
  #[\NoDiscard]
  public static function forAmf(bool $hwDecode = TRUE): self {
    return new self(
      type: HardwareType::Amf->value,
      hwDecode: $hwDecode,
      hwEncode: TRUE,
      hwPixelFormat: 'd3d11',
      swPixelFormat: 'nv12',
      deviceType: 'd3d11va',
      deviceName: 'd3d11_0',
    );
  }

  /**
   * Checks if hardware acceleration is enabled.
   *
   * @return bool
   *   TRUE if any hardware acceleration is enabled.
   */
  #[\NoDiscard]
  public function isEnabled(): bool {
    return $this->hardwareType->isHardware() && ($this->hwDecode || $this->hwEncode);
  }

  /**
   * Builds init_hw_device arguments.
   *
   * @return array<string>
   *   FFmpeg arguments for device initialization.
   */
  #[\NoDiscard]
  public function buildInitDeviceArgs(): array {
    if (!$this->isEnabled() || $this->deviceType === NULL) {
      return [];
    }

    $args = [];

    // Build device initialization string.
    $initString = $this->deviceType;
    if ($this->deviceName !== NULL) {
      $initString .= '=' . $this->deviceName;
    }
    if ($this->device !== NULL) {
      $initString .= ':' . $this->device;
    }

    $args[] = '-init_hw_device';
    $args[] = $initString;

    // Add derived device if needed.
    if ($this->deriveDevice && $this->hardwareType === HardwareType::Qsv) {
      $args[] = '-init_hw_device';
      $args[] = 'qsv=qsv0@vaapi0';
    }

    return $args;
  }

  /**
   * Builds hardware decoding arguments.
   *
   * @return array<string>
   *   FFmpeg arguments for hardware decoding.
   */
  #[\NoDiscard]
  public function buildDecodeArgs(): array {
    if (!$this->hwDecode || !$this->hardwareType->isHardware()) {
      return [];
    }

    return match ($this->hardwareType) {
      HardwareType::Vaapi => $this->device !== NULL
        ? ['-hwaccel', 'vaapi', '-hwaccel_output_format', 'vaapi', '-hwaccel_device', $this->device]
        : ['-hwaccel', 'vaapi', '-hwaccel_output_format', 'vaapi'],
      HardwareType::Vulkan => ['-hwaccel', 'vulkan', '-hwaccel_output_format', 'vulkan'],
      HardwareType::Nvenc, HardwareType::Cuda => $this->device !== NULL
        ? ['-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda', '-hwaccel_device', $this->device]
        : ['-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda'],
      HardwareType::Qsv => ['-hwaccel', 'qsv', '-hwaccel_output_format', 'qsv'],
      HardwareType::VideoToolbox => ['-hwaccel', 'videotoolbox', '-hwaccel_output_format', 'videotoolbox_vld'],
      HardwareType::Amf => ['-hwaccel', 'd3d11va', '-hwaccel_output_format', 'd3d11'],
      default => [],
    };
  }

  /**
   * Builds the hwupload filter string.
   *
   * @param bool $includeFormat
   *   Whether to include format conversion filter.
   *
   * @return string|null
   *   Filter string or NULL if not needed.
   */
  #[\NoDiscard]
  public function buildHwuploadFilter(bool $includeFormat = TRUE): ?string {
    if (!$this->hwEncode || !$this->hardwareType->isHardware()) {
      return NULL;
    }

    $filters = [];

    // Format conversion if needed.
    if ($includeFormat && $this->swPixelFormat !== NULL) {
      $filters[] = sprintf('format=%s', $this->swPixelFormat);
    }

    // Hardware upload filter.
    match ($this->hardwareType) {
      HardwareType::Vaapi => $filters[] = $this->deviceName !== NULL
        ? 'hwupload=derive_device=vaapi'
        : 'hwupload',
      HardwareType::Vulkan => $filters[] = 'hwupload=derive_device=vulkan',
      HardwareType::Nvenc, HardwareType::Cuda => $filters[] = 'hwupload_cuda',
      HardwareType::Qsv => $filters[] = 'hwupload=extra_hw_frames=64',
      HardwareType::VideoToolbox => NULL,
      HardwareType::Amf => $filters[] = 'hwupload=derive_device=d3d11va',
      default => NULL,
    };

    // Add extra filters.
    foreach ($this->extraFilters as $filter) {
      $filters[] = $filter;
    }

    // Add format specification for hardware surface.
    if ($this->hwPixelFormat !== NULL && $this->hardwareType !== HardwareType::VideoToolbox) {
      $filters[] = sprintf('format=%s', $this->hwPixelFormat);
    }

    return !empty($filters) ? implode(',', $filters) : NULL;
  }

  /**
   * Builds hwdownload filter for copying back to system memory.
   *
   * @param string $outputFormat
   *   Output pixel format.
   *
   * @return string|null
   *   Filter string or NULL if not needed.
   */
  #[\NoDiscard]
  public function buildHwdownloadFilter(string $outputFormat = 'yuv420p'): ?string {
    if (!$this->copyBack || !$this->hardwareType->isHardware()) {
      return NULL;
    }

    return sprintf('hwdownload,format=%s', $outputFormat);
  }

  /**
   * Gets the hardware encoder name for a codec.
   *
   * Delegates to HardwareType enum — single source of truth for encoder maps.
   *
   * @param string $codec
   *   Base codec name (h264, hevc, av1, vp9).
   *
   * @return string|null
   *   Hardware encoder name or NULL.
   */
  #[\NoDiscard]
  public function getHwEncoder(string $codec): ?string {
    if (!$this->hwEncode || !$this->hardwareType->isHardware()) {
      return NULL;
    }

    return $this->hardwareType->encoderForCodec($codec);
  }

  /**
   * Creates from array.
   *
   * @param array<string, mixed> $data
   *   Configuration array.
   *
   * @return self
   *   Hardware acceleration options instance.
   */
  #[\NoDiscard]
  public static function fromArray(array $data): self {
    return new self(
      type: (string) ($data['type'] ?? HardwareType::None->value),
      device: isset($data['device']) ? (string) $data['device'] : NULL,
      hwDecode: (bool) ($data['hw_decode'] ?? TRUE),
      hwEncode: (bool) ($data['hw_encode'] ?? TRUE),
      hwPixelFormat: isset($data['hw_pixel_format']) ? (string) $data['hw_pixel_format'] : NULL,
      swPixelFormat: isset($data['sw_pixel_format']) ? (string) $data['sw_pixel_format'] : NULL,
      copyBack: (bool) ($data['copy_back'] ?? FALSE),
      deviceType: isset($data['device_type']) ? (string) $data['device_type'] : NULL,
      deviceName: isset($data['device_name']) ? (string) $data['device_name'] : NULL,
      deriveDevice: (bool) ($data['derive_device'] ?? FALSE),
      extraFilters: (array) ($data['extra_filters'] ?? []),
    );
  }

  /**
   * Converts to array.
   *
   * @return array<string, bool|string|array<int, string>|null>
   *   Configuration array.
   */
  #[\NoDiscard]
  public function toArray(): array {
    return [
      'type' => $this->type,
      'device' => $this->device,
      'hw_decode' => $this->hwDecode,
      'hw_encode' => $this->hwEncode,
      'hw_pixel_format' => $this->hwPixelFormat,
      'sw_pixel_format' => $this->swPixelFormat,
      'copy_back' => $this->copyBack,
      'device_type' => $this->deviceType,
      'device_name' => $this->deviceName,
      'derive_device' => $this->deriveDevice,
      'extra_filters' => $this->extraFilters,
    ];
  }

}
