"""
pattern_library.py - Biblioteca de patrones musicales profesionales para reggaeton

Contiene patrones de dembow, bajos, progresiones de acordes, generadores de melodías
y utilidades para humanización.

Timing en beats (float), reggaeton típicamente 4/4 @ 90-100 BPM
"""

import random
from typing import List, Tuple, Optional, Dict, Any
from dataclasses import dataclass
from enum import Enum


@dataclass
class NoteEvent:
    """Representa un evento de nota MIDI"""
    pitch: int
    start_time: float  # En beats
    duration: float      # En beats
    velocity: int        # 0-127
    
    def copy(self) -> 'NoteEvent':
        return NoteEvent(self.pitch, self.start_time, self.duration, self.velocity)


class ScaleType(Enum):
    MINOR = "minor"
    MAJOR = "major"
    PENTATONIC_MINOR = "pentatonic_minor"
    BLUES = "blues"


class DembowPatterns:
    """
    Patrones de dembow profesionales para reggaeton.
    El dembow es el ritmo característico del reggaeton.
    """
    
    # Notas MIDI estándar para drums
    KICK_NOTE = 36      # C1
    SNARE_NOTE = 38     # D1
    HIHAT_CLOSED = 42   # F#1
    HIHAT_OPEN = 46     # A#1
    CLAP_NOTE = 39      # D#1
    RIMSHOT_NOTE = 37   # C#1
    
    # Tiempos de dembow en beats (cada beat = 1 cuarto nota)
    # Patrón clásico: kick en 1, snare en 2.25 y 4, etc.
    
    @staticmethod
    def get_kick_pattern(bars: int = 16, variation: str = "standard") -> List[NoteEvent]:
        """
        Genera patrón de kick/bombo.
        
        Variaciones:
        - standard: Patrón dembow clásico
        - double: Doble tiempo en ciertos beats
        - triple: Patrón tresillo
        - minimal: Menos kicks, más espacio
        """
        notes = []
        beat_duration = 0.25  # 1/16 nota = 0.25 beats
        
        if variation == "standard":
            # Dembow clásico: kick en 1, 3, 4.25, 4.75 de cada compás
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Kick en tiempo 1 (beat 0 del compás)
                notes.append(NoteEvent(
                    DembowPatterns.KICK_NOTE,
                    bar_offset + 0.0,
                    0.25,
                    120
                ))
                # Kick en tiempo 3 (beat 2 del compás)
                notes.append(NoteEvent(
                    DembowPatterns.KICK_NOTE,
                    bar_offset + 2.0,
                    0.25,
                    110
                ))
                # Kick ghost en 4.25 (anticipación)
                notes.append(NoteEvent(
                    DembowPatterns.KICK_NOTE,
                    bar_offset + 3.25,
                    0.125,
                    80
                ))
                # Kick en 4.75 (cierre)
                notes.append(NoteEvent(
                    DembowPatterns.KICK_NOTE,
                    bar_offset + 3.75,
                    0.125,
                    90
                ))
                
        elif variation == "double":
            # Más kicks, doble tiempo en ciertos momentos
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Kick fuerte en 1
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 0.0, 0.25, 127))
                # Kick en off-beat
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 0.75, 0.125, 100))
                # Kick en 2.5
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 1.5, 0.25, 115))
                # Kick en 3
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 2.0, 0.25, 120))
                # Kick en off-beat 3
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 2.75, 0.125, 95))
                # Dos kicks rápidos al final
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 3.25, 0.125, 90))
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 3.5, 0.125, 100))
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 3.75, 0.125, 110))
                
        elif variation == "triple":
            # Patrón tresillo más complejo
            tresillo_interval = 4.0 / 3.0  # Tresillo = 1.333 beats
            for bar in range(bars):
                bar_offset = bar * 4.0
                for i in range(3):
                    notes.append(NoteEvent(
                        DembowPatterns.KICK_NOTE,
                        bar_offset + (i * tresillo_interval),
                        0.3,
                        120 if i == 0 else 100
                    ))
                # Kick adicional en el último 16vo
                notes.append(NoteEvent(
                    DembowPatterns.KICK_NOTE,
                    bar_offset + 3.75,
                    0.125,
                    90
                ))
                
        elif variation == "minimal":
            # Estilo minimal, menos es más
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Solo kick en 1 y 3
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 0.0, 0.25, 125))
                if bar % 2 == 0:  # Cada dos compases
                    notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 2.0, 0.25, 110))
                    # Sub-bajo sutil en 4
                    notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 3.5, 0.25, 85))
                    
        else:
            raise ValueError(f"Variación de kick no válida: {variation}")
            
        return notes
    
    @staticmethod
    def get_snare_pattern(bars: int = 16, variation: str = "standard") -> List[NoteEvent]:
        """
        Genera patrón de snare/caja.
        
        El dembow clásico tiene snare en 2.25 (beat 2 + 1/4) y 4.
        """
        notes = []
        
        if variation == "standard":
            # Snare clásico dembow: tiempo 2.25 y 4
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Snare principal en 2.25 (el característico)
                notes.append(NoteEvent(
                    DembowPatterns.SNARE_NOTE,
                    bar_offset + 1.25,  # Beat 2 + 1/4
                    0.15,
                    115
                ))
                # Snare en 4
                notes.append(NoteEvent(
                    DembowPatterns.SNARE_NOTE,
                    bar_offset + 3.0,
                    0.2,
                    120
                ))
                # Ghost note sutil en 2.75
                if bar % 2 == 1:  # Cada dos compases
                    notes.append(NoteEvent(
                        DembowPatterns.RIMSHOT_NOTE,
                        bar_offset + 1.75,
                        0.1,
                        70
                    ))
                    
        elif variation == "double":
            # Más snares, estilo más agresivo
            for bar in range(bars):
                bar_offset = bar * 4.0
                notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 1.0, 0.15, 110))
                notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 1.25, 0.15, 120))
                notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 3.0, 0.2, 125))
                # Roll en el último beat
                notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 3.5, 0.1, 100))
                notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 3.75, 0.1, 90))
                
        elif variation == "triple":
            # Patrón tresillo para snare
            tresillo_offsets = [1.0, 2.333, 3.666]
            for bar in range(bars):
                bar_offset = bar * 4.0
                for i, offset in enumerate(tresillo_offsets):
                    notes.append(NoteEvent(
                        DembowPatterns.SNARE_NOTE,
                        bar_offset + offset,
                        0.2,
                        115
                    ))
                    
        elif variation == "minimal":
            # Snare minimalista
            for bar in range(bars):
                bar_offset = bar * 4.0
                notes.append(NoteEvent(
                    DembowPatterns.SNARE_NOTE,
                    bar_offset + 1.25,
                    0.15,
                    110
                ))
                # Solo en compases pares el segundo snare
                if bar % 2 == 0:
                    notes.append(NoteEvent(
                        DembowPatterns.SNARE_NOTE,
                        bar_offset + 3.0,
                        0.2,
                        105
                    ))
                    
        return notes
    
    @staticmethod
    def get_hihat_pattern(bars: int = 16, style: str = "8th", swing: float = 0.6) -> List[NoteEvent]:
        """
        Genera patrón de hi-hats.
        
        Estilos: "8th", "16th", "32nd", "open", "pedal"
        Swing: 0.0-1.0, donde 0.5 es recto, >0.5 es swingado
        """
        notes = []
        
        # Factor de swing: cuánto se retrasa el off-beat
        swing_amount = (swing - 0.5) * 0.5  # Rango -0.25 a +0.25
        
        if style == "8th":
            # Corcheas: en cada 1/2 beat
            for bar in range(bars):
                bar_offset = bar * 4.0
                for eighth in range(8):
                    beat_pos = bar_offset + (eighth * 0.5)
                    # Aplicar swing a los off-beats (impares)
                    if eighth % 2 == 1:
                        beat_pos += swing_amount
                    
                    # Dinámica: acentos en 2 y 4
                    velocity = 100
                    if eighth in [2, 6]:  # Tiempos 1.0 y 3.0 (beats 2 y 4)
                        velocity = 115
                    elif eighth in [0, 4]:  # Downbeats
                        velocity = 110
                    else:
                        velocity = 90
                        
                    notes.append(NoteEvent(
                        DembowPatterns.HIHAT_CLOSED,
                        beat_pos,
                        0.1,
                        velocity
                    ))
                    
        elif style == "16th":
            # Semicorcheas: más denso
            for bar in range(bars):
                bar_offset = bar * 4.0
                for sixteenth in range(16):
                    beat_pos = bar_offset + (sixteenth * 0.25)
                    # Swing en off-beats
                    if sixteenth % 2 == 1:
                        beat_pos += swing_amount * 0.5
                    
                    # Pattern de velocidades tipo "trap"
                    if sixteenth % 4 == 0:  # Cuartos
                        velocity = 110
                    elif sixteenth % 2 == 0:  # Octavas
                        velocity = 95
                    else:  # 16avos
                        velocity = 85
                        
                    notes.append(NoteEvent(
                        DembowPatterns.HIHAT_CLOSED,
                        beat_pos,
                        0.08,
                        velocity
                    ))
                    
        elif style == "32nd":
            # Fusas: muy denso, estilo moderno
            for bar in range(bars):
                bar_offset = bar * 4.0
                for i in range(32):
                    beat_pos = bar_offset + (i * 0.125)
                    # Roll de 32avos en el último beat
                    if i >= 28:
                        velocity = 100 + (i - 28) * 5  # Crescendo
                    else:
                        velocity = 80 if i % 2 == 1 else 70
                        
                    notes.append(NoteEvent(
                        DembowPatterns.HIHAT_CLOSED,
                        beat_pos,
                        0.05,
                        velocity
                    ))
                    
        elif style == "open":
            # Hi-hat abierto en ciertos tiempos
            open_times = [1.5, 3.5]  # Off-beats de 2 y 4
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Cerrados en corcheas
                for eighth in range(8):
                    beat_pos = bar_offset + (eighth * 0.5)
                    if eighth % 2 == 1:
                        beat_pos += swing_amount
                    
                    # Verificar si es tiempo de abierto
                    time_in_bar = eighth * 0.5
                    if any(abs(time_in_bar - ot) < 0.01 for ot in open_times):
                        # Hi-hat abierto
                        notes.append(NoteEvent(
                            DembowPatterns.HIHAT_OPEN,
                            beat_pos,
                            0.3,  # Más largo
                            110
                        ))
                    else:
                        notes.append(NoteEvent(
                            DembowPatterns.HIHAT_CLOSED,
                            beat_pos,
                            0.1,
                            100
                        ))
                        
        elif style == "pedal":
            # Estilo pedal - más sutil
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Solo en corcheas pares, suave
                for eighth in [0, 2, 4, 6]:
                    beat_pos = bar_offset + (eighth * 0.5)
                    notes.append(NoteEvent(
                        DembowPatterns.HIHAT_CLOSED,
                        beat_pos,
                        0.15,
                        75
                    ))
                    
        return notes


