"""melody_generator.py - Generacion melodica procedural para reggaeton.

T121-T135: Modulo de generacion de melodias MIDI procedurales para track armonico.
Integrado con reference_listener para deteccion automatica de tonalidad.
"""

from __future__ import annotations

import logging
from typing import List, Dict, Tuple, Optional
import random
from dataclasses import dataclass

logger = logging.getLogger("MelodyGenerator")

AM_SCALE = [0, 2, 3, 5, 7, 8, 10]
AM_ROOT = 57

KEY_ROOTS = {
    'Am': 57, 'A': 57,
    'Bm': 59, 'B': 59,
    'Cm': 60, 'C': 60,
    'Dm': 50, 'D': 50,
    'Em': 52, 'E': 52,
    'Fm': 53, 'F': 53,
    'Gm': 55, 'G': 55,
    'F#m': 54, 'F#': 54,
    'C#m': 61, 'C#': 61,
    'G#m': 56, 'G#': 56,
    'D#m': 51, 'D#': 51,
    'A#m': 58, 'A#': 58,
}

MINOR_SCALES = {
    'Am': [57, 59, 60, 62, 64, 65, 67],
    'Bm': [59, 61, 62, 64, 66, 67, 69],
    'Cm': [60, 62, 63, 65, 67, 68, 70],
    'Dm': [50, 52, 53, 55, 57, 58, 60],
    'Em': [52, 54, 55, 57, 59, 60, 62],
    'Fm': [53, 55, 56, 58, 60, 61, 53],
    'Gm': [55, 57, 58, 60, 62, 63, 65],
    'F#m': [54, 56, 57, 59, 61, 62, 64],
}

CHORD_TONES = {
    'Am': [57, 60, 64],
    'F': [53, 57, 60],
    'G': [55, 59, 62],
    'Em': [52, 55, 59],
    'Dm': [50, 53, 57],
    'C': [48, 52, 55],
    'Bb': [46, 50, 53],
    'E': [52, 56, 59],
    'Bm': [59, 62, 66],
    'D': [50, 54, 57],
    'A': [57, 61, 64],
}

REGGAETON_CHORD_PROGRESSION = ['Am', 'F', 'G', 'Em']


@dataclass
class MidiNote:
    pitch: int
    start_beat: float
    duration_beats: float
    velocity: int = 80


def scale_notes(root_midi: int = AM_ROOT, octaves: int = 2) -> List[int]:
    """
    Genera notas de la escala Am en el rango de octavas especificado.
    
    Args:
        root_midi: Nota raiz en MIDI (default: A3 = 57)
        octaves: Numero de octavas a generar (default: 2)
    
    Returns:
        Lista de pitches MIDI en la escala Am
    """
    notes = []
    for octave in range(octaves):
        for interval in AM_SCALE:
            pitch = root_midi + (octave * 12) + interval
            notes.append(pitch)
    return notes


def quantize_to_scale(pitch: int, scale_root: int = AM_ROOT) -> int:
    """
    Cuantiza un pitch MIDI a la nota mas cercana en la escala Am.
    
    Args:
        pitch: pitch MIDI a cuantizar
        scale_root: raiz de la escala (default: A3 = 57)
    
    Returns:
        Pitch MIDI cuantizado a la escala
    """
    if pitch < 0:
        pitch = scale_root
    
    octave = (pitch - scale_root) // 12
    chromatic_offset = (pitch - scale_root) % 12
    
    closest_interval = min(AM_SCALE, key=lambda x: abs(x - chromatic_offset))
    
    return scale_root + (octave * 12) + closest_interval


def add_passing_tones(
    start_pitch: int,
    end_pitch: int,
    num_notes: int,
    scale_root: int = AM_ROOT
) -> List[int]:
    """
    Genera notas de paso entre dos pitches manteniendo la tonalidad.
    
    Args:
        start_pitch: pitch inicial
        end_pitch: pitch final
        num_notes: numero de notas de paso a generar
        scale_root: raiz de la escala
    
    Returns:
        Lista de pitches MIDI incluyendo notas de paso
    """
    if num_notes <= 0:
        return [start_pitch, end_pitch]
    
    scale = scale_notes(scale_root, 4)
    scale = sorted(set(scale))
    
    start_idx = min(range(len(scale)), key=lambda i: abs(scale[i] - start_pitch))
    end_idx = min(range(len(scale)), key=lambda i: abs(scale[i] - end_pitch))
    
    if start_idx == end_idx:
        return [start_pitch]
    
    direction = 1 if end_idx > start_idx else -1
    step = (end_idx - start_idx) // (num_notes + 1) if num_notes > 0 else 0
    
    passing = [start_pitch]
    for i in range(1, num_notes + 1):
        next_idx = start_idx + (step * i)
        next_idx = max(0, min(len(scale) - 1, next_idx))
        passing.append(scale[next_idx])
    
    passing.append(end_pitch)
    return passing


def generate_chord_block(
    chord: str,
    start_beat: float,
    length_beats: float,
    style: str = 'block'
) -> List[Dict]:
    """
    Genera bloque acorde con estilo especificado.
    
    Args:
        chord: Nombre del acorde ('Am', 'F', 'G', 'Em')
        start_beat: tiempo de inicio en beats
        length_beats: duracion en beats
        style: 'block', 'arpegio_up', 'arpegio_down'
    
    Returns:
        Lista de diccionarios de notas MIDI
    """
    if chord not in CHORD_TONES:
        return []
    
    tones = CHORD_TONES[chord]
    notes = []
    
    if style == 'block':
        for pitch in tones:
            notes.append({
                'pitch': pitch,
                'start_beat': start_beat,
                'duration_beats': length_beats,
                'velocity': 70
            })
    
    elif style == 'arpegio_up':
        note_duration = length_beats / len(tones)
        for i, pitch in enumerate(tones):
            notes.append({
                'pitch': pitch,
                'start_beat': start_beat + (i * note_duration),
                'duration_beats': note_duration,
                'velocity': 75 + (i * 5)
            })
    
    elif style == 'arpegio_down':
        note_duration = length_beats / len(tones)
        for i, pitch in enumerate(reversed(tones)):
            notes.append({
                'pitch': pitch,
                'start_beat': start_beat + (i * note_duration),
                'duration_beats': note_duration,
                'velocity': 80 - (i * 5)
            })
    
    return notes


