#!/usr/bin/env python3
"""
LaBoom Essence → 08/15 Techno
==============================
Schritt 1: Aus dem Master-MIDI die essentielle Melodie extrahieren
           (Top-Linie, Downbeats, lange Noten)
Schritt 2: Techno-Standard-Pattern bauen
           (4-on-Floor Kick, Off-Beat Hats, 2/4 Claps, Acid-Bass)
Schritt 3: Essenz-Melodie als Pluck-Lead einsetzen
Schritt 4: Render zu MP3
"""

import mido, os, math, random
from mido import MidiFile, MidiTrack, Message, MetaMessage
from collections import defaultdict

random.seed(303)  # 303 für den Acid-Spirit

SRC_MASTER = "/root/.openclaw/workspace/downloads/midi-136bpm/laboom.mp3-midi---78aac641-9267-4c37-995f-ba33286c17f4_136bpm.mid"
OUT_DIR = "/root/.openclaw/workspace/downloads/midi-techno"
os.makedirs(OUT_DIR, exist_ok=True)

TPQ = 480           # höhere Auflösung für Techno-Präzision
BPM = 130           # clubby Standard
BAR = 4 * TPQ

# General MIDI Drum Map (Channel 10 = index 9)
KICK = 36
SNARE = 38
CLAP = 39
CLOSED_HAT = 42
OPEN_HAT = 46
CRASH = 49
RIDE = 51

# ============================================================================
# STEP 1: Essenz extrahieren
# ============================================================================