class BassPatterns:
    """
    Patrones de bajo sub para reggaeton profesional.
    """
    
    # Notas MIDI para bajo (C1 = 36, generalmente)
    
    @staticmethod
    def get_bass_line(bars: int = 16, progression: List[str] = None, 
                      key: str = "A", style: str = "sub") -> List[NoteEvent]:
        """
        Genera línea de bajo.
        
        Progresión: lista de nombres de acordes (ej: ["Am", "F", "C", "G"])
        Estilos:
        - sub: Sub-bajos largos y profundos
        - sustained: Notas sostenidas con release largo
        - pluck: Notas cortas y percusivas
        - slide: Con slides entre notas
        """
        notes = []
        
        if progression is None:
            # Progresión por defecto: vi-IV-I-V
            progression = ["Am", "F", "C", "G"]
        
        # Convertir acordes a notas raíz (MIDI)
        root_notes = BassPatterns._chords_to_roots(progression, key)
        
        # Duración por acorde
        beats_per_chord = 4.0 * bars / len(progression)
        
        if style == "sub":
            # Sub-bajos: notas largas en raíz
            for i, root in enumerate(root_notes):
                start = i * beats_per_chord
                duration = beats_per_chord * 0.9  # Dejar espacio al final
                
                # Octava baja para sub
                pitch = root - 12  # Una octava abajo
                
                notes.append(NoteEvent(pitch, start, duration, 110))
                
                # Ghost note en quinta para rellenar
                if i % 2 == 0:
                    fifth = pitch + 7
                    notes.append(NoteEvent(fifth, start + duration * 0.5, 0.25, 70))
                    
        elif style == "sustained":
            # Notas sostenidas con release
            for i, root in enumerate(root_notes):
                start = i * beats_per_chord
                duration = beats_per_chord  # Llenar todo
                
                pitch = root - 12
                
                # Velocidad con acento en el inicio
                notes.append(NoteEvent(pitch, start, duration, 120))
                
                # Octava arriba para relleno armónico
                notes.append(NoteEvent(pitch + 12, start + 0.5, duration - 0.5, 90))
                
        elif style == "pluck":
            # Notas cortas y percusivas
            for i, root in enumerate(root_notes):
                start = i * beats_per_chord
                # Dos notas por acorde
                pitch = root - 12
                
                # Nota principal
                notes.append(NoteEvent(pitch, start, 0.25, 115))
                # Octava arriba, staccato
                notes.append(NoteEvent(pitch + 12, start + 0.5, 0.15, 100))
                
                # Off-beat adicional
                notes.append(NoteEvent(pitch, start + beats_per_chord * 0.75, 0.2, 90))
                
        elif style == "slide":
            # Con slides/portamento entre notas
            for i, root in enumerate(root_notes):
                start = i * beats_per_chord
                pitch = root - 12
                
                # Nota principal larga
                notes.append(NoteEvent(pitch, start, beats_per_chord * 0.8, 110))
                
                # Slide a la siguiente nota
                if i < len(root_notes) - 1:
                    next_pitch = root_notes[i + 1] - 12
                    slide_start = start + beats_per_chord * 0.8
                    slide_duration = beats_per_chord * 0.2
                    # Nota de slide (usamos nota de paso)
                    if next_pitch > pitch:
                        slide_note = pitch + 1  # Semitono arriba
                    else:
                        slide_note = pitch - 1  # Semitono abajo
                    notes.append(NoteEvent(slide_note, slide_start, slide_duration, 80))
                    
        return notes
    
    @staticmethod
    def _chords_to_roots(progression: List[str], key: str) -> List[int]:
        """Convierte nombres de acordes a notas MIDI raíz"""
        # Notas base en octava 4 (C4 = 60)
        note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
        
        # Encontrar offset del key
        if key in note_names:
            key_offset = note_names.index(key)
        else:
            key_offset = 9  # Default A
            
        # C4 = 60, así que A3 = 57
        base_note = 57 + key_offset  # A3 por defecto si key=A
        
        # Intervalos para acordes (relativos a la tonalidad)
        roman_intervals = {
            "I": 0, "i": 0,
            "II": 2, "ii": 2,
            "III": 4, "iii": 4,
            "IV": 5, "iv": 5,
            "V": 7, "v": 7,
            "VI": 9, "vi": 9,
            "VII": 11, "vii": 11,
        }
        
        roots = []
        for chord in progression:
            # Extraer nota base del nombre del acorde
            if len(chord) >= 2 and chord[1] in ["#", "b"]:
                chord_root = chord[:2]
                quality = chord[2:]
            else:
                chord_root = chord[:1]
                quality = chord[1:]
                
            # Convertir a número de nota
            if chord_root in note_names:
                root_num = note_names.index(chord_root)
            elif chord_root.upper() in roman_intervals:
                root_num = (base_note % 12 + roman_intervals[chord_root.upper()]) % 12
            else:
                root_num = base_note % 12
                
            # Construir nota MIDI completa (octava 3)
            midi_note = 48 + root_num  # C3 base
            if midi_note < base_note - 12:
                midi_note += 12
                
            roots.append(midi_note)
            
        return roots


