"""
europa.core.convert
~~~~~~~~~~~~~~~~~~~~
Core audio conversion utilities: mono/stereo, resample, normalize, trim, pad.
"""

import numpy as np
from typing import Optional
from .audio import Audio


def to_mono(audio: Audio) -> Audio:
    """
    Convert stereo (or multi-channel) audio to mono by averaging channels.

    Args:
        audio: Input Audio object.

    Returns:
        Audio: Mono audio.
    """
    if audio.is_mono:
        return audio.copy()
    data = np.mean(audio.data, axis=0)
    return Audio(data=data, sr=audio.sr, path=audio.path, metadata=audio.metadata.copy())


def to_stereo(audio: Audio) -> Audio:
    """
    Convert mono audio to stereo by duplicating the channel.

    Args:
        audio: Input Audio object.

    Returns:
        Audio: Stereo audio with shape (2, samples).
    """
    if audio.is_stereo:
        return audio.copy()
    data = np.stack([audio.data, audio.data], axis=0)
    return Audio(data=data, sr=audio.sr, path=audio.path, metadata=audio.metadata.copy())


def resample(audio: Audio, target_sr: int) -> Audio:
    """
    Resample audio to a new sample rate.

    Args:
        audio     : Input Audio object.
        target_sr : Desired sample rate in Hz.

    Returns:
        Audio: Resampled audio.
    """
    if audio.sr == target_sr:
        return audio.copy()
    new_data = _resample_array(audio.data, audio.sr, target_sr)
    return Audio(data=new_data, sr=target_sr, path=audio.path, metadata=audio.metadata.copy())


def normalize(audio: Audio, target_db: float = -3.0) -> Audio:
    """
    Normalize audio to a target peak dB level.

    Args:
        audio     : Input Audio object.
        target_db : Target peak level in dBFS. Default -3.0 dBFS.

    Returns:
        Audio: Normalized audio.
    """
    peak = np.max(np.abs(audio.data))
    if peak == 0:
        return audio.copy()
    target_amplitude = 10 ** (target_db / 20.0)
    data = audio.data * (target_amplitude / peak)
    return Audio(data=data.astype(np.float32), sr=audio.sr, path=audio.path, metadata=audio.metadata.copy())


def trim_silence(
    audio: Audio,
    top_db: float = 60.0,
    frame_length: int = 2048,
    hop_length: int = 512,
) -> Audio:
    """
    Trim leading and trailing silence from audio.

    Args:
        audio        : Input Audio object.
        top_db       : Threshold in dB below reference to consider silence.
        frame_length : Number of samples per analysis frame.
        hop_length   : Number of samples between frames.

    Returns:
        Audio: Trimmed audio.
    """
    data = audio.data if audio.is_mono else np.mean(audio.data, axis=0)
    frames = _frame(data, frame_length, hop_length)
    rms = np.sqrt(np.mean(frames ** 2, axis=0))
    rms_db = 20 * np.log10(np.maximum(rms, 1e-10))
    ref_db = np.max(rms_db)
    non_silent = rms_db > (ref_db - top_db)

    if not np.any(non_silent):
        return audio.copy()

    start_frame = np.argmax(non_silent)
    end_frame   = len(non_silent) - np.argmax(non_silent[::-1]) - 1

    start_sample = start_frame * hop_length
    end_sample   = min((end_frame + 1) * hop_length + frame_length, audio.samples)

    new_data = audio.data[..., start_sample:end_sample]
    return Audio(data=new_data, sr=audio.sr, path=audio.path, metadata=audio.metadata.copy())


def pad(
    audio: Audio,
    target_samples: Optional[int] = None,
    target_duration: Optional[float] = None,
    mode: str = "constant",
    pad_end: bool = True,
) -> Audio:
    """
    Pad audio to a target length.

    Args:
        audio           : Input Audio object.
        target_samples  : Target length in samples.
        target_duration : Target duration in seconds (alternative to target_samples).
        mode            : NumPy pad mode ('constant', 'wrap', 'reflect', etc.).
        pad_end         : If True, pad at the end. If False, pad at the beginning.

    Returns:
        Audio: Padded audio.
    """
    if target_duration is not None:
        target_samples = int(target_duration * audio.sr)
    if target_samples is None:
        raise ValueError("Provide target_samples or target_duration.")

    n = audio.samples
    if n >= target_samples:
        return audio.copy()

    pad_width = target_samples - n
    if audio.is_mono:
        pw = (0, pad_width) if pad_end else (pad_width, 0)
        new_data = np.pad(audio.data, pw, mode=mode)
    else:
        pw = ((0, 0), (0, pad_width)) if pad_end else ((0, 0), (pad_width, 0))
        new_data = np.pad(audio.data, pw, mode=mode)

    return Audio(data=new_data.astype(np.float32), sr=audio.sr, path=audio.path, metadata=audio.metadata.copy())


# ── Internal helpers ──────────────────────────────────────────────────────────

def _frame(x: np.ndarray, frame_length: int, hop_length: int) -> np.ndarray:
    """Frame a 1D signal into overlapping windows. Returns (frame_length, n_frames)."""
    n_frames = 1 + (len(x) - frame_length) // hop_length
    shape = (frame_length, n_frames)
    strides = (x.strides[0], x.strides[0] * hop_length)
    return np.lib.stride_tricks.as_strided(x, shape=shape, strides=strides)


def _resample_array(data: np.ndarray, orig_sr: int, target_sr: int) -> np.ndarray:
    """Resample a numpy array using scipy."""
    try:
        from scipy.signal import resample_poly
        from math import gcd
        g = gcd(orig_sr, target_sr)
        up, down = target_sr // g, orig_sr // g
        if data.ndim == 1:
            return resample_poly(data, up, down).astype(np.float32)
        return np.stack([resample_poly(ch, up, down) for ch in data]).astype(np.float32)
    except ImportError:
        # Naive linear interpolation fallback
        target_len = int(len(data) * target_sr / orig_sr) if data.ndim == 1 else int(data.shape[1] * target_sr / orig_sr)
        if data.ndim == 1:
            return np.interp(
                np.linspace(0, len(data) - 1, target_len),
                np.arange(len(data)),
                data,
            ).astype(np.float32)
        return np.stack([
            np.interp(np.linspace(0, data.shape[1] - 1, target_len), np.arange(data.shape[1]), ch)
            for ch in data
        ]).astype(np.float32)
