"""Small real-time audio transforms used by the seek-bar visualizer."""

from __future__ import annotations

import numpy as np
from PySide6.QtMultimedia import QAudioBuffer, QAudioFormat


VISUALIZER_MODES = ("off", "spectrum", "waveform")


def normalize_visualizer_mode(value: object) -> str:
    mode = str(value or "waveform").strip().casefold()
    return mode if mode in VISUALIZER_MODES else "waveform"


def audio_buffer_to_mono(buffer: QAudioBuffer) -> tuple[np.ndarray, int]:
    """Copy a Qt audio buffer into normalized mono floating-point samples."""

    if buffer is None or not buffer.isValid() or not buffer.byteCount():
        return np.empty(0, dtype=np.float32), 0
    audio_format = buffer.format()
    channels = max(1, int(audio_format.channelCount()))
    sample_rate = max(0, int(audio_format.sampleRate()))
    sample_format = audio_format.sampleFormat()
    dtype_and_scale = {
        QAudioFormat.SampleFormat.UInt8: (np.dtype("u1"), 128.0),
        QAudioFormat.SampleFormat.Int16: (np.dtype("<i2"), 32768.0),
        QAudioFormat.SampleFormat.Int32: (np.dtype("<i4"), 2147483648.0),
        QAudioFormat.SampleFormat.Float: (np.dtype("<f4"), 1.0),
    }.get(sample_format)
    if dtype_and_scale is None:
        return np.empty(0, dtype=np.float32), sample_rate
    dtype, scale = dtype_and_scale
    raw = np.frombuffer(buffer.constData(), dtype=dtype)
    usable = raw.size - raw.size % channels
    if usable <= 0:
        return np.empty(0, dtype=np.float32), sample_rate
    frames = raw[:usable].reshape(-1, channels).astype(np.float32)
    if sample_format == QAudioFormat.SampleFormat.UInt8:
        frames = (frames - 128.0) / scale
    elif scale != 1.0:
        frames /= scale
    return frames.mean(axis=1, dtype=np.float32).copy(), sample_rate


def native_samples_to_mono(samples: np.ndarray) -> np.ndarray:
    """Normalize the native PCM arrays produced by the exclusive player."""

    values = np.asarray(samples)
    if not values.size:
        return np.empty(0, dtype=np.float32)
    if values.ndim == 1:
        values = values.reshape(-1, 1)
    if np.issubdtype(values.dtype, np.integer):
        scale = float(max(abs(np.iinfo(values.dtype).min), np.iinfo(values.dtype).max))
        normalized = values.astype(np.float32) / scale
    else:
        normalized = values.astype(np.float32)
    return normalized.mean(axis=1, dtype=np.float32).copy()


def visualizer_values(
    samples: np.ndarray,
    mode: object,
    sample_rate: int,
    point_count: int = 180,
) -> list[float]:
    """Build a time waveform or a low-to-high logarithmic spectrum."""

    normalized_mode = normalize_visualizer_mode(mode)
    points = max(16, int(point_count))
    mono = np.asarray(samples, dtype=np.float32).reshape(-1)
    if normalized_mode == "off" or mono.size < 8:
        return []
    mono = np.nan_to_num(mono, copy=False)
    mono = mono - float(np.mean(mono))
    if not np.any(np.abs(mono) > 1e-7):
        return [0.0] * points

    if normalized_mode == "waveform":
        source_positions = np.linspace(0, mono.size - 1, num=mono.size)
        target_positions = np.linspace(0, mono.size - 1, num=points)
        values = np.interp(target_positions, source_positions, mono)
        reference = max(0.08, float(np.percentile(np.abs(values), 95)))
        gain = min(4.0, 0.88 / reference)
        return np.clip(values * gain, -1.0, 1.0).astype(float).tolist()

    rate = max(1, int(sample_rate))
    window = np.hanning(mono.size).astype(np.float32)
    magnitudes = np.abs(np.fft.rfft(mono * window))
    if magnitudes.size < 2 or float(np.max(magnitudes)) <= 1e-12:
        return [0.0] * points
    frequencies = np.fft.rfftfreq(mono.size, d=1.0 / rate)
    maximum_frequency = max(40.0, rate / 2.0)
    targets = np.geomspace(20.0, maximum_frequency, num=points)
    sampled = np.interp(targets, frequencies, magnitudes)
    decibels = 20.0 * np.log10(sampled + 1e-12)
    peak = float(np.max(decibels))
    values = np.clip((decibels - (peak - 60.0)) / 60.0, 0.0, 1.0)
    if values.size >= 3:
        values = np.convolve(values, np.array([0.2, 0.6, 0.2]), mode="same")
    return values.astype(float).tolist()
