"""
melody_engine.py - Professional Motivic Melody Engine

Generates sophisticated melodies using motivic development techniques:
- Theme generation with scale-based melodic contours
- Variations: sequence, inversion, retrograde, expansion/contraction
- Phrase structures: antecedent-consequent, period, sentence
- Melodic contour application: arch, wave, step-wise

This engine provides professional-grade melodic composition tools for
creating memorable and coherent melodic lines in reggaeton productions.
"""
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass
from enum import Enum
import random


class VariationType(Enum):
    """Types of motivic variations."""
    SEQUENCE = "sequence"           # Repetir a intervalo diferente
    INVERSION = "inversion"         # Invertir intervalos
    RETROGRADE = "retrograde"       # Reversa (al revés)
    EXPANSION = "expansion"         # Expandir intervalos
    CONTRACTION = "contraction"     # Contraer intervalos


class PhraseStructureType(Enum):
    """Classical phrase structure types."""
    ANTECEDENT_CONSEQUENT = "antecedent_consequent"  # Pregunta-respuesta
    PERIOD = "period"                               # Período musical
    SENTENCE = "sentence"                           # Frase sentencia


class ContourType(Enum):
    """Melodic contour types."""
    ARCH = "arch"           # Subir y bajar
    WAVE = "wave"           # Múltiples picos
    STEP_WISE = "step_wise" # Notas conjuntas (graduales)
    ASCENDING = "ascending" # Ascendente
    DESCENDING = "descending"  # Descendente


