import numpy as np
import scipy.signal as signal
from app.core.audio_features import time_stretch as _time_stretch, pitch_shift as _pitch_shift

class SubTabDSPEngine:
    @staticmethod
    def change_speed(y: np.ndarray, sr: int, speed_ratio: float, preserve_pitch: bool = True) -> np.ndarray:
        """
        Alters the playback velocity (Time-Stretching) of a NumPy signal array.
        """
        if speed_ratio == 1.0:
            return y
            
        if preserve_pitch:
            return _time_stretch(y, rate=speed_ratio)
        else:
            num_samples_new = int(len(y) / speed_ratio)
            return signal.resample(y, num_samples_new)

    @staticmethod
    def normalize(y: np.ndarray, target_db: float = 0.0) -> np.ndarray:
        """
        Performs Peak Normalization on an array to scale it to the target decibel value.
        """
        target_amplitude = 10.0 ** (target_db / 20.0)
        max_amplitude = np.max(np.abs(y))
        
        if max_amplitude == 0:
            return y
            
        gain = target_amplitude / max_amplitude
        return y * gain

    @staticmethod
    def apply_volume_automation_envelope(y: np.ndarray, sr: int, nodes: list) -> np.ndarray:
        """
        Applies a user-drawn volume automation envelope onto an acoustic signal NumPy array.
        nodes: A list of point dictionaries, e.g., [{"time": 0.0, "db": 0.0}, {"time": 2.5, "db": -12.0}, ...]
        """
        if not nodes:
            return y
            
        # Sort envelope nodes chronologically by time axis
        nodes = sorted(nodes, key=lambda x: x["time"])
        
        # 1. Map node variables into distinct coordinates arrays
        node_times = np.array([node["time"] for node in nodes])
        node_dbs = np.array([node["db"] for node in nodes])
        
        # Hard-clamp boundary constraints matching the operational floor [-30.0dB, +3.0dB]
        node_dbs = np.clip(node_dbs, -30.0, 3.0)
        
        # 2. Evaluate absolute timeline timestamps for every index position inside the signal array
        total_samples = len(y)
        sample_times = np.arange(total_samples) / sr
        
        # 3. Linearly interpolate localized decibel thresholds across every single sample step
        # Handle edge cases for interpolation: if sample_times is outside node_times range,
        # np.interp uses the first/last value of node_dbs.
        interpolated_dbs = np.interp(sample_times, node_times, node_dbs, left=node_dbs[0], right=node_dbs[-1])
        
        # 4. Map logarithmic values into standard linear gain scale arrays
        linear_gains = 10.0 ** (interpolated_dbs / 20.0)
        
        # 5. Multiply the raw amplitude vector array by the linear gain modifier mask
        return y * linear_gains

    @staticmethod
    def pitch_shift(y: np.ndarray, sr: int, n_steps: float) -> np.ndarray:
        """
        Shift the pitch of an audio signal by a specified number of semitones.
        
        Args:
            y: Input audio signal
            sr: Sample rate
            n_steps: Number of semitones to shift (positive = higher pitch, negative = lower pitch)
            
        Returns:
            Pitch-shifted audio signal
        """
        if n_steps == 0:
            return y
        return _pitch_shift(y, sr=sr, n_steps=n_steps)

    @staticmethod
    def merge_back_to_parent(
        parent_track_audio: np.ndarray, 
        sr: int, 
        edited_sub_audio: np.ndarray, 
        start_seconds: float, 
        original_duration_seconds: float
    ) -> np.ndarray:
        """
        Splices the modified audio segment from the Sub-tab back into the parent track array.
        Applies a 10ms micro-crossfade at the boundaries to eliminate pop/click noise.
        """
        start_sample = int(start_seconds * sr)
        original_samples_len = int(original_duration_seconds * sr)
        edited_samples_len = len(edited_sub_audio)
        crossfade_samples = int(0.01 * sr)  # 10ms crossfade window
        
        # 1. Allocate the target output array dimension bounds
        new_total_len = len(parent_track_audio) - original_samples_len + edited_samples_len
        output_audio = np.zeros(new_total_len, dtype=np.float32)
        
        # 2. Extract leading unedited block
        output_audio[:start_sample] = parent_track_audio[:start_sample]
        
        # 3. Stitch the modified audio payload
        output_audio[start_sample:start_sample + edited_samples_len] = edited_sub_audio
        
        # 4. Extract trailing unedited block
        post_start_original = start_sample + original_samples_len
        post_start_new = start_sample + edited_samples_len
        output_audio[post_start_new:] = parent_track_audio[post_start_original:]
        
        # 5. Execute micro-crossfade across the initial splice junction
        if start_sample > crossfade_samples:
            fade_in_ramp = np.linspace(0.0, 1.0, crossfade_samples)
            fade_out_ramp = np.linspace(1.0, 0.0, crossfade_samples)
            
            # Smooth 10ms interpolation overlay
            output_audio[start_sample : start_sample + crossfade_samples] = (
                edited_sub_audio[:crossfade_samples] * fade_in_ramp + 
                parent_track_audio[start_sample : start_sample + crossfade_samples] * fade_out_ramp
            )
            
        # 6. Execute micro-crossfade across the trailing splice junction
        if post_start_new + crossfade_samples < len(output_audio):
            fade_in_ramp = np.linspace(0.0, 1.0, crossfade_samples)
            fade_out_ramp = np.linspace(1.0, 0.0, crossfade_samples)
            
            output_audio[post_start_new : post_start_new + crossfade_samples] = (
                parent_track_audio[post_start_original : post_start_original + crossfade_samples] * fade_in_ramp +
                edited_sub_audio[-crossfade_samples:] * fade_out_ramp
            )
            
        return output_audio


