#!/usr/bin/env python3
"""
LaBoom Swing Alchemy
====================
Verwandelt die 136-BPM straight-Stems in ein swingendes Jazz-Konstrukt.

Drei Layer:
  1. SWUNG STEMS   → jedes Original-Stem mit eigener Swing-Strategie
  2. WALKING BASS  → bass.mid wird zu Walking-Bass mit Chromatik
  3. JAZZ COMP     → NEUER Track: Charleston-Comping aus other.mid Harmonien
  4. MASTER MIX    → alle Layer kombiniert in ein finales File

Idee:
  - Drums: Triolen-Shuffle (3:1 swing) + Ghost-Snares
  - Bass: Walking 4-on-the-floor mit Approach-Tones
  - Other: Spread-Voicings (Open Position) + Charleston-Stabs
  - Vocals: Subtle Swing-Delay (62% ratio) + Vibrato via Pitch-Bend
"""

import mido
from mido import MidiFile, MidiTrack, Message, MetaMessage
import random, os, sys, glob, copy

random.seed(420)  # reproducible chaos

SRC_DIR = "/root/.openclaw/workspace/downloads/midi-136bpm"
OUT_DIR = "/root/.openclaw/workspace/downloads/midi-swing"
os.makedirs(OUT_DIR, exist_ok=True)

TPQ = 220  # ticks per quarter (aus den Files)
SWING_RATIO = 0.66  # Triolen-Swing: erste 8tel = 2/3, zweite = 1/3 vom Beat
VOCAL_SWING = 0.58  # weniger extrem für Vocals

# ----- Helpers ----------------------------------------------------------------

def get_tempo_us(bpm=136):
    return int(60_000_000 / bpm)

def abs_events(track):
    """Convert delta-time track to (abs_tick, msg) tuples."""
    out = []
    t = 0
    for msg in track:
        t += msg.time
        out.append((t, msg.copy()))
    return out

def to_delta_track(abs_events_list):
    """Convert (abs_tick, msg) back to a MidiTrack with delta times."""
    abs_events_list = sorted(abs_events_list, key=lambda x: (x[0], 0 if x[1].type == 'note_off' else 1))
    track = MidiTrack()
    last = 0
    for t, msg in abs_events_list:
        msg = msg.copy()
        msg.time = max(0, t - last)
        track.append(msg)
        last = t
    return track

def apply_swing_to_offbeats(abs_events_list, swing_ratio=SWING_RATIO, tpq=TPQ):
    """Delay offbeat 8th notes to create swing feel.
    
    Straight 8ths land on 0, tpq/2, tpq, 3*tpq/2, ...
    Swung 8ths: onbeat stays, offbeat moves from tpq/2 to swing_ratio*tpq.
    """
    eighth = tpq // 2
    delay_amount = int((swing_ratio - 0.5) * tpq)  # how much to push offbeats
    
    swung = []
    for t, msg in abs_events_list:
        if msg.type in ('note_on', 'note_off'):
            beat_pos = t % tpq
            # Detect offbeat (between tpq/4 and 3*tpq/4, closer to tpq/2)
            if abs(beat_pos - eighth) < tpq // 8:
                new_t = t + delay_amount
                swung.append((new_t, msg))
            else:
                swung.append((t, msg))
        else:
            swung.append((t, msg))
    return swung


# ----- LAYER 1: Swung Drums (Shuffle) ----------------------------------------

def swing_drums(src_path, out_path):
    """Triolen-Shuffle für Drums + Ghost-Snares."""
    mid = MidiFile(src_path)
    new_mid = MidiFile(ticks_per_beat=mid.ticks_per_beat)
    
    for ti, track in enumerate(mid.tracks):
        events = abs_events(track)
        
        # Schritt 1: Swing auf Hi-Hat & andere High-Pitch-Drums
        swung = []
        for t, msg in events:
            if msg.type in ('note_on', 'note_off'):
                # Hi-Hat range: 42, 44, 46
                if msg.note in (42, 44, 46, 51, 53, 57, 59):
                    beat_pos = t % TPQ
                    eighth = TPQ // 2
                    if abs(beat_pos - eighth) < TPQ // 6:
                        swung.append((t + int((SWING_RATIO - 0.5) * TPQ), msg))
                        continue
            swung.append((t, msg))
        
        # Schritt 2: Ghost-Snares vor jeder On-Beat-Snare
        ghosts = []
        for t, msg in swung:
            if msg.type == 'note_on' and msg.note == 38 and msg.velocity > 60:
                # Add a ghost note 1/16 before
                ghost_t = max(0, t - TPQ // 4)
                if ghost_t > 0:
                    ghosts.append((ghost_t, Message('note_on', channel=msg.channel,
                                                     note=38, velocity=25, time=0)))
                    ghosts.append((ghost_t + TPQ // 8, Message('note_off', channel=msg.channel,
                                                                 note=38, velocity=0, time=0)))
        
        all_events = swung + ghosts
        new_mid.tracks.append(to_delta_track(all_events))
    
    new_mid.save(out_path)
    return out_path