class ChordProgressions:
    """
    Progresiones de acordes estándar para reggaeton.
    """
    
    # Progresiones predefinidas (notas como números romanos o nombres)
    PROGRESSIONS = {
        "vi-IV-I-V": ["Am", "F", "C", "G"],
        "i-VI-VII": ["Am", "F", "G"],
        "i-iv-VII-VI": ["Am", "Dm", "G", "F"],
        "i-VI-III-VII": ["Am", "F", "C", "G"],
        "ii-V-I": ["Dm", "G", "C"],
        "I-V-vi-IV": ["C", "G", "Am", "F"],
        "vi-V-IV-III": ["Am", "G", "F", "E"],
        "i-VII-VI-VII": ["Am", "G", "F", "G"],  # Muy común en reggaeton
    }
    
    # Estructuras de acordes (triadas)
    CHORD_VOICINGS = {
        "major": [0, 4, 7],      # 1, 3, 5
        "minor": [0, 3, 7],      # 1, b3, 5
        "dim": [0, 3, 6],        # 1, b3, b5
        "aug": [0, 4, 8],        # 1, 3, #5
        "maj7": [0, 4, 7, 11],   # 1, 3, 5, 7
        "min7": [0, 3, 7, 10],   # 1, b3, 5, b7
        "dom7": [0, 4, 7, 10],   # 1, 3, 5, b7
        "sus4": [0, 5, 7],       # 1, 4, 5
    }
    
    @staticmethod
    def get_progression(name: str, key: str = "A", bars: int = 16) -> List[Dict[str, Any]]:
        """
        Obtiene progresión de acordes con timing.
        
        Retorna lista de dicts con: chord_name, root_pitch, notes, start_beat, duration
        """
        if name in ChordProgressions.PROGRESSIONS:
            chord_names = ChordProgressions.PROGRESSIONS[name]
        else:
            chord_names = name.split("-")
        
        # Convertir a notas
        result = []
        beats_per_chord = 4.0 * bars / len(chord_names)
        
        note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
        key_offset = note_names.index(key) if key in note_names else 9  # Default A
        base_note = 57  # A3
        
        for i, chord_name in enumerate(chord_names):
            # Parsear nombre de acorde
            if len(chord_name) >= 2 and chord_name[1] in ["#", "b"]:
                root_name = chord_name[:2]
                quality = chord_name[2:]
            else:
                root_name = chord_name[:1]
                quality = chord_name[1:]
                
            # Encontrar nota raíz
            if root_name in note_names:
                root_num = note_names.index(root_name)
            else:
                root_num = key_offset
                
            # Ajustar a octava apropiada
            root_pitch = 48 + root_num  # C3 base
            if root_pitch < base_note - 12:
                root_pitch += 12
                
            # Determinar calidad
            if quality in ["m", "min", "minor", "-"]:
                voicing = "min7"
            elif quality in ["7", "dom"]:
                voicing = "dom7"
            elif quality in ["maj7", "M7"]:
                voicing = "maj7"
            elif quality == "sus4":
                voicing = "sus4"
            elif quality in ["dim", "°"]:
                voicing = "dim"
            else:
                voicing = "min7" if "m" in quality else "dom7"
                
            # Construir notas del acorde
            intervals = ChordProgressions.CHORD_VOICINGS.get(voicing, ChordProgressions.CHORD_VOICINGS["minor"])
            chord_notes = [root_pitch + interval for interval in intervals]
            
            # Voicing en posición cercana (inversiones)
            chord_notes = ChordProgressions._optimize_voicing(chord_notes)
            
            result.append({
                "chord_name": chord_name,
                "root_pitch": root_pitch,
                "notes": chord_notes,
                "start_beat": i * beats_per_chord,
                "duration": beats_per_chord,
                "voicing": voicing
            })
            
        return result
    
    @staticmethod
    def _optimize_voicing(notes: List[int]) -> List[int]:
        """Optimiza voicing para que las notas estén cerca entre sí"""
        if len(notes) <= 1:
            return notes
            
        # Asegurar que todas las notas estén en un rango de una octava
        result = [notes[0]]
        for note in notes[1:]:
            # Encontrar octava más cercana
            while note - result[-1] > 6:
                note -= 12
            while note - result[-1] < -6:
                note += 12
            result.append(note)
            
        return sorted(result)
    
    @staticmethod
    def get_all_progression_names() -> List[str]:
        """Retorna todos los nombres de progresiones disponibles"""
        return list(ChordProgressions.PROGRESSIONS.keys())


