#!/usr/bin/env python3
"""
Visualization script for spectral features.

This script allows testing and visualizing the spectral feature calculations
before integrating them into the training pipeline. It shows:
- Original waveform
- Loudness sequence (default 25Hz)
- Spectral centroid sequence (default 25Hz)
- Spectral complexity sequence (default 25Hz)

Usage:
    # Full audio visualization
    python visualize_spectral_features.py --audio_path /path/to/audio.wav

    # Test cropping (mimics training pipeline)
    python visualize_spectral_features.py --audio_path /path/to/audio.wav --start_s 10 --end_s 30

    # Generate and display tags
    python visualize_spectral_features.py --audio_path /path/to/audio.wav --show_tags
"""

import argparse
import os
import sys

import matplotlib.pyplot as plt
import numpy as np
from scipy import signal

# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from spectral_features import (
    calculate_loudness_seq,
    calculate_spectral_centroid_seq,
    calculate_spectral_complexity_seq,
)

# Import suno_utils if available, otherwise provide fallback
try:
    from suno_utils.audio import Audio
except ImportError:
    print("Warning: suno_utils not available. Using basic audio loading.")
    import soundfile as sf

    class Audio:
        @staticmethod
        def from_file(path):
            data, sr = sf.read(path)
            audio_obj = type("Audio", (), {})()
            audio_obj.array_float = data.T if data.ndim == 2 else data[np.newaxis, :]
            audio_obj.sample_rate = sr
            audio_obj.n_channels = audio_obj.array_float.shape[0]
            return audio_obj

        @staticmethod
        def sum(audios):
            # Simple implementation
            return audios[0] if audios else None


def load_audio(audio_path):
    """Load audio file and return audio data."""
    audio = Audio.from_file(audio_path)
    return audio


def calculate_features(
    audio_data, sample_rate=24000, target_rate=25, smoothing_kernel_size=7, normalize=False
):
    """Calculate all spectral features from audio data.

    Args:
        audio_data: Audio array (can be stereo or mono)
        sample_rate: Sample rate of audio
        target_rate: Target resolution in Hz (default 25Hz)
        smoothing_kernel_size: Size of smoothing kernel (default 7)
        normalize: If True, normalize each feature to [0, 1] range

    Returns:
        dict: Dictionary containing all calculated features
    """
    # Resample to 24kHz if needed
    if sample_rate != 24000:
        print(f"Warning: Audio is at {sample_rate}Hz, features expect 24kHz")
        # TODO: Implement resampling if needed
        # For now, assume audio is already at correct rate

    # Convert to mono for visualization
    if audio_data.ndim > 1:
        audio_mono = np.mean(audio_data, axis=0)
    else:
        audio_mono = audio_data

    # Calculate loudness first (for use in silence detection if normalizing)
    loudness = calculate_loudness_seq(
        audio_mono,
        sample_rate=sample_rate,
        target_rate=target_rate,
        smoothing_kernel_size=smoothing_kernel_size,
        normalize=normalize,
    )

    # Calculate spectral features, passing loudness for silence detection if normalizing
    features = {
        "loudness": loudness,
        "spectral_centroid": calculate_spectral_centroid_seq(
            audio_mono,
            sample_rate=sample_rate,
            target_rate=target_rate,
            smoothing_kernel_size=smoothing_kernel_size,
            normalize=normalize,
            loudness_seq=loudness if normalize else None,
        ),
        "spectral_complexity": calculate_spectral_complexity_seq(
            audio_mono,
            sample_rate=sample_rate,
            target_rate=target_rate,
            smoothing_kernel_size=smoothing_kernel_size,
            normalize=normalize,
            loudness_seq=loudness if normalize else None,
        ),
    }

    return features, audio_mono