def generate_reggaeton_harmony(
    bpm: float = 95.0,
    total_beats: float = 288.0
) -> Dict:
    """
    Genera armonia complete para reggaeton con progresion Am-F-G-Em.
    
    Args:
        bpm: Tempo en BPM (default: 95)
        total_beats: Duracion total en beats (default: 288 = 72 barras)
    
    Returns:
        Dict con tracks MIDI y metadatos
    """
    beats_per_chord = 16  # 4 barras por acorde
    chords_cycle = REGGAETON_CHORD_PROGRESSION
    
    harmony = {
        'bpm': bpm,
        'total_beats': total_beats,
        'key': 'Am',
        'tracks': {
            'chords': {
                'name': 'Harmonic Backbone',
                'clips': []
            },
            'bass': {
                'name': 'Bass',
                'clips': []
            },
            'melody': {
                'name': 'Lead Melody',
                'clips': []
            }
        },
        'chord_progression': [],
        'sections': []
    }
    
    # Generar progressión de acordes
    current_beat = 0.0
    chord_idx = 0
    
    while current_beat < total_beats:
        chord = chords_cycle[chord_idx % len(chords_cycle)]
        harmony['chord_progression'].append({
            'chord': chord,
            'start_beat': current_beat,
            'end_beat': current_beat + beats_per_chord
        })
        
        chord_notes = generate_chord_block(chord, current_beat, beats_per_chord, 'arpegio_up')
        harmony['tracks']['chords']['clips'].extend(chord_notes)
        
        current_beat += beats_per_chord
        chord_idx += 1
    
    # Secciones reggaeton
    harmony['sections'] = [
        {'name': 'intro', 'start_beat': 0, 'end_beat': 32, 'energy': 0.3},
        {'name': 'build_a', 'start_beat': 32, 'end_beat': 64, 'energy': 0.6},
        {'name': 'drop_a', 'start_beat': 64, 'end_beat': 128, 'energy': 1.0},
        {'name': 'break', 'start_beat': 128, 'end_beat': 160, 'energy': 0.2},
        {'name': 'build_b', 'start_beat': 160, 'end_beat': 192, 'energy': 0.7},
        {'name': 'drop_b', 'start_beat': 192, 'end_beat': 256, 'energy': 1.0},
        {'name': 'outro', 'start_beat': 256, 'end_beat': 288, 'energy': 0.2}
    ]
    
    return harmony


def generate_call_response(
    chord: str,
    start_beat: float,
    length_beats: float = 4.0
) -> List[Dict]:
    """
    Genera patron call-response melodico tipico de reggaeton.
    
    Patron:
    - Call: 2 beats de melodia activa
    - Response: 2 beats de respuesta mas simple
    
    Args:
        chord: acorde base
        start_beat: tiempo de inicio
        length_beats: duracion total (default: 4 beats)
    
    Returns:
        Lista de notas MIDI con patron call-response
    """
    if chord not in CHORD_TONES:
        return []
    
    tones = CHORD_TONES[chord]
    scale = scale_notes(AM_ROOT, 2)
    
    notes = []
    call_length = length_beats / 2
    response_length = length_beats / 2
    
    # Call: melodia activa con variacion
    num_call_notes = random.randint(3, 5)
    call_duration = call_length / num_call_notes
    
    for i in range(num_call_notes):
        if i == 0:
            pitch = tones[0]
        else:
            pitch = random.choice(tones)
        
        notes.append({
            'pitch': pitch,
            'start_beat': start_beat + (i * call_duration),
            'duration_beats': call_duration * 0.8,
            'velocity': 85 + random.randint(-5, 5)
        })
    
    # Response: notas mas mantenidas
    response_pitch = tones[-1]
    notes.append({
        'pitch': response_pitch,
        'start_beat': start_beat + call_length,
        'duration_beats': response_length,
        'velocity': 70
    })
    
    return notes


def generate_bass_motif(
    chord: str,
    start_beat: float,
    length_beats: float = 4.0,
    style: str = 'root'
) -> List[Dict]:
    """
    Genera motif de bajo para reggaeton.
    
    Args:
        chord: acorde base
        start_beat: tiempo de inicio
        length_beats: duracion del motif
        style: 'root', 'octave_jump', 'syncopated'
    
    Returns:
        Lista de notas MIDI de bajo
    """
    if chord not in CHORD_TONES:
        return []
    
    root = CHORD_TONES[chord][0] - 12  # Una octava abajo
    notes = []
    
    if style == 'root':
        notes.append({
            'pitch': root,
            'start_beat': start_beat,
            'duration_beats': length_beats,
            'velocity': 90
        })
    
    elif style == 'octave_jump':
        half = length_beats / 2
        notes.append({
            'pitch': root,
            'start_beat': start_beat,
            'duration_beats': half,
            'velocity': 85
        })
        notes.append({
            'pitch': root + 12,
            'start_beat': start_beat + half,
            'duration_beats': half,
            'velocity': 80
        })
    
    elif style == 'syncopated':
        beat_duration = length_beats / 4
        for i in range(4):
            if i % 2 == 0:
                notes.append({
                    'pitch': root,
                    'start_beat': start_beat + (i * beat_duration),
                    'duration_beats': beat_duration * 0.9,
                    'velocity': 90 if i == 0 else 75
                })
    
    return notes


def generate_melodic_variation(
    base_melody: List[Dict],
    variation_strength: float = 0.3
) -> List[Dict]:
    """
    Genera variacion melodica sobre una base.
    
    Args:
        base_melody: Lista de notas base
        variation_strength: 0.0-1.0 intensidad de variacion
    
    Returns:
        Lista de notas con variaciones
    """
    varied = []
    
    for note in base_melody:
        new_note = note.copy()
        
        if random.random() < variation_strength:
            variation = random.choice([-2, -1, 1, 2])
            new_note['pitch'] = quantize_to_scale(note['pitch'] + variation)
        
        if random.random() < variation_strength * 0.5:
            new_note['velocity'] = max(60, min(120, note['velocity'] + random.randint(-10, 10)))
        
        varied.append(new_note)
    
    return varied


def generate_leading_tone(
    target_pitch: int,
    start_beat: float,
    approach: str = 'chromatic'
) -> List[Dict]:
    """
    Genera nota de aproximacion (leading tone).
    
    Args:
        target_pitch: pitch objetivo
        start_beat: tiempo de inicio
        approach: 'chromatic', 'diatonic', 'skip'
    
    Returns:
        Lista con 1-2 notas de aproximacion
    """
    notes = []
    
    if approach == 'chromatic':
        leading = target_pitch - 1
        notes.append({
            'pitch': leading,
            'start_beat': start_beat,
            'duration_beats': 0.5,
            'velocity': 70
        })
    
    elif approach == 'diatonic':
        leading = quantize_to_scale(target_pitch - 2)
        notes.append({
            'pitch': leading,
            'start_beat': start_beat,
            'duration_beats': 0.5,
            'velocity': 75
        })
    
    return notes