class MelodyGenerator:
    """
    Generador de melodías para reggaeton.
    """
    
    # Escalas (intervalos semitonos)
    SCALES = {
        "minor": [0, 2, 3, 5, 7, 8, 10],           # Natural minor
        "major": [0, 2, 4, 5, 7, 9, 11],           # Major
        "pentatonic_minor": [0, 3, 5, 7, 10],     # Pentatonic minor
        "pentatonic_major": [0, 2, 4, 7, 9],      # Pentatonic major
        "blues": [0, 3, 5, 6, 7, 10],              # Blues scale
        "dorian": [0, 2, 3, 5, 7, 9, 10],          # Dorian mode
        "phrygian": [0, 1, 3, 5, 7, 8, 10],        # Phrygian mode
        "harmonic_minor": [0, 2, 3, 5, 7, 8, 11], # Harmonic minor
    }
    
    @staticmethod
    def generate_melody(bars: int = 16, scale: str = "minor", 
                        density: float = 0.5, key: str = "A") -> List[NoteEvent]:
        """
        Genera melodía automáticamente.
        
        density: 0.0-1.0, probabilidad de nota por subdivisión
        """
        notes = []
        
        # Obtener escala
        if scale in MelodyGenerator.SCALES:
            intervals = MelodyGenerator.SCALES[scale]
        else:
            intervals = MelodyGenerator.SCALES["minor"]
            
        # Encontrar nota raíz
        note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
        key_offset = note_names.index(key) if key in note_names else 9
        root_pitch = 60 + key_offset  # C4 base
        
        # Generar notas disponibles (2 octavas)
        available_notes = []
        for octave in [0, 1]:  # 2 octavas
            for interval in intervals:
                available_notes.append(root_pitch + interval + (octave * 12))
                
        # Subdivisiones por compás según densidad
        if density < 0.3:
            subdivisions = 4  # Negras
        elif density < 0.6:
            subdivisions = 8  # Corcheas
        else:
            subdivisions = 16  # Semicorcheas
            
        subdivision_duration = 4.0 / subdivisions
        
        # Generar notas
        for bar in range(bars):
            bar_offset = bar * 4.0
            
            for sub in range(subdivisions):
                if random.random() < density:
                    start_time = bar_offset + (sub * subdivision_duration)
                    
                    # Seleccionar nota (preferir notas de acorde: 1, 3, 5)
                    if random.random() < 0.7:
                        # Nota de acorde (1, 3, 5)
                        degree = random.choice([0, 2, 4])  # Índices en escala
                        octave = random.choice([0, 1])
                        pitch = root_pitch + intervals[degree] + (octave * 12)
                    else:
                        # Cualquier nota de la escala
                        pitch = random.choice(available_notes)
                        
                    # Duración según posición
                    if sub % 4 == 0:  # Tiempo fuerte
                        duration = subdivision_duration * 2
                        velocity = 110
                    elif sub % 2 == 0:  # Semi-fuerte
                        duration = subdivision_duration * 1.5
                        velocity = 100
                    else:  # Débil
                        duration = subdivision_duration
                        velocity = 90
                        
                    notes.append(NoteEvent(pitch, start_time, duration, velocity))
                    
        # Ordenar por tiempo
        notes.sort(key=lambda n: n.start_time)
        
        # Asegurar que no haya superposiciones excesivas
        notes = MelodyGenerator._clean_overlaps(notes)
        
        return notes
    
    @staticmethod
    def _clean_overlaps(notes: List[NoteEvent]) -> List[NoteEvent]:
        """Limpia superposiciones de notas en el mismo pitch"""
        if not notes:
            return notes
            
        # Agrupar por pitch
        by_pitch = {}
        for note in notes:
            if note.pitch not in by_pitch:
                by_pitch[note.pitch] = []
            by_pitch[note.pitch].append(note)
            
        # Limpiar cada grupo
        cleaned = []
        for pitch, pitch_notes in by_pitch.items():
            pitch_notes.sort(key=lambda n: n.start_time)
            
            for i, note in enumerate(pitch_notes):
                if i > 0:
                    prev = pitch_notes[i - 1]
                    # Si se superpone, acortar la anterior
                    if prev.start_time + prev.duration > note.start_time:
                        prev.duration = note.start_time - prev.start_time
                        
            cleaned.extend(pitch_notes)
            
        # Re-ordenar
        cleaned.sort(key=lambda n: n.start_time)
        return cleaned
    
    @staticmethod
    def generate_counter_melody(main_melody: List[NoteEvent], scale: str = "minor",
                                 interval: int = 3) -> List[NoteEvent]:
        """
        Genera contramelodía a partir de melodía principal.
        
        interval: intervalo de contrapunto (3 = tercera, 6 = sexta)
        """
        counter_notes = []
        
        for note in main_melody:
            # Añadir nota a intervalo especificado
            counter_pitch = note.pitch + interval
            
            # Ajustar a escala si es necesario
            intervals = MelodyGenerator.SCALES.get(scale, MelodyGenerator.SCALES["minor"])
            root = note.pitch % 12
            target = counter_pitch % 12
            
            # Verificar si está en escala
            scale_notes = [(root + i) % 12 for i in intervals]
            if target not in scale_notes:
                # Ajustar al grado más cercano
                counter_pitch += 1 if random.random() > 0.5 else -1
                
            # Más corta y suave que la original
            counter_notes.append(NoteEvent(
                counter_pitch,
                note.start_time + 0.0625,  # Ligeramente después
                note.duration * 0.7,
                int(note.velocity * 0.75)
            ))
            
        return counter_notes


