devalang_wasm/engine/audio/interpreter/driver/
handler.rs

1// Full handler implementation copied from top-level module
2use anyhow::Result;
3use std::collections::HashMap;
4
5use crate::language::syntax::ast::{Statement, StatementKind, Value};
6
7use super::AudioInterpreter;
8
9pub fn handle_let(interpreter: &mut AudioInterpreter, name: &str, value: &Value) -> Result<()> {
10    // Check if this is a synth definition (has waveform parameter OR _plugin_ref)
11    if let Value::Map(map) = value {
12        if map.contains_key("waveform") || map.contains_key("_plugin_ref") {
13            // plugin handling simplified: reuse existing logic
14            let mut is_plugin = false;
15            let mut plugin_author: Option<String> = None;
16            let mut plugin_name: Option<String> = None;
17            let mut plugin_export: Option<String> = None;
18
19            if let Some(Value::String(plugin_ref)) = map.get("_plugin_ref") {
20                let parts: Vec<&str> = plugin_ref.split('.').collect();
21                if parts.len() == 2 {
22                    let (var_name, prop_name) = (parts[0], parts[1]);
23                    if let Some(var_value) = interpreter.variables.get(var_name) {
24                        if let Value::Map(var_map) = var_value {
25                            if let Some(Value::String(resolved_plugin)) = var_map.get(prop_name) {
26                                if resolved_plugin.starts_with("plugin:") {
27                                    let ref_parts: Vec<&str> =
28                                        resolved_plugin["plugin:".len()..].split(':').collect();
29                                    if ref_parts.len() == 2 {
30                                        let full_plugin_name = ref_parts[0];
31                                        let export_name = ref_parts[1];
32                                        let plugin_parts: Vec<&str> =
33                                            full_plugin_name.split('.').collect();
34                                        if plugin_parts.len() == 2 {
35                                            plugin_author = Some(plugin_parts[0].to_string());
36                                            plugin_name = Some(plugin_parts[1].to_string());
37                                            plugin_export = Some(export_name.to_string());
38                                            is_plugin = true;
39                                        }
40                                    }
41                                }
42                            } else if let Some(Value::Map(export_map)) = var_map.get(prop_name) {
43                                if let (
44                                    Some(Value::String(author)),
45                                    Some(Value::String(name)),
46                                    Some(Value::String(export)),
47                                ) = (
48                                    export_map.get("_plugin_author"),
49                                    export_map.get("_plugin_name"),
50                                    export_map.get("_export_name"),
51                                ) {
52                                    plugin_author = Some(author.clone());
53                                    plugin_name = Some(name.clone());
54                                    plugin_export = Some(export.clone());
55                                    is_plugin = true;
56                                }
57                            }
58                        }
59                    }
60                }
61            }
62
63            let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
64            let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
65            let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
66            let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
67            let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
68
69            let synth_type = if let Some(Value::String(t)) = map.get("type") {
70                let clean = t.trim_matches('"').trim_matches('\'');
71                if clean.is_empty() || clean == "synth" {
72                    None
73                } else {
74                    Some(clean.to_string())
75                }
76            } else {
77                None
78            };
79
80            let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
81                crate::engine::audio::events::extract_filters(filters_arr)
82            } else {
83                Vec::new()
84            };
85
86            let mut options = std::collections::HashMap::new();
87            let reserved_keys = if is_plugin {
88                vec![
89                    "attack",
90                    "decay",
91                    "sustain",
92                    "release",
93                    "type",
94                    "filters",
95                    "_plugin_ref",
96                ]
97            } else {
98                vec![
99                    "waveform",
100                    "attack",
101                    "decay",
102                    "sustain",
103                    "release",
104                    "type",
105                    "filters",
106                    "_plugin_ref",
107                ]
108            };
109
110            for (key, val) in map.iter() {
111                if !reserved_keys.contains(&key.as_str()) {
112                    match val {
113                        Value::Number(n) => {
114                            options.insert(key.clone(), *n);
115                        }
116                        Value::String(s) => {
117                            if is_plugin && key == "waveform" {
118                                let waveform_id = match s
119                                    .trim_matches('"')
120                                    .trim_matches('\'')
121                                    .to_lowercase()
122                                    .as_str()
123                                {
124                                    "sine" => 0.0,
125                                    "saw" => 1.0,
126                                    "square" => 2.0,
127                                    "triangle" => 3.0,
128                                    _ => 1.0,
129                                };
130                                options.insert(key.clone(), waveform_id);
131                            }
132                        }
133                        _ => {}
134                    }
135                }
136            }
137
138            if is_plugin && map.contains_key("decay") {
139                options.insert("decay".to_string(), decay);
140            }
141
142            let final_waveform = if is_plugin {
143                "plugin".to_string()
144            } else {
145                waveform
146            };
147
148            let synth_def = crate::engine::audio::events::SynthDefinition {
149                waveform: final_waveform,
150                attack,
151                decay,
152                sustain,
153                release,
154                synth_type,
155                filters,
156                options,
157                plugin_author,
158                plugin_name,
159                plugin_export,
160            };
161
162            interpreter.events.add_synth(name.to_string(), synth_def);
163        }
164    }
165
166    interpreter
167        .variables
168        .insert(name.to_string(), value.clone());
169    Ok(())
170}
171
172pub fn handle_call(interpreter: &mut AudioInterpreter, name: &str) -> Result<()> {
173    // ============================================================================
174    // CALL EXECUTION (Sequential)
175    // ============================================================================
176    // Call accepts:
177    // - Patterns (defined with `pattern name with trigger = "x---"`)
178    // - Groups (defined with `group name:`)
179    // ============================================================================
180
181    // Check inline pattern or pattern variable or group call
182    // Clone the variable value first to avoid holding an immutable borrow across a mutable call
183    if let Some(pattern_value) = interpreter.variables.get(name).cloned() {
184        if let Value::Statement(stmt_box) = pattern_value {
185            if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
186                if let Some(tgt) = target.as_ref() {
187                    let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
188                    if let Some(pat) = pattern_str {
189                        interpreter.execute_pattern(tgt.as_str(), &pat, options)?;
190                        return Ok(());
191                    }
192                }
193            }
194        }
195    }
196
197    if let Some(body) = interpreter.groups.get(name).cloned() {
198        super::collector::collect_events(interpreter, &body)?;
199    } else {
200        println!("⚠️  Warning: Group or pattern '{}' not found", name);
201    }
202
203    Ok(())
204}
205
206pub fn execute_print(interpreter: &AudioInterpreter, value: &Value) -> Result<()> {
207    let message = match value {
208        Value::String(s) => {
209            if s.contains('{') && s.contains('}') {
210                interpreter.interpolate_string(s)
211            } else {
212                s.clone()
213            }
214        }
215        Value::Identifier(id) => {
216            // Resolve variable from interpreter.variables
217            if let Some(v) = interpreter.variables.get(id) {
218                match v {
219                    Value::String(s) => s.clone(),
220                    Value::Number(n) => n.to_string(),
221                    Value::Boolean(b) => b.to_string(),
222                    Value::Array(arr) => format!("{:?}", arr),
223                    Value::Map(map) => format!("{:?}", map),
224                    _ => format!("{:?}", v),
225                }
226            } else {
227                format!("Identifier(\"{}\")", id)
228            }
229        }
230        Value::Number(n) => n.to_string(),
231        Value::Boolean(b) => b.to_string(),
232        Value::Array(arr) => format!("{:?}", arr),
233        Value::Map(map) => format!("{:?}", map),
234        _ => format!("{:?}", value),
235    };
236
237    println!("💬 {}", message);
238    Ok(())
239}
240
241pub fn execute_if(
242    interpreter: &mut AudioInterpreter,
243    condition: &Value,
244    body: &[Statement],
245    else_body: &Option<Vec<Statement>>,
246) -> Result<()> {
247    let condition_result = interpreter.evaluate_condition(condition)?;
248
249    if condition_result {
250        super::collector::collect_events(interpreter, body)?;
251    } else if let Some(else_stmts) = else_body {
252        super::collector::collect_events(interpreter, else_stmts)?;
253    }
254
255    Ok(())
256}
257
258pub fn execute_event_handlers(interpreter: &mut AudioInterpreter, event_name: &str) -> Result<()> {
259    let handlers = interpreter.event_registry.get_handlers_matching(event_name);
260
261    for (index, handler) in handlers.iter().enumerate() {
262        if handler.once
263            && !interpreter
264                .event_registry
265                .should_execute_once(event_name, index)
266        {
267            continue;
268        }
269
270        let body_clone = handler.body.clone();
271        super::collector::collect_events(interpreter, &body_clone)?;
272    }
273
274    Ok(())
275}
276
277pub fn handle_assign(
278    interpreter: &mut AudioInterpreter,
279    target: &str,
280    property: &str,
281    value: &Value,
282) -> Result<()> {
283    if let Some(var) = interpreter.variables.get_mut(target) {
284        if let Value::Map(map) = var {
285            map.insert(property.to_string(), value.clone());
286
287            if interpreter.events.synths.contains_key(target) {
288                let map_clone = map.clone();
289                let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
290                interpreter
291                    .events
292                    .synths
293                    .insert(target.to_string(), updated_def);
294            }
295        } else {
296            return Err(anyhow::anyhow!(
297                "Cannot assign property '{}' to non-map variable '{}'",
298                property,
299                target
300            ));
301        }
302    } else {
303        return Err(anyhow::anyhow!("Variable '{}' not found", target));
304    }
305
306    Ok(())
307}
308
309pub fn extract_synth_def_from_map(
310    _interpreter: &AudioInterpreter,
311    map: &HashMap<String, Value>,
312) -> Result<crate::engine::audio::events::SynthDefinition> {
313    use crate::engine::audio::events::extract_filters;
314
315    let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
316    let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
317    let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
318    let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
319    let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
320
321    let synth_type = if let Some(Value::String(t)) = map.get("type") {
322        let clean = t.trim_matches('"').trim_matches('\'');
323        if clean.is_empty() || clean == "synth" {
324            None
325        } else {
326            Some(clean.to_string())
327        }
328    } else {
329        None
330    };
331
332    let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
333        extract_filters(filters_arr)
334    } else {
335        Vec::new()
336    };
337
338    let plugin_author = if let Some(Value::String(s)) = map.get("plugin_author") {
339        Some(s.clone())
340    } else {
341        None
342    };
343    let plugin_name = if let Some(Value::String(s)) = map.get("plugin_name") {
344        Some(s.clone())
345    } else {
346        None
347    };
348    let plugin_export = if let Some(Value::String(s)) = map.get("plugin_export") {
349        Some(s.clone())
350    } else {
351        None
352    };
353
354    let mut options = HashMap::new();
355    for (key, val) in map.iter() {
356        if ![
357            "waveform",
358            "attack",
359            "decay",
360            "sustain",
361            "release",
362            "type",
363            "filters",
364            "plugin_author",
365            "plugin_name",
366            "plugin_export",
367        ]
368        .contains(&key.as_str())
369        {
370            if let Value::Number(n) = val {
371                options.insert(key.clone(), *n);
372            } else if let Value::String(s) = val {
373                if key == "waveform" || key.starts_with("_") {
374                    continue;
375                }
376                if let Ok(n) = s.parse::<f32>() {
377                    options.insert(key.clone(), n);
378                }
379            }
380        }
381    }
382
383    Ok(crate::engine::audio::events::SynthDefinition {
384        waveform,
385        attack,
386        decay,
387        sustain,
388        release,
389        synth_type,
390        filters,
391        options,
392        plugin_author,
393        plugin_name,
394        plugin_export,
395    })
396}
397
398pub fn handle_load(interpreter: &mut AudioInterpreter, source: &str, alias: &str) -> Result<()> {
399    use std::path::Path;
400
401    let path = Path::new(source);
402    // Determine extension
403    if let Some(ext) = path
404        .extension()
405        .and_then(|s| s.to_str())
406        .map(|s| s.to_lowercase())
407    {
408        match ext.as_str() {
409            "mid" | "midi" => {
410                use crate::engine::audio::midi::load_midi_file;
411                let midi_data = load_midi_file(path)?;
412                interpreter.variables.insert(alias.to_string(), midi_data);
413                // MIDI file loaded (silent)
414                Ok(())
415            }
416            "wav" | "flac" | "mp3" | "ogg" => {
417                // For native/CLI builds, register sample via the samples subsystem.
418                #[cfg(feature = "cli")]
419                {
420                    use crate::engine::audio::samples;
421                    let registered = samples::register_sample_from_path(path)?;
422                    // Record the sample URI under the alias variable as a string (consistent with triggers)
423                    interpreter
424                        .variables
425                        .insert(alias.to_string(), Value::String(registered.clone()));
426                    // Sample file loaded (silent)
427                    return Ok(());
428                }
429
430                // For non-CLI builds (WASM/plugins), fallback to storing the original path as a string.
431                #[cfg(not(feature = "cli"))]
432                {
433                    interpreter
434                        .variables
435                        .insert(alias.to_string(), Value::String(source.to_string()));
436                    return Ok(());
437                }
438            }
439            _ => Err(anyhow::anyhow!("Unsupported file type for @load: {}", ext)),
440        }
441    } else {
442        Err(anyhow::anyhow!(
443            "Cannot determine file extension for {}",
444            source
445        ))
446    }
447}
448
449pub fn handle_bind(
450    interpreter: &mut AudioInterpreter,
451    source: &str,
452    target: &str,
453    options: &Value,
454) -> Result<()> {
455    use std::collections::HashMap as StdHashMap;
456
457    // Support bindings that reference runtime MIDI device mappings like:
458    //   bind myKickPattern -> mapping.out.myDeviceA with { port: 1, channel: 10 }
459    //   bind mapping.in.myDeviceB with { port: 2, channel: 10 } -> mySynth
460    // When a 'mapping.*' path is used, register a lightweight mapping entry in the
461    // interpreter variables and expose convenience variables:
462    //   mapping.<in|out>.<device>.<noteOn|noteOff|rest>
463    if source.starts_with("mapping.") || target.starts_with("mapping.") {
464        // extract options if present
465        let opts_map: StdHashMap<String, Value> = if let Value::Map(m) = options {
466            m.clone()
467        } else {
468            StdHashMap::new()
469        };
470
471        // Helper function to create mapping variables and bookkeeping
472        fn create_and_insert(
473            path: &str,
474            opts_map: &StdHashMap<String, Value>,
475            interpreter: &mut AudioInterpreter,
476        ) -> Option<(String, String)> {
477            let parts: Vec<&str> = path.split('.').collect();
478            if parts.len() >= 3 {
479                let direction = parts[1]; // "in" or "out"
480                let device = parts[2];
481
482                let mut map = StdHashMap::new();
483                map.insert(
484                    "_type".to_string(),
485                    Value::String("midi_mapping".to_string()),
486                );
487                map.insert(
488                    "direction".to_string(),
489                    Value::String(direction.to_string()),
490                );
491                map.insert("device".to_string(), Value::String(device.to_string()));
492
493                // merge provided options
494                for (k, v) in opts_map.iter() {
495                    map.insert(k.clone(), v.clone());
496                }
497
498                interpreter
499                    .variables
500                    .insert(path.to_string(), Value::Map(map.clone()));
501
502                // Expose event variables for convenience
503                let note_on = format!("mapping.{}.{}.noteOn", direction, device);
504                let note_off = format!("mapping.{}.{}.noteOff", direction, device);
505                let rest = format!("mapping.{}.{}.rest", direction, device);
506                interpreter
507                    .variables
508                    .insert(note_on.clone(), Value::String(note_on.clone()));
509                interpreter
510                    .variables
511                    .insert(note_off.clone(), Value::String(note_off.clone()));
512                interpreter
513                    .variables
514                    .insert(rest.clone(), Value::String(rest.clone()));
515
516                return Some((direction.to_string(), device.to_string()));
517            }
518            None
519        }
520
521        // If source is mapping.* (incoming mapping binds to target instrument)
522        if source.starts_with("mapping.") {
523            if let Some((direction, device)) = create_and_insert(source, &opts_map, interpreter) {
524                // Record association when target is not also mapping.*
525                if !target.starts_with("mapping.") {
526                    let mut bmap = StdHashMap::new();
527                    bmap.insert("instrument".to_string(), Value::String(target.to_string()));
528                    bmap.insert("direction".to_string(), Value::String(direction.clone()));
529                    bmap.insert("device".to_string(), Value::String(device.clone()));
530                    for (k, v) in opts_map.iter() {
531                        bmap.insert(k.clone(), v.clone());
532                    }
533                    interpreter
534                        .variables
535                        .insert(format!("__mapping_bind::{}", source), Value::Map(bmap));
536
537                    // If we have a midi_manager, try to open the input port (for incoming mappings)
538                    #[cfg(feature = "cli")]
539                    if let Some(manager) = &mut interpreter.midi_manager {
540                        if let Some(Value::Number(port_num)) = opts_map.get("port") {
541                            let idx = *port_num as usize;
542                            if let Ok(mut mgr) = manager.lock() {
543                                // use device as name for identification
544                                let _ = mgr.open_input_by_index(idx, &device);
545                            }
546                        }
547                    }
548                }
549            }
550        }
551
552        // If target is mapping.* (binding a sequence/instrument to an external device)
553        if target.starts_with("mapping.") {
554            if let Some((direction, device)) = create_and_insert(target, &opts_map, interpreter) {
555                if !source.starts_with("mapping.") {
556                    let mut bmap = StdHashMap::new();
557                    bmap.insert("source".to_string(), Value::String(source.to_string()));
558                    bmap.insert("direction".to_string(), Value::String(direction.clone()));
559                    bmap.insert("device".to_string(), Value::String(device.clone()));
560                    for (k, v) in opts_map.iter() {
561                        bmap.insert(k.clone(), v.clone());
562                    }
563                    interpreter
564                        .variables
565                        .insert(format!("__mapping_bind::{}", target), Value::Map(bmap));
566
567                    // If we have a midi_manager, try to open the output port (for outgoing mappings)
568                    #[cfg(feature = "cli")]
569                    if let Some(manager) = &mut interpreter.midi_manager {
570                        if let Some(Value::Number(port_num)) = opts_map.get("port") {
571                            let idx = *port_num as usize;
572                            if let Ok(mut mgr) = manager.lock() {
573                                let _ = mgr.open_output_by_name(&device, idx);
574                            }
575                        }
576                    }
577                }
578            }
579        }
580
581        // Nothing more to schedule here at audio event level; actual MIDI I/O handlers
582        // will be responsible for reacting to incoming messages and emitting events into
583        // the interpreter event registry, and for flushing outgoing bound sequences to
584        // MIDI device ports when appropriate.
585        return Ok(());
586    }
587
588    // Fallback: existing behaviour (binding MIDI file data to a synth)
589    let midi_data = interpreter
590        .variables
591        .get(source)
592        .ok_or_else(|| anyhow::anyhow!("MIDI source '{}' not found", source))?
593        .clone();
594
595    if let Value::Map(midi_map) = &midi_data {
596        let notes = midi_map
597            .get("notes")
598            .ok_or_else(|| anyhow::anyhow!("MIDI data has no notes"))?;
599
600        if let Value::Array(notes_array) = notes {
601            let _synth_def = interpreter
602                .events
603                .synths
604                .get(target)
605                .ok_or_else(|| anyhow::anyhow!("Synth '{}' not found", target))?
606                .clone();
607
608            let default_velocity = 100;
609            let mut velocity = default_velocity;
610
611            if let Value::Map(opts) = options {
612                if let Some(Value::Number(v)) = opts.get("velocity") {
613                    velocity = *v as u8;
614                }
615            }
616
617            // Determine MIDI file BPM (if present) so we can rescale times to interpreter BPM
618            // Default to interpreter.bpm when the MIDI file has no BPM metadata
619            let midi_bpm =
620                crate::engine::audio::events::extract_number(midi_map, "bpm", interpreter.bpm);
621
622            for note_val in notes_array {
623                if let Value::Map(note_map) = note_val {
624                    let time = crate::engine::audio::events::extract_number(note_map, "time", 0.0);
625                    let note =
626                        crate::engine::audio::events::extract_number(note_map, "note", 60.0) as u8;
627                    let note_velocity = crate::engine::audio::events::extract_number(
628                        note_map,
629                        "velocity",
630                        velocity as f32,
631                    ) as u8;
632                    // Duration may be present (ms) from MIDI loader; fallback to 500 ms
633                    let duration_ms =
634                        crate::engine::audio::events::extract_number(note_map, "duration", 500.0);
635
636                    use crate::engine::audio::events::AudioEvent;
637                    let synth_def = interpreter
638                        .events
639                        .get_synth(target)
640                        .cloned()
641                        .unwrap_or_default();
642                    // Rescale times according to interpreter BPM vs MIDI file BPM.
643                    // If midi_bpm == interpreter.bpm this is a no-op. We compute factor = midi_bpm / interpreter.bpm
644                    let interp_bpm = interpreter.bpm;
645                    let factor = if interp_bpm > 0.0 {
646                        midi_bpm / interp_bpm
647                    } else {
648                        1.0
649                    };
650
651                    let start_time_s = (time / 1000.0) * factor;
652                    let duration_s = (duration_ms / 1000.0) * factor;
653
654                    let event = AudioEvent::Note {
655                        midi: note,
656                        start_time: start_time_s,
657                        duration: duration_s,
658                        velocity: note_velocity as f32,
659                        synth_id: target.to_string(),
660                        synth_def,
661                        pan: 0.0,
662                        detune: 0.0,
663                        gain: 1.0,
664                        attack: None,
665                        release: None,
666                        delay_time: None,
667                        delay_feedback: None,
668                        delay_mix: None,
669                        reverb_amount: None,
670                        drive_amount: None,
671                        drive_color: None,
672                    };
673
674                    // Diagnostic: log each scheduled note from bind (midi, time ms, start_time sec)
675                    // bound note scheduled
676                    interpreter.events.events.push(event);
677                }
678            }
679
680            // Bound notes from source to target
681        }
682    }
683
684    Ok(())
685}
686
687#[cfg(feature = "cli")]
688pub fn handle_use_plugin(
689    interpreter: &mut AudioInterpreter,
690    author: &str,
691    name: &str,
692    alias: &str,
693) -> Result<()> {
694    use crate::engine::plugin::loader::load_plugin;
695
696    match load_plugin(author, name) {
697        Ok((info, _wasm_bytes)) => {
698            let mut plugin_map = HashMap::new();
699            plugin_map.insert("_type".to_string(), Value::String("plugin".to_string()));
700            plugin_map.insert("_author".to_string(), Value::String(info.author.clone()));
701            plugin_map.insert("_name".to_string(), Value::String(info.name.clone()));
702
703            if let Some(version) = &info.version {
704                plugin_map.insert("_version".to_string(), Value::String(version.clone()));
705            }
706
707            for export in &info.exports {
708                let mut export_map = HashMap::new();
709                export_map.insert(
710                    "_plugin_author".to_string(),
711                    Value::String(info.author.clone()),
712                );
713                export_map.insert("_plugin_name".to_string(), Value::String(info.name.clone()));
714                export_map.insert(
715                    "_export_name".to_string(),
716                    Value::String(export.name.clone()),
717                );
718                export_map.insert(
719                    "_export_kind".to_string(),
720                    Value::String(export.kind.clone()),
721                );
722
723                plugin_map.insert(export.name.clone(), Value::Map(export_map));
724            }
725
726            interpreter
727                .variables
728                .insert(alias.to_string(), Value::Map(plugin_map));
729        }
730        Err(e) => {
731            eprintln!("❌ Failed to load plugin {}.{}: {}", author, name, e);
732            return Err(anyhow::anyhow!("Failed to load plugin: {}", e));
733        }
734    }
735
736    Ok(())
737}
738
739#[cfg(not(feature = "cli"))]
740pub fn handle_use_plugin(
741    interpreter: &mut AudioInterpreter,
742    author: &str,
743    name: &str,
744    alias: &str,
745) -> Result<()> {
746    // Plugin loading not supported in this build (WASM/plugin builds). Insert a minimal placeholder so scripts can still reference the alias.
747    let mut plugin_map = HashMap::new();
748    plugin_map.insert(
749        "_type".to_string(),
750        Value::String("plugin_stub".to_string()),
751    );
752    plugin_map.insert("_author".to_string(), Value::String(author.to_string()));
753    plugin_map.insert("_name".to_string(), Value::String(name.to_string()));
754    interpreter
755        .variables
756        .insert(alias.to_string(), Value::Map(plugin_map));
757    Ok(())
758}
759
760pub fn handle_bank(
761    interpreter: &mut AudioInterpreter,
762    name: &str,
763    alias: &Option<String>,
764) -> Result<()> {
765    let target_alias = alias
766        .clone()
767        .unwrap_or_else(|| name.split('.').last().unwrap_or(name).to_string());
768
769    if let Some(existing_value) = interpreter.variables.get(name) {
770        interpreter
771            .variables
772            .insert(target_alias.clone(), existing_value.clone());
773    } else {
774        #[cfg(feature = "wasm")]
775        {
776            use crate::web::registry::banks::REGISTERED_BANKS;
777            REGISTERED_BANKS.with(|banks| {
778                for bank in banks.borrow().iter() {
779                    if bank.full_name == *name {
780                        if let Some(Value::Map(bank_map)) = interpreter.variables.get(&bank.alias) {
781                            interpreter
782                                .variables
783                                .insert(target_alias.clone(), Value::Map(bank_map.clone()));
784                        }
785                    }
786                }
787            });
788        }
789
790        #[cfg(not(feature = "wasm"))]
791        {
792            if let Ok(current_dir) = std::env::current_dir() {
793                match interpreter.banks.register_bank(
794                    target_alias.clone(),
795                    &name,
796                    &current_dir,
797                    &current_dir,
798                ) {
799                    Ok(_) => {
800                        let mut bank_map = HashMap::new();
801                        bank_map.insert("_name".to_string(), Value::String(name.to_string()));
802                        bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
803                        interpreter
804                            .variables
805                            .insert(target_alias.clone(), Value::Map(bank_map));
806                    }
807                    Err(e) => {
808                        eprintln!("⚠️ Failed to register bank '{}': {}", name, e);
809                        let mut bank_map = HashMap::new();
810                        bank_map.insert("_name".to_string(), Value::String(name.to_string()));
811                        bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
812                        interpreter
813                            .variables
814                            .insert(target_alias.clone(), Value::Map(bank_map));
815                    }
816                }
817            } else {
818                let mut bank_map = HashMap::new();
819                bank_map.insert("_name".to_string(), Value::String(name.to_string()));
820                bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
821                interpreter
822                    .variables
823                    .insert(target_alias.clone(), Value::Map(bank_map));
824                eprintln!(
825                    "⚠️ Could not determine cwd to register bank '{}', registered minimal alias.",
826                    name
827                );
828            }
829        }
830    }
831
832    Ok(())
833}
834
835pub fn handle_trigger(interpreter: &mut AudioInterpreter, entity: &str) -> Result<()> {
836    let resolved_entity = if entity.starts_with('.') {
837        &entity[1..]
838    } else {
839        entity
840    };
841
842    if resolved_entity.contains('.') {
843        let parts: Vec<&str> = resolved_entity.split('.').collect();
844        if parts.len() == 2 {
845            let (var_name, property) = (parts[0], parts[1]);
846
847            if let Some(Value::Map(map)) = interpreter.variables.get(var_name) {
848                if let Some(Value::String(sample_uri)) = map.get(property) {
849                    let uri = sample_uri.trim_matches('"').trim_matches('\'');
850                    interpreter
851                        .events
852                        .add_sample_event(uri, interpreter.cursor_time, 1.0);
853                    let beat_duration = interpreter.beat_duration();
854                    interpreter.cursor_time += beat_duration;
855                } else {
856                    #[cfg(not(feature = "wasm"))]
857                    {
858                        // First try to produce an internal devalang://bank URI (preferred, supports lazy loading)
859                        let resolved_uri = interpreter.resolve_sample_uri(resolved_entity);
860                        if resolved_uri != resolved_entity {
861                            interpreter.events.add_sample_event(
862                                &resolved_uri,
863                                interpreter.cursor_time,
864                                1.0,
865                            );
866                            let beat_duration = interpreter.beat_duration();
867                            interpreter.cursor_time += beat_duration;
868                        } else if let Some(pathbuf) =
869                            interpreter.banks.resolve_trigger(var_name, property)
870                        {
871                            if let Some(path_str) = pathbuf.to_str() {
872                                interpreter.events.add_sample_event(
873                                    path_str,
874                                    interpreter.cursor_time,
875                                    1.0,
876                                );
877                                let beat_duration = interpreter.beat_duration();
878                                interpreter.cursor_time += beat_duration;
879                            } else {
880                                println!(
881                                    "⚠️ Resolution failed for {}.{} (invalid path)",
882                                    var_name, property
883                                );
884                            }
885                        } else {
886                            println!("⚠️ No path found for {} via BankRegistry", resolved_entity);
887                        }
888                    }
889                }
890            }
891        }
892    } else {
893        if let Some(Value::String(sample_uri)) = interpreter.variables.get(resolved_entity) {
894            let uri = sample_uri.trim_matches('"').trim_matches('\'');
895            interpreter
896                .events
897                .add_sample_event(uri, interpreter.cursor_time, 1.0);
898            let beat_duration = interpreter.beat_duration();
899            interpreter.cursor_time += beat_duration;
900        }
901    }
902
903    // Note: do not call interpreter.render_audio() here - rendering is handled by the build pipeline.
904    // Trigger queued for rendering (events collected)
905
906    Ok(())
907}
908
909pub fn extract_pattern_data(
910    _interpreter: &AudioInterpreter,
911    value: &Value,
912) -> (Option<String>, Option<HashMap<String, f32>>) {
913    match value {
914        Value::String(pattern) => (Some(pattern.clone()), None),
915        Value::Map(map) => {
916            let pattern = map.get("pattern").and_then(|v| {
917                if let Value::String(s) = v {
918                    Some(s.clone())
919                } else {
920                    None
921                }
922            });
923
924            let mut options = HashMap::new();
925            for (key, val) in map.iter() {
926                if key != "pattern" {
927                    if let Value::Number(num) = val {
928                        options.insert(key.clone(), *num);
929                    }
930                }
931            }
932
933            let opts = if options.is_empty() {
934                None
935            } else {
936                Some(options)
937            };
938            (pattern, opts)
939        }
940        _ => (None, None),
941    }
942}
943
944pub fn execute_pattern(
945    interpreter: &mut AudioInterpreter,
946    target: &str,
947    pattern: &str,
948    options: Option<HashMap<String, f32>>,
949) -> Result<()> {
950    use crate::engine::audio::events::AudioEvent;
951
952    let swing = options
953        .as_ref()
954        .and_then(|o| o.get("swing").copied())
955        .unwrap_or(0.0);
956    let humanize = options
957        .as_ref()
958        .and_then(|o| o.get("humanize").copied())
959        .unwrap_or(0.0);
960    let velocity_mult = options
961        .as_ref()
962        .and_then(|o| o.get("velocity").copied())
963        .unwrap_or(1.0);
964    let tempo_override = options.as_ref().and_then(|o| o.get("tempo").copied());
965
966    let effective_bpm = tempo_override.unwrap_or(interpreter.bpm);
967
968    let resolved_uri = resolve_sample_uri(interpreter, target);
969
970    let pattern_chars: Vec<char> = pattern.chars().filter(|c| !c.is_whitespace()).collect();
971    let step_count = pattern_chars.len() as f32;
972    if step_count == 0.0 {
973        return Ok(());
974    }
975
976    let bar_duration = (60.0 / effective_bpm) * 4.0;
977    let step_duration = bar_duration / step_count;
978
979    for (i, &ch) in pattern_chars.iter().enumerate() {
980        if ch == 'x' || ch == 'X' {
981            let mut time = interpreter.cursor_time + (i as f32 * step_duration);
982            if swing > 0.0 && i % 2 == 1 {
983                time += step_duration * swing;
984            }
985
986            #[cfg(any(feature = "cli", feature = "wasm"))]
987            if humanize > 0.0 {
988                use rand::Rng;
989                let mut rng = rand::thread_rng();
990                let offset = rng.gen_range(-humanize..humanize);
991                time += offset;
992            }
993
994            let event = AudioEvent::Sample {
995                uri: resolved_uri.clone(),
996                start_time: time,
997                velocity: velocity_mult, // Already in 0-1 range, not MIDI 0-127
998            };
999            interpreter.events.events.push(event);
1000        }
1001    }
1002
1003    interpreter.cursor_time += bar_duration;
1004    Ok(())
1005}
1006
1007pub fn resolve_sample_uri(interpreter: &AudioInterpreter, target: &str) -> String {
1008    if let Some(dot_pos) = target.find('.') {
1009        let bank_alias = &target[..dot_pos];
1010        let trigger_name = &target[dot_pos + 1..];
1011        if let Some(Value::Map(bank_map)) = interpreter.variables.get(bank_alias) {
1012            if let Some(Value::String(bank_name)) = bank_map.get("_name") {
1013                return format!("devalang://bank/{}/{}", bank_name, trigger_name);
1014            }
1015        }
1016    }
1017    target.to_string()
1018}