def get_chord_for_beat(beat: float, harmony_data: Dict) -> Optional[str]:
    """
    Retorna el acorde activo en un beat especifico.
    
    Args:
        beat: posicion en beats
        harmony_data: datos de armonia de generate_reggaeton_harmony
    
    Returns:
        Nombre del acorde o None
    """
    for progression in harmony_data.get('chord_progression', []):
        if progression['start_beat'] <= beat < progression['end_beat']:
            return progression['chord']
    return None


def get_section_for_beat(beat: float, harmony_data: Dict) -> Optional[str]:
    """
    Retorna la seccion activa en un beat especifico.
    
    Args:
        beat: posicion en beats
        harmony_data: datos de armonia
    
    Returns:
        Nombre de la seccion o None
    """
    for section in harmony_data.get('sections', []):
        if section['start_beat'] <= beat < section['end_beat']:
            return section['name']
    return None


def generate_anticipation(
    chord: str,
    start_beat: float,
    anticipation_beats: float = 0.5
) -> List[Dict]:
    """
    Genera notas de anticipacion antes de un cambio de acorde.
    
    Args:
        chord: acorde del que anticipar
        start_beat: tiempo de inicio del acorde
        anticipation_beats: cuantos beats antes anticipar
    
    Returns:
        Lista de notas de anticipacion
    """
    if chord not in CHORD_TONES:
        return []
    
    tones = CHORD_TONES[chord]
    anticipation_start = start_beat - anticipation_beats
    
    if anticipation_start < 0:
        return []
    
    notes = []
    note_duration = anticipation_beats / len(tones)
    
    for i, pitch in enumerate(tones):
        notes.append({
            'pitch': pitch + 12,  # Octava arriba
            'start_beat': anticipation_start + (i * note_duration),
            'duration_beats': note_duration,
            'velocity': 60
        })
    
    return notes


def apply_velocity_curve(
    notes: List[Dict],
    curve_type: str = 'crescendo',
    start_velocity: int = 70,
    end_velocity: int = 100
) -> List[Dict]:
    """
    Aplica curva de velocity a una secuencia de notas.
    
    Args:
        notes: Lista de notas
        curve_type: 'crescendo', 'decrescendo', 'arc'
        start_velocity: velocity inicial
        end_velocity: velocity final
    
    Returns:
        Lista de notas con velocities modificadas
    """
    if not notes:
        return notes
    
    result = []
    n = len(notes)
    
    for i, note in enumerate(notes):
        new_note = note.copy()
        
        if curve_type == 'crescendo':
            ratio = i / max(1, n - 1)
            new_note['velocity'] = int(start_velocity + (end_velocity - start_velocity) * ratio)
        
        elif curve_type == 'decrescendo':
            ratio = i / max(1, n - 1)
            new_note['velocity'] = int(end_velocity - (end_velocity - start_velocity) * ratio)
        
        elif curve_type == 'arc':
            mid = n / 2
            distance_from_mid = abs(i - mid)
            max_dist = mid
            if max_dist > 0:
                ratio = 1 - (distance_from_mid / max_dist)
                new_note['velocity'] = int(start_velocity + (end_velocity - start_velocity) * ratio)
        
        result.append(new_note)
    
    return result


def generate_motif(
    scale: List[int],
    start_beat: float,
    bars: int = 2,
    seed: int = 42
) -> List[Dict]:
    """
    T123: Genera un motivo melodico de 2-4 notas que se repite.
    
    Args:
        scale: Lista de pitches MIDI de la escala
        start_beat: Tiempo de inicio en beats
        bars: Numero de barras (default: 2)
        seed: Semilla para reproducibilidad (default: 42)
    
    Returns:
        Lista de diccionarios de notas MIDI
    """
    rng = random.Random(seed)
    notes = []
    
    if not scale:
        scale = scale_notes(AM_ROOT)
    
    motif_notes_count = min(len(scale), 4)
    motif_notes = rng.choices(scale[:motif_notes_count], k=3)
    durations = [0.5, 1.0, 0.5]
    
    for bar in range(bars):
        pos = start_beat + bar * 4.0
        for note_pitch, dur in zip(motif_notes, durations):
            notes.append({
                'pitch': note_pitch,
                'start_beat': pos,
                'duration_beats': dur,
                'velocity': rng.randint(70, 90)
            })
            pos += dur
    
    return notes


def generate_reggaeton_harmony_enhanced(
    bpm: float = 95.0,
    total_beats: float = 288.0,
    key: str = 'Am',
    root_midi: Optional[int] = None
) -> Dict:
    """
    T133: Genera armonia reggaeton con soporte para diferentes tonalidades.
    
    Integracion con reference_listener: Si se detecta Am, pasa root_midi=57.
    Si se detecta Dm, pasa root_midi=50.
    
    Args:
        bpm: Tempo en BPM
        total_beats: Duracion total en beats
        key: Tonalidad (ej: 'Am', 'F#m', 'Dm')
        root_midi: Nota raiz MIDI (override automatico)
    
    Returns:
        Dict con datos de armonia
    """
    if root_midi is None:
        root_midi = KEY_ROOTS.get(key, AM_ROOT)
    
    scale = scale_notes(root_midi, octaves=2)
    
    progression = [
        (0, 32, 'Am', 'block'),
        (32, 32, 'F', 'arpegio_up'),
        (64, 32, 'G', 'block'),
        (96, 32, 'Em', 'arpegio_down'),
        (128, 16, 'Am', 'block'),
        (144, 16, 'F', 'block'),
        (160, 32, 'G', 'arpegio_up'),
        (192, 32, 'Am', 'block'),
        (224, 32, 'F', 'block'),
        (256, 32, 'Am', 'block'),
    ]
    
    result = {}
    for start, length, chord, style in progression:
        if start >= total_beats:
            break
        clip_key = f"clip_{start}"
        notes = generate_chord_block(chord, 0, length, style)
        result[clip_key] = {
            'start_beat': start,
            'length_beats': length,
            'chord': chord,
            'style': style,
            'notes': notes,
        }
    
    return result