@dataclass
class Note:
    """Represents a single note in a melody."""
    pitch: int          # MIDI pitch
    duration: float     # Duration in beats
    velocity: int       # Velocity 0-127
    start_time: float   # Start position in beats
    
    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary format for Ableton."""
        return {
            "pitch": self.pitch,
            "start_time": self.start_time,
            "duration": self.duration,
            "velocity": self.velocity
        }
    
    def copy(self) -> 'Note':
        """Create a copy of this note."""
        return Note(self.pitch, self.duration, self.velocity, self.start_time)


@dataclass
class Motive:
    """A melodic motive - the basic unit of melodic composition."""
    notes: List[Note]
    name: str = ""
    
    def get_intervals(self) -> List[int]:
        """Extract intervals between consecutive notes."""
        if len(self.notes) < 2:
            return []
        intervals = []
        for i in range(len(self.notes) - 1):
            intervals.append(self.notes[i + 1].pitch - self.notes[i].pitch)
        return intervals
    
    def get_total_duration(self) -> float:
        """Get total duration of the motive."""
        if not self.notes:
            return 0.0
        last_note = max(self.notes, key=lambda n: n.start_time)
        return last_note.start_time + last_note.duration
    
    def transpose(self, semitones: int) -> 'Motive':
        """Transpose the motive by semitones."""
        transposed_notes = []
        for note in self.notes:
            new_note = note.copy()
            new_note.pitch += semitones
            transposed_notes.append(new_note)
        return Motive(transposed_notes, f"{self.name}_transposed")


class MelodyEngine:
    """
    Professional melody generation engine using motivic techniques.
    
    Provides:
    - Theme/motive generation
    - Classical variations (sequence, inversion, retrograde, expansion)
    - Phrase structures (antecedent-consequent, period, sentence)
    - Melodic contour application
    """
    
    # Scale definitions (intervals from root)
    SCALES = {
        "minor": [0, 2, 3, 5, 7, 8, 10],
        "major": [0, 2, 4, 5, 7, 9, 11],
        "harmonic_minor": [0, 2, 3, 5, 7, 8, 11],
        "melodic_minor": [0, 2, 3, 5, 7, 9, 11],
        "pentatonic_minor": [0, 3, 5, 7, 10],
        "pentatonic_major": [0, 2, 4, 7, 9],
        "dorian": [0, 2, 3, 5, 7, 9, 10],
        "phrygian": [0, 1, 3, 5, 7, 8, 10],
        "mixolydian": [0, 2, 4, 5, 7, 9, 10],
    }
    
    # Rhythm patterns (durations in beats)
    RHYTHM_PATTERNS = {
        "simple": [1.0, 1.0, 2.0],           # Negra, negra, blanca
        "syncopated": [0.5, 1.0, 0.5, 2.0],  # Corchea, negra, corchea, blanca
        "flowing": [0.75, 0.75, 0.5, 2.0],   # Punteado
        "rhythmic": [0.5, 0.5, 0.5, 0.5, 2.0], # Corcheas
        "long": [2.0, 2.0],                   # Dos blancas
        "short_long": [0.5, 0.5, 3.0],        # Corcheas + puntada
    }
    
    def __init__(self, seed: Optional[int] = None):
        """Initialize the melody engine."""
        if seed is not None:
            random.seed(seed)
        self._generated_themes: List[Motive] = []
    
    def generate_theme(self, 
                       scale: str = "minor",
                       bars: int = 4,
                       density: str = "medium",
                       root_pitch: int = 60) -> Motive:
        """
        Generate a main theme/motive.
        
        Args:
            scale: Scale type (minor, major, harmonic_minor, pentatonic_minor, etc.)
            bars: Number of bars (default 4)
            density: Note density - "sparse", "medium", "dense"
            root_pitch: Root MIDI pitch (default 60 = C4)
        
        Returns:
            Motive object containing the generated theme
        """
        # Get scale intervals
        scale_intervals = self.SCALES.get(scale, self.SCALES["minor"])
        
        # Determine number of notes based on density
        density_notes = {
            "sparse": bars * 2,      # 2 notas por compás
            "medium": bars * 4,      # 4 notas por compás
            "dense": bars * 8        # 8 notas por compás
        }
        num_notes = density_notes.get(density, bars * 4)
        
        # Select rhythm pattern
        rhythm_key = random.choice(list(self.RHYTHM_PATTERNS.keys()))
        rhythm = self.RHYTHM_PATTERNS[rhythm_key]
        
        # Generate notes
        notes = []
        current_time = 0.0
        beats_per_bar = 4.0
        total_beats = bars * beats_per_bar
        
        for i in range(num_notes):
            if current_time >= total_beats:
                break
            
            # Select pitch from scale (with some octave variation)
            octave_offset = random.choice([0, 12, -12]) if random.random() > 0.7 else 0
            scale_degree = random.choice(scale_intervals)
            pitch = root_pitch + scale_degree + octave_offset
            
            # Select duration from rhythm pattern
            duration = rhythm[i % len(rhythm)]
            
            # Ensure we don't exceed bar limit
            if current_time + duration > total_beats:
                duration = total_beats - current_time
            
            # Velocity with some variation
            velocity = random.randint(80, 110)
            
            note = Note(pitch, duration, velocity, current_time)
            notes.append(note)
            
            current_time += duration
        
        theme = Motive(notes, name=f"Theme_{scale}_{bars}bars")
        self._generated_themes.append(theme)
        return theme
    
    def create_variation(self, 
                         theme: Motive,
                         variation_type: str) -> Motive:
        """
        Create a variation of a theme.
        
        Args:
            theme: The original motive/theme
            variation_type: Type of variation:
                - "sequence": Repetir a intervalo diferente (repeat at different interval)
                - "inversion": Invertir intervalos (invert intervals)
                - "retrograde": Reversa (backwards)
                - "expansion": Expandir intervalos (widen intervals)
                - "contraction": Contraer intervalos (narrow intervals)
        
        Returns:
            Motive with the variation applied
        """
        var_type = VariationType(variation_type)
        
        if var_type == VariationType.SEQUENCE:
            return self._sequence_variation(theme)
        elif var_type == VariationType.INVERSION:
            return self._inversion_variation(theme)
        elif var_type == VariationType.RETROGRADE:
            return self._retrograde_variation(theme)
        elif var_type == VariationType.EXPANSION:
            return self._expansion_variation(theme)
        elif var_type == VariationType.CONTRACTION:
            return self._contraction_variation(theme)
        
        return theme  # Fallback
    
    def _sequence_variation(self, theme: Motive) -> Motive:
        """Sequence: repeat at a different pitch level."""
        # Transpose by a random interval (2-7 semitones)
        interval = random.choice([2, 3, 4, 5, 7])
        return theme.transpose(interval)
    
    def _inversion_variation(self, theme: Motive) -> Motive:
        """Inversion: invert all intervals (mirror image)."""
        if len(theme.notes) < 2:
            return theme
        
        intervals = theme.get_intervals()
        inverted_intervals = [-i for i in intervals]
        
        # Build inverted melody
        notes = [theme.notes[0].copy()]  # Start with same first note
        current_pitch = notes[0].pitch
        
        for i, interval in enumerate(inverted_intervals):
            current_pitch += interval
            original_note = theme.notes[i + 1]
            new_note = Note(
                pitch=current_pitch,
                duration=original_note.duration,
                velocity=original_note.velocity,
                start_time=original_note.start_time
            )
            notes.append(new_note)
        
        return Motive(notes, name=f"{theme.name}_inversion")
    
    def _retrograde_variation(self, theme: Motive) -> Motive:
        """Retrograde: play the melody backwards."""
        if not theme.notes:
            return theme
        
        # Reverse the order of notes
        reversed_notes = []
        total_duration = theme.get_total_duration()
        
        for note in reversed(theme.notes):
            # Calculate new start time (mirrored)
            note_end = note.start_time + note.duration
            new_start = total_duration - note_end
            
            new_note = Note(
                pitch=note.pitch,
                duration=note.duration,
                velocity=note.velocity,
                start_time=new_start
            )
            reversed_notes.append(new_note)
        
        # Sort by start time
        reversed_notes.sort(key=lambda n: n.start_time)
        return Motive(reversed_notes, name=f"{theme.name}_retrograde")
    
    def _expansion_variation(self, theme: Motive) -> Motive:
        """Expansion: widen all intervals."""
        if len(theme.notes) < 2:
            return theme
        
        intervals = theme.get_intervals()
        expanded_intervals = [i * 2 for i in intervals]  # Double intervals
        
        notes = [theme.notes[0].copy()]
        current_pitch = notes[0].pitch
        
        for i, interval in enumerate(expanded_intervals):
            current_pitch += interval
            original_note = theme.notes[i + 1]
            new_note = Note(
                pitch=current_pitch,
                duration=original_note.duration,
                velocity=original_note.velocity,
                start_time=original_note.start_time
            )
            notes.append(new_note)
        
        return Motive(notes, name=f"{theme.name}_expansion")
    
    def _contraction_variation(self, theme: Motive) -> Motive:
        """Contraction: narrow all intervals."""
        if len(theme.notes) < 2:
            return theme
        
        intervals = theme.get_intervals()
        contracted_intervals = [max(1, abs(i) // 2) * (1 if i > 0 else -1) for i in intervals]
        
        notes = [theme.notes[0].copy()]
        current_pitch = notes[0].pitch
        
        for i, interval in enumerate(contracted_intervals):
            current_pitch += interval
            original_note = theme.notes[i + 1]
            new_note = Note(
                pitch=current_pitch,
                duration=original_note.duration,
                velocity=original_note.velocity,
                start_time=original_note.start_time
            )
            notes.append(new_note)
        
        return Motive(notes, name=f"{theme.name}_contraction")
    
    def generate_phrase_structure(self,
                                   structure_type: str,
                                   theme: Optional[Motive] = None,
                                   scale: str = "minor",
                                   bars: int = 8) -> List[Motive]:
        """
        Generate a complete phrase structure.
        
        Args:
            structure_type: Type of phrase structure:
                - "antecedent_consequent": Pregunta-respuesta (4+4 bars)
                - "period": Período musical (4+4 bars con cadencia)
                - "sentence": Sentencia (2+2+4 bars)
            theme: Optional existing theme to base the structure on
            scale: Scale to use if generating new theme
            bars: Total bars for the phrase
        
        Returns:
            List of Motives forming the phrase structure
        """
        struct_type = PhraseStructureType(structure_type)
        
        # Generate theme if not provided
        if theme is None:
            theme = self.generate_theme(scale=scale, bars=bars // 2)
        
        if struct_type == PhraseStructureType.ANTECEDENT_CONSEQUENT:
            return self._antecedent_consequent(theme, bars)
        elif struct_type == PhraseStructureType.PERIOD:
            return self._period_structure(theme, bars)
        elif struct_type == PhraseStructureType.SENTENCE:
            return self._sentence_structure(theme, bars)
        
        return [theme]
    
    def _antecedent_consequent(self, theme: Motive, total_bars: int) -> List[Motive]:
        """
        Antecedent-Consequent (Question-Answer) structure.
        First phrase ends on dominant (pregunta), second on tonic (respuesta).
        """
        # Antecedent - ends with tension (usually on dominant/V)
        antecedent = theme
        
        # Consequent - similar but ends on tonic, slight variation
        # Use sequence or inversion for variation
        variation_type = random.choice(["sequence", "inversion"])
        consequent = self.create_variation(theme, variation_type)
        
        return [antecedent, consequent]
    
    def _period_structure(self, theme: Motive, total_bars: int) -> List[Motive]:
        """
        Period structure: antecedent + consequent with stronger cadential feel.
        Both phrases related but consequent has stronger resolution.
        """
        # First phrase (antecedent)
        phrase_a = theme
        
        # Second phrase (consequent) - with retrograde or inversion
        variation_type = random.choice(["retrograde", "inversion", "sequence"])
        phrase_b = self.create_variation(theme, variation_type)
        
        return [phrase_a, phrase_b]
    
    def _sentence_structure(self, theme: Motive, total_bars: int) -> List[Motive]:
        """
        Sentence structure: presentation (2+2) + continuation (4).
        a + a' + b (where b is new material derived from a).
        """
        # First presentation (a)
        presentation_a = theme
        
        # Second presentation (a') - sequence or slight variation
        presentation_a_prime = self.create_variation(theme, "sequence")
        
        # Continuation (b) - more developed, often with expansion
        continuation = self.create_variation(theme, "expansion")
        
        return [presentation_a, presentation_a_prime, continuation]
    
    def apply_melodic_contour(self,
                               notes: List[Note],
                               contour_type: str,
                               start_pitch: Optional[int] = None,
                               end_pitch: Optional[int] = None) -> List[Note]:
        """
        Apply a melodic contour to existing notes.
        
        Args:
            notes: List of notes to apply contour to
            contour_type: Type of contour:
                - "arch": Subir y bajar (rise then fall)
                - "wave": Múltiples picos (multiple peaks)
                - "step_wise": Notas conjuntas (gradual steps)
                - "ascending": Ascendente
                - "descending": Descendente
            start_pitch: Optional starting pitch to force
            end_pitch: Optional ending pitch to force
        
        Returns:
            List of notes with contour applied
        """
        if not notes:
            return notes
        
        contour = ContourType(contour_type)
        total_duration = max(n.start_time + n.duration for n in notes)
        
        modified_notes = []
        
        for note in notes:
            # Calculate position in phrase (0.0 to 1.0)
            position = note.start_time / total_duration if total_duration > 0 else 0
            
            # Calculate contour offset
            if contour == ContourType.ARCH:
                # Arch: rise to middle then fall
                # Parabola: 4 * x * (1-x) peaks at 0.5
                arch_factor = 4 * position * (1 - position)
                offset = int(arch_factor * 7)  # Up to 7 semitones
                
            elif contour == ContourType.WAVE:
                # Wave: multiple peaks using sine-like function
                wave_factor = abs((position * 4) % 2 - 1)  # Sawtooth-like
                offset = int(wave_factor * 5)
                
            elif contour == ContourType.STEP_WISE:
                # Step-wise: gradual movement, limit jumps
                # Keep original pitch but smooth out large leaps
                offset = 0
                if modified_notes:
                    prev_pitch = modified_notes[-1].pitch
                    leap = note.pitch - prev_pitch
                    if abs(leap) > 3:
                        # Reduce large leap
                        offset = -leap + (3 if leap > 0 else -3)
                        
            elif contour == ContourType.ASCENDING:
                # Ascending: gradually rise
                offset = int(position * 12)  # Rise up to octave
                
            elif contour == ContourType.DESCENDING:
                # Descending: gradually fall
                offset = int(-position * 12)  # Fall up to octave
                
            else:
                offset = 0
            
            # Apply contour offset
            new_note = note.copy()
            new_note.pitch += offset
            
            # Apply forced start/end pitches if specified
            if len(modified_notes) == 0 and start_pitch is not None:
                new_note.pitch = start_pitch
            
            modified_notes.append(new_note)
        
        # Apply end pitch to last note if specified
        if end_pitch is not None and modified_notes:
            modified_notes[-1].pitch = end_pitch
        
        return modified_notes
    
    def generate_melody_with_variations(self,
                                        scale: str = "minor",
                                        bars: int = 16,
                                        num_variations: int = 3) -> Dict[str, Any]:
        """
        Generate a complete melody with theme and variations.
        
        Args:
            scale: Scale to use
            bars: Total bars
            num_variations: Number of variations to generate
        
        Returns:
            Dictionary with theme, variations, and combined melody
        """
        # Generate main theme
        theme = self.generate_theme(scale=scale, bars=bars // 4)
        
        # Generate variations
        variations = []
        variation_types = ["sequence", "inversion", "retrograde", "expansion"]
        
        for i in range(min(num_variations, len(variation_types))):
            var = self.create_variation(theme, variation_types[i])
            variations.append({
                "type": variation_types[i],
                "motive": var
            })
        
        # Create phrase structure
        phrase_structure = self.generate_phrase_structure(
            "antecedent_consequent",
            theme=theme,
            bars=bars // 2
        )
        
        return {
            "theme": theme,
            "variations": variations,
            "phrase_structure": phrase_structure,
            "scale": scale,
            "total_bars": bars
        }
    
    def notes_to_ableton_format(self, notes: List[Note]) -> List[Dict[str, Any]]:
        """Convert notes to Ableton-compatible format."""
        return [note.to_dict() for note in notes]
    
    def motive_to_ableton_format(self, motive: Motive) -> List[Dict[str, Any]]:
        """Convert a motive to Ableton-compatible note format."""
        return self.notes_to_ableton_format(motive.notes)


# Convenience functions for direct usage
def generate_motivic_melody(scale: str = "minor",
                          bars: int = 8,
                          variation_types: Optional[List[str]] = None,
                          phrase_structure: Optional[str] = None,
                          contour: Optional[str] = None,
                          seed: Optional[int] = None) -> Dict[str, Any]:
    """
    Generate a complete motivic melody with optional variations and structure.
    
    This is the main entry point for the melody engine.
    
    Args:
        scale: Scale type (minor, major, harmonic_minor, pentatonic_minor, etc.)
        bars: Number of bars for the melody
        variation_types: List of variation types to apply (optional)
        phrase_structure: Phrase structure type (optional)
        contour: Melodic contour type (optional)
        seed: Random seed for reproducibility (optional)
    
    Returns:
        Dictionary containing:
        - theme: Main theme notes
        - variations: List of variations if requested
        - combined_notes: All notes for Ableton
        - metadata: Info about the generation
    """
    engine = MelodyEngine(seed=seed)
    
    # Generate theme
    theme = engine.generate_theme(scale=scale, bars=bars // 2 if bars > 4 else bars)
    
    result = {
        "theme": engine.motive_to_ableton_format(theme),
        "variations": [],
        "combined_notes": [],
        "metadata": {
            "scale": scale,
            "bars": bars,
            "theme_duration": theme.get_total_duration()
        }
    }
    
    # Apply contour if specified
    if contour:
        themed_notes = engine.apply_melodic_contour(theme.notes, contour)
        theme = Motive(themed_notes, theme.name)
        result["metadata"]["contour"] = contour
    
    all_notes = list(theme.notes)
    
    # Generate variations if requested
    if variation_types:
        for i, var_type in enumerate(variation_types):
            try:
                variation = engine.create_variation(theme, var_type)
                
                # Offset variation in time
                offset = theme.get_total_duration() * (i + 1)
                for note in variation.notes:
                    note.start_time += offset
                
                result["variations"].append({
                    "type": var_type,
                    "notes": engine.motive_to_ableton_format(variation)
                })
                all_notes.extend(variation.notes)
            except ValueError:
                # Invalid variation type, skip
                continue
    
    # Generate phrase structure if specified
    if phrase_structure:
        try:
            phrases = engine.generate_phrase_structure(
                phrase_structure,
                theme=theme,
                scale=scale,
                bars=bars
            )
            result["phrase_structure"] = {
                "type": phrase_structure,
                "phrases": [engine.motive_to_ableton_format(p) for p in phrases]
            }
            result["metadata"]["phrase_structure"] = phrase_structure
        except ValueError:
            pass
    
    # Combine all notes and sort by start time
    all_notes.sort(key=lambda n: n.start_time)
    result["combined_notes"] = engine.notes_to_ableton_format(all_notes)
    
    return result


# Module exports
__all__ = [
    'MelodyEngine',
    'Motive',
    'Note',
    'VariationType',
    'PhraseStructureType',
    'ContourType',
    'generate_motivic_melody',
]