# ----- LAYER 2: Walking Bass --------------------------------------------------

def walking_bass(src_path, out_path):
    """Erzeugt Walking-Bass aus den Bass-Noten: jede Note wird auf 4 Quarters
    gestretcht mit Chromatik-Approach zur nächsten."""
    mid = MidiFile(src_path)
    new_mid = MidiFile(ticks_per_beat=mid.ticks_per_beat)
    
    # Erstes Track: Meta-Events
    meta_track = MidiTrack()
    for msg in mid.tracks[0]:
        if msg.is_meta:
            meta_track.append(msg.copy())
    new_mid.tracks.append(meta_track)
    
    # Extrahiere Bass-Noten aus allen Tracks
    bass_notes = []  # list of (start_tick, end_tick, note, velocity)
    for track in mid.tracks:
        active = {}
        t = 0
        for msg in track:
            t += msg.time
            if msg.type == 'note_on' and msg.velocity > 0:
                active[msg.note] = (t, msg.velocity, msg.channel)
            elif msg.type in ('note_off',) or (msg.type == 'note_on' and msg.velocity == 0):
                if msg.note in active:
                    start, vel, ch = active.pop(msg.note)
                    bass_notes.append((start, t, msg.note, vel, ch))
    
    bass_notes.sort()
    if not bass_notes:
        new_mid.save(out_path)
        return out_path
    
    # Walking-Pattern: für jede Bass-Note generiere 4 Quarters
    # Pattern: ROOT - 5th - 3rd - approach (chromatic to next)
    walk_events = []
    for i, (start, end, note, vel, ch) in enumerate(bass_notes):
        next_root = bass_notes[i+1][2] if i+1 < len(bass_notes) else note
        
        # Quantize start to nearest beat
        beat_start = (start // TPQ) * TPQ
        duration = max(TPQ, end - beat_start)
        num_quarters = max(1, duration // TPQ)
        
        # Walking pattern based on jazz harmony
        # Simple: root → +7 (5th) → +4 (3rd) → approach
        pattern_intervals = [0, 7, 4, 0]
        
        for q in range(num_quarters):
            interval = pattern_intervals[q % 4]
            
            # Last quarter: approach next root chromatically
            if q == num_quarters - 1:
                if next_root > note:
                    interval = (next_root - note - 1)
                elif next_root < note:
                    interval = (next_root - note + 1)
            
            walk_note = note + interval
            # Keep in bass range
            while walk_note > 55: walk_note -= 12
            while walk_note < 28: walk_note += 12
            
            on_t = beat_start + q * TPQ
            off_t = on_t + int(TPQ * 0.85)  # slight staccato
            walk_vel = vel if q % 2 == 0 else max(50, vel - 20)
            
            walk_events.append((on_t, Message('note_on', channel=ch, note=walk_note, velocity=walk_vel, time=0)))
            walk_events.append((off_t, Message('note_off', channel=ch, note=walk_note, velocity=0, time=0)))
    
    walking_track = to_delta_track(walk_events)
    walking_track.insert(0, Message('program_change', channel=0, program=32, time=0))  # Acoustic Bass
    new_mid.tracks.append(walking_track)
    new_mid.save(out_path)
    return out_path


# ----- LAYER 3: Charleston Jazz Comping --------------------------------------

def jazz_comp(src_path, out_path):
    """Erzeugt einen NEUEN Track: Charleston-Comping aus den Other-Harmonien.
    
    Charleston Rhythmus: viertel-pause-pause-achtel | pause-viertel-pause-achtel
    Voicings: Spread (root + 7 + 10 + 14) = Jazz-Pianoist-Style
    """
    mid = MidiFile(src_path)
    new_mid = MidiFile(ticks_per_beat=mid.ticks_per_beat)
    
    meta_track = MidiTrack()
    for msg in mid.tracks[0]:
        if msg.is_meta:
            meta_track.append(msg.copy())
    new_mid.tracks.append(meta_track)
    
    # Sammle alle aktiven Noten pro Tick → Akkord-Grid
    chord_starts = {}  # tick → list of notes
    for track in mid.tracks:
        t = 0
        for msg in track:
            t += msg.time
            if msg.type == 'note_on' and msg.velocity > 0:
                chord_starts.setdefault(t, []).append(msg.note)
    
    # Gruppiere in Bars (4 Quarters = 4*TPQ ticks)
    bar_length = 4 * TPQ
    end_tick = max(chord_starts.keys()) if chord_starts else 0
    
    comp_events = []
    for bar_start in range(0, end_tick + bar_length, bar_length):
        # Sammle alle Noten in diesem Bar
        bar_notes = []
        for t, notes in chord_starts.items():
            if bar_start <= t < bar_start + bar_length:
                bar_notes.extend(notes)
        
        if not bar_notes:
            continue
        
        # Bestimme Tonalitäts-Zentrum (häufigste Note mod 12)
        from collections import Counter
        roots = Counter([n % 12 for n in bar_notes])
        root = roots.most_common(1)[0][0]
        
        # Spread-Voicing aufbauen
        # Octave 4 als Basis (Mitte des Pianos)
        base = 48 + root  # C4 = 60, so root C = 60
        voicing = [
            base,           # Root
            base + 4,       # Major 3rd (oder 3 = minor; wir nehmen major als default)
            base + 7,       # 5th
            base + 11,      # Major 7th
            base + 14,      # 9th
        ]
        
        # Erkennen ob minor-Kontext (wenn häufig minor-3rd vorkommt)
        has_minor_3 = any((n - root) % 12 == 3 for n in bar_notes)
        if has_minor_3:
            voicing = [base, base + 3, base + 7, base + 10, base + 14]  # m7add9
        
        # Charleston-Rhythmus: Beat 1 (lang, viertel) + Beat 2.5 (kurz, achtel)
        # Pattern in einem 4/4-Bar:
        #   Stab auf Beat 1 (Tick 0)         → dauert bis Beat 2 (Tick TPQ)
        #   Stab auf Beat 2.5 (Tick TPQ*1.5) → dauert nur 1/8 (Tick TPQ/2)
        
        # Mit Swing: zweiter Stab kommt etwas später
        stab_times = [
            (0, int(TPQ * 0.9), 75),                                    # Beat 1: lang
            (int(TPQ * 1.5 * 1.1), int(TPQ * 0.5), 90),                 # Beat 2.5: kurz, akzentuiert
            (int(TPQ * 3.0), int(TPQ * 0.7), 70),                       # Beat 4: medium
        ]
        
        # Manchmal Variation: nur Beat 1+ wie ein "and-of-1"-Stab
        if (bar_start // bar_length) % 4 == 3:
            # alle 4 Bars eine Wendung: Synkopen-Pattern
            stab_times = [
                (int(TPQ * 0.6), int(TPQ * 0.8), 80),
                (int(TPQ * 2.6), int(TPQ * 0.7), 75),
            ]
        
        for offset, duration, vel in stab_times:
            t_on = bar_start + offset
            t_off = t_on + duration
            for note in voicing:
                # Stagger die Notes minimal (humanize)
                jitter = random.randint(-3, 3)
                comp_events.append((t_on + jitter, Message('note_on', channel=2, note=note, velocity=vel, time=0)))
                comp_events.append((t_off + jitter, Message('note_off', channel=2, note=note, velocity=0, time=0)))
    
    comp_track = to_delta_track(comp_events)
    comp_track.insert(0, Message('program_change', channel=2, program=4, time=0))  # Electric Piano 1
    new_mid.tracks.append(comp_track)
    new_mid.save(out_path)
    return out_path


# ----- LAYER 4: Vocal Swing + Vibrato ----------------------------------------

def swing_vocals(src_path, out_path):
    """Vocals: subtler Swing + Vibrato via Pitch-Bend."""
    mid = MidiFile(src_path)
    new_mid = MidiFile(ticks_per_beat=mid.ticks_per_beat)
    
    for track in mid.tracks:
        events = abs_events(track)
        swung = apply_swing_to_offbeats(events, swing_ratio=VOCAL_SWING, tpq=TPQ)
        
        # Vibrato: für jede Note > 1 Beat lang, füge LFO-Pitch-Bend hinzu
        notes_active = {}
        vibrato_events = []
        for t, msg in swung:
            if msg.type == 'note_on' and msg.velocity > 0:
                notes_active[msg.note] = (t, msg.channel)
            elif (msg.type == 'note_off' or (msg.type == 'note_on' and msg.velocity == 0)):
                if msg.note in notes_active:
                    start, ch = notes_active.pop(msg.note)
                    dur = t - start
                    if dur > TPQ // 2:  # only long notes
                        # 5 Hz Vibrato → 1 cycle = TPQ * (60/136) / 0.2 ticks
                        # Simpler: alle TPQ/8 Ticks ein Bend-Wert
                        for vt in range(start + TPQ//4, t, TPQ // 8):
                            phase = (vt - start) / (TPQ // 4)
                            import math
                            bend = int(math.sin(phase * math.pi * 2) * 600)  # ±600 of 8192
                            vibrato_events.append((vt, Message('pitchwheel', channel=ch, pitch=bend, time=0)))
                        vibrato_events.append((t, Message('pitchwheel', channel=ch, pitch=0, time=0)))
        
        all_events = swung + vibrato_events
        new_mid.tracks.append(to_delta_track(all_events))
    
    new_mid.save(out_path)
    return out_path


# ----- LAYER 5: Master Mix ---------------------------------------------------

def merge_layers(layers_paths, out_path):
    """Kombiniert mehrere MIDI-Files in ein Multi-Track-File.
    Jedes File wird zu eigenen Tracks im Output."""
    merged = MidiFile(ticks_per_beat=TPQ)
    
    # Meta-Track mit Tempo (136 BPM)
    meta = MidiTrack()
    meta.append(MetaMessage('set_tempo', tempo=get_tempo_us(136), time=0))
    meta.append(MetaMessage('time_signature', numerator=4, denominator=4, time=0))
    meta.append(MetaMessage('track_name', name='LaBoom Swung', time=0))
    merged.tracks.append(meta)
    
    channel_counter = 0
    used_channels = set()
    
    for layer_name, path in layers_paths:
        if not os.path.exists(path):
            continue
        mid = MidiFile(path)
        for ti, track in enumerate(mid.tracks):
            # Skip pure-meta tracks (already have one)
            has_notes = any(msg.type in ('note_on', 'note_off') for msg in track)
            if not has_notes:
                continue
            
            # Assign unique channel
            while channel_counter in (9, ) and 'drum' not in layer_name.lower():  # 9 = drums
                channel_counter += 1
            
            ch = 9 if 'drum' in layer_name.lower() else channel_counter
            if ch not in (9,):
                channel_counter += 1
                if channel_counter >= 16:
                    channel_counter = 0
            
            new_track = MidiTrack()
            new_track.append(MetaMessage('track_name', name=f"{layer_name}_{ti}", time=0))
            
            for msg in track:
                if msg.type in ('note_on', 'note_off', 'pitchwheel', 'control_change', 'program_change'):
                    new_msg = msg.copy()
                    new_msg.channel = ch
                    new_track.append(new_msg)
                elif msg.is_meta and msg.type != 'set_tempo':
                    new_track.append(msg.copy())
            
            merged.tracks.append(new_track)
    
    merged.save(out_path)
    return out_path


# ----- Main pipeline ---------------------------------------------------------

def find(pattern):
    files = sorted(glob.glob(os.path.join(SRC_DIR, pattern)))
    return files[0] if files else None

def main():
    print("🎷 LaBoom Swing Alchemy starting...\n")
    
    src = {
        'drums':  find('*drums*.mid'),
        'bass':   find('*bass*.mid'),
        'other':  find('*other*.mid'),
        'vocals': find('*vocals*.mid'),
        'master': find('laboom.mp3-midi---*.mid'),
    }
    
    for k, v in src.items():
        print(f"  source[{k}]: {os.path.basename(v) if v else 'MISSING'}")
    print()
    
    outputs = {}
    
    if src['drums']:
        outputs['swung_drums'] = os.path.join(OUT_DIR, 'swung_drums.mid')
        swing_drums(src['drums'], outputs['swung_drums'])
        print(f"  ✓ swung_drums → {outputs['swung_drums']}")
    
    if src['bass']:
        outputs['walking_bass'] = os.path.join(OUT_DIR, 'walking_bass.mid')
        walking_bass(src['bass'], outputs['walking_bass'])
        print(f"  ✓ walking_bass → {outputs['walking_bass']}")
    
    if src['other']:
        outputs['jazz_comp'] = os.path.join(OUT_DIR, 'jazz_comp.mid')
        jazz_comp(src['other'], outputs['jazz_comp'])
        print(f"  ✓ jazz_comp → {outputs['jazz_comp']}")
    
    if src['vocals']:
        outputs['swung_vocals'] = os.path.join(OUT_DIR, 'swung_vocals.mid')
        swing_vocals(src['vocals'], outputs['swung_vocals'])
        print(f"  ✓ swung_vocals → {outputs['swung_vocals']}")
    
    # Final Master Mix
    layers = [
        ('drums', outputs.get('swung_drums')),
        ('bass', outputs.get('walking_bass')),
        ('comp', outputs.get('jazz_comp')),
        ('vocals', outputs.get('swung_vocals')),
    ]
    layers = [(n, p) for n, p in layers if p]
    
    master_out = os.path.join(OUT_DIR, 'laboom_SWUNG_master.mid')
    merge_layers(layers, master_out)
    print(f"\n  🎺 MASTER → {master_out}\n")
    
    # Verify
    print("📊 Verifying outputs...")
    for name, path in [('MASTER', master_out)] + list(outputs.items()):
        mid = MidiFile(path)
        notes = sum(1 for tr in mid.tracks for msg in tr if msg.type == 'note_on' and msg.velocity > 0)
        print(f"  {name:15} {len(mid.tracks):2d} tracks, {notes:5d} notes, {mid.length:6.1f}s")

if __name__ == '__main__':
    main()