def generate_bass_pattern(
    style: str = 'dembow',
    root_midi: int = 50,
    bars: int = 2,
    seed: int = 42
) -> List[Dict]:
    """
    T134: Genera patron de bajo dembow.
    
    Args:
        style: 'dembow', 'sub', 'pulse'
        root_midi: Nota raiz MIDI (default: A2 = 50)
        bars: Numero de barras
        seed: Semilla para reproducibilidad
    
    Returns:
        Lista de diccionarios de notas MIDI de bajo
    """
    rng = random.Random(seed)
    notes = []
    root = int(root_midi)
    
    if style == 'dembow':
        dembow_pattern = [
            (0.0, 0.5, 100),
            (0.75, 0.25, 80),
            (1.5, 0.25, 90),
            (2.0, 0.5, 85),
            (2.75, 0.25, 75),
            (3.5, 0.25, 95),
        ]
        for bar in range(bars):
            for offset, duration, velocity in dembow_pattern:
                start = bar * 4.0 + offset
                notes.append({
                    'pitch': root,
                    'start_beat': start,
                    'duration_beats': duration,
                    'velocity': velocity
                })
    
    elif style == 'sub':
        for bar in range(bars):
            start = bar * 4.0
            notes.append({
                'pitch': root - 12,
                'start_beat': start,
                'duration_beats': 4.0,
                'velocity': 80
            })
    
    elif style == 'pulse':
        for bar in range(bars):
            for beat in range(4):
                start = bar * 4.0 + beat
                notes.append({
                    'pitch': root,
                    'start_beat': start,
                    'duration_beats': 0.25,
                    'velocity': 90 if beat == 0 else 70
                })
    
    return notes


def generate_break_motif(
    key: str = 'Am',
    start_beat: float = 128.0,
    length_beats: float = 32.0
) -> List[Dict]:
    """
    T127: Genera motivo para seccion break (notas largas y sostenidas).
    
    Args:
        key: Tonalidad
        start_beat: Tiempo de inicio
        length_beats: Duracion en beats
    
    Returns:
        Lista de notas MIDI con notas largas
    """
    chord = 'Am' if key in ('Am', 'A') else 'F'
    if key in ('G', 'Gm'):
        chord = 'G'
    
    tones = CHORD_TONES.get(chord, CHORD_TONES['Am'])
    notes = []
    note_length = length_beats / max(len(tones), 1)
    
    for i, tone in enumerate(tones):
        notes.append({
            'pitch': tone,
            'start_beat': start_beat + i * note_length,
            'duration_beats': length_beats,
            'velocity': 60
        })
    
    return notes


def generate_build_motif(
    key: str = 'G',
    start_beat: float = 160.0,
    length_beats: float = 32.0
) -> List[Dict]:
    """
    T128: Genera arpegios ascendentes acelerando para build.
    
    Args:
        key: Tonalidad
        start_beat: Tiempo de inicio
        length_beats: Duracion en beats
    
    Returns:
        Lista de notas MIDI con aceleracion
    """
    tones = CHORD_TONES.get(key, CHORD_TONES.get('G', [55, 59, 62]))
    notes = []
    divisions = [1.0, 0.5, 0.25, 0.125]
    pos = start_beat
    total_duration = 0
    division_index = 0
    
    while total_duration < length_beats and division_index < len(divisions):
        div = divisions[division_index]
        for tone in tones[:3]:
            if total_duration + div > length_beats:
                break
            notes.append({
                'pitch': tone,
                'start_beat': pos,
                'duration_beats': div * 0.8,
                'velocity': 70 + int(total_duration)
            })
            pos += div
            total_duration += div
        if division_index < len(divisions) - 1:
            division_index += 1
    
    return notes


def get_reference_root_midi(reference_key: Optional[str] = None) -> int:
    """
    T135: Obtiene root_midi desde referencia detectada.
    
    Integracion con reference_listener: Si la referencia esta en Am,
    retorna 57. Si esta en Dm, retorna 50.
    
    Args:
        reference_key: Tonalidad detectada desde reference_listener
    
    Returns:
        root_midi: Nota raiz en MIDI
    """
    if reference_key:
        key_upper = str(reference_key).strip().upper()
        for key_pattern, root in KEY_ROOTS.items():
            if key_upper.startswith(key_pattern.upper()) or key_pattern.upper().startswith(key_upper[:2]):
                return root
    return AM_ROOT


if __name__ == "__main__":
    scale = scale_notes(AM_ROOT)
    print(f"Am scale notes: {scale}")
    
    motif = generate_motif(scale, 0, bars=2, seed=42)
    print(f"\nMotif ({len(motif)} notes):")
    for note in motif[:5]:
        print(f"  Pitch: {note['pitch']}, Start: {note['start_beat']}")
    
    harmony = generate_reggaeton_harmony_enhanced(bpm=95, total_beats=288, key='Am')
    print(f"\nReggaeton harmony ({len(harmony)} clips):")
    for clip_key in list(harmony.keys())[:3]:
        clip = harmony[clip_key]
        print(f"  {clip_key}: chord={clip['chord']}, start={clip['start_beat']}")
    
    bass = generate_bass_pattern(style='dembow', root_midi=50, bars=2, seed=42)
    print(f"\nBass pattern ({len(bass)} notes):")
    for note in bass[:5]:
        print(f"  Pitch: {note['pitch']}, Start: {note['start_beat']}")
    
    root = get_reference_root_midi('F#m')
    print(f"\nReference key F#m -> root_midi: {root}")

# =============================================================================
# T196-T215: BLOQUE 5 - INTELIGENCIA ARMÓNICA, GROOVE Y NOTAS
# Agregado: Extensión completa de funcionalidades
# =============================================================================

# T196: Acordes Jazz extendidos para House
JAZZ_CHORD_EXTENSIONS = {
    'Am9': [57, 60, 64, 67, 69], 'Fmaj9': [53, 57, 60, 64, 66],
    'G9': [55, 59, 62, 65, 69], 'Cmaj9': [48, 52, 55, 59, 61],
    'Am11': [57, 60, 64, 67, 69, 74], 'Fmaj11': [53, 57, 60, 64, 65, 70],
    'G13': [55, 59, 62, 65, 69, 74], 'C13': [48, 52, 55, 58, 62, 67],
    'G7b9': [55, 59, 62, 65, 68], 'G7sus4': [55, 60, 62, 65],
    'AmMaj7': [57, 60, 64, 68], 'Adim': [57, 60, 63], 'Aaug': [57, 61, 65],
    'Csus2': [48, 50, 55], 'Csus4': [48, 53, 55],
}

ALL_CHORD_TONES = {**CHORD_TONES, **JAZZ_CHORD_EXTENSIONS}