class HumanFeel:
    """
    Aplica humanización a patrones MIDI para hacerlos más naturales.
    """
    
    @staticmethod
    def apply_micro_timing(notes: List[NoteEvent], variance_ms: float = 15) -> List[NoteEvent]:
        """
        Ajusta timing de notas ±variance_ms milisegundos.
        
        Asume BPM promedio de 95 para convertir ms a beats.
        """
        bpm = 95.0
        ms_per_beat = 60000.0 / bpm  # ms por beat
        variance_beats = variance_ms / ms_per_beat
        
        result = []
        for note in notes:
            new_note = note.copy()
            # Variación aleatoria gaussiana
            offset = random.gauss(0, variance_beats)
            new_note.start_time += offset
            # Asegurar que no sea negativo
            new_note.start_time = max(0, new_note.start_time)
            result.append(new_note)
            
        return result
    
    @staticmethod
    def apply_velocity_variation(notes: List[NoteEvent], variance: int = 10) -> List[NoteEvent]:
        """
        Aplica variación de velocidad ±variance.
        """
        result = []
        for note in notes:
            new_note = note.copy()
            # Variación aleatoria
            vel_change = random.randint(-variance, variance)
            new_note.velocity = max(1, min(127, note.velocity + vel_change))
            result.append(new_note)
            
        return result
    
    @staticmethod
    def apply_length_variation(notes: List[NoteEvent], variance_percent: float = 5.0) -> List[NoteEvent]:
        """
        Aplica variación de duración ±variance_percent%.
        """
        result = []
        variance_decimal = variance_percent / 100.0
        
        for note in notes:
            new_note = note.copy()
            # Variación porcentual
            factor = 1.0 + random.uniform(-variance_decimal, variance_decimal)
            new_note.duration = max(0.01, note.duration * factor)
            result.append(new_note)
            
        return result
    
    @staticmethod
    def apply_all_humanization(notes: List[NoteEvent], 
                                timing_variance_ms: float = 15,
                                velocity_variance: int = 10,
                                length_variance_percent: float = 5.0) -> List[NoteEvent]:
        """
        Aplica todas las humanizaciones en secuencia.
        """
        result = HumanFeel.apply_micro_timing(notes, timing_variance_ms)
        result = HumanFeel.apply_velocity_variation(result, velocity_variance)
        result = HumanFeel.apply_length_variation(result, length_variance_percent)
        return result
    
    @staticmethod
    def apply_timing_bias(notes: List[NoteEvent], bias: str = "lay_back") -> List[NoteEvent]:
        """
        Aplica sesgo de timing al compás.
        
        bias: "lay_back" (detrás del beat), "ahead" (adelante), "center" (centro)
        """
        bpm = 95.0
        ms_per_beat = 60000.0 / bpm
        
        if bias == "lay_back":
            # Detrás del beat: +10-20ms
            offset_ms = random.uniform(10, 20)
        elif bias == "ahead":
            # Adelante del beat: -10-20ms
            offset_ms = random.uniform(-20, -10)
        else:
            return [n.copy() for n in notes]
            
        offset_beats = offset_ms / ms_per_beat
        
        result = []
        for note in notes:
            new_note = note.copy()
            new_note.start_time += offset_beats
            new_note.start_time = max(0, new_note.start_time)
            result.append(new_note)
            
        return result