class DSPAudioModulator:
    @staticmethod
    def apply_automation_and_panning(
        y_raw: np.ndarray, 
        sr: int, 
        volume_points: list,     # [{"time": 0.5, "db": -6.0}, ...]
        panning_points: list,    # [{"time": 1.0, "pan": -0.7}, ...]
        fade_in_sec: float = 0.0,
        fade_out_sec: float = 0.0
    ) -> np.ndarray:
        """
        Applies multi-point volume envelopes, constant-power panning, and trigonometric fades
        directly onto a 1D (Mono) or 2D (Stereo) acoustic NumPy signal array.
        
        Input: y_raw maps to the raw sound array (Mono/Stereo matrix bounded inside [-1.0, 1.0]).
        Output: y_processed yields a 2D interleaved Stereo NumPy array (2, N) with baked modulations.
        """
        total_samples = y_raw.shape[-1] if len(y_raw.shape) > 1 else len(y_raw)
        duration_sec = total_samples / sr
        
        # 1. Guarantee Stereo geometry dimensions (2 discrete channels) for Panning operations
        if len(y_raw.shape) == 1:
            # For Mono arrays, clone sample metrics symmetrically to Left/Right matrices
            y_stereo = np.vstack((y_raw, y_raw))
        else:
            y_stereo = np.copy(y_raw)

        # 2. Allocate Envelope Mask arrays matching total track samples limits
        volume_envelope = np.ones(total_samples, dtype=np.float32)
        pan_envelope = np.zeros(total_samples, dtype=np.float32) # Default initialization: Center (0.0)

        # 3. Compile the Volume Envelope using linear interpolation bounds across nodes
        if volume_points and len(volume_points) > 0:
            # Enforce strict chronological sorting down the timeline axis
            points = sorted(volume_points, key=lambda x: x["time"])
            
            # Pad introductory bounds if the initial point coordinate sits past t = 0.0s
            if points[0]["time"] > 0:
                first_gain = 10.0 ** (points[0]["db"] / 20.0)
                idx_end = int(points[0]["time"] * sr)
                volume_envelope[:idx_end] = first_gain
                
            for i in range(len(points) - 1):
                p1, p2 = points[i], points[i+1]
                idx_start = int(p1["time"] * sr)
                idx_end = int(p2["time"] * sr)
                
                gain_start = 10.0 ** (p1["db"] / 20.0)
                gain_end = 10.0 ** (p2["db"] / 20.0)
                
                # Linearly interpolate vector increments between adjacent anchor positions
                volume_envelope[idx_start:idx_end] = np.linspace(gain_start, gain_end, idx_end - idx_start)
                
            # Pad trailing bounds from the final milestone extending through end-of-file
            if points[-1]["time"] < duration_sec:
                last_gain = 10.0 ** (points[-1]["db"] / 20.0)
                idx_start = int(points[-1]["time"] * sr)
                volume_envelope[idx_start:] = last_gain

        # 4. Compile the Panning Envelope using linear interpolation bounds across nodes
        if panning_points and len(panning_points) > 0:
            points = sorted(panning_points, key=lambda x: x["time"])
            
            if points[0]["time"] > 0:
                pan_envelope[:int(points[0]["time"] * sr)] = points[0]["pan"]
                
            for i in range(len(points) - 1):
                p1, p2 = points[i], points[i+1]
                idx_start = int(p1["time"] * sr)
                idx_end = int(p2["time"] * sr)
                pan_envelope[idx_start:idx_end] = np.linspace(p1["pan"], p2["pan"], idx_end - idx_start)
                
            if points[-1]["time"] < duration_sec:
                pan_envelope[int(points[-1]["time"] * sr):] = points[-1]["pan"]

        # 5. Apply Trigonometric Cosine Fade-In / Fade-Out functions onto the Volume Envelope mask
        if fade_in_sec > 0:
            fade_in_samples = min(total_samples, int(fade_in_sec * sr))
            x_fade = np.linspace(0.0, np.pi, fade_in_samples)
            cosine_ramp = (1.0 - np.cos(x_fade)) / 2.0
            volume_envelope[:fade_in_samples] *= cosine_ramp
            
        if fade_out_sec > 0:
            fade_out_samples = min(total_samples, int(fade_out_sec * sr))
            x_fade = np.linspace(0.0, np.pi, fade_out_samples)
            cosine_ramp = (1.0 + np.cos(x_fade)) / 2.0
            volume_envelope[-fade_out_samples:] *= cosine_ramp

        # 6. Bake Volume Envelope matrices onto the Left and Right discrete audio paths
        y_stereo[0, :] *= volume_envelope
        y_stereo[1, :] *= volume_envelope

        # 7. Apply Constant-Power Stereo Panning allocations
        # Map panning metrics range [-1.0, 1.0] onto angular radians field array [0, pi/2]
        theta_envelope = ((pan_envelope + 1.0) / 2.0) * (np.pi / 2.0)
        
        # Evaluate localized amplitude coefficients for physical channels split
        gain_left = np.cos(theta_envelope)
        gain_right = np.sin(theta_envelope)
        
        # Multiply scaling factors directly across corresponding discrete matrices
        y_stereo[0, :] *= gain_left
        y_stereo[1, :] *= gain_right

        return y_stereo