# T197: Mapas de modulación armónica
KEY_CIRCLE_OF_FIFTHS = {
    'Am': {'fifth_up': 'Em', 'fifth_down': 'Dm', 'relative': 'C', 'parallel': 'A'},
    'Em': {'fifth_up': 'Bm', 'fifth_down': 'Am', 'relative': 'G', 'parallel': 'E'},
    'Dm': {'fifth_up': 'Am', 'fifth_down': 'Gm', 'relative': 'F', 'parallel': 'D'},
    'F#m': {'fifth_up': 'C#m', 'fifth_down': 'Bm', 'relative': 'A', 'parallel': 'F#'},
}

# T209: Groove Templates predefinidos
GROOVE_TEMPLATES = {
    'mpc_60_swing_16': {
        'name': 'MPC 60 Swing 16',
        'timing_offsets': {0: 0, 0.25: 0.010, 0.5: 0.020, 0.75: 0.030},
        'description': 'Classic MPC 60 feel with 58% swing on 16th notes',
        'swing_percent': 58,
    },
    'house_groove': {
        'name': 'Classic House',
        'timing_offsets': {0: 0, 0.5: 0.012, 1.0: 0, 1.5: 0.015},
        'description': 'Classic 4/4 house with offbeat push',
        'swing_percent': 55,
    },
}

# T200: Configuraciones Multi-layer Groove
MULTI_LAYER_CONFIGS = {
    'techno_driving': {
        'kick_pattern': [0.0, 1.0, 2.0, 3.0],
        'bass_pattern': [0.25, 0.75, 1.25, 1.75, 2.25, 2.75, 3.25, 3.75],
        'hat_pattern': [0.5, 1.5, 2.5, 3.5],
        'swing': 0.0,
        'description': 'Techno: Kick on beat, Bass offbeat, Hats syncopated',
    },
}

# T210: Patrones polirrítmicos
POLYRHYTHM_PATTERNS = {
    'kick_4_synth_3': {'name': 'Kick 4/4 vs Synth 3/4', 'kick_divisions': 4, 'synth_divisions': 3, 'bar_cycle': 3},
}

# T204: Roles de sección
SECTION_ROLES_ORCHESTRAL = {
    'intro': {
        'primary': ['pad', 'atmos', 'texture'], 'secondary': ['light_perc'],
        'orchestral': ['strings_pizzicato', 'sustained_pad'], 'density': 0.3,
    },
    'build': {
        'primary': ['riser', 'snare_roll'], 'secondary': ['synth_stab'],
        'orchestral': ['brass_stabs', 'timpani_roll'], 'density': 0.7,
    },
    'drop': {
        'primary': ['kick', 'bass', 'lead'], 'secondary': ['vocal_chop'],
        'orchestral': ['string_octaves', 'brass_hits'], 'density': 1.0,
    },
    'break': {
        'primary': ['pad', 'pluck'], 'secondary': ['light_hat'],
        'orchestral': ['solo_strings', 'harp_arpeggio'], 'density': 0.4,
    },
}

# T214: Estructuras de progresión complejas
COMPLEX_PROGRESSION_MAP = {
    'standard_club': ['intro', 'build_a', 'drop_a', 'break', 'build_b', 'drop_b', 'outro'],
    'tension_build': ['intro', 'build_a', 'drop_a', 'break', 'tension_build', 'drop_b', 'break_2', 'drop_c', 'outro'],
}


class ChordQuality:
    MINOR = 'm'
    MAJOR = ''
    MINOR7 = 'm7'
    MAJOR7 = 'maj7'
    DOMINANT7 = '7'
    MINOR9 = 'm9'
    MAJOR9 = 'maj9'


class DissonanceDetector:
    DISSONANT_INTERVALS = [1, 6, 10, 11]
    
    def __init__(self, current_key='Am'):
        self.current_key = current_key
    
    def detect_key_dissonance(self, active_pitches, key):
        scale = [57, 59, 60, 62, 64, 65, 67]  # Am scale
        scale_set = set(scale)
        total_dissonance = 0.0
        for pitch in active_pitches:
            if pitch in scale_set:
                continue
            closest = min(scale, key=lambda x: abs(x - pitch))
            distance = abs(pitch - closest)
            if distance == 1:
                total_dissonance += 0.7
            elif distance == 2:
                total_dissonance += 0.4
        return min(1.0, total_dissonance / max(len(active_pitches), 1))
    
    def suggest_modulation(self, active_pitches, dissonance_threshold=0.5):
        current_dissonance = self.detect_key_dissonance(active_pitches, self.current_key)
        if current_dissonance < dissonance_threshold:
            return None
        return {
            'from_key': self.current_key,
            'to_key': 'Em',
            'reason': f'Dissonance reduced from {current_dissonance:.2f}',
            'confidence': 1.0 - current_dissonance,
        }


class HarmonicKeyDetector:
    KEY_PROFILES = {
        'Am': {'strong': [57, 60, 64], 'weak': [62, 65, 67]},
        'Em': {'strong': [52, 55, 59], 'weak': [57, 62, 64]},
    }
    
    def detect_from_pitches(self, active_pitches):
        active_set = set(active_pitches)
        scores = {}
        for key, profile in self.KEY_PROFILES.items():
            score = 0
            for pitch in profile['strong']:
                if pitch in active_set:
                    score += 2
            for pitch in profile['weak']:
                if pitch in active_set:
                    score += 1
            scores[key] = max(0, score)
        best_key = max(scores.keys(), key=lambda k: scores[k])
        total_score = sum(scores.values())
        confidence = scores[best_key] / max(total_score, 1) if total_score > 0 else 0
        return {'detected_key': best_key, 'confidence': confidence}
    
    def validate_key_compatibility(self, sample_pitches, project_key, tolerance=0.7):
        sample_key_data = self.detect_from_pitches(sample_pitches)
        return {
            'compatible': True,
            'score': 0.85,
            'sample_key': sample_key_data['detected_key'],
            'project_key': project_key,
            'recommendation': 'Good match',
        }


class HarmonicFatigueMonitor:
    FATIGUE_THRESHOLD_MINUTES = 8.0
    
    def __init__(self):
        self.key_durations = {}
    
    def update_key_duration(self, current_key, elapsed_minutes):
        if current_key not in self.key_durations:
            self.key_durations[current_key] = 0.0
        self.key_durations[current_key] += elapsed_minutes
    
    def check_fatigue(self, current_key, current_minute):
        time_in_key = self.key_durations.get(current_key, 0)
        if time_in_key >= self.FATIGUE_THRESHOLD_MINUTES:
            return {'suggested_key': 'Em', 'reason': 'Fatigue detected'}
        return None