def crop_features_and_audio(audio_mono, features, start_s, end_s, sample_rate=24000, target_rate=25):
    """Crop both audio and features to specified time range.

    This mimics what happens in the training pipeline.

    Args:
        audio_mono: Mono audio array
        features: Dict of feature arrays
        start_s: Start time in seconds
        end_s: End time in seconds
        sample_rate: Audio sample rate
        target_rate: Feature resolution in Hz (default 25Hz)

    Returns:
        tuple: (cropped_audio, cropped_features)
    """
    # Crop audio
    start_sample = int(start_s * sample_rate)
    end_sample = int(end_s * sample_rate)
    cropped_audio = audio_mono[start_sample:end_sample]

    # Crop features (at target_rate resolution)
    start_frame = int(start_s * target_rate)
    end_frame = int(end_s * target_rate)

    cropped_features = {}
    for key, feature_array in features.items():
        cropped_features[key] = feature_array[start_frame:end_frame]

    # Verify alignment
    expected_frames = len(cropped_audio) / sample_rate * target_rate
    actual_frames = len(cropped_features["loudness"])
    if abs(expected_frames - actual_frames) > 1:
        print(
            f"Warning: Feature alignment issue. Expected {expected_frames:.1f} frames, got {actual_frames}"
        )

    return cropped_audio, cropped_features


def generate_tag_examples(features, normalized=False):
    """Generate example tags from features (simplified version).

    Args:
        features: Dict of feature arrays
        normalized: Whether features are normalized to [0, 1] range

    Returns:
        str: Example tag string
    """
    tags = []

    # Loudness tags
    loudness = features["loudness"]
    if normalized:
        activity = np.sum(loudness > 0.1) / len(loudness) * 100
    else:
        activity = np.sum(loudness > 0.01) / len(loudness) * 100
    tags.append(f"activity:{int(round(activity / 10) * 10)}%")

    # Spectral centroid tags
    centroid = features["spectral_centroid"]
    mean_centroid = np.mean(centroid)

    if normalized:
        # Centroid is in [0, 1] range
        if mean_centroid > 0.7:
            tags.append("centroid:bright")
        elif mean_centroid < 0.3:
            tags.append("centroid:dark")
        else:
            tags.append("centroid:balanced")
        tags.append(f"centroid_mean:{mean_centroid:.2f}")

        # Example span with normalized values (first 10 frames)
        if len(centroid) >= 10:
            span_values = centroid[:10]
            values_str = ",".join([f"{v:.2f}" for v in span_values])
            tags.append(f"centroid[0:10]:[{values_str}]")
    else:
        # Centroid is in Hz
        if mean_centroid > 8000:
            tags.append("centroid:bright")
        elif mean_centroid < 3000:
            tags.append("centroid:dark")
        else:
            tags.append("centroid:balanced")
        tags.append(f"centroid_mean:{int(mean_centroid)}hz")

        # Example span with Hz values (first 10 frames)
        if len(centroid) >= 10:
            span_values = centroid[:10]
            values_str = ",".join([f"{int(round(v, -2))}hz" for v in span_values])
            tags.append(f"centroid[0:10]:[{values_str}]")

    # Spectral complexity tags (always in 0-1 range, but normalization stretches it)
    complexity = features["spectral_complexity"]
    mean_complexity = np.mean(complexity)
    if mean_complexity > 0.7:
        tags.append("complexity:rich")
    elif mean_complexity < 0.4:
        tags.append("complexity:simple")
    else:
        tags.append("complexity:moderate")
    tags.append(f"complexity_mean:{mean_complexity:.2f}")

    return "{" + ";".join(tags) + "}"


