<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\ValueObject;

/**
 * Immutable value object for audio encoding profile settings.
 *
 * Configures consistent audio processing including sample rate, channels,
 * codec, bitrate, and optional loudness normalization (EBU R128).
 */
final readonly class AudioProfile {

  /**
   * Normalization mode constants.
   */
  public const string MODE_LOUDNORM = 'loudnorm';
  public const string MODE_DYNAUDNORM = 'dynaudnorm';
  public const string MODE_EBU_R128 = 'ebur128';
  public const string MODE_NONE = 'none';

  /**
   * Common sample rates.
   */
  public const int SAMPLE_RATE_44100 = 44100;
  public const int SAMPLE_RATE_48000 = 48000;
  public const int SAMPLE_RATE_96000 = 96000;

  /**
   * Constructs an AudioProfile object.
   *
   * @param int $sampleRate
   *   Audio sample rate in Hz. 48000 is broadcast standard.
   * @param int $channels
   *   Number of audio channels. 2 = stereo, 6 = 5.1 surround.
   * @param string $codec
   *   Audio codec (aac, libopus, libvorbis, flac, etc.).
   * @param string $bitrate
   *   Audio bitrate (e.g., '128k', '192k', '256k').
   * @param string|null $profile
   *   Codec profile. For AAC: 'aac_low' (LC), 'aac_he', 'aac_he_v2'.
   * @param bool $normalizeEnabled
   *   Enable loudness normalization.
   * @param string $normalizeMode
   *   Normalization mode: 'loudnorm', 'dynaudnorm', 'ebur128', 'none'.
   * @param float $targetLufs
   *   Target integrated loudness in LUFS. -14 = streaming, -24 = broadcast.
   * @param float $truePeak
   *   Maximum true peak level in dBTP. -1.0 is safe for lossy codecs.
   * @param float $loudnessRange
   *   Target loudness range (LRA) in LU. 7-11 is typical.
   * @param bool $dualMono
   *   Enable dual mono mode for speech content.
   *
   * @throws \InvalidArgumentException
   *   If any parameter is out of valid range.
   */
  public function __construct(
    public int $sampleRate = self::SAMPLE_RATE_48000,
    public int $channels = 2,
    public string $codec = 'libopus',
    public string $bitrate = '128k',
    public ?string $profile = 'aac_low',
    public bool $normalizeEnabled = FALSE,
    public string $normalizeMode = self::MODE_LOUDNORM,
    public float $targetLufs = -14.0,
    public float $truePeak = -1.0,
    public float $loudnessRange = 11.0,
    public bool $dualMono = FALSE,
  ) {
    if ($sampleRate < 8000 || $sampleRate > 192000) {
      throw new \InvalidArgumentException('Sample rate must be between 8000 and 192000 Hz.');
    }
    if ($channels < 1 || $channels > 8) {
      throw new \InvalidArgumentException('Channels must be between 1 and 8.');
    }
    if ($targetLufs > 0 || $targetLufs < -70) {
      throw new \InvalidArgumentException('Target LUFS must be between -70 and 0.');
    }
    if ($truePeak > 0 || $truePeak < -10) {
      throw new \InvalidArgumentException('True peak must be between -10 and 0 dBTP.');
    }
    if ($loudnessRange < 1 || $loudnessRange > 20) {
      throw new \InvalidArgumentException('Loudness range must be between 1 and 20 LU.');
    }

    $validModes = [self::MODE_LOUDNORM, self::MODE_DYNAUDNORM, self::MODE_EBU_R128, self::MODE_NONE];
    if (!in_array($normalizeMode, $validModes, TRUE)) {
      throw new \InvalidArgumentException(
        sprintf('Invalid normalize mode: %s. Valid modes: %s', $normalizeMode, implode(', ', $validModes))
      );
    }
  }

  /**
   * Creates AudioProfile from an array.
   *
   * @param array<string, mixed> $data
   *   Associative array of settings.
   *
   * @return self
   *   New AudioProfile instance.
   */
  #[\NoDiscard]
  public static function fromArray(array $data): self {
    return new self(
      sampleRate: (int) ($data['sample_rate'] ?? self::SAMPLE_RATE_48000),
      channels: (int) ($data['channels'] ?? 2),
      codec: (string) ($data['codec'] ?? 'libopus'),
      bitrate: (string) ($data['bitrate'] ?? '128k'),
      profile: isset($data['profile']) ? (string) $data['profile'] : 'aac_low',
      normalizeEnabled: (bool) ($data['normalize_enabled'] ?? FALSE),
      normalizeMode: (string) ($data['normalize_mode'] ?? self::MODE_LOUDNORM),
      targetLufs: (float) ($data['target_lufs'] ?? -14.0),
      truePeak: (float) ($data['true_peak'] ?? -1.0),
      loudnessRange: (float) ($data['loudness_range'] ?? 11.0),
      dualMono: (bool) ($data['dual_mono'] ?? FALSE),
    );
  }

  /**
   * Creates profile optimized for streaming (Spotify/YouTube/Netflix).
   *
   * @return self
   *   Profile with -14 LUFS target.
   */
  #[\NoDiscard]
  public static function forStreaming(): self {
    return new self(
      sampleRate: self::SAMPLE_RATE_48000,
      channels: 2,
      codec: 'libopus',
      bitrate: '128k',
      profile: 'aac_low',
      normalizeEnabled: TRUE,
      normalizeMode: self::MODE_LOUDNORM,
      targetLufs: -14.0,
      truePeak: -1.0,
      loudnessRange: 11.0,
    );
  }

  /**
   * Creates profile optimized for broadcast (EBU R128).
   *
   * @return self
   *   Profile with -23 LUFS target (EBU R128 standard).
   */
  #[\NoDiscard]
  public static function forBroadcast(): self {
    return new self(
      sampleRate: self::SAMPLE_RATE_48000,
      channels: 2,
      codec: 'libopus',
      bitrate: '192k',
      profile: 'aac_low',
      normalizeEnabled: TRUE,
      normalizeMode: self::MODE_LOUDNORM,
      targetLufs: -23.0,
      truePeak: -1.0,
      loudnessRange: 7.0,
    );
  }

  /**
   * Creates profile optimized for podcasts/speech.
   *
   * @return self
   *   Profile with settings optimized for voice content.
   */
  #[\NoDiscard]
  public static function forPodcast(): self {
    return new self(
      sampleRate: self::SAMPLE_RATE_48000,
      channels: 1,
      codec: 'libopus',
      bitrate: '96k',
      profile: 'aac_low',
      normalizeEnabled: TRUE,
      normalizeMode: self::MODE_LOUDNORM,
      targetLufs: -16.0,
      truePeak: -1.0,
      loudnessRange: 8.0,
      dualMono: TRUE,
    );
  }

  /**
   * Creates profile for high-quality music.
   *
   * @return self
   *   Profile with higher bitrate and dynamic range.
   */
  #[\NoDiscard]
  public static function forMusic(): self {
    return new self(
      sampleRate: self::SAMPLE_RATE_48000,
      channels: 2,
      codec: 'libopus',
      bitrate: '256k',
      profile: 'aac_low',
      normalizeEnabled: TRUE,
      normalizeMode: self::MODE_LOUDNORM,
      targetLufs: -14.0,
      truePeak: -1.0,
      loudnessRange: 11.0,
    );
  }

  /**
   * Creates a new instance with normalization enabled.
   *
   * @param float $targetLufs
   *   Target integrated loudness in LUFS.
   *
   * @return self
   *   New instance with normalization enabled.
   */
  #[\NoDiscard]
  public function withNormalization(float $targetLufs = -14.0): self {
    return clone($this, [
      'normalizeEnabled' => TRUE,
      'targetLufs' => $targetLufs,
    ]);
  }

  /**
   * Creates a new instance with different codec.
   *
   * @param string $codec
   *   Audio codec name.
   * @param string $bitrate
   *   Audio bitrate.
   * @param string|null $profile
   *   Codec profile.
   *
   * @return self
   *   New instance with updated codec settings.
   */
  #[\NoDiscard]
  public function withCodec(string $codec, string $bitrate = '128k', ?string $profile = NULL): self {
    return clone($this, [
      'codec' => $codec,
      'bitrate' => $bitrate,
      'profile' => $profile,
    ]);
  }

  /**
   * Creates a new instance with different channel count.
   *
   * @param int $channels
   *   Number of channels.
   *
   * @return self
   *   New instance with updated channels.
   */
  #[\NoDiscard]
  public function withChannels(int $channels): self {
    return clone($this, ['channels' => $channels]);
  }

  /**
   * Builds the loudnorm filter string.
   *
   * @return string
   *   FFmpeg loudnorm filter string.
   */
  #[\NoDiscard]
  public function buildNormalizationFilter(): string {
    if (!$this->normalizeEnabled) {
      return '';
    }

    return match ($this->normalizeMode) {
      self::MODE_LOUDNORM => sprintf(
        'loudnorm=I=%.1f:TP=%.1f:LRA=%.1f%s',
        $this->targetLufs,
        $this->truePeak,
        $this->loudnessRange,
        $this->dualMono ? ':dual_mono=true' : '',
      ),
      self::MODE_DYNAUDNORM => 'dynaudnorm=f=150:g=15:p=0.95',
      self::MODE_EBU_R128 => 'ebur128',
      default => '',
    };
  }

  /**
   * Builds FFmpeg audio arguments.
   *
   * @return array<int, string>
   *   Array of FFmpeg arguments.
   */
  #[\NoDiscard]
  public function buildArguments(): array {
    $args = [
      '-ar', (string) $this->sampleRate,
      '-ac', (string) $this->channels,
      '-c:a', $this->codec,
      '-b:a', $this->bitrate,
    ];

    // Add codec profile for AAC.
    if ($this->profile !== NULL && $this->codec === 'aac') {
      $args[] = '-profile:a';
      $args[] = $this->profile;
    }

    // Add normalization filter.
    if ($this->normalizeEnabled) {
      $filter = $this->buildNormalizationFilter();
      if ($filter !== '') {
        $args[] = '-af';
        $args[] = $filter;
      }
    }

    return $args;
  }

  /**
   * Builds filter chain for audio processing.
   *
   * @param array<int, string> $additionalFilters
   *   Additional audio filters to include.
   *
   * @return string
   *   Combined audio filter string.
   */
  #[\NoDiscard]
  public function buildFilterChain(array $additionalFilters = []): string {
    $filters = [];

    // Add normalization filter first.
    if ($this->normalizeEnabled) {
      $normFilter = $this->buildNormalizationFilter();
      if ($normFilter !== '') {
        $filters[] = $normFilter;
      }
    }

    // Add channel downmix if needed.
    if ($this->channels === 2) {
      $filters[] = 'aformat=channel_layouts=stereo';
    }
    elseif ($this->channels === 1) {
      $filters[] = 'aformat=channel_layouts=mono';
    }

    // Add any additional filters.
    $filters = array_merge($filters, $additionalFilters);

    return $filters
      |> array_filter(...)
      |> (fn(array $f): string => implode(',', $f));
  }

  /**
   * Converts settings to array format.
   *
   * @return array<string, bool|float|int|string|null>
   *   Associative array of settings.
   */
  #[\NoDiscard]
  public function toArray(): array {
    return [
      'sample_rate' => $this->sampleRate,
      'channels' => $this->channels,
      'codec' => $this->codec,
      'bitrate' => $this->bitrate,
      'profile' => $this->profile,
      'normalize_enabled' => $this->normalizeEnabled,
      'normalize_mode' => $this->normalizeMode,
      'target_lufs' => $this->targetLufs,
      'true_peak' => $this->truePeak,
      'loudness_range' => $this->loudnessRange,
      'dual_mono' => $this->dualMono,
    ];
  }

}