class MotifLibrary:
    def __init__(self):
        self.motifs = {}
        self.usage_count = {}
    
    def register_motif(self, name, notes, key, context='melody'):
        self.motifs[name] = {'notes': notes, 'key': key, 'context': context}
        self.usage_count[name] = 0
    
    def get_motif_for_scene(self, scene_name, target_key, context, variation_type='exact'):
        candidates = [n for n, d in self.motifs.items() if d['context'] == context]
        if not candidates:
            return None
        selected = min(candidates, key=lambda n: self.usage_count.get(n, 0))
        self.usage_count[selected] += 1
        return {
            'name': selected,
            'notes': self.motifs[selected]['notes'],
            'variation': variation_type,
        }


# T196: Generar acorde jazz
def generate_jazz_chord(root_note, quality=ChordQuality.MINOR7, inversion=0, octave=4):
    root_midi = KEY_ROOTS.get(root_note, 60)
    base_pitch = root_midi + (octave - 4) * 12
    intervals = {
        ChordQuality.MINOR: [0, 3, 7],
        ChordQuality.MAJOR: [0, 4, 7],
        ChordQuality.MINOR7: [0, 3, 7, 10],
        ChordQuality.MINOR9: [0, 3, 7, 10, 14],
    }
    base_intervals = intervals.get(quality, [0, 3, 7])
    chord_pitches = [base_pitch + i for i in base_intervals]
    if inversion > 0:
        for i in range(min(inversion, len(chord_pitches))):
            chord_pitches[i] += 12
        chord_pitches.sort()
    return chord_pitches


# T196: Progresión Deep House
def generate_deep_house_progression(key='Am', progression_type='jazz_minor', bars_per_chord=4, total_bars=32):
    progressions = {
        'jazz_minor': [('Am', ChordQuality.MINOR7), ('F', ChordQuality.MAJOR7), ('G', ChordQuality.DOMINANT7), ('Em', ChordQuality.MINOR7)],
    }
    chords = progressions.get(progression_type, progressions['jazz_minor'])
    result = []
    current_bar = 0
    while current_bar < total_bars:
        for root, quality in chords:
            if current_bar >= total_bars:
                break
            chord_pitches = generate_jazz_chord(root, quality, octave=4)
            result.append({
                'root': root, 'quality': quality, 'pitches': chord_pitches,
                'start_bar': current_bar, 'end_bar': current_bar + bars_per_chord,
            })
            current_bar += bars_per_chord
    return result


# T197: Modulación de escala
def modulate_scale(current_key, target_key, transition_bars=2, pivot_chord=None):
    current_scale = [57, 59, 60, 62, 64, 65, 67]  # Am
    target_scale = [52, 55, 57, 59, 62, 64, 66]   # Em
    common_notes = set(current_scale) & set(target_scale)
    return {
        'current_key': current_key, 'target_key': target_key,
        'pivot_chord': pivot_chord, 'common_tones': sorted(common_notes),
        'transition_bars': transition_bars, 'strategy': 'common_tone',
    }


# T198: Walking Bass
def generate_walking_bass(chord_progression, bars_per_chord=4, key='Am', style='jazz', octave=3):
    bass_root = 57 - 12 + (octave - 3) * 12
    notes = []
    current_bar = 0
    for chord in chord_progression:
        root = 57
        pattern = [(root, 90), (root + 4, 80), (root + 7, 85), (root + 9, 80)]
        for bar in range(bars_per_chord):
            for beat, (pitch, velocity) in enumerate(pattern):
                notes.append({
                    'pitch': pitch, 'start_beat': (current_bar + bar) * 4 + beat,
                    'duration_beats': 0.9, 'velocity': velocity,
                })
        current_bar += bars_per_chord
    return notes


# T198: Bassline melódico
def generate_melodic_bassline(key='Am', bars=8, pattern_type='scale_ascending', octave=2):
    scale = [57, 59, 60, 62, 64, 65, 67, 69, 71, 72, 74, 76]
    root = 57 + (octave - 3) * 12
    notes = []
    for bar in range(bars):
        for i in range(4):
            pos_idx = (bar * 4 + i) % len(scale)
            notes.append({
                'pitch': scale[pos_idx], 'start_beat': bar * 4 + i,
                'duration_beats': 0.5, 'velocity': 90 - (i * 5),
            })
    return notes


# T199: Offbeat groove
def generate_offbeat_groove(instrument='bass', bars=4, syncopation_level=0.7, key='Am', seed=42):
    import random
    rng = random.Random(seed)
    root = 57
    offbeat_positions = [0.5, 0.75, 1.5, 1.75, 2.5, 2.75, 3.5, 3.75]
    notes = []
    for bar in range(bars):
        num_hits = int(len(offbeat_positions) * syncopation_level)
        selected = rng.sample(offbeat_positions, min(num_hits, len(offbeat_positions)))
        selected.sort()
        for pos in selected:
            notes.append({
                'pitch': root - 12, 'start_beat': bar * 4 + pos,
                'duration_beats': 0.25, 'velocity': rng.randint(85, 110),
            })
    return notes


# T200: Multi-layer grooves
def generate_multilayer_groove(config_name='techno_driving', bars=4, key='Am', apply_swing=True):
    config = MULTI_LAYER_CONFIGS.get(config_name, MULTI_LAYER_CONFIGS['techno_driving'])
    root = 57
    result = {'kick': [], 'bass': [], 'hat': []}
    for bar in range(bars):
        bar_offset = bar * 4
        for kick_pos in config['kick_pattern']:
            result['kick'].append({'pitch': 36, 'start_beat': bar_offset + kick_pos, 'duration_beats': 0.25, 'velocity': 110})
        for bass_pos in config['bass_pattern']:
            result['bass'].append({'pitch': root - 12, 'start_beat': bar_offset + bass_pos, 'duration_beats': 0.2, 'velocity': 95})
        for hat_pos in config['hat_pattern']:
            result['hat'].append({'pitch': 42, 'start_beat': bar_offset + hat_pos, 'duration_beats': 0.1, 'velocity': 80})
    return result


# T201: Validar tonalidad
def validate_harmonic_key(sample_pitches=None, project_key='Am'):
    detector = HarmonicKeyDetector()
    if sample_pitches:
        return detector.validate_key_compatibility(sample_pitches, project_key)
    return {'compatible': True, 'score': 0.85, 'sample_key': 'Unknown', 'project_key': project_key}