def visualize_features(
    audio_path,
    start_s=None,
    end_s=None,
    show_tags=False,
    output_path=None,
    target_rate=25,
    smoothing_kernel_size=7,
    normalize=False,
):
    """Visualize audio and spectral features.

    Args:
        audio_path: Path to audio file
        start_s: Optional start time for cropping
        end_s: Optional end time for cropping
        show_tags: Whether to show generated tags
        output_path: Optional path to save figure
        target_rate: Target resolution in Hz (default 25Hz)
        smoothing_kernel_size: Size of smoothing kernel (default 7)
        normalize: If True, normalize each feature to [0, 1] range
    """
    print(f"Loading audio from: {audio_path}")
    audio = load_audio(audio_path)

    # Get audio data
    audio_data = audio.array_float
    sample_rate = audio.sample_rate

    # Handle both mono and stereo audio shapes
    if audio_data.ndim == 1:
        duration_s = len(audio_data) / sample_rate
        n_channels = 1
    else:
        duration_s = audio_data.shape[1] / sample_rate
        n_channels = audio_data.shape[0]

    print(f"Audio: {n_channels} channels, {sample_rate}Hz, {duration_s:.2f}s")

    # Calculate features on full audio
    norm_str = " (normalized)" if normalize else ""
    print(
        f"Calculating features at {target_rate}Hz with kernel size {smoothing_kernel_size}{norm_str}..."
    )
    features, audio_mono = calculate_features(
        audio_data,
        sample_rate,
        target_rate=target_rate,
        smoothing_kernel_size=smoothing_kernel_size,
        normalize=normalize,
    )

    # Apply cropping if specified
    if start_s is not None or end_s is not None:
        start_s = start_s or 0.0
        end_s = end_s or (len(audio_mono) / sample_rate)
        print(f"Cropping to {start_s:.2f}s - {end_s:.2f}s")
        audio_mono, features = crop_features_and_audio(
            audio_mono, features, start_s, end_s, sample_rate, target_rate=target_rate
        )
        time_offset = start_s
    else:
        time_offset = 0.0

    # Generate tags if requested
    tag_string = ""
    if show_tags:
        tag_string = generate_tag_examples(features, normalized=normalize)
        print(f"\nGenerated tags:\n{tag_string}\n")

    # Create visualization
    fig, axes = plt.subplots(4, 1, figsize=(14, 10))
    fig.suptitle(f"Spectral Features: {os.path.basename(audio_path)}", fontsize=14, fontweight="bold")

    # Time arrays - calculate actual rate for each feature based on its length
    audio_time = np.arange(len(audio_mono)) / sample_rate + time_offset
    audio_duration = len(audio_mono) / sample_rate

    # Calculate actual rates from feature lengths and audio duration
    loudness_actual_rate = len(features["loudness"]) / audio_duration
    centroid_actual_rate = len(features["spectral_centroid"]) / audio_duration
    complexity_actual_rate = len(features["spectral_complexity"]) / audio_duration

    # Create separate time arrays for each feature based on their actual lengths
    loudness_time = np.arange(len(features["loudness"])) / loudness_actual_rate + time_offset
    centroid_time = np.arange(len(features["spectral_centroid"])) / centroid_actual_rate + time_offset
    complexity_time = (
        np.arange(len(features["spectral_complexity"])) / complexity_actual_rate + time_offset
    )

    # Row 1: Spectrogram
    nperseg = min(2048, len(audio_mono) // 4)  # Window size for spectrogram
    f, t, Sxx = signal.spectrogram(audio_mono, sample_rate, nperseg=nperseg, noverlap=nperseg // 2)
    spec_db = 10 * np.log10(Sxx + 1e-10)  # Convert to dB

    # Adjust time axis for offset
    t_adjusted = t + time_offset

    axes[0].pcolormesh(
        t_adjusted,
        f / 1000,
        spec_db,
        shading="gouraud",
        cmap="viridis",
        vmin=np.percentile(spec_db, 5),
        vmax=np.percentile(spec_db, 95),
    )
    axes[0].set_ylabel("Frequency (kHz)")
    axes[0].set_title("Spectrogram", fontsize=10, fontweight="bold")
    axes[0].set_ylim(0, 12)  # Show up to 12 kHz
    axes[0].set_xlim(audio_time[0], audio_time[-1])

    # Row 2: Loudness
    axes[1].plot(loudness_time, features["loudness"], linewidth=2, color="green")

    if normalize:
        axes[1].set_ylabel("Normalized Loudness (0-1)")
        axes[1].set_title(
            f"Loudness @ {loudness_actual_rate:.1f}Hz (Normalized)", fontsize=10, fontweight="bold"
        )
        axes[1].set_ylim(-0.05, 1.05)
    else:
        axes[1].axhline(
            y=0.01, color="red", linestyle="--", linewidth=1, alpha=0.5, label="Activity threshold"
        )
        axes[1].set_ylabel("RMS Loudness")
        axes[1].set_title(f"Loudness @ {loudness_actual_rate:.1f}Hz", fontsize=10, fontweight="bold")
        axes[1].legend(loc="upper right", fontsize=8)

    axes[1].grid(True, alpha=0.3)
    axes[1].set_xlim(audio_time[0], audio_time[-1])

    # Row 3: Spectral Centroid
    axes[2].plot(centroid_time, features["spectral_centroid"], linewidth=2, color="orange")

    if normalize:
        axes[2].set_ylabel("Normalized Brightness (0-1)")
        axes[2].set_title(
            f"Spectral Centroid @ {centroid_actual_rate:.1f}Hz (Normalized)",
            fontsize=10,
            fontweight="bold",
        )
        axes[2].set_ylim(-0.05, 1.05)
    else:
        axes[2].set_ylabel("Frequency (Hz)")
        axes[2].set_title(
            f"Spectral Centroid @ {centroid_actual_rate:.1f}Hz (Brightness)",
            fontsize=10,
            fontweight="bold",
        )
        # Add reference lines for brightness categories (only in non-normalized mode)
        axes[2].axhline(
            y=8000, color="yellow", linestyle="--", linewidth=1, alpha=0.3, label="Bright threshold"
        )
        axes[2].axhline(
            y=3000, color="purple", linestyle="--", linewidth=1, alpha=0.3, label="Dark threshold"
        )
        axes[2].legend(loc="upper right", fontsize=8)

    axes[2].grid(True, alpha=0.3)
    axes[2].set_xlim(audio_time[0], audio_time[-1])

    # Row 4: Spectral Complexity
    axes[3].plot(complexity_time, features["spectral_complexity"], linewidth=2, color="purple")
    axes[3].set_ylabel("Complexity (0-1)")
    axes[3].set_xlabel("Time (seconds)")

    if normalize:
        axes[3].set_title(
            f"Spectral Complexity @ {complexity_actual_rate:.1f}Hz (Normalized)",
            fontsize=10,
            fontweight="bold",
        )
    else:
        axes[3].set_title(
            f"Spectral Complexity @ {complexity_actual_rate:.1f}Hz (Entropy)",
            fontsize=10,
            fontweight="bold",
        )
        # Add reference lines for complexity categories (only in non-normalized mode)
        axes[3].axhline(
            y=0.7, color="red", linestyle="--", linewidth=1, alpha=0.3, label="Rich threshold"
        )
        axes[3].axhline(
            y=0.4, color="blue", linestyle="--", linewidth=1, alpha=0.3, label="Simple threshold"
        )
        axes[3].legend(loc="upper right", fontsize=8)

    axes[3].grid(True, alpha=0.3)
    axes[3].set_ylim(-0.05, 1.05)
    axes[3].set_xlim(audio_time[0], audio_time[-1])

    # Add tags as text if requested
    if show_tags:
        fig.text(
            0.5,
            0.02,
            f"Tags: {tag_string}",
            ha="center",
            fontsize=9,
            bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5),
            wrap=True,
        )

    plt.tight_layout(rect=[0, 0.03, 1, 0.97])

    if output_path:
        print(f"Saving figure to: {output_path}")
        plt.savefig(output_path, dpi=150, bbox_inches="tight")

    plt.show()


def main():
    parser = argparse.ArgumentParser(description="Visualize spectral features from audio file")
    parser.add_argument("--audio_path", type=str, required=True, help="Path to audio file")
    parser.add_argument(
        "--start_s", type=float, default=None, help="Start time in seconds (for cropping)"
    )
    parser.add_argument("--end_s", type=float, default=None, help="End time in seconds (for cropping)")
    parser.add_argument("--show_tags", action="store_true", help="Generate and show example tags")
    parser.add_argument("--output_path", type=str, default=None, help="Path to save figure")

    args = parser.parse_args()

    if not os.path.exists(args.audio_path):
        print(f"Error: Audio file not found: {args.audio_path}")
        sys.exit(1)

    visualize_features(
        args.audio_path,
        start_s=args.start_s,
        end_s=args.end_s,
        show_tags=args.show_tags,
        output_path=args.output_path,
    )


if __name__ == "__main__":
    main()
