//! Real-time pattern sequencer: evaluates Forth scripts per step and produces audio/MIDI commands.

use arc_swap::ArcSwap;
use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
use parking_lot::Mutex;
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::collections::HashMap;
#[cfg(feature = "desktop")]
use std::sync::atomic::AtomicU32;
use std::sync::atomic::{AtomicI64, AtomicU64};
use std::sync::Arc;
use std::thread::{self, JoinHandle};

use super::dispatcher::{dispatcher_loop, MidiDispatch, TimedMidiCommand};
use super::realtime::set_realtime_priority;
use super::{substeps_in_window, LinkState, StepTiming, SyncTime};
use crate::model::{
    CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables,
};
use crate::model::{FollowUp, LaunchQuantization, SyncMode, MAX_BANKS, MAX_PATTERNS};
use crate::state::LiveKeyState;

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct PatternId {
    pub bank: usize,
    pub pattern: usize,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum PatternChange {
    Start { bank: usize, pattern: usize },
    Stop { bank: usize, pattern: usize },
}

impl PatternChange {
    pub fn pattern_id(&self) -> PatternId {
        match self {
            PatternChange::Start { bank, pattern } => PatternId {
                bank: *bank,
                pattern: *pattern,
            },
            PatternChange::Stop { bank, pattern } => PatternId {
                bank: *bank,
                pattern: *pattern,
            },
        }
    }
}

pub enum AudioCommand {
    Evaluate { cmd: String, time: Option<f64> },
    Hush,
    Panic,
    LoadSamples(Vec<doux::sampling::SampleEntry>),
    LoadSoundfont(std::path::PathBuf),
}

#[derive(Clone, Debug)]
pub enum MidiCommand {
    NoteOn {
        device: u8,
        channel: u8,
        note: u8,
        velocity: u8,
    },
    NoteOff {
        device: u8,
        channel: u8,
        note: u8,
    },
    CC {
        device: u8,
        channel: u8,
        cc: u8,
        value: u8,
    },
    PitchBend {
        device: u8,
        channel: u8,
        value: u16,
    },
    Pressure {
        device: u8,
        channel: u8,
        value: u8,
    },
    ProgramChange {
        device: u8,
        channel: u8,
        program: u8,
    },
    Clock {
        device: u8,
    },
    Start {
        device: u8,
    },
    Stop {
        device: u8,
    },
    Continue {
        device: u8,
    },
}

pub enum SeqCommand {
    PatternUpdate {
        bank: usize,
        pattern: usize,
        data: PatternSnapshot,
    },
    PatternStart {
        bank: usize,
        pattern: usize,
        quantization: LaunchQuantization,
        sync_mode: SyncMode,
    },
    PatternStop {
        bank: usize,
        pattern: usize,
        quantization: LaunchQuantization,
    },
    SetMuteState {
        muted: std::collections::HashSet<(usize, usize)>,
        soloed: std::collections::HashSet<(usize, usize)>,
    },
    ScriptUpdate {
        script: String,
        speed: crate::model::PatternSpeed,
        length: usize,
    },
    StopAll,
    RestartAll,
    ResetScriptState,
    Shutdown,
}

#[derive(Clone)]
pub struct PatternSnapshot {
    pub speed: crate::model::PatternSpeed,
    pub length: usize,
    pub steps: Vec<StepSnapshot>,
    #[allow(dead_code)]
    pub quantization: LaunchQuantization,
    pub sync_mode: SyncMode,
    pub follow_up: FollowUp,
}

#[derive(Clone)]
pub struct StepSnapshot {
    pub active: bool,
    pub script: String,
    pub source: Option<u8>,
}

#[derive(Clone, Copy, Default, Debug)]
pub struct ActivePatternState {
    pub bank: usize,
    pub pattern: usize,
    pub step_index: usize,
    pub iter: usize,
    pub last_step_beat: f64,
}

pub type StepTracesMap = HashMap<(usize, usize, usize), ExecutionTrace>;

#[derive(Clone, Default)]
pub struct SharedSequencerState {
    pub active_patterns: Vec<ActivePatternState>,
    pub step_traces: Arc<StepTracesMap>,
    pub event_count: usize,
    pub tempo: f64,
    pub beat: f64,
    pub script_trace: Option<ExecutionTrace>,
}

pub struct SequencerSnapshot {
    pub active_patterns: Vec<ActivePatternState>,
    step_traces: Arc<StepTracesMap>,
    pub event_count: usize,
    pub tempo: f64,
    pub beat: f64,
    script_trace: Option<ExecutionTrace>,
}

impl From<&SharedSequencerState> for SequencerSnapshot {
    fn from(s: &SharedSequencerState) -> Self {
        Self {
            active_patterns: s.active_patterns.clone(),
            step_traces: Arc::clone(&s.step_traces),
            event_count: s.event_count,
            tempo: s.tempo,
            beat: s.beat,
            script_trace: s.script_trace.clone(),
        }
    }
}

impl SequencerSnapshot {
    #[allow(dead_code)]
    pub fn empty() -> Self {
        Self {
            active_patterns: Vec::new(),
            step_traces: Arc::new(HashMap::new()),
            event_count: 0,
            tempo: 0.0,
            beat: 0.0,
            script_trace: None,
        }
    }

    pub fn is_playing(&self, bank: usize, pattern: usize) -> bool {
        self.active_patterns
            .iter()
            .any(|p| p.bank == bank && p.pattern == pattern)
    }

    pub fn get_step(&self, bank: usize, pattern: usize) -> Option<usize> {
        self.active_patterns
            .iter()
            .find(|p| p.bank == bank && p.pattern == pattern)
            .map(|p| p.step_index)
    }

    /// Returns smooth progress (0.0..1.0) through the pattern by interpolating
    /// between discrete steps using beat timing.
    pub fn get_smooth_progress(&self, bank: usize, pattern: usize, length: usize, speed_mult: f64) -> Option<f64> {
        let p = self.active_patterns.iter().find(|p| p.bank == bank && p.pattern == pattern)?;
        if length == 0 || self.tempo <= 0.0 {
            return Some(0.0);
        }
        let step_duration_beats = 1.0 / (4.0 * speed_mult);
        let elapsed = (self.beat - p.last_step_beat).max(0.0);
        let phase = (elapsed / step_duration_beats).clamp(0.0, 1.0);
        Some((p.step_index as f64 + phase) / length as f64)
    }

    pub fn get_iter(&self, bank: usize, pattern: usize) -> Option<usize> {
        self.active_patterns
            .iter()
            .find(|p| p.bank == bank && p.pattern == pattern)
            .map(|p| p.iter)
    }

    pub fn get_trace(&self, bank: usize, pattern: usize, step: usize) -> Option<&ExecutionTrace> {
        self.step_traces.get(&(bank, pattern, step))
    }

    pub fn script_trace(&self) -> Option<&ExecutionTrace> {
        self.script_trace.as_ref()
    }
}

pub struct SequencerHandle {
    pub cmd_tx: Sender<SeqCommand>,
    pub audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
    pub midi_tx: Arc<ArcSwap<Sender<MidiCommand>>>,
    shared_state: Arc<ArcSwap<SharedSequencerState>>,
    thread: JoinHandle<()>,
}

impl SequencerHandle {
    pub fn snapshot(&self) -> SequencerSnapshot {
        let state = self.shared_state.load();
        SequencerSnapshot::from(state.as_ref())
    }

    pub fn swap_audio_channel(&self) -> Receiver<AudioCommand> {
        let (new_tx, new_rx) = unbounded::<AudioCommand>();
        self.audio_tx.store(Arc::new(new_tx));
        new_rx
    }

    pub fn swap_midi_channel(&self) -> Receiver<MidiCommand> {
        let (new_tx, new_rx) = bounded::<MidiCommand>(256);
        self.midi_tx.store(Arc::new(new_tx));
        new_rx
    }

    pub fn shutdown(self) {
        let _ = self.cmd_tx.send(SeqCommand::Shutdown);
        if let Err(e) = self.thread.join() {
            eprintln!("Sequencer thread panicked: {e:?}");
        }
    }
}

#[derive(Clone, Copy, Default)]
struct ActivePattern {
    bank: usize,
    pattern: usize,
    step_index: usize,
    iter: usize,
    last_step_beat: f64,
}

#[derive(Clone, Copy)]
struct PendingPattern {
    id: PatternId,
    quantization: LaunchQuantization,
    sync_mode: SyncMode,
}

struct AudioState {
    prev_beat: f64,
    active_patterns: HashMap<PatternId, ActivePattern>,
    pending_starts: Vec<PendingPattern>,
    pending_stops: Vec<PendingPattern>,
    flush_midi_notes: bool,
}

impl AudioState {
    fn new() -> Self {
        Self {
            prev_beat: -1.0,
            active_patterns: HashMap::new(),
            pending_starts: Vec::new(),
            pending_stops: Vec::new(),
            flush_midi_notes: false,
        }
    }
}

pub struct SequencerConfig {
    pub audio_sample_pos: Arc<AtomicU64>,
    pub sample_rate: Arc<std::sync::atomic::AtomicU32>,
    pub cc_access: Option<Arc<dyn CcAccess>>,
    pub variables: Variables,
    pub dict: Dictionary,
    #[cfg(feature = "desktop")]
    pub mouse_x: Arc<AtomicU32>,
    #[cfg(feature = "desktop")]
    pub mouse_y: Arc<AtomicU32>,
    #[cfg(feature = "desktop")]
    pub mouse_down: Arc<AtomicU32>,
}

pub fn spawn_sequencer(
    link: Arc<LinkState>,
    playing: Arc<std::sync::atomic::AtomicBool>,
    quantum: f64,
    live_keys: Arc<LiveKeyState>,
    nudge_us: Arc<AtomicI64>,
    config: SequencerConfig,
) -> (
    SequencerHandle,
    Receiver<AudioCommand>,
    Receiver<MidiCommand>,
) {
    let (cmd_tx, cmd_rx) = bounded::<SeqCommand>(64);
    let (audio_tx, audio_rx) = unbounded::<AudioCommand>();
    let (midi_tx, midi_rx) = bounded::<MidiCommand>(256);
    let audio_tx = Arc::new(ArcSwap::from_pointee(audio_tx));
    let midi_tx = Arc::new(ArcSwap::from_pointee(midi_tx));

    // Dispatcher channel — MIDI only (unbounded to avoid blocking the scheduler)
    let (dispatch_tx, dispatch_rx) = unbounded::<TimedMidiCommand>();

    let shared_state = Arc::new(ArcSwap::from_pointee(SharedSequencerState::default()));
    let shared_state_clone = Arc::clone(&shared_state);

    let variables = config.variables;
    let dict = config.dict;
    #[cfg(feature = "desktop")]
    let mouse_x = config.mouse_x;
    #[cfg(feature = "desktop")]
    let mouse_y = config.mouse_y;
    #[cfg(feature = "desktop")]
    let mouse_down = config.mouse_down;

    // Spawn dispatcher thread (MIDI only — audio goes direct to doux)
    let dispatcher_link = Arc::clone(&link);
    let dispatcher_midi_tx = Arc::clone(&midi_tx);
    thread::Builder::new()
        .name("cagire-dispatcher".into())
        .spawn(move || {
            dispatcher_loop(dispatch_rx, dispatcher_midi_tx, dispatcher_link);
        })
        .expect("Failed to spawn dispatcher thread");

    let sequencer_audio_tx = Arc::clone(&audio_tx);
    let thread = thread::Builder::new()
        .name("sequencer".into())
        .spawn(move || {
            sequencer_loop(
                cmd_rx,
                dispatch_tx,
                sequencer_audio_tx,
                link,
                playing,
                quantum,
                shared_state_clone,
                live_keys,
                nudge_us,
                config.audio_sample_pos,
                config.sample_rate,
                config.cc_access,
                variables,
                dict,
                #[cfg(feature = "desktop")]
                mouse_x,
                #[cfg(feature = "desktop")]
                mouse_y,
                #[cfg(feature = "desktop")]
                mouse_down,
            );
        })
        .expect("Failed to spawn sequencer thread");

    let handle = SequencerHandle {
        cmd_tx,
        audio_tx,
        midi_tx,
        shared_state,
        thread,
    };
    (handle, audio_rx, midi_rx)
}

struct PatternCache {
    patterns: [[Option<PatternSnapshot>; MAX_PATTERNS]; MAX_BANKS],
}

impl PatternCache {
    fn new() -> Self {
        Self {
            patterns: std::array::from_fn(|_| std::array::from_fn(|_| None)),
        }
    }

    fn get(&self, bank: usize, pattern: usize) -> Option<&PatternSnapshot> {
        self.patterns
            .get(bank)
            .and_then(|b| b.get(pattern))
            .and_then(|p| p.as_ref())
    }

    fn set(&mut self, bank: usize, pattern: usize, data: PatternSnapshot) {
        if bank < MAX_BANKS && pattern < MAX_PATTERNS {
            self.patterns[bank][pattern] = Some(data);
        }
    }
}

impl PatternSnapshot {
    fn resolve_source(&self, index: usize) -> usize {
        let mut current = index;
        for _ in 0..self.steps.len() {
            if let Some(step) = self.steps.get(current) {
                if let Some(source) = step.source {
                    current = source as usize;
                } else {
                    return current;
                }
            } else {
                return index;
            }
        }
        index
    }

    fn resolve_script(&self, index: usize) -> Option<&str> {
        let source_idx = self.resolve_source(index);
        self.steps.get(source_idx).map(|s| s.script.as_str())
    }
}

fn check_quantization_boundary(
    quantization: LaunchQuantization,
    beat: f64,
    prev_beat: f64,
    quantum: f64,
) -> bool {
    match quantization {
        LaunchQuantization::Immediate => prev_beat >= 0.0,
        LaunchQuantization::Beat => StepTiming::NextBeat.crossed(prev_beat, beat, quantum),
        LaunchQuantization::Bar => StepTiming::NextBar.crossed(prev_beat, beat, quantum),
        LaunchQuantization::Bars2 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 2.0),
        LaunchQuantization::Bars4 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 4.0),
        LaunchQuantization::Bars8 => StepTiming::NextBar.crossed(prev_beat, beat, quantum * 8.0),
    }
}