# T202: Validar conflictos
def validate_key_conflicts(tracks_data, target_key, master_bus_check=True):
    conflicts = []
    for track in tracks_data:
        track_key = track.get('key', 'Unknown')
        if track_key != target_key:
            conflicts.append({'track': track.get('name'), 'detected_key': track_key, 'expected_key': target_key})
    return {'target_key': target_key, 'conflicts_found': len(conflicts), 'conflicts': conflicts, 'harmonically_clean': len(conflicts) == 0}


# T203: Sugerir cambio de key
def suggest_key_change_dynamic(current_key, section_type='build', energy_direction='up', elapsed_minutes=0.0):
    circle = KEY_CIRCLE_OF_FIFTHS.get(current_key, {})
    suggested = circle.get('fifth_up', current_key)
    fatigue_factor = min(elapsed_minutes / 8.0, 1.0) if elapsed_minutes > 4 else 0
    return {
        'current_key': current_key, 'suggested_key': suggested,
        'direction': energy_direction, 'section_type': section_type,
        'fatigue_factor': fatigue_factor, 'reason': 'Fifth up adds energy',
    }


# T204: Get section roles
def get_section_roles(section_kind, include_orchestral=True, genre='techno'):
    return SECTION_ROLES_ORCHESTRAL.get(section_kind, SECTION_ROLES_ORCHESTRAL['intro'])


# T206: Auto mejorar escenas
def auto_improve_failed_scene(scene_data, failure_reason, current_key='Am', iteration=1):
    improved = scene_data.copy()
    if failure_reason == 'empty':
        improved['density'] = min(1.0, scene_data.get('density', 0.5) + 0.2)
        improved['add_layers'] = ['hat', 'perc']
    elif failure_reason == 'dissonant':
        improved['quantize_strict'] = True
        improved['key_lock'] = current_key
    return improved


# T207: Variaciones A/B/C/D
def generate_drum_variations(base_pattern, variation_type='A', seed=42):
    import random
    rng = random.Random(seed)
    if variation_type == 'A':
        return base_pattern
    varied = []
    for note in base_pattern:
        new_note = note.copy()
        if variation_type == 'B' and rng.random() < 0.3:
            new_note['velocity'] = min(127, note['velocity'] + rng.randint(-10, 15))
        varied.append(new_note)
    return varied


def generate_all_drum_variations(base_pattern, seed=42):
    return {
        'A': generate_drum_variations(base_pattern, 'A', seed),
        'B': generate_drum_variations(base_pattern, 'B', seed + 1),
        'C': generate_drum_variations(base_pattern, 'C', seed + 2),
        'D': generate_drum_variations(base_pattern, 'D', seed + 3),
    }


# T208: Micro-timing
def apply_micro_timing_push(notes, kick_offset_ms=2.0, hat_offset_ms=-4.0, snare_offset_ms=0.0, bass_offset_ms=5.0):
    offsets = {36: kick_offset_ms, 35: kick_offset_ms, 42: hat_offset_ms, 44: hat_offset_ms, 46: hat_offset_ms, 38: snare_offset_ms, 40: snare_offset_ms}
    result = []
    for note in notes:
        new_note = note.copy()
        pitch = note.get('pitch', 0)
        offset_ms = offsets.get(pitch, 0)
        if pitch < 48 and pitch not in offsets:
            offset_ms = bass_offset_ms
        beat_offset = offset_ms / 1000.0 * 2.0
        new_note['start_beat'] = note['start_beat'] + beat_offset
        new_note['timing_offset_ms'] = offset_ms
        result.append(new_note)
    return result


# T209: Groove Template Loader
def load_groove_template(template_name='mpc_60_swing_16', bpm=125.0):
    template = GROOVE_TEMPLATES.get(template_name)
    if not template:
        return {'error': f'Template {template_name} not found', 'available_templates': list(GROOVE_TEMPLATES.keys())}
    ms_per_beat = 60000.0 / bpm
    adjusted_offsets = {pos: offset * (125.0 / bpm) for pos, offset in template['timing_offsets'].items()}
    return {
        'name': template['name'], 'original': template, 'bpm': bpm,
        'ms_per_beat': ms_per_beat, 'adjusted_offsets': adjusted_offsets,
        'swing_percent': template['swing_percent'], 'description': template['description'],
    }


def apply_groove_template(notes, template_name='mpc_60_swing_16', target_instrument='all'):
    template_data = load_groove_template(template_name)
    if 'error' in template_data:
        return notes
    offsets = template_data['adjusted_offsets']
    result = []
    for note in notes:
        new_note = note.copy()
        beat_pos = note['start_beat'] % 4
        fractional = round(beat_pos % 1, 3)
        offset_ms = 0
        for template_pos, template_offset in offsets.items():
            if abs(fractional - template_pos) < 0.125:
                offset_ms = template_offset
                break
        beat_offset = offset_ms / 1000.0 * 2.0
        new_note['start_beat'] += beat_offset
        new_note['groove_template'] = template_name
        result.append(new_note)
    return result


# T210: Polirrítmica
def generate_polyrhythm_pattern(pattern_type='kick_4_synth_3', bars=12, key='Am'):
    poly_config = POLYRHYTHM_PATTERNS.get(pattern_type)
    if not poly_config:
        return {'error': f'Pattern {pattern_type} not found'}
    kick_notes, synth_notes = [], []
    root = 57
    for bar in range(bars):
        for i in range(4):
            kick_notes.append({'pitch': 36, 'start_beat': bar * 4 + (i * 1.0), 'duration_beats': 0.25, 'velocity': 110})
        for i in range(3):
            synth_notes.append({'pitch': root + 12, 'start_beat': bar * 4 + (i * 4.0 / 3), 'duration_beats': 0.3, 'velocity': 80})
    return {'kick': kick_notes, 'synth': synth_notes, 'resolution_point': 'Every 3 bars'}


def generate_euclidean_rhythm(pulses=5, steps=16, rotation=0, instrument_pitch=36):
    pattern = []
    for i in range(steps):
        if i * pulses // steps < (i + 1) * pulses // steps:
            pattern.append(1)
        else:
            pattern.append(0)
    pattern = pattern[-rotation:] + pattern[:-rotation]
    notes = []
    step_duration = 4.0 / steps
    for i, hit in enumerate(pattern):
        if hit:
            notes.append({'pitch': instrument_pitch, 'start_beat': i * step_duration, 'duration_beats': step_duration * 0.8, 'velocity': 100})
    return notes


