"""
reggaeton_patterns.py - Biblioteca de patrones rítmicos avanzados para reggaeton

Implementa patrones especializados:
- Dembow clásico y variaciones
- Moombahton (más lento y pesado)
- Perreo acelerado (rápido, 160-180bpm feel)
- Trapeton (hi-hats en 32avos)
- Kicks sincopados (off-beat)
- Ghost notes en snare
- Open hat placement

Cada patrón retorna lista de eventos rímicos con position, velocity y sample_type.
"""

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


@dataclass
class RhythmicEvent:
    """Evento rítmico con posición, velocidad y tipo de sample."""
    position: float  # Posición en beats (float)
    velocity: int    # Velocidad MIDI 0-127
    sample_type: str # Tipo: kick, snare, hihat_closed, hihat_open, clap, percussion, etc.
    
    def to_dict(self) -> Dict[str, Any]:
        """Convertir a diccionario para serialización."""
        return {
            "position": self.position,
            "velocity": self.velocity,
            "sample_type": self.sample_type
        }


class PatternType(Enum):
    """Tipos de patrones rítmicos disponibles."""
    DEMBOW_CLASSIC = "dembow_classic"
    MOOMBAHTON = "moombahton"
    PERREO_ACELERADO = "perreo_acelerado"
    TRAPETON = "trapeton"
    SYNCOPATED_KICK = "syncopated_kick"
    GHOST_SNARE = "ghost_snare"
    OPEN_HAT = "open_hat"