class PercussionLibrary:
    """
    Librería de percusiones adicionales y efectos para reggaeton.
    """
    
    # Notas MIDI para percusión
    PERCUSSION_NOTES = {
        "timbal": 47,       # High floor tom
        "conga_low": 48,    # High tom
        "conga_mid": 50,    # High tom 2
        "conga_high": 45,   # Low tom
        "bongo_low": 60,    # High bongo
        "bongo_high": 61,   # Low bongo
        "claves": 75,       # Claves
        "guiro": 73,        # Short guiro
        "guiro_long": 74,   # Long guiro
        "maracas": 70,      # Maracas
        "cabasa": 69,       # Cabasa
        "tambourine": 54,   # Tambourine
        "agogo": 67,        # High agogo
        "whistle": 72,      # Whistle
        "triangle": 80,     # Triangle
        "shaker": 82,       # Shaker
        "timbale": 65,      # High timbale
        "timbale_low": 66,  # Low timbale
    }
    
    FX_NOTES = {
        "riser": 93,        # Efecto de subida
        "downer": 91,       # Efecto de bajada
        "sweep": 92,        # Sweep
        "impact": 94,       # Impacto
        "crash": 49,        # Crash cymbal
        "reverse_crash": 55,# Reverse cymbal
        "fx_hit": 95,       # Hit FX
        "noise": 96,        # Noise burst
        "sub_drop": 97,     # Sub drop
        "tape_stop": 98,    # Tape stop effect
    }
    
    @staticmethod
    def get_percussion_fill(bars: int = 4, intensity: float = 0.7) -> List[NoteEvent]:
        """
        Genera fill de percusión latina.
        
        intensity: 0.0-1.0, densidad del fill
        """
        notes = []
        
        # Instrumentos a usar según intensidad
        instruments = ["conga_mid", "conga_high", "timbale"]
        if intensity > 0.5:
            instruments.extend(["timbal", "bongo_high"])
        if intensity > 0.7:
            instruments.append("claves")
            
        # Patrón de fills típico de reggaeton
        fill_patterns = [
            # Patrón 1: Roll descendente
            [(0, "conga_high"), (0.25, "conga_mid"), (0.5, "conga_low"), (0.75, "timbale")],
            # Patrón 2: Alternado
            [(0, "conga_mid"), (0.125, "timbale"), (0.25, "conga_mid"), (0.375, "timbale"),
             (0.5, "conga_high"), (0.75, "conga_mid")],
            # Patrón 3: Tumbao
            [(0, "conga_low"), (0.5, "conga_mid"), (0.75, "conga_high"), (0.875, "conga_mid")],
        ]
        
        pattern = random.choice(fill_patterns)
        
        # Generar notas del fill
        for bar_offset_mul in range(bars):
            bar_offset = bar_offset_mul * 4.0
            
            for time_offset, instrument in pattern:
                start = bar_offset + time_offset
                pitch = PercussionLibrary.PERCUSSION_NOTES.get(instrument, 60)
                
                # Velocidad según intensidad
                base_vel = 80 + int(intensity * 40)
                velocity = min(127, base_vel + random.randint(-10, 10))
                
                notes.append(NoteEvent(pitch, start, 0.15, velocity))
                
        return notes
    
    @staticmethod
    def get_fx_hit(position: float, fx_type: str = "riser", duration: float = 2.0) -> NoteEvent:
        """
        Genera un efecto FX en posición específica.
        
        position: tiempo en beats
        fx_type: "riser", "downer", "impact", "crash", "sweep"
        duration: duración del FX en beats
        """
        pitch = PercussionLibrary.FX_NOTES.get(fx_type, 93)
        velocity = 110 if fx_type in ["impact", "crash"] else 100
        
        return NoteEvent(pitch, position, duration, velocity)
    
    @staticmethod
    def get_intro_buildup(bars: int = 4) -> List[NoteEvent]:
        """
        Genera buildup para intro (subida de tensión).
        """
        notes = []
        
        # Cada vez más denso
        for bar in range(bars):
            bar_offset = bar * 4.0
            density = (bar + 1) / bars  # 0.25, 0.5, 0.75, 1.0
            
            # Shaker cada vez más rápido
            subdivisions = int(4 + (density * 12))  # 4 a 16
            for i in range(subdivisions):
                start = bar_offset + (i * (4.0 / subdivisions))
                vel = 60 + int(density * 60)  # Crescendo
                notes.append(NoteEvent(
                    PercussionLibrary.PERCUSSION_NOTES["shaker"],
                    start, 0.05, min(127, vel)
                ))
                
        # Riser final
        notes.append(PercussionLibrary.get_fx_hit(bars * 4.0 - 2.0, "riser", 2.0))
        
        return notes
    
    @staticmethod
    def get_transition_fill(position: float, type: str = "break") -> List[NoteEvent]:
        """
        Genera fill de transición.
        
        type: "break", "build", "drop", "impact"
        """
        notes = []
        
        if type == "break":
            # Silencio seguido de impacto
            notes.append(PercussionLibrary.get_fx_hit(position + 0.5, "reverse_crash", 1.0))
            notes.append(PercussionLibrary.get_fx_hit(position + 1.0, "impact", 0.5))
            
        elif type == "build":
            # Build con congas
            for i in range(8):
                start = position + (i * 0.125)
                notes.append(NoteEvent(
                    PercussionLibrary.PERCUSSION_NOTES["conga_mid"],
                    start, 0.1, 80 + i * 5
                ))
            notes.append(PercussionLibrary.get_fx_hit(position + 1.0, "sweep", 0.5))
            
        elif type == "drop":
            # Drop con sub
            notes.append(PercussionLibrary.get_fx_hit(position, "sub_drop", 1.0))
            notes.append(PercussionLibrary.get_fx_hit(position, "crash", 1.0))
            
        elif type == "impact":
            # Impacto fuerte
            notes.append(PercussionLibrary.get_fx_hit(position, "impact", 0.8))
            notes.append(NoteEvent(
                PercussionLibrary.FX_NOTES["crash"],
                position, 1.0, 127
            ))
            
        return notes