type StepKey = (usize, usize, usize);

struct RunsCounter {
    counts: HashMap<StepKey, usize>,
}

impl RunsCounter {
    fn new() -> Self {
        Self {
            counts: HashMap::new(),
        }
    }

    fn get_and_increment(&mut self, bank: usize, pattern: usize, step: usize) -> usize {
        let key = (bank, pattern, step);
        let count = self.counts.entry(key).or_insert(0);
        let current = *count;
        *count += 1;
        current
    }

    fn clear_pattern(&mut self, bank: usize, pattern: usize) {
        self.counts
            .retain(|&(b, p, _), _| b != bank || p != pattern);
    }
}

pub struct TickInput {
    pub commands: Vec<SeqCommand>,
    pub playing: bool,
    pub beat: f64,
    pub lookahead_end: f64,
    pub tempo: f64,
    pub quantum: f64,
    pub fill: bool,
    pub nudge_secs: f64,
    pub current_time_us: SyncTime,
    pub engine_time: f64,
    pub mouse_x: f64,
    pub mouse_y: f64,
    pub mouse_down: f64,
}

pub struct TimestampedCommand {
    pub cmd: String,
    pub time: Option<f64>,
}

pub struct TickOutput {
    pub audio_commands: Vec<TimestampedCommand>,
    pub new_tempo: Option<f64>,
    pub shared_state: SharedSequencerState,
    pub flush_midi_notes: bool,
}

