<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\VideoFilter;

/**
 * Collects and composes video filters from tagged providers.
 *
 * @internal
 */
final class VideoFilterCollector implements VideoFilterCollectorInterface {

  /**
   * Sorted providers cache.
   *
   * @var array<int, \Drupal\ffmpeg_media\VideoFilter\VideoFilterProviderInterface>|null
   */
  private ?array $sortedProviders = NULL;

  /**
   * Constructs a VideoFilterCollector.
   *
   * @param iterable<\Drupal\ffmpeg_media\VideoFilter\VideoFilterProviderInterface> $providers
   *   Tagged video filter providers.
   */
  public function __construct(
    private readonly iterable $providers,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function collectFilters(FilterContext $context): ?VideoFilterResult {
    $results = [];
    foreach ($this->getSortedProviders() as $provider) {
      $result = $provider->getFilter($context);
      if ($result !== NULL) {
        $results[] = $result;
      }
    }

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

    return $this->composeResults($results);
  }

  /**
   * Composes multiple filter results into a single chained result.
   *
   * Chains filter_complex fragments using intermediate labels so each
   * provider's output feeds into the next provider's input.
   *
   * @param array<int, \Drupal\ffmpeg_media\VideoFilter\VideoFilterResult> $results
   *   Non-empty array of filter results.
   *
   * @return \Drupal\ffmpeg_media\VideoFilter\VideoFilterResult
   *   Composed result.
   */
  private function composeResults(array $results): VideoFilterResult {
    if (count($results) === 1) {
      return $results[0];
    }

    // Chain multiple filters: each provider gets the previous provider's
    // output as input. We replace [video_in]/[video_out] labels with
    // unique intermediate labels.
    $allInputs = [];
    $allInputLabels = [];
    $fragments = [];

    foreach ($results as $index => $result) {
      $fragment = $result->filterComplex;
      $inputLabel = $index === 0 ? '[video_in]' : sprintf('[vf_%d]', $index - 1);
      $outputLabel = $index === count($results) - 1 ? '[video_out]' : sprintf('[vf_%d]', $index);

      foreach ($result->extraInputLabels as $labelIndex => $label) {
        $resolvedLabel = sprintf('[vf_extra_%d_%d]', $index, $labelIndex);
        $fragment = str_replace($label, $resolvedLabel, $fragment);
        $allInputs[] = $result->extraInputs[$labelIndex];
        $allInputLabels[] = $resolvedLabel;
      }

      $fragment = str_replace('[video_in]', $inputLabel, $fragment);
      $fragment = str_replace('[video_out]', $outputLabel, $fragment);

      $fragments[] = $fragment;
    }

    return new VideoFilterResult(
      filterComplex: implode(';', $fragments),
      extraInputs: $allInputs,
      extraInputLabels: $allInputLabels,
    );
  }

  /**
   * Returns providers sorted by weight.
   *
   * @return array<int, \Drupal\ffmpeg_media\VideoFilter\VideoFilterProviderInterface>
   *   Sorted providers.
   */
  private function getSortedProviders(): array {
    if ($this->sortedProviders === NULL) {
      $this->sortedProviders = [];
      foreach ($this->providers as $provider) {
        $this->sortedProviders[] = $provider;
      }
      usort(
        $this->sortedProviders,
        static fn(VideoFilterProviderInterface $a, VideoFilterProviderInterface $b): int => $a->getWeight() <=> $b->getWeight(),
      );
    }

    return $this->sortedProviders;
  }

}
