<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\ffmpeg_media\Enum\HardwareType;
use Drupal\ffmpeg_media\ValueObject\ConversionOptions;
use Drupal\ffmpeg_media\ValueObject\HwAccelOptions;

/**
 * Service for building complete hardware-accelerated encoding pipelines.
 *
 * Handles the complexity of hardware encoding pipelines including:
 * - Device initialization (-init_hw_device)
 * - Hardware decoding (-hwaccel, -hwaccel_output_format)
 * - Filter chain construction (hwupload, scale_vaapi, etc.)
 * - Encoder selection and configuration.
 *
 * @internal
 */
final class HardwarePipeline implements HardwarePipelineInterface {

  /**
   * Constructs a HardwarePipeline.
   *
   * @param \Drupal\ffmpeg_media\Service\HardwareDetectorInterface $hardwareDetector
   *   Hardware detector service.
   * @param \Drupal\ffmpeg_media\Service\FFmpegExecutorInterface $ffmpegExecutor
   *   FFmpeg executor service.
   */
  public function __construct(
    protected readonly HardwareDetectorInterface $hardwareDetector,
    protected readonly FFmpegExecutorInterface $ffmpegExecutor,
  ) {}

  /**
   * {@inheritdoc}
   *
   * @return array{init_args: array<string>, decode_args: array<string>, filter_complex: string|null, encode_args: array<string>, encoder: string|null}
   *   Pipeline components for hardware acceleration.
   */
  #[\Override]
  #[\NoDiscard]
  public function buildPipeline(
    string $codec,
    ConversionOptions $options,
    ?HwAccelOptions $hwAccel = NULL,
  ): array {
    // If no hardware specified, return empty (use software).
    if ($hwAccel === NULL || !$hwAccel->isEnabled()) {
      return [
        'init_args' => [],
        'decode_args' => [],
        'filter_complex' => NULL,
        'encode_args' => [],
        'encoder' => NULL,
      ];
    }

    // Build complete pipeline.
    return [
      'init_args' => $hwAccel->buildInitDeviceArgs(),
      'decode_args' => $hwAccel->buildDecodeArgs(),
      'filter_complex' => $this->buildFilterComplex($hwAccel, $options),
      'encode_args' => $this->buildEncodeArgs($codec, $hwAccel, $options),
      'encoder' => $hwAccel->getHwEncoder($codec),
    ];
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function buildFilterComplex(
    HwAccelOptions $hwAccel,
    ConversionOptions $options,
  ): ?string {
    if (!$hwAccel->isEnabled()) {
      return NULL;
    }

    $filters = [];

    // If not using hardware decode, need to upload frames first.
    if (!$hwAccel->hwDecode) {
      $uploadFilter = $hwAccel->buildHwuploadFilter(includeFormat: TRUE);
      if ($uploadFilter !== NULL) {
        $filters[] = $uploadFilter;
      }
    }

    // Add scaling filter if resizing.
    $scaleFilter = $this->buildHwScaleFilter($hwAccel, $options);
    if ($scaleFilter !== NULL) {
      $filters[] = $scaleFilter;
    }

    // Deinterlace filter could be added here if ConversionOptions
    // supports a deinterlace property in the future.
    // Add format conversion filter for encoder compatibility.
    // NVENC/CUDA encoders need pixel format set via the GPU filter chain
    // (scale_cuda=format=...) because -pix_fmt on the encoder triggers an
    // impossible CPU format conversion when frames are in cuda memory.
    // VAAPI handles this via scale_vaapi's format=nv12 parameter.
    // When scaling is active, the format is embedded in the scale filter.
    // When no scaling is active, add a format-only conversion filter.
    if ($scaleFilter === NULL) {
      $formatFilter = $this->buildHwFormatFilter($hwAccel, $options);
      if ($formatFilter !== NULL) {
        $filters[] = $formatFilter;
      }
    }

    // If copying back to system memory.
    if ($hwAccel->copyBack) {
      $downloadFilter = $hwAccel->buildHwdownloadFilter($options->pixelFormat ?? 'yuv420p');
      if ($downloadFilter !== NULL) {
        $filters[] = $downloadFilter;
      }
    }

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

  /**
   * {@inheritdoc}
   *
   * @return array<string>
   *   Encoder arguments.
   */
  #[\Override]
  #[\NoDiscard]
  public function buildEncodeArgs(
    string $codec,
    HwAccelOptions $hwAccel,
    ConversionOptions $options,
  ): array {
    if (!$hwAccel->hwEncode) {
      return [];
    }

    $args = [];
    $encoder = $hwAccel->getHwEncoder($codec);

    if ($encoder === NULL) {
      return [];
    }

    // Add encoder.
    $args[] = '-c:v';
    $args[] = $encoder;

    // Add hardware-specific encoder options.
    $args = array_merge($args, $this->getHwEncoderOptions($hwAccel, $codec, $options));

    return $args;
  }

  /**
   * {@inheritdoc}
   *
   * @return array<string>
   *   Full FFmpeg argument list.
   */
  #[\Override]
  #[\NoDiscard]
  public function getFullCommandArgs(
    string $inputPath,
    string $outputPath,
    string $codec,
    ConversionOptions $options,
    ?HwAccelOptions $hwAccel = NULL,
  ): array {
    $pipeline = $this->buildPipeline($codec, $options, $hwAccel);

    $args = [];

    // Device initialization first.
    $args = array_merge($args, $pipeline['init_args']);

    // Hardware decoding.
    $args = array_merge($args, $pipeline['decode_args']);

    // Input file.
    $args[] = '-i';
    $args[] = $inputPath;

    // Filter complex if needed.
    if ($pipeline['filter_complex'] !== NULL) {
      $args[] = '-vf';
      $args[] = $pipeline['filter_complex'];
    }

    // Encoding arguments.
    $args = array_merge($args, $pipeline['encode_args']);

    // Output.
    $args[] = '-y';
    $args[] = $outputPath;

    return $args;
  }

  /**
   * Builds hardware-accelerated scaling filter.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\HwAccelOptions $hwAccel
   *   Hardware acceleration options.
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options.
   *
   * @return string|null
   *   Scale filter string or NULL.
   */
  protected function buildHwScaleFilter(HwAccelOptions $hwAccel, ConversionOptions $options): ?string {
    $width = $options->maxWidth;
    $height = $options->maxHeight;

    if ($width === NULL && $height === NULL) {
      return NULL;
    }

    // Build scale expression.
    $scaleWidth = $width ?? -2;
    $scaleHeight = $height ?? -2;

    return match ($hwAccel->hardwareType) {
      HardwareType::Vaapi => sprintf(
        'scale_vaapi=w=%s:h=%s:format=nv12',
        $scaleWidth === -2 ? '-2' : "min($scaleWidth\\,iw)",
        $scaleHeight === -2 ? '-2' : "min($scaleHeight\\,ih)",
      ),
      HardwareType::Vulkan => sprintf(
        'scale_vulkan=w=%s:h=%s',
        $scaleWidth === -2 ? '-2' : "min($scaleWidth\\,iw)",
        $scaleHeight === -2 ? '-2' : "min($scaleHeight\\,ih)",
      ),
      HardwareType::Nvenc, HardwareType::Cuda => sprintf(
        'scale_cuda=%s:%s:interp_algo=lanczos:format=%s',
        $scaleWidth === -2 ? '-2' : "min($scaleWidth\\,iw)",
        $scaleHeight === -2 ? '-2' : "min($scaleHeight\\,ih)",
        self::cudaPixelFormat($options->pixelFormat !== '' ? $options->pixelFormat : 'yuv420p'),
      ),
      HardwareType::Qsv => sprintf(
        'scale_qsv=w=%s:h=%s',
        $scaleWidth === -2 ? '-2' : "min($scaleWidth\\,iw)",
        $scaleHeight === -2 ? '-2' : "min($scaleHeight\\,ih)",
      ),
      // Use software scale.
      HardwareType::VideoToolbox => NULL,
      HardwareType::Amf => sprintf(
        'scale=w=%s:h=%s',
        $scaleWidth === -2 ? '-2' : "min($scaleWidth\\,iw)",
        $scaleHeight === -2 ? '-2' : "min($scaleHeight\\,ih)",
      ),
      default => NULL,
    };
  }

  /**
   * Builds hardware-accelerated deinterlace filter.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\HwAccelOptions $hwAccel
   *   Hardware acceleration options.
   *
   * @return string|null
   *   Deinterlace filter string or NULL.
   */
  protected function buildHwDeinterlaceFilter(HwAccelOptions $hwAccel): ?string {
    return match ($hwAccel->hardwareType) {
      HardwareType::Vaapi => 'deinterlace_vaapi',
      HardwareType::Nvenc, HardwareType::Cuda => 'yadif_cuda',
      HardwareType::Qsv => 'deinterlace_qsv',
      default => NULL,
    };
  }

  /**
   * Builds format filter for hardware surface.
   *
   * Handles two cases:
   * 1. No hardware decode: frames are in CPU memory and need hwupload.
   * 2. Hardware decode active (NVENC/CUDA): frames are in GPU memory but
   *    may be in an unsupported pixel format (e.g. yuv444p). Use
   *    scale_cuda=format=... for GPU-side format conversion.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\HwAccelOptions $hwAccel
   *   Hardware acceleration options.
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options containing the target pixel format.
   *
   * @return string|null
   *   Format filter string or NULL.
   */
  protected function buildHwFormatFilter(HwAccelOptions $hwAccel, ConversionOptions $options): ?string {
    if ($hwAccel->hwPixelFormat === NULL) {
      return NULL;
    }

    // For hardware without decode, need hwupload from CPU memory.
    if (!$hwAccel->hwDecode) {
      return $hwAccel->buildHwuploadFilter(includeFormat: TRUE);
    }

    // For NVENC/CUDA with hardware decode: frames are in cuda memory and
    // may have an incompatible pixel format (e.g. source is yuv444p but
    // av1_nvenc only supports yuv420p/nv12). Use scale_cuda for GPU-side
    // format conversion without rescaling.
    if ($hwAccel->hardwareType === HardwareType::Nvenc || $hwAccel->hardwareType === HardwareType::Cuda) {
      $targetFormat = $options->pixelFormat !== '' ? $options->pixelFormat : 'yuv420p';
      return sprintf('scale_cuda=format=%s', self::cudaPixelFormat($targetFormat));
    }

    return NULL;
  }

  /**
   * Gets hardware encoder-specific options.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\HwAccelOptions $hwAccel
   *   Hardware acceleration options.
   * @param string $codec
   *   Target codec.
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options.
   *
   * @return array<string>
   *   Encoder-specific FFmpeg arguments.
   */
  protected function getHwEncoderOptions(
    HwAccelOptions $hwAccel,
    string $codec,
    ConversionOptions $options,
  ): array {
    $args = [];

    // Rate control based on hardware type.
    $rateControl = $options->rateControl;

    switch ($hwAccel->hardwareType) {
      case HardwareType::Vaapi:
        // VAAPI uses different rate control.
        if ($rateControl !== NULL) {
          $targetBitrate = $rateControl->targetBitrate;
          if ($targetBitrate !== NULL) {
            $args[] = '-b:v';
            $args[] = $targetBitrate . 'k';
          }
          $maxBitrate = $rateControl->maxBitrate;
          if ($maxBitrate !== NULL) {
            $args[] = '-maxrate';
            $args[] = $maxBitrate . 'k';
            $args[] = '-bufsize';
            $args[] = ($rateControl->bufferSize ?? $maxBitrate * 2) . 'k';
          }
        }
        else {
          // Use global_quality for CRF-like behavior.
          $args[] = '-global_quality';
          // VAAPI quality differs.
          $args[] = (string) ($options->crf + 10);
        }
        $args[] = '-compression_level';
        // Speed vs quality tradeoff.
        $args[] = '1';
        break;

      case HardwareType::Vulkan:
        // Vulkan encoder options.
        if ($rateControl !== NULL && $rateControl->targetBitrate !== NULL) {
          $args[] = '-b:v';
          $args[] = $rateControl->targetBitrate . 'k';
        }
        break;

      case HardwareType::Nvenc:
      case HardwareType::Cuda:
        // NVENC has good CRF-like mode with -cq.
        $args[] = '-preset';
        // Balanced preset.
        $args[] = 'p4';
        $args[] = '-tune';
        $args[] = 'hq';
        if ($rateControl !== NULL && $rateControl->targetBitrate !== NULL) {
          $args[] = '-b:v';
          $args[] = $rateControl->targetBitrate . 'k';
          $args[] = '-maxrate';
          $args[] = ($rateControl->maxBitrate ?? (int) ($rateControl->targetBitrate * 1.5)) . 'k';
          $args[] = '-bufsize';
          $args[] = ($rateControl->bufferSize ?? $rateControl->targetBitrate * 2) . 'k';
        }
        else {
          $args[] = '-cq';
          $args[] = (string) $options->crf;
        }
        break;

      case HardwareType::Qsv:
        // QSV rate control.
        if ($rateControl !== NULL && $rateControl->targetBitrate !== NULL) {
          $args[] = '-b:v';
          $args[] = $rateControl->targetBitrate . 'k';
        }
        else {
          $args[] = '-global_quality';
          $args[] = (string) $options->crf;
        }
        $args[] = '-preset';
        $args[] = 'medium';
        break;

      case HardwareType::VideoToolbox:
        // VideoToolbox options.
        if ($rateControl !== NULL && $rateControl->targetBitrate !== NULL) {
          $args[] = '-b:v';
          $args[] = $rateControl->targetBitrate . 'k';
        }
        else {
          $args[] = '-q:v';
          // Invert for VT quality.
          $args[] = (string) (65 - $options->crf);
        }
        $args[] = '-realtime';
        $args[] = '0';
        break;

      case HardwareType::Amf:
        // AMF encoder options.
        $args[] = '-quality';
        $args[] = 'balanced';
        if ($rateControl !== NULL && $rateControl->targetBitrate !== NULL) {
          $args[] = '-b:v';
          $args[] = $rateControl->targetBitrate . 'k';
        }
        else {
          $args[] = '-qp_i';
          $args[] = (string) $options->crf;
          $args[] = '-qp_p';
          $args[] = (string) ($options->crf + 2);
        }
        break;

      default:
        break;
    }

    // GOP settings if provided.
    if ($options->gopSettings !== NULL) {
      $args = array_merge($args, $this->buildHwGopArgs($hwAccel, $options));
    }

    return $args;
  }

  /**
   * Builds GOP arguments for hardware encoders.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\HwAccelOptions $hwAccel
   *   Hardware acceleration options.
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $options
   *   Conversion options.
   *
   * @return array<string>
   *   GOP-related FFmpeg arguments.
   */
  protected function buildHwGopArgs(HwAccelOptions $hwAccel, ConversionOptions $options): array {
    $args = [];
    $gop = $options->gopSettings;

    if ($gop === NULL) {
      return [];
    }

    // Most hardware encoders use -g for keyframe interval.
    $keyframeInterval = $gop->keyframeInterval;
    if ($keyframeInterval > 0) {
      $args[] = '-g';
      $args[] = (string) $keyframeInterval;
    }

    // B-frames (where supported).
    if ($gop->bFrames > 0) {
      switch ($hwAccel->hardwareType) {
        case HardwareType::Nvenc:
        case HardwareType::Cuda:
        case HardwareType::Qsv:
          $args[] = '-bf';
          $args[] = (string) $gop->bFrames;
          break;

        default:
          break;
      }
    }

    // Force keyframes for ABR streaming.
    if ($gop->forceKeyframes) {
      $args[] = '-force_key_frames';
      $args[] = sprintf('expr:gte(t,n_forced*%d)', $keyframeInterval > 0 ? (int) ($keyframeInterval / 30) : 2);
    }

    return $args;
  }

  /**
   * Maps software pixel format names to CUDA-compatible equivalents.
   *
   * The scale_cuda filter uses hardware-native format names which differ
   * from standard FFmpeg software pixel format names. For example, 10-bit
   * YUV 4:2:0 is "yuv420p10le" in software but "p010le" in CUDA.
   *
   * @param string $format
   *   Software pixel format name (e.g. "yuv420p", "yuv420p10le").
   *
   * @return string
   *   CUDA-compatible format name.
   */
  protected static function cudaPixelFormat(string $format): string {
    return match ($format) {
      'yuv420p10le' => 'p010le',
      'yuv444p16le' => 'yuv444p16le',
      default => $format,
    };
  }

}