struct StepResult {
    any_step_fired: bool,
}

fn format_speed_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
    use std::fmt::Write;
    buf.clear();
    write!(buf, "__speed_{bank}_{pattern}__").unwrap();
    buf
}

pub struct SequencerState {
    audio_state: AudioState,
    pattern_cache: PatternCache,
    pending_updates: HashMap<(usize, usize), PatternSnapshot>,
    runs_counter: RunsCounter,
    step_traces: Arc<StepTracesMap>,
    event_count: usize,
    script_engine: ScriptEngine,
    variables: Variables,
    dict: Dictionary,
    speed_overrides: HashMap<(usize, usize), f64>,
    speed_key_buf: String,
    buf_audio_commands: Vec<TimestampedCommand>,
    buf_activated: Vec<PatternId>,
    buf_stopped: Vec<PatternId>,
    buf_completed_iterations: Vec<PatternId>,
    cc_access: Option<Arc<dyn CcAccess>>,
    muted: std::collections::HashSet<(usize, usize)>,
    soloed: std::collections::HashSet<(usize, usize)>,
    last_tempo: f64,
    last_beat: f64,
    script_text: String,
    script_speed: crate::model::PatternSpeed,
    script_length: usize,
    script_frontier: f64,
    script_step: usize,
    script_trace: Option<ExecutionTrace>,
}