# Funciones de conveniencia

def create_drum_pattern(style: str = "dembow", bars: int = 16, humanize: bool = True) -> Dict[str, List[NoteEvent]]:
    """
    Crea patrón completo de batería.
    
    Retorna dict con: kick, snare, hihat
    """
    dembow = DembowPatterns()
    
    kicks = dembow.get_kick_pattern(bars, variation=style if style in ["standard", "double", "triple", "minimal"] else "standard")
    snares = dembow.get_snare_pattern(bars, variation="standard")
    hihats = dembow.get_hihat_pattern(bars, style="16th", swing=0.6)
    
    if humanize:
        humanizer = HumanFeel()
        kicks = humanizer.apply_all_humanization(kicks, 10, 8, 3)
        snares = humanizer.apply_all_humanization(snares, 15, 10, 5)
        hihats = humanizer.apply_all_humanization(hihats, 5, 5, 2)
    
    return {
        "kick": kicks,
        "snare": snares,
        "hihat": hihats
    }


def create_full_arrangement(bars_per_section: int = 16, key: str = "A") -> Dict[str, Any]:
    """
    Crea arreglo completo de reggaeton.
    
    Retorna estructura con: intro, verse, chorus, bridge, outro
    """
    arrangement = {}
    
    # Progresión
    prog = ChordProgressions.get_progression("vi-IV-I-V", key, bars_per_section)
    
    # Intro
    arrangement["intro"] = {
        "drums": create_drum_pattern("minimal", bars_per_section, True),
        "bass": BassPatterns.get_bass_line(bars_per_section, ["Am", "F"], key, "sustained"),
        "chords": prog,
        "percussion": PercussionLibrary.get_intro_buildup(4)
    }
    
    # Verso
    arrangement["verse"] = {
        "drums": create_drum_pattern("standard", bars_per_section, True),
        "bass": BassPatterns.get_bass_line(bars_per_section, ["Am", "F", "C", "G"], key, "sub"),
        "chords": prog,
        "melody": MelodyGenerator.generate_melody(bars_per_section, "pentatonic_minor", 0.4, key)
    }
    
    # Coro
    arrangement["chorus"] = {
        "drums": create_drum_pattern("double", bars_per_section, True),
        "bass": BassPatterns.get_bass_line(bars_per_section, ["Am", "F", "C", "G"], key, "pluck"),
        "chords": prog,
        "melody": MelodyGenerator.generate_melody(bars_per_section, "minor", 0.6, key)
    }
    
    return arrangement