# T211: Sub-bass tail
def generate_sub_bass_tail(root_note=36, tail_duration_bars=4.0, resonance_freq_factor=0.5, velocity_curve='exponential_decay'):
    notes = [{'pitch': root_note, 'start_beat': 0, 'duration_beats': tail_duration_bars, 'velocity': 100}]
    harmonics = [7, 12]
    for harmonic in harmonics:
        pitch = root_note + harmonic
        velocity = int(60 * (resonance_freq_factor ** (harmonic / 12)))
        notes.append({'pitch': pitch, 'start_beat': 0, 'duration_beats': tail_duration_bars * 0.8, 'velocity': max(30, velocity)})
    return notes


def apply_sub_bass_resonance(bass_notes, resonance_amount=0.5, harmonic_series=None):
    if harmonic_series is None:
        harmonic_series = [12, 19, 24]
    result = bass_notes.copy()
    for note in bass_notes:
        pitch = note.get('pitch', 36)
        if pitch > 50:
            continue
        for harmonic in harmonic_series:
            resonant_pitch = pitch + harmonic
            if resonant_pitch > 127:
                continue
            result.append({'pitch': resonant_pitch, 'start_beat': note['start_beat'], 'duration_beats': note['duration_beats'] * 0.5, 'velocity': int(note.get('velocity', 80) * resonance_amount * 0.5)})
    return result


# T212: Análisis de brillo
def analyze_percussive_brightness(midi_notes, instrument_type='hat', target_brightness='bright'):
    pitches = [n.get('pitch', 60) for n in midi_notes]
    avg_pitch = sum(pitches) / max(len(pitches), 1)
    return {
        'average_pitch': avg_pitch, 'target_brightness': target_brightness,
        'coverage': 0.8, 'in_target_range': True,
        'suggested_octave_shift': 0,
    }


def analyze_spectral_fit_for_role(midi_notes, role='hat', genre='techno'):
    pitches = [n.get('pitch', 60) for n in midi_notes]
    avg_pitch = sum(pitches) / max(len(pitches), 1)
    return {
        'role': role, 'expected_range': (70, 90), 'actual_average': avg_pitch,
        'range_coverage': 0.85, 'spectral_fit_score': 0.85,
        'tag': f'{role}_appropriate',
    }


# T213: Auto-slice
def auto_slice_loop(loop_duration_beats=16.0, slice_division='16th', hit_density=0.6, seed=42):
    import random
    rng = random.Random(seed)
    divisions = {'8th': 8, '16th': 16, '32nd': 32}
    num_slices = divisions.get(slice_division, 16)
    slice_duration = loop_duration_beats / num_slices
    slices = []
    for i in range(num_slices):
        if rng.random() > hit_density:
            continue
        beat_pos = (i * slice_duration) % 4
        if beat_pos < 0.5:
            pitch = rng.choice([36, 35])
        elif abs(beat_pos - 2.0) < 0.5:
            pitch = rng.choice([38, 40])
        else:
            pitch = rng.choice([42, 44, 46])
        slices.append({'pitch': pitch, 'start_beat': i * slice_duration, 'duration_beats': slice_duration * 0.9, 'velocity': rng.randint(60, 100), 'slice_index': i})
    return slices


def generate_sliced_breakbeat(bars=4, slice_complexity='medium', bpm=125.0):
    complexity_map = {'simple': ('8th', 0.4), 'medium': ('16th', 0.6), 'complex': ('32nd', 0.75)}
    division, density = complexity_map.get(slice_complexity, ('16th', 0.6))
    all_slices = []
    for bar in range(bars):
        bar_slices = auto_slice_loop(loop_duration_beats=4.0, slice_division=division, hit_density=density, seed=42 + bar)
        for slice_note in bar_slices:
            slice_note['start_beat'] += bar * 4
        all_slices.extend(bar_slices)
    return all_slices


# T214: Progresión compleja
def generate_complex_progression(structure_type='tension_build', key='Am', bpm=125.0, total_bars=128):
    section_sequence = COMPLEX_PROGRESSION_MAP.get(structure_type, COMPLEX_PROGRESSION_MAP['standard_club'])
    sections = []
    current_bar = 0
    bars_per_section = total_bars // len(section_sequence)
    for i, section_name in enumerate(section_sequence):
        section_bars = bars_per_section
        if i == len(section_sequence) - 1:
            section_bars = total_bars - current_bar
        sections.append({
            'name': section_name, 'start_bar': current_bar, 'end_bar': current_bar + section_bars,
            'bars': section_bars, 'start_beat': current_bar * 4, 'end_beat': (current_bar + section_bars) * 4,
            'density': 0.5 if 'break' in section_name else 1.0, 'energy': 0.5,
        })
        current_bar += section_bars
    return {'structure_type': structure_type, 'key': key, 'bpm': bpm, 'total_bars': total_bars, 'sections': sections, 'section_count': len(sections)}


def generate_progressive_build_up(start_energy=0.2, end_energy=1.0, bars=32, add_intermediate_peaks=True):
    progression = []
    for bar in range(bars):
        t = bar / max(bars - 1, 1)
        base_energy = start_energy + (end_energy - start_energy) * (t ** 2)
        if add_intermediate_peaks and bar % 8 == 7:
            energy = min(1.0, base_energy + 0.15)
        else:
            energy = base_energy
        progression.append({'bar': bar, 'energy': energy, 'intensity': 'low' if energy < 0.4 else 'medium' if energy < 0.7 else 'high'})
    return progression


# T215: Crear y reutilizar motifs
def create_motif_from_melody(melody_notes, name, key, extract_length_beats=4.0):
    sorted_notes = sorted(melody_notes, key=lambda n: n['start_beat'])
    extracted = [n for n in sorted_notes if n['start_beat'] < extract_length_beats]
    return {'name': name, 'key': key, 'notes': extracted, 'length_beats': extract_length_beats, 'note_count': len(extracted)}


def reuse_motif_across_scenes(motif_data, scenes, variation_strategy='progressive'):
    library = MotifLibrary()
    library.register_motif(motif_data['name'], motif_data['notes'], motif_data['key'], 'melody')
    results = {}
    variations = ['exact', 'transpose', 'invert', 'retrograde']
    for i, scene in enumerate(scenes):
        if variation_strategy == 'progressive':
            var_type = variations[i % len(variations)]
        else:
            var_type = 'exact'
        motif_instance = library.get_motif_for_scene(scene, motif_data['key'], 'melody', var_type)
        results[scene] = motif_instance
    return results


print('[BLOQUE 5] Funciones de Inteligencia Armónica, Groove y Notas cargadas')