impl SequencerState {
    pub fn new(
        variables: Variables,
        dict: Dictionary,
        rng: Rng,
        cc_access: Option<Arc<dyn CcAccess>>,
    ) -> Self {
        let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&dict), rng);
        Self {
            audio_state: AudioState::new(),
            pattern_cache: PatternCache::new(),
            pending_updates: HashMap::new(),
            runs_counter: RunsCounter::new(),
            step_traces: Arc::new(HashMap::new()),
            event_count: 0,
            script_engine,
            variables,
            dict,
            speed_overrides: HashMap::with_capacity(MAX_PATTERNS),
            speed_key_buf: String::with_capacity(24),
            buf_audio_commands: Vec::with_capacity(32),
            buf_activated: Vec::with_capacity(16),
            buf_stopped: Vec::with_capacity(16),
            buf_completed_iterations: Vec::with_capacity(16),
            cc_access,
            muted: std::collections::HashSet::new(),
            soloed: std::collections::HashSet::new(),
            last_tempo: 0.0,
            last_beat: 0.0,
            script_text: String::new(),
            script_speed: crate::model::PatternSpeed::default(),
            script_length: 16,
            script_frontier: -1.0,
            script_step: 0,
            script_trace: None,
        }
    }

    fn is_effectively_muted(&self, bank: usize, pattern: usize) -> bool {
        let key = (bank, pattern);
        if self.muted.contains(&key) {
            return true;
        }
        if !self.soloed.is_empty() && !self.soloed.contains(&key) {
            return true;
        }
        false
    }

    fn process_commands(&mut self, commands: Vec<SeqCommand>) {
        for cmd in commands {
            match cmd {
                SeqCommand::PatternUpdate {
                    bank,
                    pattern,
                    data,
                } => {
                    let id = PatternId { bank, pattern };
                    let is_active = self.audio_state.active_patterns.contains_key(&id);
                    let has_cache = self.pattern_cache.get(bank, pattern).is_some();
                    if is_active && has_cache {
                        self.pending_updates.insert((bank, pattern), data);
                    } else {
                        self.pattern_cache.set(bank, pattern, data);
                    }
                }
                SeqCommand::PatternStart {
                    bank,
                    pattern,
                    quantization,
                    sync_mode,
                } => {
                    let id = PatternId { bank, pattern };
                    self.audio_state.pending_stops.retain(|p| p.id != id);
                    if !self.audio_state.pending_starts.iter().any(|p| p.id == id) {
                        self.audio_state.pending_starts.push(PendingPattern {
                            id,
                            quantization,
                            sync_mode,
                        });
                    }
                }
                SeqCommand::PatternStop {
                    bank,
                    pattern,
                    quantization,
                } => {
                    let id = PatternId { bank, pattern };
                    self.audio_state.pending_starts.retain(|p| p.id != id);
                    if !self.audio_state.pending_stops.iter().any(|p| p.id == id) {
                        self.audio_state.pending_stops.push(PendingPattern {
                            id,
                            quantization,
                            sync_mode: SyncMode::Reset,
                        });
                    }
                }
                SeqCommand::SetMuteState { muted, soloed } => {
                    let newly_muted: Vec<(usize, usize)> = self
                        .audio_state
                        .active_patterns
                        .keys()
                        .filter(|id| {
                            let key = (id.bank, id.pattern);
                            let was_muted = self.is_effectively_muted(id.bank, id.pattern);
                            let now_muted = muted.contains(&key)
                                || (!soloed.is_empty() && !soloed.contains(&key));
                            !was_muted && now_muted
                        })
                        .map(|id| (id.bank, id.pattern))
                        .collect();

                    self.muted = muted;
                    self.soloed = soloed;

                    if !newly_muted.is_empty() {
                        self.audio_state.flush_midi_notes = true;
                    }
                }
                SeqCommand::ScriptUpdate { script, speed, length } => {
                    self.script_text = script;
                    self.script_speed = speed;
                    self.script_length = length;
                }
                SeqCommand::StopAll => {
                    // Flush pending updates so cache stays current for future launches
                    for ((bank, pattern), snapshot) in self.pending_updates.drain() {
                        self.pattern_cache.set(bank, pattern, snapshot);
                    }
                    self.audio_state.active_patterns.clear();
                    self.audio_state.pending_starts.clear();
                    self.audio_state.pending_stops.clear();
                    Arc::make_mut(&mut self.step_traces).clear();
                    self.runs_counter.counts.clear();
                    self.audio_state.flush_midi_notes = true;
                }
                SeqCommand::RestartAll => {
                    for active in self.audio_state.active_patterns.values_mut() {
                        active.step_index = 0;
                        active.iter = 0;
                    }
                    self.audio_state.prev_beat = -1.0;
                    self.script_frontier = -1.0;
                    self.script_step = 0;
                    self.script_trace = None;
                    self.variables.store(Arc::new(HashMap::new()));
                    self.dict.lock().clear();
                    self.speed_overrides.clear();
                    self.script_engine.clear_global_params();
                    self.runs_counter.counts.clear();
                    Arc::make_mut(&mut self.step_traces).clear();
                    self.audio_state.flush_midi_notes = true;
                }
                SeqCommand::ResetScriptState => {
                    // Clear shared state instead of replacing - preserves sharing with app
                    self.variables.store(Arc::new(HashMap::new()));
                    self.dict.lock().clear();
                    self.speed_overrides.clear();
                    self.script_engine.clear_global_params();
                }
                SeqCommand::Shutdown => {}
            }
        }
    }

    pub fn tick(&mut self, input: TickInput) -> TickOutput {
        self.process_commands(input.commands);
        self.last_tempo = input.tempo;
        self.last_beat = input.beat;

        if !input.playing {
            return self.tick_paused();
        }

        let frontier = self.audio_state.prev_beat;
        let lookahead_end = input.lookahead_end;

        if frontier < 0.0 {
            self.realign_phaselock_patterns(lookahead_end);
        }

        self.activate_pending(lookahead_end, frontier, input.quantum);
        self.deactivate_pending(lookahead_end, frontier, input.quantum);

        let steps = self.execute_steps(
            input.beat,
            frontier,
            lookahead_end,
            input.tempo,
            input.quantum,
            input.fill,
            input.nudge_secs,
            input.current_time_us,
            input.engine_time,
            input.mouse_x,
            input.mouse_y,
            input.mouse_down,
        );

        self.execute_periodic_script(
            input.beat,
            frontier,
            lookahead_end,
            input.tempo,
            input.quantum,
            input.fill,
            input.nudge_secs,
            input.engine_time,
            input.mouse_x,
            input.mouse_y,
            input.mouse_down,
        );

        let new_tempo = self.read_tempo_variable(steps.any_step_fired);
        self.apply_follow_ups();

        self.audio_state.prev_beat = lookahead_end;

        let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
        TickOutput {
            audio_commands: std::mem::take(&mut self.buf_audio_commands),
            new_tempo,
            shared_state: self.build_shared_state(),
            flush_midi_notes: flush,
        }
    }

    fn tick_paused(&mut self) -> TickOutput {
        for pending in self.audio_state.pending_stops.drain(..) {
            self.audio_state.active_patterns.remove(&pending.id);
            Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
                bank != pending.id.bank || pattern != pending.id.pattern
            });
            let key = (pending.id.bank, pending.id.pattern);
            if let Some(snapshot) = self.pending_updates.remove(&key) {
                self.pattern_cache.set(key.0, key.1, snapshot);
            }
        }
        self.audio_state.prev_beat = -1.0;
        self.script_frontier = -1.0;
        self.script_step = 0;
        self.script_trace = None;
        self.buf_audio_commands.clear();
        let flush = std::mem::take(&mut self.audio_state.flush_midi_notes);
        TickOutput {
            audio_commands: std::mem::take(&mut self.buf_audio_commands),
            new_tempo: None,
            shared_state: self.build_shared_state(),
            flush_midi_notes: flush,
        }
    }

    fn realign_phaselock_patterns(&mut self, beat: f64) {
        for (id, active) in &mut self.audio_state.active_patterns {
            let Some(pattern) = self.pattern_cache.get(id.bank, id.pattern) else {
                continue;
            };
            if pattern.sync_mode != SyncMode::PhaseLock {
                continue;
            }
            let speed_mult = pattern.speed.multiplier();
            let subs_per_beat = 4.0 * speed_mult;
            let step = (beat * subs_per_beat).floor() as usize + 1;
            active.step_index = step % pattern.length;
        }
    }

    fn activate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
        self.buf_activated.clear();
        for pending in &self.audio_state.pending_starts {
            if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
                let start_step = match pending.sync_mode {
                    SyncMode::Reset => 0,
                    SyncMode::PhaseLock => {
                        if let Some(pat) =
                            self.pattern_cache.get(pending.id.bank, pending.id.pattern)
                        {
                            let speed_mult = pat.speed.multiplier();
                            let subs_per_beat = 4.0 * speed_mult;
                            let first_sub = (prev_beat * subs_per_beat).floor() as usize + 1;
                            first_sub % pat.length
                        } else {
                            0
                        }
                    }
                };
                self.runs_counter
                    .clear_pattern(pending.id.bank, pending.id.pattern);
                self.audio_state.active_patterns.insert(
                    pending.id,
                    ActivePattern {
                        bank: pending.id.bank,
                        pattern: pending.id.pattern,
                        step_index: start_step,
                        iter: 0,
                        last_step_beat: beat,
                    },
                );
                self.buf_activated.push(pending.id);
            }
        }
        let activated = &self.buf_activated;
        self.audio_state
            .pending_starts
            .retain(|p| !activated.contains(&p.id));
    }

    fn deactivate_pending(&mut self, beat: f64, prev_beat: f64, quantum: f64) {
        self.buf_stopped.clear();
        for pending in &self.audio_state.pending_stops {
            if check_quantization_boundary(pending.quantization, beat, prev_beat, quantum) {
                self.audio_state.active_patterns.remove(&pending.id);
                Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
                    bank != pending.id.bank || pattern != pending.id.pattern
                });
                // Flush pending update so cache stays current for future launches
                let key = (pending.id.bank, pending.id.pattern);
                if let Some(snapshot) = self.pending_updates.remove(&key) {
                    self.pattern_cache.set(key.0, key.1, snapshot);
                }
                self.buf_stopped.push(pending.id);
            }
        }
        let stopped = &self.buf_stopped;
        self.audio_state
            .pending_stops
            .retain(|p| !stopped.contains(&p.id));
    }

    #[allow(clippy::too_many_arguments)]
    fn execute_steps(
        &mut self,
        beat: f64,
        frontier: f64,
        lookahead_end: f64,
        tempo: f64,
        quantum: f64,
        fill: bool,
        nudge_secs: f64,
        _current_time_us: SyncTime,
        engine_time: f64,
        mouse_x: f64,
        mouse_y: f64,
        mouse_down: f64,
    ) -> StepResult {
        self.buf_audio_commands.clear();
        self.buf_completed_iterations.clear();
        let mut result = StepResult {
            any_step_fired: false,
        };

        self.speed_overrides.clear();
        {
            let vars = self.variables.load_full();
            for id in self.audio_state.active_patterns.keys() {
                let key = format_speed_key(&mut self.speed_key_buf, id.bank, id.pattern);
                if let Some(v) = vars.get(key).and_then(|v: &Value| v.as_float().ok()) {
                    self.speed_overrides.insert((id.bank, id.pattern), v);
                }
            }
        }

        for (_id, active) in self.audio_state.active_patterns.iter_mut() {
            let Some(pattern) = self.pattern_cache.get(active.bank, active.pattern) else {
                continue;
            };

            let speed_mult = self
                .speed_overrides
                .get(&(active.bank, active.pattern))
                .copied()
                .unwrap_or_else(|| pattern.speed.multiplier());

            let step_beats = substeps_in_window(frontier, lookahead_end, speed_mult);

            for step_beat in step_beats {
                result.any_step_fired = true;
                active.last_step_beat = step_beat;
                let step_idx = active.step_index % pattern.length;

                let beat_delta = step_beat - beat;
                let time_delta = if tempo > 0.0 {
                    (beat_delta / tempo) * 60.0
                } else {
                    0.0
                };
                let event_time = Some(engine_time + time_delta);

                if let Some(step) = pattern.steps.get(step_idx) {
                    let resolved_script = pattern.resolve_script(step_idx);
                    let has_script = resolved_script
                        .map(|s| !s.trim().is_empty())
                        .unwrap_or(false);

                    if step.active && has_script {
                        let pattern_key = (active.bank, active.pattern);
                        let is_muted = self.muted.contains(&pattern_key)
                            || (!self.soloed.is_empty() && !self.soloed.contains(&pattern_key));

                        if !is_muted {
                            let source_idx = pattern.resolve_source(step_idx);
                            let runs = self.runs_counter.get_and_increment(
                                active.bank,
                                active.pattern,
                                source_idx,
                            );
                            let speed_key = format_speed_key(&mut self.speed_key_buf, active.bank, active.pattern);
                            let ctx = StepContext {
                                step: step_idx,
                                beat: step_beat,
                                bank: active.bank,
                                pattern: active.pattern,
                                tempo,
                                phase: step_beat % quantum,
                                slot: 0,
                                runs,
                                iter: active.iter,
                                speed: speed_mult,
                                fill,
                                nudge_secs,
                                cc_access: self.cc_access.as_deref(),
                                speed_key,
                                mouse_x,
                                mouse_y,
                                mouse_down,
                            };
                            if let Some(script) = resolved_script {
                                let mut trace = ExecutionTrace::default();
                                if let Ok(cmds) = self
                                    .script_engine
                                    .evaluate_with_trace(script, &ctx, &mut trace)
                                {
                                    Arc::make_mut(&mut self.step_traces).insert(
                                        (active.bank, active.pattern, source_idx),
                                        std::mem::take(&mut trace),
                                    );

                                    for cmd in cmds {
                                        self.event_count += 1;
                                        self.buf_audio_commands.push(TimestampedCommand {
                                            cmd,
                                            time: event_time,
                                        });
                                    }
                                }
                            }
                        }
                    }
                }

                let next_step = active.step_index + 1;
                if next_step >= pattern.length {
                    active.iter += 1;
                    self.buf_completed_iterations.push(PatternId {
                        bank: active.bank,
                        pattern: active.pattern,
                    });
                }
                active.step_index = next_step % pattern.length;
            }
        }

        // Apply deferred updates for patterns that just completed an iteration
        for completed_id in &self.buf_completed_iterations {
            let key = (completed_id.bank, completed_id.pattern);
            if let Some(snapshot) = self.pending_updates.remove(&key) {
                self.pattern_cache.set(key.0, key.1, snapshot);
            }
        }

        result
    }

    #[allow(clippy::too_many_arguments)]
    fn execute_periodic_script(
        &mut self,
        beat: f64,
        frontier: f64,
        lookahead_end: f64,
        tempo: f64,
        quantum: f64,
        fill: bool,
        nudge_secs: f64,
        engine_time: f64,
        mouse_x: f64,
        mouse_y: f64,
        mouse_down: f64,
    ) {
        if self.script_text.trim().is_empty() {
            return;
        }

        let script_frontier = if self.script_frontier < 0.0 {
            frontier
        } else {
            self.script_frontier
        };

        let speed_mult = self.script_speed.multiplier();
        let fire_beats = substeps_in_window(script_frontier, lookahead_end, speed_mult);

        for step_beat in fire_beats {
            let beat_delta = step_beat - beat;
            let time_delta = if tempo > 0.0 {
                (beat_delta / tempo) * 60.0
            } else {
                0.0
            };
            let event_time = Some(engine_time + time_delta);

            let step_in_cycle = self.script_step % self.script_length;

            if step_in_cycle == 0 {
                let ctx = StepContext {
                    step: 0,
                    beat: step_beat,
                    bank: 0,
                    pattern: 0,
                    tempo,
                    phase: step_beat % quantum,
                    slot: 0,
                    runs: self.script_step / self.script_length,
                    iter: self.script_step / self.script_length,
                    speed: speed_mult,
                    fill,
                    nudge_secs,
                    cc_access: self.cc_access.as_deref(),
                    speed_key: "",
                    mouse_x,
                    mouse_y,
                    mouse_down,
                };

                let mut trace = ExecutionTrace::default();
                if let Ok(cmds) =
                    self.script_engine
                        .evaluate_with_trace(&self.script_text, &ctx, &mut trace)
                {
                    for cmd in cmds {
                        self.event_count += 1;
                        self.buf_audio_commands.push(TimestampedCommand {
                            cmd,
                            time: event_time,
                        });
                    }
                }
                self.script_trace = Some(trace);
            }
            self.script_step += 1;
        }

        self.script_frontier = lookahead_end;
    }

    fn read_tempo_variable(&self, any_step_fired: bool) -> Option<f64> {
        if !any_step_fired {
            return None;
        }

        let vars = self.variables.load_full();
        let new_tempo = vars
            .get("__tempo__")
            .and_then(|v: &Value| v.as_float().ok());

        if new_tempo.is_some() {
            let mut new_vars = (*vars).clone();
            new_vars.remove("__tempo__");
            self.variables.store(Arc::new(new_vars));
        }

        new_tempo
    }

    fn apply_follow_ups(&mut self) {
        for completed_id in &self.buf_completed_iterations {
            let Some(pattern) = self.pattern_cache.get(completed_id.bank, completed_id.pattern) else {
                continue;
            };

            match pattern.follow_up {
                FollowUp::Loop => {}
                FollowUp::Stop => {
                    self.audio_state.pending_stops.push(PendingPattern {
                        id: *completed_id,
                        quantization: LaunchQuantization::Immediate,
                        sync_mode: SyncMode::Reset,
                    });
                }
                FollowUp::Chain { bank, pattern } => {
                    self.audio_state.pending_stops.push(PendingPattern {
                        id: *completed_id,
                        quantization: LaunchQuantization::Immediate,
                        sync_mode: SyncMode::Reset,
                    });
                    let target = PatternId { bank, pattern };
                    if !self.audio_state.pending_starts.iter().any(|p| p.id == target) {
                        self.audio_state.pending_starts.push(PendingPattern {
                            id: target,
                            quantization: LaunchQuantization::Immediate,
                            sync_mode: SyncMode::Reset,
                        });
                    }
                }
            }
        }
    }

    fn build_shared_state(&self) -> SharedSequencerState {
        SharedSequencerState {
            active_patterns: self
                .audio_state
                .active_patterns
                .values()
                .map(|a| ActivePatternState {
                    bank: a.bank,
                    pattern: a.pattern,
                    step_index: a.step_index,
                    iter: a.iter,
                    last_step_beat: a.last_step_beat,
                })
                .collect(),
            step_traces: Arc::clone(&self.step_traces),
            event_count: self.event_count,
            tempo: self.last_tempo,
            beat: self.last_beat,
            script_trace: self.script_trace.clone(),
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn sequencer_loop(
    cmd_rx: Receiver<SeqCommand>,
    dispatch_tx: Sender<TimedMidiCommand>,
    audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
    link: Arc<LinkState>,
    playing: Arc<std::sync::atomic::AtomicBool>,
    quantum: f64,
    shared_state: Arc<ArcSwap<SharedSequencerState>>,
    live_keys: Arc<LiveKeyState>,
    nudge_us: Arc<AtomicI64>,
    audio_sample_pos: Arc<AtomicU64>,
    sample_rate: Arc<std::sync::atomic::AtomicU32>,
    cc_access: Option<Arc<dyn CcAccess>>,
    variables: Variables,
    dict: Dictionary,
    #[cfg(feature = "desktop")] mouse_x: Arc<AtomicU32>,
    #[cfg(feature = "desktop")] mouse_y: Arc<AtomicU32>,
    #[cfg(feature = "desktop")] mouse_down: Arc<AtomicU32>,
) {
    use std::sync::atomic::Ordering;

    set_realtime_priority();

    let rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
    let mut seq_state = SequencerState::new(variables, dict, rng, cc_access);

    // Lookahead window: ~20ms expressed in beats, recomputed each tick
    const LOOKAHEAD_SECS: f64 = 0.02;
    // Wake cadence: how long to sleep between scheduling passes
    const WAKE_INTERVAL: std::time::Duration = std::time::Duration::from_millis(3);

    loop {
        // Drain all pending commands, also serves as the sleep mechanism
        let mut commands = Vec::with_capacity(8);
        match cmd_rx.recv_timeout(WAKE_INTERVAL) {
            Ok(cmd) => {
                if matches!(cmd, SeqCommand::Shutdown) {
                    return;
                }
                commands.push(cmd);
            }
            Err(crossbeam_channel::RecvTimeoutError::Disconnected) => return,
            Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
        }
        while let Ok(cmd) = cmd_rx.try_recv() {
            if matches!(cmd, SeqCommand::Shutdown) {
                return;
            }
            commands.push(cmd);
        }

        let state = link.capture_app_state();
        let current_time_us = link.clock_micros() as SyncTime;
        let beat = state.beat_at_time(current_time_us as i64, quantum);
        let tempo = state.tempo();

        let lookahead_beats = if tempo > 0.0 {
            LOOKAHEAD_SECS * tempo / 60.0
        } else {
            0.0
        };
        let lookahead_end = beat + lookahead_beats;

        let sr = sample_rate.load(Ordering::Relaxed) as f64;
        let audio_samples = audio_sample_pos.load(Ordering::Acquire);
        let engine_time = if sr > 0.0 {
            audio_samples as f64 / sr
        } else {
            0.0
        };
        let input = TickInput {
            commands,
            playing: playing.load(Ordering::Relaxed),
            beat,
            lookahead_end,
            tempo,
            quantum,
            fill: live_keys.fill(),
            nudge_secs: nudge_us.load(Ordering::Relaxed) as f64 / 1_000_000.0,
            current_time_us,
            engine_time,
            #[cfg(feature = "desktop")]
            mouse_x: f32::from_bits(mouse_x.load(Ordering::Relaxed)) as f64,
            #[cfg(not(feature = "desktop"))]
            mouse_x: 0.5,
            #[cfg(feature = "desktop")]
            mouse_y: f32::from_bits(mouse_y.load(Ordering::Relaxed)) as f64,
            #[cfg(not(feature = "desktop"))]
            mouse_y: 0.5,
            #[cfg(feature = "desktop")]
            mouse_down: f32::from_bits(mouse_down.load(Ordering::Relaxed)) as f64,
            #[cfg(not(feature = "desktop"))]
            mouse_down: 0.0,
        };

        let output = seq_state.tick(input);

        // Route commands: audio direct to doux, MIDI through dispatcher
        for tsc in output.audio_commands {
            if let Some((midi_cmd, dur, delta_secs)) = parse_midi_command(&tsc.cmd) {
                let target_time_us =
                    current_time_us + (delta_secs * 1_000_000.0) as SyncTime;
                let _ = dispatch_tx.send(TimedMidiCommand {
                    command: MidiDispatch::Send(midi_cmd.clone()),
                    target_time_us,
                });

                if let (
                    MidiCommand::NoteOn {
                        device,
                        channel,
                        note,
                        ..
                    },
                    Some(dur_secs),
                ) = (&midi_cmd, dur)
                {
                    let off_time_us = target_time_us + (dur_secs * 1_000_000.0) as SyncTime;
                    let _ = dispatch_tx.send(TimedMidiCommand {
                        command: MidiDispatch::Send(MidiCommand::NoteOff {
                            device: *device,
                            channel: *channel,
                            note: *note,
                        }),
                        target_time_us: off_time_us,
                    });
                }
            } else {
                let _ = audio_tx.load().send(AudioCommand::Evaluate {
                    cmd: tsc.cmd,
                    time: tsc.time,
                });
            }
        }

        if output.flush_midi_notes {
            let _ = dispatch_tx.send(TimedMidiCommand {
                command: MidiDispatch::FlushAll,
                target_time_us: current_time_us,
            });
        }

        if let Some(t) = output.new_tempo {
            link.set_tempo(t);
        }

        shared_state.store(Arc::new(output.shared_state));
    }
}

pub fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>, f64)> {
    if !cmd.starts_with("/midi/") {
        return None;
    }
    let mut parts: [&str; 16] = [""; 16];
    let mut count = 0;
    for part in cmd.split('/').filter(|s| !s.is_empty()) {
        if count < 16 {
            parts[count] = part;
            count += 1;
        }
    }
    if count < 2 {
        return None;
    }
    let parts = &parts[..count];

    let find_param = |key: &str| -> Option<&str> {
        parts
            .iter()
            .position(|&s| s == key)
            .and_then(|i| parts.get(i + 1).copied())
    };

    let device: u8 = find_param("dev").and_then(|s| s.parse().ok()).unwrap_or(0);
    let delta: f64 = find_param("delta").and_then(|s| s.parse().ok()).unwrap_or(0.0);

    match parts[1] {
        "note" => {
            let note: u8 = parts.get(2)?.parse().ok()?;
            let vel: u8 = find_param("vel")?.parse().ok()?;
            let chan: u8 = find_param("chan")?.parse().ok()?;
            let dur: Option<f64> = find_param("dur").and_then(|s| s.parse().ok());
            Some((
                MidiCommand::NoteOn {
                    device,
                    channel: chan,
                    note,
                    velocity: vel,
                },
                dur,
                delta,
            ))
        }
        "cc" => {
            let cc: u8 = parts.get(2)?.parse().ok()?;
            let val: u8 = parts.get(3)?.parse().ok()?;
            let chan: u8 = find_param("chan")?.parse().ok()?;
            Some((
                MidiCommand::CC {
                    device,
                    channel: chan,
                    cc,
                    value: val,
                },
                None,
                delta,
            ))
        }
        "bend" => {
            let value: u16 = parts.get(2)?.parse().ok()?;
            let chan: u8 = find_param("chan")?.parse().ok()?;
            Some((
                MidiCommand::PitchBend {
                    device,
                    channel: chan,
                    value,
                },
                None,
                delta,
            ))
        }
        "pressure" => {
            let value: u8 = parts.get(2)?.parse().ok()?;
            let chan: u8 = find_param("chan")?.parse().ok()?;
            Some((
                MidiCommand::Pressure {
                    device,
                    channel: chan,
                    value,
                },
                None,
                delta,
            ))
        }
        "program" => {
            let program: u8 = parts.get(2)?.parse().ok()?;
            let chan: u8 = find_param("chan")?.parse().ok()?;
            Some((
                MidiCommand::ProgramChange {
                    device,
                    channel: chan,
                    program,
                },
                None,
                delta,
            ))
        }
        "clock" => Some((MidiCommand::Clock { device }, None, delta)),
        "start" => Some((MidiCommand::Start { device }, None, delta)),
        "stop" => Some((MidiCommand::Stop { device }, None, delta)),
        "continue" => Some((MidiCommand::Continue { device }, None, delta)),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use arc_swap::ArcSwap;
    use parking_lot::Mutex;

    fn make_state() -> SequencerState {
        let variables: Variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
        let dict: Dictionary = Arc::new(Mutex::new(HashMap::new()));
        let rng: Rng = Arc::new(Mutex::new(
            <rand::rngs::StdRng as rand::SeedableRng>::seed_from_u64(0),
        ));
        SequencerState::new(variables, dict, rng, None)
    }

    fn simple_pattern(length: usize) -> PatternSnapshot {
        PatternSnapshot {
            speed: Default::default(),
            length,
            steps: (0..length)
                .map(|_| StepSnapshot {
                    active: true,
                    script: "test".into(),
                    source: None,
                })
                .collect(),
            quantization: LaunchQuantization::Immediate,
            sync_mode: SyncMode::Reset,
            follow_up: FollowUp::default(),
        }
    }

    fn pid(bank: usize, pattern: usize) -> PatternId {
        PatternId { bank, pattern }
    }

    fn tick_at(beat: f64, playing: bool) -> TickInput {
        TickInput {
            commands: Vec::new(),
            playing,
            beat,
            lookahead_end: beat,
            tempo: 120.0,
            quantum: 4.0,
            fill: false,
            nudge_secs: 0.0,
            current_time_us: 0,
            engine_time: 0.0,
            mouse_x: 0.5,
            mouse_y: 0.5,
            mouse_down: 0.0,
        }
    }

    fn tick_with(commands: Vec<SeqCommand>, beat: f64) -> TickInput {
        TickInput {
            commands,
            playing: true,
            beat,
            lookahead_end: beat,
            tempo: 120.0,
            quantum: 4.0,
            fill: false,
            nudge_secs: 0.0,
            current_time_us: 0,
            engine_time: 0.0,
            mouse_x: 0.5,
            mouse_y: 0.5,
            mouse_down: 0.0,
        }
    }

    #[test]
    fn test_stop_all_clears_everything() {
        let mut state = make_state();

        state.tick(tick_with(
            vec![SeqCommand::PatternUpdate {
                bank: 0,
                pattern: 0,
                data: simple_pattern(4),
            }],
            0.0,
        ));

        let output = state.tick(tick_with(
            vec![
                SeqCommand::PatternUpdate {
                    bank: 0,
                    pattern: 0,
                    data: simple_pattern(4),
                },
                SeqCommand::PatternStart {
                    bank: 0,
                    pattern: 0,
                    quantization: LaunchQuantization::Immediate,
                    sync_mode: SyncMode::Reset,
                },
            ],
            1.0,
        ));
        assert!(!output.shared_state.active_patterns.is_empty());

        let output = state.tick(tick_with(vec![SeqCommand::StopAll], 1.5));
        assert!(output.shared_state.active_patterns.is_empty());
    }

    #[test]
    fn test_stops_apply_while_paused() {
        let mut state = make_state();

        state.tick(tick_with(
            vec![SeqCommand::PatternUpdate {
                bank: 0,
                pattern: 0,
                data: simple_pattern(4),
            }],
            0.0,
        ));
        state.tick(tick_with(
            vec![SeqCommand::PatternStart {
                bank: 0,
                pattern: 0,
                quantization: LaunchQuantization::Immediate,
                sync_mode: SyncMode::Reset,
            }],
            1.0,
        ));

        assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));

        let output = state.tick(TickInput {
            commands: vec![SeqCommand::PatternStop {
                bank: 0,
                pattern: 0,
                quantization: LaunchQuantization::Bar,
            }],
            playing: false,
            ..tick_at(1.5, false)
        });

        assert!(output.shared_state.active_patterns.is_empty());
        assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0)));
    }

    #[test]
    fn test_pattern_start_cancels_pending_stop() {
        let mut state = make_state();

        state.tick(tick_with(
            vec![
                SeqCommand::PatternUpdate {
                    bank: 0,
                    pattern: 0,
                    data: simple_pattern(4),
                },
                SeqCommand::PatternStop {
                    bank: 0,
                    pattern: 0,
                    quantization: LaunchQuantization::Bar,
                },
            ],
            0.0,
        ));

        assert!(!state.audio_state.pending_stops.is_empty());

        state.tick(tick_with(
            vec![SeqCommand::PatternStart {
                bank: 0,
                pattern: 0,
                quantization: LaunchQuantization::Immediate,
                sync_mode: SyncMode::Reset,
            }],
            0.5,
        ));

        assert!(state.audio_state.pending_stops.is_empty());
    }

    #[test]
    fn test_quantization_boundaries() {
        assert!(check_quantization_boundary(
            LaunchQuantization::Immediate,
            1.5,
            1.0,
            4.0
        ));
        assert!(check_quantization_boundary(
            LaunchQuantization::Beat,
            2.0,
            1.9,
            4.0
        ));
        assert!(!check_quantization_boundary(
            LaunchQuantization::Beat,
            1.5,
            1.2,
            4.0
        ));
        assert!(check_quantization_boundary(
            LaunchQuantization::Bar,
            4.0,
            3.9,
            4.0
        ));
        assert!(!check_quantization_boundary(
            LaunchQuantization::Bar,
            3.5,
            3.2,
            4.0
        ));
        assert!(!check_quantization_boundary(
            LaunchQuantization::Immediate,
            1.0,
            -1.0,
            4.0
        ));
    }

    #[test]
    fn test_pattern_lifecycle() {
        let mut state = make_state();

        state.tick(tick_with(
            vec![SeqCommand::PatternUpdate {
                bank: 0,
                pattern: 0,
                data: simple_pattern(2),
            }],
            0.0,
        ));

        state.tick(tick_with(
            vec![SeqCommand::PatternStart {
                bank: 0,
                pattern: 0,
                quantization: LaunchQuantization::Immediate,
                sync_mode: SyncMode::Reset,
            }],
            0.5,
        ));

        // beat_int at 0.5 is 2, prev_beat_int at 0.0 is 0
        // steps_to_fire = 2-0 = 2, firing steps 0 and 1, wrapping to 0
        let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
        assert_eq!(ap.step_index, 0);
        assert_eq!(ap.iter, 1);

        // beat_int at 0.75 is 3, prev is 2, fires 1 step (step 0), advances to 1
        state.tick(tick_at(0.75, true));
        let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
        assert_eq!(ap.step_index, 1);
        assert_eq!(ap.iter, 1);

        // beat_int at 1.0 is 4, prev is 3, fires 1 step (step 1), wraps to 0
        state.tick(tick_at(1.0, true));
        let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
        assert_eq!(ap.step_index, 0);
        assert_eq!(ap.iter, 2);
    }

    #[test]
    fn test_speed_multiplier_step_advance() {
        let mut state = make_state();

        let mut pat = simple_pattern(8);
        pat.speed = crate::model::PatternSpeed::DOUBLE;

        state.tick(tick_with(
            vec![SeqCommand::PatternUpdate {
                bank: 0,
                pattern: 0,
                data: pat,
            }],
            0.0,
        ));

        state.tick(tick_with(
            vec![SeqCommand::PatternStart {
                bank: 0,
                pattern: 0,
                quantization: LaunchQuantization::Immediate,
                sync_mode: SyncMode::Reset,
            }],
            0.5,
        ));

        // At 2x speed: beat_int at 0.5 is (0.5*4*2)=4, prev at 0.0 is 0
        // Fires 4 steps (0,1,2,3), advancing to step 4
        let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
        assert_eq!(ap.step_index, 4);

        // beat_int at 0.625 is (0.625*4*2)=5, prev is 4, fires 1 step
        state.tick(tick_at(0.625, true));
        let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
        assert_eq!(ap.step_index, 5);
    }

    #[test]
    fn test_start_and_stop_same_pattern_same_tick() {
        let mut state = make_state();

        state.tick(tick_with(
            vec![SeqCommand::PatternUpdate {
                bank: 0,
                pattern: 0,
                data: simple_pattern(4),
            }],
            0.0,
        ));

        // Start then stop in same batch: stop cancels the start
        state.tick(tick_with(
            vec![
                SeqCommand::PatternStart {
                    bank: 0,
                    pattern: 0,
                    quantization: LaunchQuantization::Immediate,
                    sync_mode: SyncMode::Reset,
                },
                SeqCommand::PatternStop {
                    bank: 0,
                    pattern: 0,
                    quantization: LaunchQuantization::Immediate,
                },
            ],
            1.0,
        ));

        assert!(state.audio_state.pending_starts.is_empty());
        assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0)));
    }

    #[test]
    fn test_multiple_patterns_independent_quantization() {
        let mut state = make_state();

        state.tick(tick_with(
            vec![
                SeqCommand::PatternUpdate {
                    bank: 0,
                    pattern: 0,
                    data: simple_pattern(4),
                },
                SeqCommand::PatternUpdate {
                    bank: 0,
                    pattern: 1,
                    data: simple_pattern(4),
                },
                SeqCommand::PatternStart {
                    bank: 0,
                    pattern: 0,
                    quantization: LaunchQuantization::Bar,
                    sync_mode: SyncMode::Reset,
                },
                SeqCommand::PatternStart {
                    bank: 0,
                    pattern: 1,
                    quantization: LaunchQuantization::Beat,
                    sync_mode: SyncMode::Reset,
                },
            ],
            0.0,
        ));

        // Beat boundary at 1.0: Beat-quantized pattern activates, Bar does not
        state.tick(tick_at(1.0, true));
        assert!(!state.audio_state.active_patterns.contains_key(&pid(0, 0)));
        assert!(state.audio_state.active_patterns.contains_key(&pid(0, 1)));

        // Bar boundary at 4.0: Bar-quantized pattern also activates
        state.tick(tick_at(2.0, true));
        state.tick(tick_at(3.0, true));
        state.tick(tick_at(4.0, true));
        assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
        assert!(state.audio_state.active_patterns.contains_key(&pid(0, 1)));
    }

    #[test]
    fn test_pattern_update_while_running() {
        let mut state = make_state();

        state.tick(tick_with(
            vec![SeqCommand::PatternUpdate {
                bank: 0,
                pattern: 0,
                data: simple_pattern(4),
            }],
            0.0,
        ));
        state.tick(tick_with(
            vec![SeqCommand::PatternStart {
                bank: 0,
                pattern: 0,
                quantization: LaunchQuantization::Immediate,
                sync_mode: SyncMode::Reset,
            }],
            0.5,
        ));

        // beat_int at 0.5 is 2, prev at 0.0 is 0, fires 2 steps (0,1), step_index=2
        let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
        assert_eq!(ap.step_index, 2);

        // beat_int at 0.75 is 3, prev is 2, fires 1 step (2), step_index=3
        state.tick(tick_at(0.75, true));
        let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
        assert_eq!(ap.step_index, 3);

        // beat_int at 1.0 is 4, prev is 3, fires 1 step (3), wraps to step_index=0
        state.tick(tick_at(1.0, true));
        let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
        assert_eq!(ap.step_index, 0);

        // Update pattern to length 2 while running — deferred until iteration boundary
        // beat=1.25: update is deferred (pattern active), still length 4
        // step_index=0 fires, advances to 1
        state.tick(tick_with(
            vec![SeqCommand::PatternUpdate {
                bank: 0,
                pattern: 0,
                data: simple_pattern(2),
            }],
            1.25,
        ));
        let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
        assert_eq!(ap.step_index, 1); // still length 4

        // Advance through remaining steps of original length-4 pattern
        state.tick(tick_at(1.5, true)); // step 1→2
        state.tick(tick_at(1.75, true)); // step 2→3
        state.tick(tick_at(2.0, true)); // step 3→wraps to 0, iteration completes, update applies

        // Now length=2 is applied. Next tick uses new length.
        // beat=2.25: step 0 fires, advances to 1
        state.tick(tick_at(2.25, true));
        let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
        assert_eq!(ap.step_index, 1);

        // beat=2.5: step 1 fires, wraps to 0 (length 2)
        state.tick(tick_at(2.5, true));
        let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
        assert_eq!(ap.step_index, 0);
    }

    #[test]
    fn test_start_while_paused_is_preserved() {
        let mut state = make_state();

        state.tick(tick_with(
            vec![SeqCommand::PatternUpdate {
                bank: 0,
                pattern: 0,
                data: simple_pattern(4),
            }],
            0.0,
        ));

        // Start while paused: pending_starts preserved for resume
        state.tick(TickInput {
            commands: vec![SeqCommand::PatternStart {
                bank: 0,
                pattern: 0,
                quantization: LaunchQuantization::Immediate,
                sync_mode: SyncMode::Reset,
            }],
            ..tick_at(1.0, false)
        });

        assert!(!state.audio_state.pending_starts.is_empty());

        // Resume playing — first tick resets prev_beat from -1 to 2.0
        state.tick(tick_at(2.0, true));
        // Second tick: prev_beat is now >= 0, so Immediate fires
        state.tick(tick_at(2.25, true));
        assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
    }

    #[test]
    fn test_resuming_after_pause_preserves_active() {
        let mut state = make_state();

        state.tick(tick_with(
            vec![SeqCommand::PatternUpdate {
                bank: 0,
                pattern: 0,
                data: simple_pattern(4),
            }],
            0.0,
        ));
        state.tick(tick_with(
            vec![SeqCommand::PatternStart {
                bank: 0,
                pattern: 0,
                quantization: LaunchQuantization::Immediate,
                sync_mode: SyncMode::Reset,
            }],
            1.0,
        ));
        assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));

        // Pause (no stop commands)
        state.tick(tick_at(2.0, false));
        assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));

        // Resume
        state.tick(tick_at(3.0, true));
        assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
    }

    #[test]
    fn test_duplicate_start_commands_ignored() {
        let mut state = make_state();

        state.tick(tick_with(
            vec![
                SeqCommand::PatternUpdate {
                    bank: 0,
                    pattern: 0,
                    data: simple_pattern(4),
                },
                SeqCommand::PatternStart {
                    bank: 0,
                    pattern: 0,
                    quantization: LaunchQuantization::Bar,
                    sync_mode: SyncMode::Reset,
                },
                SeqCommand::PatternStart {
                    bank: 0,
                    pattern: 0,
                    quantization: LaunchQuantization::Bar,
                    sync_mode: SyncMode::Reset,
                },
            ],
            0.0,
        ));

        let pending_count = state
            .audio_state
            .pending_starts
            .iter()
            .filter(|p| p.id == pid(0, 0))
            .count();
        assert_eq!(pending_count, 1);
    }

    #[test]
    fn test_tempo_applies_without_iteration_complete() {
        let mut state = make_state();

        // Pattern of length 16 — won't complete iteration for many ticks
        state.tick(tick_with(
            vec![SeqCommand::PatternUpdate {
                bank: 0,
                pattern: 0,
                data: simple_pattern(16),
            }],
            0.0,
        ));
        state.tick(tick_with(
            vec![SeqCommand::PatternStart {
                bank: 0,
                pattern: 0,
                quantization: LaunchQuantization::Immediate,
                sync_mode: SyncMode::Reset,
            }],
            0.5,
        ));

        // Script fires at beat 1.0 (step 0). Set __tempo__ as if the script did.
        {
            let mut vars = (**state.variables.load()).clone();
            vars.insert("__tempo__".to_string(), Value::Float(140.0, None));
            state.variables.store(Arc::new(vars));
        }

        let output = state.tick(tick_at(1.0, true));
        assert_eq!(output.new_tempo, Some(140.0));
    }

    fn pattern_with_sound(length: usize) -> PatternSnapshot {
        PatternSnapshot {
            speed: Default::default(),
            length,
            steps: (0..length)
                .map(|_| StepSnapshot {
                    active: true,
                    script: "sine sound 500 freq .".into(),
                    source: None,
                })
                .collect(),
            quantization: LaunchQuantization::Immediate,
            sync_mode: SyncMode::Reset,
            follow_up: FollowUp::default(),
        }
    }

    #[test]
    fn test_continuous_step_firing() {
        let mut state = make_state();

        state.tick(tick_with(
            vec![SeqCommand::PatternUpdate {
                bank: 0,
                pattern: 0,
                data: pattern_with_sound(16),
            }],
            0.0,
        ));

        state.tick(tick_with(
            vec![SeqCommand::PatternStart {
                bank: 0,
                pattern: 0,
                quantization: LaunchQuantization::Immediate,
                sync_mode: SyncMode::Reset,
            }],
            0.5,
        ));

        // Tick through many bars, counting steps
        let mut step_count = 0;
        for i in 1..400 {
            let beat = 0.5 + (i as f64) * 0.25;
            let output = state.tick(tick_at(beat, true));
            if !output.audio_commands.is_empty() {
                step_count += 1;
            }
        }

        // Should fire steps continuously without gaps
        assert!(
            step_count > 350,
            "Expected continuous steps, got {step_count}"
        );
    }

    #[test]
    fn test_multiple_patterns_fire_together() {
        let mut state = make_state();

        state.tick(tick_with(
            vec![
                SeqCommand::PatternUpdate {
                    bank: 0,
                    pattern: 0,
                    data: pattern_with_sound(4),
                },
                SeqCommand::PatternUpdate {
                    bank: 0,
                    pattern: 1,
                    data: pattern_with_sound(4),
                },
            ],
            0.0,
        ));

        state.tick(tick_with(
            vec![
                SeqCommand::PatternStart {
                    bank: 0,
                    pattern: 0,
                    quantization: LaunchQuantization::Immediate,
                    sync_mode: SyncMode::Reset,
                },
                SeqCommand::PatternStart {
                    bank: 0,
                    pattern: 1,
                    quantization: LaunchQuantization::Immediate,
                    sync_mode: SyncMode::Reset,
                },
            ],
            0.5,
        ));

        // Both patterns should be active
        assert!(state.audio_state.active_patterns.contains_key(&pid(0, 0)));
        assert!(state.audio_state.active_patterns.contains_key(&pid(0, 1)));

        // Tick and verify both produce commands
        let output = state.tick(tick_at(1.0, true));
        // Should have commands from both patterns (2 patterns * 1 command each)
        assert!(output.audio_commands.len() >= 2);
    }
}