def extract_essence(src_path):
    """Top-Linie pro Beat, lange Noten, Downbeats bevorzugt."""
    mid = MidiFile(src_path)
    src_tpq = mid.ticks_per_beat
    
    # Sammle alle Noten als (start_tick, dur, pitch, velocity)
    notes = []
    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)
            elif msg.type == 'note_off' or (msg.type == 'note_on' and msg.velocity == 0):
                if msg.note in active:
                    s, v = active.pop(msg.note)
                    dur = t - s
                    if dur > 0:
                        notes.append((s, dur, msg.note, v))
    
    print(f"  Original notes: {len(notes)}")
    
    # Skaliere auf neue TPQ
    scale = TPQ / src_tpq
    notes = [(int(s * scale), int(d * scale), p, v) for s, d, p, v in notes]
    
    # Quantize starts auf 8tel-Grid (TPQ/2)
    eighth = TPQ // 2
    notes = [(round(s / eighth) * eighth, d, p, v) for s, d, p, v in notes]
    
    # Filter: nur Noten mit Dauer >= 8tel
    notes = [n for n in notes if n[1] >= TPQ // 2]
    print(f"  After duration filter (>= 8th): {len(notes)}")
    
    # Top-Linie pro Tick: nehme höchste Note
    by_tick = defaultdict(list)
    for s, d, p, v in notes:
        by_tick[s].append((s, d, p, v))
    
    top_line = []
    for s in sorted(by_tick.keys()):
        # Nimm die höchste Note an diesem Tick
        candidates = by_tick[s]
        best = max(candidates, key=lambda n: n[2])  # highest pitch
        top_line.append(best)
    
    print(f"  After top-line: {len(top_line)}")
    
    # Bevorzuge Downbeats: filtere weiter wenn zu dicht
    # Behalte alles auf Beat 1+3, von Beat 2+4 nur jede 2., Off-Beats nur die längsten
    essence = []
    for s, d, p, v in top_line:
        beat_in_bar = (s % BAR) / TPQ  # 0..3.99
        beat_pos = s % TPQ
        
        is_downbeat = (beat_pos == 0)
        is_onbeat = beat_pos < TPQ // 8
        is_strong_beat = is_downbeat and beat_in_bar in (0, 2)
        
        if is_strong_beat:
            essence.append((s, d, p, v + 10))  # accent
        elif is_onbeat and d >= TPQ:
            essence.append((s, d, p, v))
        elif d >= TPQ * 2:  # very long note (half+) always keep
            essence.append((s, d, p, v))
    
    print(f"  After downbeat preference: {len(essence)}")
    
    # Letzte Konsolidierung: keine 2 Noten innerhalb 1/8 hintereinander
    essence.sort(key=lambda n: n[0])
    cleaned = []
    last_t = -TPQ
    for s, d, p, v in essence:
        if s - last_t >= TPQ // 2:
            # Truncate dur so it doesn't overlap next
            cleaned.append((s, d, p, v))
            last_t = s
    
    print(f"  Final essence: {len(cleaned)} notes")
    
    # Transponiere für Techno-Lead-Sound in Octave 5 (C5=72)
    pitches = [n[2] for n in cleaned]
    avg = sum(pitches) / len(pitches) if pitches else 60
    target = 72  # C5
    shift = round((target - avg) / 12) * 12
    cleaned = [(s, d, p + shift, v) for s, d, p, v in cleaned]
    print(f"  Pitch-shifted by {shift} semitones (avg now ~{avg+shift:.0f})")
    
    return cleaned


# ============================================================================
# STEP 2: Techno-Layer bauen
# ============================================================================

def make_drum_pattern(num_bars):
    """4-on-Floor Kick, Off-Beat-Hats, 2/4 Claps, Open Hat auf "and"."""
    events = []
    
    for bar in range(num_bars):
        bar_start = bar * BAR
        
        # KICK: 4-on-floor
        for beat in range(4):
            t = bar_start + beat * TPQ
            vel = 110 if beat == 0 else 100
            events.append((t, Message('note_on', channel=9, note=KICK, velocity=vel, time=0)))
            events.append((t + TPQ // 4, Message('note_off', channel=9, note=KICK, velocity=0, time=0)))
        
        # CLOSED HAT: 8th notes (alle off-beats + on-beats außer wo Kick)
        for eighth in range(8):
            t = bar_start + eighth * (TPQ // 2)
            # Skip downbeats (Kick is there) for typical techno
            vel = 60 if eighth % 2 == 0 else 80  # accent off-beats
            events.append((t, Message('note_on', channel=9, note=CLOSED_HAT, velocity=vel, time=0)))
            events.append((t + TPQ // 8, Message('note_off', channel=9, note=CLOSED_HAT, velocity=0, time=0)))
        
        # OPEN HAT: auf 2+ und 4+ (das klassische "tss tss")
        for beat in (1, 3):
            t = bar_start + beat * TPQ + TPQ // 2
            events.append((t, Message('note_on', channel=9, note=OPEN_HAT, velocity=90, time=0)))
            events.append((t + TPQ // 2, Message('note_off', channel=9, note=OPEN_HAT, velocity=0, time=0)))
        
        # CLAP/SNARE: auf 2 und 4
        for beat in (1, 3):
            t = bar_start + beat * TPQ
            events.append((t, Message('note_on', channel=9, note=CLAP, velocity=105, time=0)))
            events.append((t + TPQ // 4, Message('note_off', channel=9, note=CLAP, velocity=0, time=0)))
            events.append((t, Message('note_on', channel=9, note=SNARE, velocity=85, time=0)))
            events.append((t + TPQ // 4, Message('note_off', channel=9, note=SNARE, velocity=0, time=0)))
        
        # CRASH auf Bar 1, 17 (Drop), 33
        if bar in (0, 16, 32):
            events.append((bar_start, Message('note_on', channel=9, note=CRASH, velocity=115, time=0)))
            events.append((bar_start + TPQ, Message('note_off', channel=9, note=CRASH, velocity=0, time=0)))
    
    return events


def make_acid_bass(essence_notes, num_bars):
    """TB-303-style Acid-Bass: 16tel-Pattern, Root der nächstgelegenen Essenz-Note,
       mit Slide (Pitch-Bend) auf manchen Noten + Filter-Sweep via CC74."""
    events = []
    sixteenth = TPQ // 4
    
    # Hole "Root-Map": welcher Pitch dominiert in welcher Bar
    bar_roots = {}
    for s, d, p, v in essence_notes:
        b = s // BAR
        bar_roots.setdefault(b, []).append(p % 12)
    
    # Default-Root falls leer
    def get_root(b):
        if b in bar_roots and bar_roots[b]:
            from collections import Counter
            return Counter(bar_roots[b]).most_common(1)[0][0]
        return 0  # C
    
    # Acid-Pattern: typisches 16-Step-Sequenz mit gelegentlichen Akzenten + Slides
    # x = root, 0 = silence, + = octave up, - = -1 semi (chromatic), o = oct down, s = slide-to-next
    pattern = ['x', '0', 'x', '+', '0', 'x', 'o', 'x',
               '+', 's', '-', 'x', '0', 'x', '+', 'o']
    
    for bar in range(num_bars):
        # Silence im Build-Up vor Drop
        if bar in (14, 15, 30, 31):
            continue
        
        bar_start = bar * BAR
        root = get_root(bar)
        base = 36 + root  # C2 area, deep bass
        
        for step, sym in enumerate(pattern):
            t = bar_start + step * sixteenth
            
            if sym == '0':
                continue
            
            pitch_offset = {'x': 0, '+': 12, '-': -1, 'o': -12, 's': 0}[sym]
            pitch = base + pitch_offset
            vel = 110 if step % 4 == 0 else (90 if sym in ('+', 's') else 80)
            
            # Längere Note für Slides
            dur = int(sixteenth * 1.5) if sym == 's' else int(sixteenth * 0.7)
            
            events.append((t, Message('note_on', channel=0, note=pitch, velocity=vel, time=0)))
            events.append((t + dur, Message('note_off', channel=0, note=pitch, velocity=0, time=0)))
            
            # Filter-Sweep via CC74 — gradually opens over the bar
            if step % 2 == 0:
                cutoff = 30 + int((step / 16) * 90) + random.randint(-5, 10)
                cutoff = max(20, min(127, cutoff))
                events.append((t, Message('control_change', channel=0, control=74, value=cutoff, time=0)))
        
        # Resonance-Sweep over bar (CC71)
        for s in range(0, BAR, TPQ // 2):
            t = bar_start + s
            res = 80 + int(20 * math.sin(s / BAR * math.pi * 2))
            events.append((t, Message('control_change', channel=0, control=71, value=res, time=0)))
    
    return events


def make_lead(essence_notes, num_bars):
    """Pluck-Lead aus Essenz-Noten, alle 4 Bars Stutter-Effekt."""
    events = []
    
    # Loop die Essenz über num_bars
    if not essence_notes:
        return events
    
    max_essence_tick = max(s + d for s, d, _, _ in essence_notes)
    essence_bars = (max_essence_tick // BAR) + 1
    
    for loop in range((num_bars // essence_bars) + 1):
        offset = loop * essence_bars * BAR
        for s, d, p, v in essence_notes:
            new_s = s + offset
            if new_s >= num_bars * BAR:
                break
            
            # Pluck = kurze Dauer (1/8 max)
            pluck_dur = min(d, TPQ // 2)
            
            events.append((new_s, Message('note_on', channel=1, note=p, velocity=min(127, v + 5), time=0)))
            events.append((new_s + pluck_dur, Message('note_off', channel=1, note=p, velocity=0, time=0)))
            
            # Octave doubling für mehr Glanz auf Akzenten
            if v > 90 and p < 96:
                events.append((new_s, Message('note_on', channel=1, note=p + 12, velocity=v - 20, time=0)))
                events.append((new_s + pluck_dur, Message('note_off', channel=1, note=p + 12, velocity=0, time=0)))
        
        # Stutter-Effekt: alle 4 Bars die letzte Note 4x wiederholen
        bar_idx = loop * essence_bars
        if bar_idx % 4 == 3 and essence_notes:
            stutter_pitch = essence_notes[-1][2]
            stutter_start = bar_idx * BAR + 3 * TPQ  # Beat 4 of bar
            for i in range(4):
                t = stutter_start + i * (TPQ // 4)
                events.append((t, Message('note_on', channel=1, note=stutter_pitch, velocity=100 - i*10, time=0)))
                events.append((t + TPQ // 8, Message('note_off', channel=1, note=stutter_pitch, velocity=0, time=0)))
    
    return events


def make_buildup(num_bars):
    """Rising Noise/Snare-Roll vor Drop bei Bar 16 und 32."""
    events = []
    
    for drop_bar in (16, 32):
        if drop_bar > num_bars:
            continue
        
        # 2-Bar Build-up vor dem Drop (bar 14-15, 30-31)
        build_start = (drop_bar - 2) * BAR
        
        # Accelerating snare roll: 16tel → 32tel → 64tel
        intervals = [TPQ // 4] * 8 + [TPQ // 8] * 16 + [TPQ // 16] * 16
        t = build_start
        for i, interval in enumerate(intervals):
            if t >= drop_bar * BAR:
                break
            vel = 50 + int((i / len(intervals)) * 70)
            events.append((t, Message('note_on', channel=9, note=SNARE, velocity=vel, time=0)))
            events.append((t + interval // 2, Message('note_off', channel=9, note=SNARE, velocity=0, time=0)))
            t += interval
        
        # Pitched-Up Riser auf einem Sweep-Synth (Channel 3)
        riser_start = build_start
        for i in range(64):
            t = riser_start + i * (BAR * 2 // 64)
            if t >= drop_bar * BAR:
                break
            pitch = 36 + (i * 60 // 64)  # rising from low to high
            events.append((t, Message('note_on', channel=3, note=pitch, velocity=70, time=0)))
            events.append((t + TPQ // 8, Message('note_off', channel=3, note=pitch, velocity=0, time=0)))
            # filter open
            events.append((t, Message('control_change', channel=3, control=74, value=int(min(127, 30 + i*1.5)), time=0)))
    
    return events


# ============================================================================
# STEP 3: Build & Save
# ============================================================================

def build_track():
    print(f"📡 Extracting essence from: {os.path.basename(SRC_MASTER)}")
    essence = extract_essence(SRC_MASTER)
    
    # Save essence as separate file
    essence_path = os.path.join(OUT_DIR, "essence_only.mid")
    save_simple_midi(essence, essence_path, program=80, channel=1)  # square lead
    print(f"  → essence saved: {essence_path}\n")
    
    # Build techno
    num_bars = 48  # ~88 seconds at 130 BPM (4 bars/8 beats per phrase)
    print(f"🛠  Building techno arrangement: {num_bars} bars @ {BPM} BPM")
    
    mid = MidiFile(ticks_per_beat=TPQ)
    
    # Meta track
    meta = MidiTrack()
    meta.append(MetaMessage('set_tempo', tempo=int(60_000_000 / BPM), time=0))
    meta.append(MetaMessage('time_signature', numerator=4, denominator=4, time=0))
    meta.append(MetaMessage('track_name', name='LaBoom Techno Essence', time=0))
    mid.tracks.append(meta)
    
    # Drums
    drum_events = make_drum_pattern(num_bars)
    print(f"  ✓ Drums: {len(drum_events)//2} hits")
    mid.tracks.append(events_to_track(drum_events, name="Drums"))
    
    # Bass
    bass_events = make_acid_bass(essence, num_bars)
    print(f"  ✓ Acid-Bass: {sum(1 for _, m in bass_events if m.type == 'note_on')} notes")
    bass_track = events_to_track(bass_events, name="Acid-Bass")
    bass_track.insert(0, Message('program_change', channel=0, program=38, time=0))  # Synth Bass 1
    mid.tracks.append(bass_track)
    
    # Lead
    lead_events = make_lead(essence, num_bars)
    print(f"  ✓ Lead: {sum(1 for _, m in lead_events if m.type == 'note_on')} notes")
    lead_track = events_to_track(lead_events, name="Lead")
    lead_track.insert(0, Message('program_change', channel=1, program=80, time=0))  # Square Lead
    mid.tracks.append(lead_track)
    
    # Build-up + Riser
    build_events = make_buildup(num_bars)
    print(f"  ✓ Build-up: {sum(1 for _, m in build_events if m.type == 'note_on')} hits")
    build_track_obj = events_to_track(build_events, name="Buildup")
    build_track_obj.insert(0, Message('program_change', channel=3, program=84, time=0))  # Lead 5
    mid.tracks.append(build_track_obj)
    
    out_path = os.path.join(OUT_DIR, "laboom_TECHNO.mid")
    mid.save(out_path)
    print(f"\n  🎶 TECHNO → {out_path}")
    return out_path, essence_path


def save_simple_midi(notes, path, program=80, channel=1):
    mid = MidiFile(ticks_per_beat=TPQ)
    meta = MidiTrack()
    meta.append(MetaMessage('set_tempo', tempo=int(60_000_000 / 136), time=0))  # original tempo
    meta.append(MetaMessage('track_name', name='Essence', time=0))
    mid.tracks.append(meta)
    
    events = []
    for s, d, p, v in notes:
        events.append((s, Message('note_on', channel=channel, note=p, velocity=v, time=0)))
        events.append((s + d, Message('note_off', channel=channel, note=p, velocity=0, time=0)))
    
    track = events_to_track(events, name="Essence")
    track.insert(0, Message('program_change', channel=channel, program=program, time=0))
    mid.tracks.append(track)
    mid.save(path)


def events_to_track(events, name="Track"):
    events = sorted(events, key=lambda x: (x[0], 0 if x[1].type == 'note_off' else 1))
    track = MidiTrack()
    track.append(MetaMessage('track_name', name=name, time=0))
    last_t = 0
    for t, msg in events:
        msg = msg.copy()
        msg.time = max(0, t - last_t)
        track.append(msg)
        last_t = t
    return track


if __name__ == '__main__':
    techno_path, essence_path = build_track()
    print(f"\n✅ Files:")
    print(f"   essence: {essence_path}")
    print(f"   techno:  {techno_path}")
