#!/usr/bin/env python3
"""
Melody-Fix für v3: Chord-aware Melodieführung.
Ersetzt build_melody_phrase() durch eine Version, die pro Bar zum aktuellen 
Akkord passt.

REGELN:
1. Beat 1 + Beat 3 = AKKORDTON (starke Zählzeit)
2. Beat 2 + Beat 4 = Akkordton ODER Durchgangston (mit Auflösung)
3. Achtel-Bewegungen füllen Lücken mit Skalentönen
4. Phrasen-Bogen: aufsteigend in den ersten 4 Bars, absteigend in 5-8
5. Letzte 2 Bars: vollständige Kadenz V→I mit Leittonauflösung
6. Sopran soll mit Bass eher Gegenbewegung machen
"""

import os, sys

# Patch into existing techno_classical script
sys.path.insert(0, os.path.dirname(__file__))

import mido
from mido import MidiFile, MidiTrack, Message, MetaMessage

# Constants (same as parent)
TPQ = 480
BAR = 4 * TPQ
MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11]
HARMONIC_MINOR = [0, 2, 3, 5, 7, 8, 11]


def nearest_pitch_with_pc(target_pc, near_pitch, prefer_above=False):
    """Find the nearest pitch with the given pitch class to near_pitch."""
    candidates = []
    for octave in range(2, 9):
        p = octave * 12 + target_pc
        if 50 <= p <= 90:
            candidates.append(p)
    if not candidates:
        return near_pitch
    if prefer_above:
        above = [c for c in candidates if c > near_pitch]
        if above:
            return min(above, key=lambda c: c - near_pitch)
    return min(candidates, key=lambda c: abs(c - near_pitch))