class ReggaetonPatterns:
    """
    Biblioteca de patrones rítmicos avanzados para reggaeton.
    
    Cada método retorna una lista de RhythmicEvent que puede ser
    convertida a notas MIDI o usada para triggerar samples.
    """
    
    # Constantes de notas MIDI para drums
    KICK_NOTE = 36
    SNARE_NOTE = 38
    HIHAT_CLOSED = 42
    HIHAT_OPEN = 46
    CLAP_NOTE = 39
    RIMSHOT_NOTE = 37
    
    # Velocidades por defecto
    VEL_STRONG = 120
    VEL_MEDIUM = 100
    VEL_SOFT = 80
    VEL_GHOST = 60
    
    @staticmethod
    def get_dembow_classic(bars: int = 4, intensity: str = "standard") -> List[RhythmicEvent]:
        """
        Patrón dembow clásico - El ritmo característico del reggaeton.
        
        Args:
            bars: Número de compases (default 4)
            intensity: "minimal", "standard", "intense"
            
        Returns:
            Lista de RhythmicEvent con el patrón completo
        """
        events = []
        
        for bar in range(bars):
            bar_offset = bar * 4.0
            
            # Kicks clásicos dembow: tiempos 1 y 3
            events.append(RhythmicEvent(bar_offset + 0.0, 120, "kick"))
            events.append(RhythmicEvent(bar_offset + 2.0, 110, "kick"))
            
            # Kick anticipación en 4.25 (el característico)
            events.append(RhythmicEvent(bar_offset + 3.25, 90, "kick"))
            
            # Snares en 2.25 y 4 (dembow clásico)
            if intensity == "minimal":
                events.append(RhythmicEvent(bar_offset + 3.0, 115, "snare"))
            else:
                events.append(RhythmicEvent(bar_offset + 1.25, 110, "snare"))
                events.append(RhythmicEvent(bar_offset + 3.0, 120, "snare"))
            
            # Hi-hats cerrados en 16avos
            for i in range(16):
                pos = bar_offset + (i * 0.25)
                # Acentos en tiempos fuertes
                vel = 100 if i % 4 == 0 else 80
                events.append(RhythmicEvent(pos, vel, "hihat_closed"))
            
            # Open hat en el "and" del 2 (2.5)
            if intensity in ("standard", "intense"):
                events.append(RhythmicEvent(bar_offset + 1.5, 100, "hihat_open"))
            
            # Extra snare/ghost notes para intense
            if intensity == "intense":
                events.append(RhythmicEvent(bar_offset + 1.75, 70, "snare"))  # Ghost
                events.append(RhythmicEvent(bar_offset + 3.5, 100, "snare"))
        
        return events
    
    @staticmethod
    def get_moombahton_pattern(bars: int = 4, heaviness: str = "medium") -> List[RhythmicEvent]:
        """
        Patrón moombahton - Más lento y pesado, típicamente 100-110 BPM.
        Mezcla de reggaeton y house, con énfasis en el bajo.
        
        Args:
            bars: Número de compases
            heaviness: "light", "medium", "heavy"
            
        Returns:
            Lista de RhythmicEvent
        """
        events = []
        
        # Velocidades según heaviness
        kick_vel = {"light": 110, "medium": 125, "heavy": 127}[heaviness]
        snare_vel = {"light": 100, "medium": 115, "heavy": 125}[heaviness]
        
        for bar in range(bars):
            bar_offset = bar * 4.0
            
            # Kicks en 1, 2.5, 3 - patrón más espaciado y pesado
            events.append(RhythmicEvent(bar_offset + 0.0, kick_vel, "kick"))
            events.append(RhythmicEvent(bar_offset + 1.5, int(kick_vel * 0.9), "kick"))
            events.append(RhythmicEvent(bar_offset + 2.0, kick_vel, "kick"))
            
            # Extra sub-kick para "heavy"
            if heaviness == "heavy":
                events.append(RhythmicEvent(bar_offset + 3.5, 100, "kick"))
            
            # Snares en 2 y 4 (house-style, más recto que dembow)
            events.append(RhythmicEvent(bar_offset + 1.0, snare_vel, "snare"))
            events.append(RhythmicEvent(bar_offset + 3.0, snare_vel, "snare"))
            
            # Claps en 2 y 4 (layer opcional)
            if heaviness in ("medium", "heavy"):
                events.append(RhythmicEvent(bar_offset + 1.0, int(snare_vel * 0.8), "clap"))
                events.append(RhythmicEvent(bar_offset + 3.0, int(snare_vel * 0.8), "clap"))
            
            # Hi-hats más espaciados (8vos)
            for i in range(8):
                pos = bar_offset + (i * 0.5)
                vel = 90 if i % 2 == 0 else 75
                events.append(RhythmicEvent(pos, vel, "hihat_closed"))
            
            # Open hats para dar aire
            if bar % 2 == 0:  # Cada dos compases
                events.append(RhythmicEvent(bar_offset + 3.5, 100, "hihat_open"))
        
        return events
    
    @staticmethod
    def get_perreo_acelerado(bars: int = 4, fill_density: str = "medium") -> List[RhythmicEvent]:
        """
        Patrón perreo acelerado - Ritmo rápido con feel de 160-180 BPM.
        Intenso, para momentos de alta energía.
        
        Args:
            bars: Número de compases
            fill_density: "low", "medium", "high" - cantidad de fills
            
        Returns:
            Lista de RhythmicEvent
        """
        events = []
        
        for bar in range(bars):
            bar_offset = bar * 4.0
            
            # Kicks rápidos - doble tiempo feel
            events.append(RhythmicEvent(bar_offset + 0.0, 127, "kick"))
            events.append(RhythmicEvent(bar_offset + 0.75, 100, "kick"))
            events.append(RhythmicEvent(bar_offset + 1.5, 110, "kick"))
            events.append(RhythmicEvent(bar_offset + 2.0, 120, "kick"))
            events.append(RhythmicEvent(bar_offset + 2.75, 95, "kick"))
            events.append(RhythmicEvent(bar_offset + 3.25, 105, "kick"))
            events.append(RhythmicEvent(bar_offset + 3.75, 115, "kick"))
            
            # Snares rápidos
            events.append(RhythmicEvent(bar_offset + 1.0, 120, "snare"))
            events.append(RhythmicEvent(bar_offset + 1.25, 90, "snare"))  # Ghost
            events.append(RhythmicEvent(bar_offset + 3.0, 125, "snare"))
            events.append(RhythmicEvent(bar_offset + 3.25, 85, "snare"))  # Ghost
            
            # Hi-hats en 16avos fuertes
            for i in range(16):
                pos = bar_offset + (i * 0.25)
                vel = 110 if i % 4 == 0 else 90
                events.append(RhythmicEvent(pos, vel, "hihat_closed"))
            
            # Fills según densidad
            if fill_density == "high" or (fill_density == "medium" and bar % 2 == 1):
                # Snare fill en último beat
                events.append(RhythmicEvent(bar_offset + 3.5, 110, "snare"))
                events.append(RhythmicEvent(bar_offset + 3.625, 95, "snare"))
                events.append(RhythmicEvent(bar_offset + 3.75, 100, "snare"))
                events.append(RhythmicEvent(bar_offset + 3.875, 90, "snare"))
        
        return events
    
    @staticmethod
    def get_trapeton_pattern(bars: int = 4, hat_speed: str = "32nd") -> List[RhythmicEvent]:
        """
        Patrón trapeton - Mezcla de reggaeton con trap.
        Hi-hats en 32avos para efecto de "roll".
        
        Args:
            bars: Número de compases
            hat_speed: "16th", "32nd", "64th_triplet"
            
        Returns:
            Lista de RhythmicEvent
        """
        events = []
        
        for bar in range(bars):
            bar_offset = bar * 4.0
            
            # Kicks trap-style (más espaciados, con 808 feel)
            events.append(RhythmicEvent(bar_offset + 0.0, 120, "kick"))
            events.append(RhythmicEvent(bar_offset + 0.75, 100, "kick"))
            events.append(RhythmicEvent(bar_offset + 2.5, 110, "kick"))
            
            # Snares en 2.25 y 4 (dembow) + extra en 3 para trap feel
            events.append(RhythmicEvent(bar_offset + 1.25, 115, "snare"))
            events.append(RhythmicEvent(bar_offset + 3.0, 120, "snare"))
            
            # Hi-hats según velocidad seleccionada
            if hat_speed == "16th":
                for i in range(16):
                    pos = bar_offset + (i * 0.25)
                    vel = 100 if i % 4 == 0 else 80
                    events.append(RhythmicEvent(pos, vel, "hihat_closed"))
                    
            elif hat_speed == "32nd":
                # 32avos básicos
                for i in range(32):
                    pos = bar_offset + (i * 0.125)
                    vel = 95 if i % 8 == 0 else 75
                    events.append(RhythmicEvent(pos, vel, "hihat_closed"))
                
                # Rolls de 32avos destacados
                if bar % 2 == 1:
                    # Roll ascendente en último beat
                    for i in range(8):
                        pos = bar_offset + 3.0 + (i * 0.125)
                        vel = 80 + (i * 5)  # Crescendo
                        events.append(RhythmicEvent(pos, vel, "hihat_closed"))
                        
            elif hat_speed == "64th_triplet":
                # Triplets rápidos para efecto
                triplet_interval = 0.125 / 3  # ~0.0417
                for i in range(24):  # 8 triplet groups
                    pos = bar_offset + (i * triplet_interval)
                    vel = 70 if i % 3 == 0 else 60
                    events.append(RhythmicEvent(pos, vel, "hihat_closed"))
            
            # Open hats estratégicos
            events.append(RhythmicEvent(bar_offset + 1.5, 100, "hihat_open"))
            if bar % 2 == 0:
                events.append(RhythmicEvent(bar_offset + 3.5, 90, "hihat_open"))
        
        return events
    
    @staticmethod
    def get_syncopated_kick(bars: int = 4, complexity: str = "medium") -> List[RhythmicEvent]:
        """
        Patrón de kicks sincopados - Kicks en off-beats para groove.
        
        Args:
            bars: Número de compases
            complexity: "simple", "medium", "complex"
            
        Returns:
            Lista de RhythmicEvent (solo kicks para layering)
        """
        events = []
        
        for bar in range(bars):
            bar_offset = bar * 4.0
            
            if complexity == "simple":
                # Kicks básicos + uno off-beat
                events.append(RhythmicEvent(bar_offset + 0.0, 120, "kick"))
                events.append(RhythmicEvent(bar_offset + 1.5, 100, "kick"))  # Off-beat
                events.append(RhythmicEvent(bar_offset + 2.0, 110, "kick"))
                events.append(RhythmicEvent(bar_offset + 3.5, 95, "kick"))  # Off-beat
                
            elif complexity == "medium":
                # Más sincopación
                events.append(RhythmicEvent(bar_offset + 0.0, 120, "kick"))
                events.append(RhythmicEvent(bar_offset + 0.75, 90, "kick"))
                events.append(RhythmicEvent(bar_offset + 1.5, 100, "kick"))
                events.append(RhythmicEvent(bar_offset + 2.0, 115, "kick"))
                events.append(RhythmicEvent(bar_offset + 2.75, 85, "kick"))
                events.append(RhythmicEvent(bar_offset + 3.25, 95, "kick"))
                events.append(RhythmicEvent(bar_offset + 3.75, 100, "kick"))
                
            elif complexity == "complex":
                # Máxima sincopación - casi latin jazz
                kick_positions = [
                    (0.0, 120), (0.5, 85), (0.75, 90),
                    (1.25, 80), (1.5, 100), (1.875, 75),
                    (2.0, 115), (2.5, 90), (2.75, 85),
                    (3.0, 100), (3.25, 95), (3.5, 90), (3.75, 85)
                ]
                for pos, vel in kick_positions:
                    events.append(RhythmicEvent(bar_offset + pos, vel, "kick"))
        
        return events
    
    @staticmethod
    def get_ghost_snare_pattern(bars: int = 4, ghost_density: str = "medium") -> List[RhythmicEvent]:
        """
        Patrón de ghost notes en snare - Notas sutiles para groove humano.
        
        Args:
            bars: Número de compases
            ghost_density: "low", "medium", "high"
            
        Returns:
            Lista de RhythmicEvent (snares principales + ghosts)
        """
        events = []
        
        ghost_vel = {"low": 50, "medium": 60, "high": 70}[ghost_density]
        
        for bar in range(bars):
            bar_offset = bar * 4.0
            
            # Snares principales (siempre presentes)
            events.append(RhythmicEvent(bar_offset + 1.25, 115, "snare"))
            events.append(RhythmicEvent(bar_offset + 3.0, 120, "snare"))
            
            # Ghost notes según densidad
            if ghost_density == "low":
                # Solo una ghost note
                events.append(RhythmicEvent(bar_offset + 1.75, ghost_vel, "snare"))
                
            elif ghost_density == "medium":
                # Ghost notes en off-beats
                events.append(RhythmicEvent(bar_offset + 0.75, ghost_vel, "snare"))
                events.append(RhythmicEvent(bar_offset + 1.75, int(ghost_vel * 1.1), "snare"))
                events.append(RhythmicEvent(bar_offset + 2.75, ghost_vel, "snare"))
                events.append(RhythmicEvent(bar_offset + 3.5, int(ghost_vel * 0.9), "snare"))
                
            elif ghost_density == "high":
                # Muchas ghost notes - casi drum solo
                ghost_positions = [
                    (0.5, 0.8), (0.75, 1.0), (1.5, 0.9),
                    (1.75, 1.1), (2.25, 0.85), (2.5, 0.7),
                    (2.75, 1.0), (3.25, 0.9), (3.5, 0.8), (3.75, 0.75)
                ]
                for pos, mult in ghost_positions:
                    events.append(RhythmicEvent(
                        bar_offset + pos,
                        int(ghost_vel * mult),
                        "snare"
                    ))
        
        return events
    
    @staticmethod
    def get_open_hat_placement(bars: int = 4, style: str = "dembow") -> List[RhythmicEvent]:
        """
        Retorna posiciones óptimas para open hi-hats según estilo.
        
        Args:
            bars: Número de compases
            style: "dembow", "moombahton", "trap", "minimal"
            
        Returns:
            Lista de RhythmicEvent (solo open hats)
        """
        events = []
        
        for bar in range(bars):
            bar_offset = bar * 4.0
            
            if style == "dembow":
                # Open hat en el "and" del 2 (característico)
                events.append(RhythmicEvent(bar_offset + 1.5, 100, "hihat_open"))
                # Otro en el 4.5 cada dos compases
                if bar % 2 == 1:
                    events.append(RhythmicEvent(bar_offset + 3.5, 90, "hihat_open"))
                    
            elif style == "moombahton":
                # Más espaciado - house style
                events.append(RhythmicEvent(bar_offset + 3.5, 100, "hihat_open"))
                if bar % 2 == 0:
                    events.append(RhythmicEvent(bar_offset + 1.5, 85, "hihat_open"))
                    
            elif style == "trap":
                # Open hats marcando secciones
                events.append(RhythmicEvent(bar_offset + 0.0, 110, "hihat_open"))
                events.append(RhythmicEvent(bar_offset + 1.5, 100, "hihat_open"))
                events.append(RhythmicEvent(bar_offset + 3.0, 95, "hihat_open"))
                
            elif style == "minimal":
                # Muy espaciado
                if bar % 4 == 3:  # Cada 4 compases, último compás
                    events.append(RhythmicEvent(bar_offset + 3.5, 100, "hihat_open"))
        
        return events
    
    @classmethod
    def get_pattern(cls, pattern_type: str, bars: int = 4, **kwargs) -> List[Dict[str, Any]]:
        """
        Método unificado para obtener cualquier patrón por nombre.
        
        Args:
            pattern_type: Tipo de patrón (dembow_classic, moombahton, etc.)
            bars: Número de compases
            **kwargs: Parámetros específicos del patrón
            
        Returns:
            Lista de diccionarios con position, velocity, sample_type
        """
        pattern_map = {
            "dembow_classic": cls.get_dembow_classic,
            "moombahton": cls.get_moombahton_pattern,
            "perreo_acelerado": cls.get_perreo_acelerado,
            "trapeton": cls.get_trapeton_pattern,
            "syncopated_kick": cls.get_syncopated_kick,
            "ghost_snare": cls.get_ghost_snare_pattern,
            "open_hat": cls.get_open_hat_placement,
        }
        
        if pattern_type not in pattern_map:
            raise ValueError(f"Patrón no válido: {pattern_type}. "
                           f"Opciones: {list(pattern_map.keys())}")
        
        events = pattern_map[pattern_type](bars=bars, **kwargs)
        return [e.to_dict() for e in events]
    
    @classmethod
    def combine_patterns(cls, patterns: List[List[RhythmicEvent]]) -> List[RhythmicEvent]:
        """
        Combina múltiples patrones en uno solo.
        Útil para layering (ej: kicks sincopados + snares clásicos).
        
        Args:
            patterns: Lista de listas de RhythmicEvent
            
        Returns:
            Lista combinada y ordenada por posición
        """
        combined = []
        for pattern in patterns:
            combined.extend(pattern)
        
        # Ordenar por posición
        combined.sort(key=lambda e: e.position)
        return combined
    
    @staticmethod
    def pattern_to_midi_notes(events: List[RhythmicEvent], 
                               note_map: Optional[Dict[str, int]] = None) -> List[Dict[str, Any]]:
        """
        Convierte eventos rítmicos a notas MIDI.
        
        Args:
            events: Lista de RhythmicEvent
            note_map: Mapeo de sample_type a nota MIDI
                     (default: kick=36, snare=38, hihat_closed=42, etc.)
                     
        Returns:
            Lista de dicts con pitch, start_time, duration, velocity
        """
        if note_map is None:
            note_map = {
                "kick": 36,
                "snare": 38,
                "clap": 39,
                "hihat_closed": 42,
                "hihat_open": 46,
                "rimshot": 37,
                "percussion": 50,
            }
        
        # Duraciones por tipo de sample
        durations = {
            "kick": 0.25,
            "snare": 0.2,
            "clap": 0.2,
            "hihat_closed": 0.1,
            "hihat_open": 0.3,
            "rimshot": 0.15,
            "percussion": 0.2,
        }
        
        notes = []
        for event in events:
            sample_type = event.sample_type
            notes.append({
                "pitch": note_map.get(sample_type, 36),
                "start_time": event.position,
                "duration": durations.get(sample_type, 0.2),
                "velocity": event.velocity
            })
        
        return notes


# Función de conveniencia para uso rápido
def get_rhythmic_pattern(pattern_type: str, bars: int = 4, **kwargs) -> List[Dict[str, Any]]:
    """
    Obtiene un patrón rítmico por nombre.
    
    Args:
        pattern_type: Tipo de patrón
        bars: Número de compases
        **kwargs: Parámetros específicos
        
    Returns:
        Lista de eventos rítmicos como diccionarios
    """
    return ReggaetonPatterns.get_pattern(pattern_type, bars, **kwargs)