# Constantes útiles
NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
DRUM_NOTES = {
    "kick": 36,
    "snare": 38,
    "clap": 39,
    "rim": 37,
    "hihat_closed": 42,
    "hihat_open": 46,
    "hihat_pedal": 44,
    "crash": 49,
    "ride": 51,
    "tom1": 50,
    "tom2": 47,
    "tom3": 43,
}


def notes_to_dict_list(notes: List[NoteEvent]) -> List[Dict[str, Any]]:
    """Convierte lista de NoteEvent a lista de diccionarios"""
    return [
        {
            "pitch": n.pitch,
            "start_time": n.start_time,
            "duration": n.duration,
            "velocity": n.velocity
        }
        for n in notes
    ]


def dict_list_to_notes(dict_list: List[Dict[str, Any]]) -> List[NoteEvent]:
    """Convierte lista de diccionarios a lista de NoteEvent"""
    return [
        NoteEvent(
            d["pitch"],
            d["start_time"],
            d["duration"],
            d["velocity"]
        )
        for d in dict_list
    ]


def get_patterns(pattern_type: str, **kwargs) -> Any:
    """
    Función conveniencia para obtener patrones musicales.
    
    Args:
        pattern_type: Tipo de patrón ('drum', 'bass', 'chords', 'melody', 'percussion', 'arrangement')
        **kwargs: Argumentos específicos para cada tipo de patrón
        
    Returns:
        Patrón solicitado del tipo especificado
        
    Examples:
        >>> get_patterns('drum', style='dembow', bars=16)
        >>> get_patterns('bass', progression=['Am', 'F', 'C', 'G'], key='A', style='sub')
        >>> get_patterns('chords', progression_type='vi-IV-I-V', key='A', bars=16)
    """
    if pattern_type == "drum":
        return create_drum_pattern(**kwargs)
    elif pattern_type == "bass":
        return BassPatterns.get_bass_line(**kwargs)
    elif pattern_type == "chords":
        return ChordProgressions.get_progression(**kwargs)
    elif pattern_type == "melody":
        return MelodyGenerator.generate_melody(**kwargs)
    elif pattern_type == "percussion":
        return PercussionLibrary.get_layered_percussion(**kwargs)
    elif pattern_type == "arrangement":
        return create_full_arrangement(**kwargs)
    else:
        raise ValueError(f"Tipo de patrón no soportado: {pattern_type}")