def scale_step(pitch, scale_pcs_sorted, direction):
    """Move one scale step from pitch in direction (+1/-1)."""
    pc = pitch % 12
    n = len(scale_pcs_sorted)
    if pc in scale_pcs_sorted:
        idx = scale_pcs_sorted.index(pc)
    else:
        # Find nearest scale tone first
        idx = min(range(n), key=lambda i: min((scale_pcs_sorted[i] - pc) % 12, (pc - scale_pcs_sorted[i]) % 12))
    
    next_idx = (idx + direction) % n
    next_pc = scale_pcs_sorted[next_idx]
    
    if direction > 0:
        # Move up
        if next_pc > pc:
            return (pitch // 12) * 12 + next_pc
        else:
            return ((pitch // 12) + 1) * 12 + next_pc
    else:
        if next_pc < pc:
            return (pitch // 12) * 12 + next_pc
        else:
            return ((pitch // 12) - 1) * 12 + next_pc


def build_chord_aware_melody(key_root_pc, key_mode, chord_progression_8bars, register=72):
    """
    Build an 8-bar melody that:
    - Lands on chord tones on beats 1 and 3
    - Uses passing tones on beats 2 and 4
    - Follows Mozart phrase shape: arc up then down
    - Ends on tonic with leading tone resolution
    
    chord_progression_8bars: list of 8 tuples (chord_root_pc, [intervals])
    register: target central pitch for soprano (default C5=72)
    
    Returns: list of (start_tick, dur_ticks, pitch, velocity)
    """
    scale = MAJOR_SCALE if key_mode == 'major' else HARMONIC_MINOR
    scale_pcs_sorted = sorted([(key_root_pc + s) % 12 for s in scale])
    tonic_pc = key_root_pc
    leading_pc = (key_root_pc + 11) % 12
    
    melody = []  # (start_tick, dur, pitch, vel)
    
    quarter = TPQ
    eighth = TPQ // 2
    half = TPQ * 2
    
    # Phrase shape: register goes ~+4 in middle, back to start at end
    target_high = register + 7   # one fifth higher
    target_low = register - 2    # slightly below
    
    # Track previous pitch for smooth voice-leading
    prev_pitch = None
    
    for bar_idx, (chord_root_pc, intervals) in enumerate(chord_progression_8bars):
        chord_pcs = [(chord_root_pc + iv) % 12 for iv in intervals]
        chord_pcs_sorted = sorted(chord_pcs)
        
        bar_start = bar_idx * BAR
        
        # Determine target register for this bar (phrase arc)
        # bars 0-3 ascending, bars 4-6 descending, bar 7 final cadence
        if bar_idx == 0:
            current_target = register
        elif bar_idx == 1:
            current_target = register + 3
        elif bar_idx == 2:
            current_target = register + 5
        elif bar_idx == 3:
            current_target = target_high  # peak
        elif bar_idx == 4:
            current_target = register + 5
        elif bar_idx == 5:
            current_target = register + 2
        elif bar_idx == 6:
            current_target = register   # back home
        elif bar_idx == 7:
            current_target = register   # cadence
        
        # ----- Last bar: full cadence (V->I with leading tone) -----
        if bar_idx == 7:
            # Bar 8: assume V in the previous bar's progression
            # We land on tonic via leading tone -> tonic
            # Quarter notes: leading_tone (beat 1) → tonic (beat 2, sustained)
            
            # Find leading tone near register
            lt_pitch = nearest_pitch_with_pc(leading_pc, current_target)
            tonic_pitch = lt_pitch + 1
            # Ensure tonic is the actual tonic_pc
            while tonic_pitch % 12 != tonic_pc:
                tonic_pitch += 1
            
            melody.append((bar_start, quarter, lt_pitch, 90))
            # Resolve to tonic, hold for 3 quarters
            melody.append((bar_start + quarter, 3 * quarter - TPQ//8, tonic_pitch, 95))
            prev_pitch = tonic_pitch
            continue
        
        # ----- Beat 1: CHORD TONE near current target -----
        # Choose chord tone closest to current_target
        beat1_pitches = []
        for pc in chord_pcs_sorted:
            p = nearest_pitch_with_pc(pc, current_target)
            beat1_pitches.append((abs(p - current_target), p))
        beat1_pitches.sort()
        beat1 = beat1_pitches[0][1]
        
        # If we have prev_pitch, prefer stepwise motion or small leap
        if prev_pitch is not None:
            # Prefer beat1 within 5 semitones of prev_pitch
            close_options = [(abs(p - prev_pitch), p) for _, p in beat1_pitches if abs(p - prev_pitch) <= 7]
            if close_options:
                close_options.sort()
                beat1 = close_options[0][1]
        
        # Beat 1 strong, accent on bar 1 of phrase
        vel = 95 if bar_idx in (0, 4) else 88
        
        # ----- Beat 2: PASSING TONE or chord tone (stepwise from beat 1) -----
        # Try stepwise motion: scale_step toward next anticipated target
        # Determine direction toward beat 3
        # For now: simple — go scale-step up if ascending phrase, down if descending
        if bar_idx < 4:
            direction = 1 if (bar_idx % 2 == 0) else -1  # zig-zag for variety
        else:
            direction = -1 if (bar_idx % 2 == 0) else 1
        
        beat2 = scale_step(beat1, scale_pcs_sorted, direction)
        # Make sure beat2 isn't out of register
        while abs(beat2 - current_target) > 10:
            beat2 = scale_step(beat2, scale_pcs_sorted, -direction)
        
        # ----- Beat 3: CHORD TONE again (resolve passing tone) -----
        # Find chord tone near beat2, prefer stepwise motion from beat2
        beat3_options = []
        for pc in chord_pcs_sorted:
            p = nearest_pitch_with_pc(pc, beat2)
            distance = abs(p - beat2)
            beat3_options.append((distance, p))
        beat3_options.sort()
        beat3 = beat3_options[0][1]
        
        # ----- Beat 4: PASSING TONE toward next bar's beat 1 -----
        # Look ahead to next chord
        if bar_idx + 1 < len(chord_progression_8bars):
            next_chord_root_pc, next_intervals = chord_progression_8bars[bar_idx + 1]
            next_chord_pcs = [(next_chord_root_pc + iv) % 12 for iv in next_intervals]
            # Find nearest tone of next chord
            next_target_options = []
            for pc in next_chord_pcs:
                p = nearest_pitch_with_pc(pc, beat3)
                next_target_options.append((abs(p - beat3), p))
            next_target_options.sort()
            next_chord_target = next_target_options[0][1]
            
            # Beat 4 should be a step away from next_chord_target
            if next_chord_target > beat3:
                beat4 = scale_step(beat3, scale_pcs_sorted, 1)
            elif next_chord_target < beat3:
                beat4 = scale_step(beat3, scale_pcs_sorted, -1)
            else:
                # If next is same, oscillate
                beat4 = scale_step(beat3, scale_pcs_sorted, -1)
        else:
            beat4 = scale_step(beat3, scale_pcs_sorted, -1)
        
        # ----- Emit beats -----
        beats = [
            (0, quarter, beat1, vel),
            (quarter, quarter, beat2, 78),
            (2*quarter, quarter, beat3, 82),
            (3*quarter, quarter, beat4, 75),
        ]
        for s, d, p, v in beats:
            melody.append((bar_start + s, d - TPQ//12, p, v))  # slight staccato
        
        prev_pitch = beat4
        
        # ----- Special: add 8th-note ornaments in bars 2, 4, 6 (Mozart cantilena) -----
        if bar_idx in (2, 5):
            # Replace some quarters with 8th-note pairs
            # Take beat 3 and beat 4 and double their density
            del melody[-2:]  # remove beat 3, beat 4 quarters
            
            ornament_pitches = []
            
            # Beat 3: chord tone + step up
            ornament_pitches.append(beat3)
            ornament_pitches.append(scale_step(beat3, scale_pcs_sorted, 1))
            # Beat 4: step down, step down (resolution)
            ornament_pitches.append(scale_step(ornament_pitches[-1], scale_pcs_sorted, -1))
            ornament_pitches.append(scale_step(ornament_pitches[-1], scale_pcs_sorted, -1))
            
            for i, p in enumerate(ornament_pitches):
                s = bar_start + 2*quarter + i * eighth
                melody.append((s, eighth - TPQ//12, p, 75))
            
            prev_pitch = ornament_pitches[-1]
    
    return melody


# ============================================================================
# Validation
# ============================================================================
def validate(melody, chord_progression_8bars):
    NAMES = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']
    issues = 0
    total = 0
    for s, d, p, v in melody:
        bar = s // BAR
        if bar >= len(chord_progression_8bars):
            continue
        beat = (s % BAR) // TPQ
        is_strong = (beat in (0, 2)) and ((s % TPQ) < TPQ // 4)
        
        chord = chord_progression_8bars[bar]
        chord_pcs = set((chord[0] + iv) % 12 for iv in chord[1])
        
        total += 1
        if is_strong and (p % 12) not in chord_pcs:
            print(f"  ⚠ bar{bar} b{beat}: {NAMES[p%12]}{p//12} NOT in chord {NAMES[chord[0]]}")
            issues += 1
    
    print(f"\nValidation: {total - issues}/{total} notes correct ({issues} dissonances on strong beats)")
    return issues


if __name__ == '__main__':
    # Test
    NAMES = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']
    
    # G major BUILD progression: I-V-vi-IV-I-V-IV-V
    test_prog = [
        (7,  [0,4,7]),  # Gmaj
        (2,  [0,4,7]),  # Dmaj
        (4,  [0,3,7]),  # Emin
        (0,  [0,4,7]),  # Cmaj
        (7,  [0,4,7]),  # Gmaj
        (2,  [0,4,7]),  # Dmaj
        (0,  [0,4,7]),  # Cmaj
        (2,  [0,4,7]),  # Dmaj (V)
    ]
    
    print("TEST: BUILD phrase, G-major, I-V-vi-IV-I-V-IV-V")
    print("=" * 60)
    melody = build_chord_aware_melody(7, 'major', test_prog, register=72)
    
    print("\nGenerated melody:")
    for s, d, p, v in melody:
        bar = s // BAR
        beat = (s % BAR) // TPQ
        sub = (s % TPQ) / TPQ
        chord = test_prog[bar] if bar < len(test_prog) else test_prog[-1]
        chord_pcs = set((chord[0] + iv) % 12 for iv in chord[1])
        scale_pcs = {7, 9, 11, 0, 2, 4, 6}
        in_chord = p % 12 in chord_pcs
        in_scale = p % 12 in scale_pcs
        status = "CHRD" if in_chord else ("scal" if in_scale else "OUT!")
        chord_name = NAMES[chord[0]] + ('maj' if 4 in chord[1] else 'min')
        print(f"  bar{bar} b{beat}+{sub:.2f}  {NAMES[p%12]}{p//12:<2d} v{v}  [{chord_name:6s}]  {status}")
    
    print()
    validate(melody, test_prog)
