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::engine::audio::events::AudioEvent;
6use crate::language::syntax::ast::{Statement, StatementKind, Value};
7
8use super::AudioInterpreter;
9
10pub fn handle_let(interpreter: &mut AudioInterpreter, name: &str, value: &Value) -> Result<()> {
11    // Check if this is a synth definition (has waveform parameter OR _plugin_ref)
12    if let Value::Map(orig_map) = value {
13        // Clone la map pour modification
14        let mut map = orig_map.clone();
15
16        // Normalize older key `synth_type` into `type` so downstream logic reads the same key.
17        // Prefer chained synth_type over default "synth" placeholder when present.
18        if let Some(synth_val) = map.get("synth_type").cloned() {
19            let should_replace = match map.get("type") {
20                Some(Value::String(s)) => {
21                    let clean = s.trim_matches('"').trim_matches('\'');
22                    clean == "synth" || clean.is_empty()
23                }
24                Some(Value::Identifier(id)) => {
25                    let clean = id.trim_matches('"').trim_matches('\'');
26                    clean == "synth" || clean.is_empty()
27                }
28                None => true,
29                _ => false,
30            };
31            if should_replace {
32                map.insert("type".to_string(), synth_val.clone());
33                map.remove("synth_type");
34            } else {
35                // If we didn't replace, still keep synth_type (back-compat) but don't remove it
36            }
37        }
38
39        // Merge chained parameter sub-maps (e.g. "params", "adsr", "envelope")
40        let chain_keys = ["params", "adsr", "envelope"];
41        for key in &chain_keys {
42            if let Some(Value::Map(submap)) = map.get(*key) {
43                // Temporary buffer to avoid mutable/immutable borrow conflict
44                let to_insert: Vec<(String, Value)> =
45                    submap.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
46                for (k, v) in to_insert {
47                    map.insert(k, v);
48                }
49                map.remove(*key);
50            }
51        }
52
53        // Ensure default properties are present on instantiated objects (synths/plugins)
54        // so that dotted access like `mySynth.volume` resolves to a concrete Value.
55        // These defaults are safe no-ops for objects that don't use them.
56        map.entry("volume".to_string())
57            .or_insert(Value::Number(1.0));
58        map.entry("gain".to_string()).or_insert(Value::Number(1.0));
59        map.entry("pan".to_string()).or_insert(Value::Number(0.0));
60        map.entry("detune".to_string())
61            .or_insert(Value::Number(0.0));
62        // Ensure a visible type key exists for prints; prefer existing value if present
63        map.entry("type".to_string())
64            .or_insert(Value::String("synth".to_string()));
65
66        // Ensure defaults for synth-like objects
67        crate::utils::props::ensure_default_properties(&mut map, Some("synth"));
68
69        if map.contains_key("waveform") || map.contains_key("_plugin_ref") {
70            // plugin handling simplified: reuse existing logic
71            let mut is_plugin = false;
72            let mut plugin_author: Option<String> = None;
73            let mut plugin_name: Option<String> = None;
74            let mut plugin_export: Option<String> = None;
75
76            if let Some(Value::String(plugin_ref)) = map.get("_plugin_ref") {
77                let parts: Vec<&str> = plugin_ref.split('.').collect();
78                if parts.len() == 2 {
79                    let (var_name, prop_name_raw) = (parts[0], parts[1]);
80                    let prop_name = prop_name_raw.trim_end_matches(';'); // Remove semicolon
81                    if let Some(var_value) = interpreter.variables.get(var_name) {
82                        if let Value::Map(var_map) = var_value {
83                            if let Some(Value::String(resolved_plugin)) = var_map.get(prop_name) {
84                                if resolved_plugin.starts_with("plugin:") {
85                                    let ref_parts: Vec<&str> =
86                                        resolved_plugin["plugin:".len()..].split(':').collect();
87                                    if ref_parts.len() == 2 {
88                                        let full_plugin_name = ref_parts[0];
89                                        let export_name = ref_parts[1];
90                                        let plugin_parts: Vec<&str> =
91                                            full_plugin_name.split('.').collect();
92                                        if plugin_parts.len() == 2 {
93                                            plugin_author = Some(plugin_parts[0].to_string());
94                                            plugin_name = Some(plugin_parts[1].to_string());
95                                            plugin_export = Some(export_name.to_string());
96                                            is_plugin = true;
97                                        }
98                                    }
99                                }
100                            } else if let Some(Value::Map(export_map)) = var_map.get(prop_name) {
101                                if let (
102                                    Some(Value::String(author)),
103                                    Some(Value::String(name)),
104                                    Some(Value::String(export)),
105                                ) = (
106                                    export_map.get("_plugin_author"),
107                                    export_map.get("_plugin_name"),
108                                    export_map.get("_export_name"),
109                                ) {
110                                    plugin_author = Some(author.clone());
111                                    plugin_name = Some(name.clone());
112                                    plugin_export = Some(export.clone());
113                                    is_plugin = true;
114                                }
115                            }
116                        }
117                    }
118                }
119            }
120
121            let waveform = crate::engine::audio::events::extract_string(&map, "waveform", "sine");
122            let attack = crate::engine::audio::events::extract_number(&map, "attack", 0.01);
123            let decay = crate::engine::audio::events::extract_number(&map, "decay", 0.1);
124            let sustain = crate::engine::audio::events::extract_number(&map, "sustain", 0.7);
125            let release = crate::engine::audio::events::extract_number(&map, "release", 0.2);
126
127            // Accept both String and Identifier for type (parser may emit Identifier for bare words)
128            let synth_type = if let Some(v) = map.get("type") {
129                match v {
130                    Value::String(t) => {
131                        let clean = t.trim_matches('"').trim_matches('\'');
132                        if clean.is_empty() || clean == "synth" {
133                            None
134                        } else {
135                            Some(clean.to_string())
136                        }
137                    }
138                    Value::Identifier(id) => {
139                        let clean = id.trim_matches('"').trim_matches('\'');
140                        if clean.is_empty() || clean == "synth" {
141                            None
142                        } else {
143                            Some(clean.to_string())
144                        }
145                    }
146                    _ => None,
147                }
148            } else {
149                None
150            };
151
152            let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
153                crate::engine::audio::events::extract_filters(filters_arr)
154            } else {
155                Vec::new()
156            };
157
158            // Extract LFO configuration if present
159            let lfo = if let Some(Value::Map(lfo_map)) = map.get("lfo") {
160                use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
161
162                // Parse rate (Hz or tempo-synced like "1/4")
163                let rate_str = if let Some(Value::Number(n)) = lfo_map.get("rate") {
164                    n.to_string()
165                } else if let Some(Value::String(s)) = lfo_map.get("rate") {
166                    s.clone()
167                } else {
168                    "5.0".to_string() // Default rate
169                };
170                let rate = LfoRate::from_value(&rate_str);
171
172                // Parse depth (0-1)
173                let depth = if let Some(Value::Number(n)) = lfo_map.get("depth") {
174                    (*n).clamp(0.0, 1.0)
175                } else {
176                    0.5 // Default depth
177                };
178
179                // Parse waveform (sine, triangle, square, saw)
180                let waveform_str = if let Some(Value::String(s)) = lfo_map.get("shape") {
181                    s.clone()
182                } else if let Some(Value::String(s)) = lfo_map.get("waveform") {
183                    s.clone()
184                } else {
185                    "sine".to_string() // Default waveform
186                };
187                let lfo_waveform = LfoWaveform::from_str(&waveform_str);
188
189                // Parse target (volume, pitch, filter, pan)
190                let target = if let Some(Value::String(s)) = lfo_map.get("target") {
191                    LfoTarget::from_str(s).unwrap_or(LfoTarget::Volume)
192                } else {
193                    LfoTarget::Volume // Default target
194                };
195
196                // Parse initial phase (0-1)
197                let phase = if let Some(Value::Number(n)) = lfo_map.get("phase") {
198                    (*n).fract().abs() // Ensure 0-1 range
199                } else {
200                    0.0 // Default phase
201                };
202
203                Some(LfoParams {
204                    rate,
205                    depth,
206                    waveform: lfo_waveform,
207                    target,
208                    phase,
209                })
210            } else {
211                None
212            };
213
214            let mut options = std::collections::HashMap::new();
215            let reserved_keys = if is_plugin {
216                vec![
217                    "attack",
218                    "decay",
219                    "sustain",
220                    "release",
221                    "type",
222                    "filters",
223                    "_plugin_ref",
224                    "lfo",
225                    "waveform", // Don't auto-convert waveform for plugins - let plugin defaults handle it
226                    // Audio parameters (applied post-render, NOT plugin options)
227                    "pan",
228                    "gain",
229                    "volume",
230                    "detune",
231                    "spread",
232                    // Other metadata keys
233                    "synth_type", // Alias for type
234                    "chain",      // Effects chain
235                    "effects",    // Effects (deprecated)
236                ]
237            } else {
238                vec![
239                    "waveform",
240                    "attack",
241                    "decay",
242                    "sustain",
243                    "release",
244                    "type",
245                    "filters",
246                    "_plugin_ref",
247                    "lfo",
248                    // Audio parameters (applied post-render, NOT synth options)
249                    "pan",
250                    "gain",
251                    "volume",
252                    "detune",
253                    "spread",
254                    // Other metadata keys
255                    "synth_type", // Alias for type
256                    "chain",      // Effects chain
257                    "effects",    // Effects (deprecated)
258                ]
259            };
260
261            for (key, val) in map.iter() {
262                if !reserved_keys.contains(&key.as_str()) {
263                    match val {
264                        Value::Number(n) => {
265                            options.insert(key.clone(), *n);
266                        }
267                        Value::String(s) => {
268                            if is_plugin && key == "waveform" {
269                                let waveform_id = match s
270                                    .trim_matches('"')
271                                    .trim_matches('\'')
272                                    .to_lowercase()
273                                    .as_str()
274                                {
275                                    "sine" => 0.0,
276                                    "saw" => 1.0,
277                                    "square" => 2.0,
278                                    "triangle" => 3.0,
279                                    _ => 1.0,
280                                };
281                                options.insert(key.clone(), waveform_id);
282                            }
283                        }
284                        _ => {}
285                    }
286                }
287            }
288
289            if is_plugin && map.contains_key("decay") {
290                options.insert("decay".to_string(), decay);
291            }
292
293            // Extract parameters from chain if present (e.g., from synth acid.synth -> cutoff(800) -> resonance(0.75) -> ...)
294            if let Some(Value::Array(chain_arr)) = map.get("chain") {
295                for chain_item in chain_arr {
296                    if let Value::Map(chain_map) = chain_item {
297                        // Each chain item has "type" (method name) and "value" (parameter value)
298                        if let Some(Value::String(method)) = chain_map.get("type") {
299                            // Handle string values (e.g., decay("auto"))
300                            if let Some(Value::String(str_val)) = chain_map.get("value") {
301                                if method == "decay" && str_val == "auto" {
302                                    options.insert("decay_mode".to_string(), 1.0); // 1.0 = auto mode
303                                }
304                            }
305                            // Handle numeric values
306                            else if let Some(Value::Number(val)) = chain_map.get("value") {
307                                options.insert(method.clone(), *val);
308                            }
309                        }
310                    }
311                }
312            }
313
314            let final_waveform = if is_plugin {
315                "plugin".to_string()
316            } else {
317                waveform
318            };
319
320            let synth_def = crate::engine::audio::events::SynthDefinition {
321                waveform: final_waveform,
322                attack,
323                decay,
324                sustain,
325                release,
326                synth_type,
327                filters,
328                options,
329                plugin_author,
330                plugin_name,
331                plugin_export,
332                lfo,
333            };
334
335            interpreter.events.add_synth(name.to_string(), synth_def);
336        }
337
338        // Insert the normalized/augmented map into variables so dotted-property reads
339        // (e.g., `mySynth.volume`) return concrete values instead of Identifier("...").
340        interpreter
341            .variables
342            .insert(name.to_string(), Value::Map(map.clone()));
343        return Ok(());
344    }
345
346    // Non-map values: fall back to storing the original value
347    // If the value is a stored Trigger statement, normalize it into a Map so dotted
348    // property access like `myTrigger.effects.reverb.size` works and `print myTrigger`
349    // displays a clean map rather than a Statement debug dump.
350    if let Value::Statement(stmt_box) = value {
351        if let StatementKind::Trigger {
352            entity,
353            duration,
354            effects,
355        } = &stmt_box.kind
356        {
357            let mut map = HashMap::new();
358            map.insert("kind".to_string(), Value::String("Trigger".to_string()));
359            map.insert("entity".to_string(), Value::String(entity.clone()));
360            map.insert("duration".to_string(), Value::Duration(duration.clone()));
361            if let Some(eff) = effects {
362                map.insert("effects".to_string(), eff.clone());
363            }
364
365            // Ensure defaults for trigger objects
366            crate::utils::props::ensure_default_properties(&mut map, Some("trigger"));
367
368            interpreter
369                .variables
370                .insert(name.to_string(), Value::Map(map));
371            return Ok(());
372        }
373    }
374
375    interpreter
376        .variables
377        .insert(name.to_string(), value.clone());
378    Ok(())
379}
380
381pub fn handle_call(interpreter: &mut AudioInterpreter, name: &str, args: &[Value]) -> Result<()> {
382    // ============================================================================
383    // CALL EXECUTION (Sequential)
384    // ============================================================================
385    // Call accepts:
386    // - Patterns (defined with `pattern name with trigger = "x---"`)
387    // - Groups (defined with `group name:`)
388    // ============================================================================
389
390    // Check for user-defined function stored as a variable
391    if let Some(var_val) = interpreter.variables.get(name).cloned() {
392        if let Value::Statement(stmt_box) = var_val {
393            if let StatementKind::Function {
394                name: _fname,
395                parameters,
396                body,
397            } = &stmt_box.kind
398            {
399                // Create a local variable scope for function execution
400                let vars_snapshot = interpreter.variables.clone();
401
402                // Bind parameters: resolve passed args into actual values
403                for (i, param) in parameters.iter().enumerate() {
404                    let bound = args.get(i).cloned().unwrap_or(Value::Null);
405                    // If the bound value is an Identifier, resolve it to its actual value
406                    let bound_val = match bound {
407                        Value::Identifier(ref id) => {
408                            interpreter.resolve_value(&Value::Identifier(id.clone()))?
409                        }
410                        other => other,
411                    };
412                    interpreter.variables.insert(param.clone(), bound_val);
413                }
414
415                // Execute the function body. Track function call depth so 'return'
416                // statements are only valid within a function context.
417                interpreter.function_call_depth += 1;
418                let exec_result = super::collector::collect_events(interpreter, body);
419                // decrement depth regardless of success or error
420                interpreter.function_call_depth = interpreter.function_call_depth.saturating_sub(1);
421                exec_result?;
422
423                // Capture return value if the function executed a `return` statement.
424                let mut captured_return: Option<Value> = None;
425                if interpreter.returning_flag {
426                    captured_return = interpreter.return_value.clone();
427                    // clear the interpreter return state now that we've captured it
428                    interpreter.returning_flag = false;
429                    interpreter.return_value = None;
430                }
431
432                // Restore variables (local scope ends). We deliberately do not touch interpreter.events
433                // so synth definitions or other global registrations performed by the function persist.
434                interpreter.variables = vars_snapshot;
435
436                // If there was a returned value, expose it to the caller scope via a special variable
437                // named "__return" so callers can inspect the result. This is a simple mechanism
438                // for now; higher-level expression support can be added later.
439                if let Some(rv) = captured_return {
440                    interpreter.variables.insert("__return".to_string(), rv);
441                }
442
443                return Ok(());
444            }
445            // If it's a stored pattern (inline pattern stored as Statement), handle below
446            if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
447                if let Some(tgt) = target.as_ref() {
448                    let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
449                    if let Some(pat) = pattern_str {
450                        // Pass pattern name as source for routing
451                        execute_pattern_with_source(
452                            interpreter,
453                            tgt.as_str(),
454                            &pat,
455                            options,
456                            Some(name),
457                        )?;
458                        return Ok(());
459                    }
460                }
461            }
462        }
463    }
464
465    // If it's a group call, execute the group body
466    if let Some(body) = interpreter.groups.get(name).cloned() {
467        super::collector::collect_events(interpreter, &body)?;
468        return Ok(());
469    }
470
471    println!(
472        "⚠️  Warning: Group, pattern or function '{}' not found",
473        name
474    );
475    Ok(())
476}
477
478/// Execute a call as an expression and return its resulting Value.
479/// This is similar to `handle_call` but returns the captured `return` value
480/// from a function when present. Groups and patterns return `Value::Null`.
481pub fn call_function(
482    interpreter: &mut AudioInterpreter,
483    name: &str,
484    args: &[Value],
485) -> Result<Value> {
486    // If it's a stored variable that is a function statement, execute and capture return
487    if let Some(var_val) = interpreter.variables.get(name).cloned() {
488        if let Value::Statement(stmt_box) = var_val {
489            if let StatementKind::Function {
490                name: _fname,
491                parameters,
492                body,
493            } = &stmt_box.kind
494            {
495                // create local variable snapshot
496                let vars_snapshot = interpreter.variables.clone();
497
498                // Bind parameters: use provided args (they are already resolved by caller)
499                for (i, param) in parameters.iter().enumerate() {
500                    let bound = args.get(i).cloned().unwrap_or(Value::Null);
501                    // If the bound value is an Identifier, resolve it to its actual value
502                    let bound_val = match bound {
503                        Value::Identifier(ref id) => {
504                            interpreter.resolve_value(&Value::Identifier(id.clone()))?
505                        }
506                        other => other,
507                    };
508                    interpreter.variables.insert(param.clone(), bound_val);
509                }
510
511                // Execute body in function context
512                interpreter.function_call_depth += 1;
513
514                let exec_result = super::collector::collect_events(interpreter, body);
515                interpreter.function_call_depth = interpreter.function_call_depth.saturating_sub(1);
516                exec_result?;
517
518                // Capture return value if present
519                let mut captured_return: Option<Value> = None;
520                if interpreter.returning_flag {
521                    captured_return = interpreter.return_value.clone();
522                    interpreter.returning_flag = false;
523                    interpreter.return_value = None;
524                }
525
526                // Restore variables (local scope ends)
527                interpreter.variables = vars_snapshot;
528
529                if let Some(rv) = captured_return {
530                    return Ok(rv);
531                }
532
533                return Ok(Value::Null);
534            }
535            // Patterns fallthrough to below handling
536            if let StatementKind::Pattern { target, .. } = &stmt_box.kind {
537                if let Some(tgt) = target.as_ref() {
538                    let (pattern_str, options) = interpreter.extract_pattern_data(&stmt_box.value);
539                    if let Some(pat) = pattern_str {
540                        interpreter.execute_pattern(tgt.as_str(), &pat, options)?;
541                        return Ok(Value::Null);
542                    }
543                }
544            }
545        }
546    }
547
548    // If it's a group call, execute and return null
549    if let Some(body) = interpreter.groups.get(name).cloned() {
550        super::collector::collect_events(interpreter, &body)?;
551        return Ok(Value::Null);
552    }
553
554    println!(
555        "⚠️  Warning: Group, pattern or function '{}' not found",
556        name
557    );
558    Ok(Value::Null)
559}
560
561pub fn execute_print(interpreter: &mut AudioInterpreter, value: &Value) -> Result<()> {
562    let message = match value {
563        Value::Call { name: _, args: _ } => {
564            // Evaluate the call expression and convert the returned value to string
565            let resolved = interpreter.resolve_value(value)?;
566            interpreter.value_to_string(&resolved)
567        }
568
569        Value::String(s) => {
570            if s.contains('{') && s.contains('}') {
571                interpreter.interpolate_string(s)
572            } else {
573                s.clone()
574            }
575        }
576        Value::Identifier(id) => {
577            // Resolve identifier values (supports post-increment shorthand)
578            // Support post-increment shorthand in prints: `i++` should mutate the variable.
579            if id.ends_with("++") {
580                let varname = id[..id.len() - 2].trim();
581                // read current
582                let cur = match interpreter.variables.get(varname) {
583                    Some(Value::Number(n)) => *n as isize,
584                    _ => 0,
585                };
586                // set new value
587                interpreter
588                    .variables
589                    .insert(varname.to_string(), Value::Number((cur + 1) as f32));
590                cur.to_string()
591            } else {
592                // Resolve the identifier using the interpreter resolver so dotted paths
593                // (e.g., mySynth.volume) are properly traversed.
594                let resolved = interpreter.resolve_value(&Value::Identifier(id.clone()))?;
595                match resolved {
596                    Value::String(s) => s.clone(),
597                    Value::Number(n) => n.to_string(),
598                    Value::Boolean(b) => b.to_string(),
599                    Value::Array(arr) => format!("{:?}", arr),
600                    Value::Map(map) => format!("{:?}", map),
601                    Value::Null => format!("Identifier(\"{}\")", id),
602                    other => format!("{:?}", other),
603                }
604            }
605        }
606        Value::Number(n) => n.to_string(),
607        Value::Boolean(b) => b.to_string(),
608        Value::Array(arr) => {
609            // Treat arrays used in print as concatenation parts: resolve each part and join
610            let mut parts = Vec::new();
611            for v in arr.iter() {
612                // If identifier with ++, handle mutation here
613                if let Value::Identifier(idtok) = v {
614                    if idtok.ends_with("++") {
615                        let varname = idtok[..idtok.len() - 2].trim();
616                        let cur = match interpreter.variables.get(varname) {
617                            Some(Value::Number(n)) => *n as isize,
618                            _ => 0,
619                        };
620                        interpreter
621                            .variables
622                            .insert(varname.to_string(), Value::Number((cur + 1) as f32));
623                        parts.push(cur.to_string());
624                        continue;
625                    }
626                }
627                let resolved = interpreter.resolve_value(v)?;
628                parts.push(interpreter.value_to_string(&resolved));
629            }
630            parts.join("")
631        }
632        Value::Map(map) => format!("{:?}", map),
633        _ => format!("{:?}", value),
634    };
635    // Always record prints as scheduled log events so they can be replayed at playback time.
636    // Record the scheduled log message (no debug-origin annotation in production)
637    let log_message = message.clone();
638    // Use the interpreter cursor_time as the event time.
639    interpreter
640        .events
641        .add_log_event(log_message.clone(), interpreter.cursor_time);
642    // If the interpreter is explicitly allowed to print (runtime interpreter), print immediately.
643    // For offline renders (`suppress_print == true`) we do NOT print now; prints are scheduled
644    // into `interpreter.events.logs` and written to a `.printlog` sidecar during the build.
645    // The live playback engine replays that sidecar so prints appear in real time during playback.
646    if !interpreter.suppress_print {
647        // Use the global CLI logger formatting for prints so they appear as [PRINT]
648        // Guard CLI-only logger behind the `cli` feature so WASM builds don't
649        // reference the `crate::tools` module (which is only available for native builds).
650        #[cfg(feature = "cli")]
651        {
652            crate::tools::logger::Logger::new().print(message.clone());
653        }
654
655        // For non-CLI builds (wasm/plugins) try forwarding to the realtime print
656        // channel if provided so prints are surfaced in UIs that consume them.
657        #[cfg(not(feature = "cli"))]
658        {
659            if let Some(tx) = &interpreter.realtime_print_tx {
660                let _ = tx.send((interpreter.cursor_time, log_message.clone()));
661            }
662        }
663    } else {
664        // If printing is suppressed (offline render) but a realtime replay channel was provided,
665        // forward the scheduled print to the replay thread so it can be displayed in real-time
666        // while the offline render proceeds. This is a best-effort path used by some callers
667        // that pipe scheduled prints directly into a playback session (when set).
668        if let Some(tx) = &interpreter.realtime_print_tx {
669            // forward the scheduled log message to realtime replayers (best-effort)
670            let _ = tx.send((interpreter.cursor_time, log_message.clone()));
671        }
672    }
673    Ok(())
674}
675
676pub fn execute_if(
677    interpreter: &mut AudioInterpreter,
678    condition: &Value,
679    body: &[Statement],
680    else_body: &Option<Vec<Statement>>,
681) -> Result<()> {
682    let condition_result = interpreter.evaluate_condition(condition)?;
683
684    if condition_result {
685        super::collector::collect_events(interpreter, body)?;
686    } else if let Some(else_stmts) = else_body {
687        super::collector::collect_events(interpreter, else_stmts)?;
688    }
689
690    Ok(())
691}
692
693pub fn execute_while(
694    interpreter: &mut AudioInterpreter,
695    condition: &Value,
696    body: &[Statement],
697) -> Result<()> {
698    let mut iter_count: usize = 0;
699    let hard_iter_cap: usize = 100_000; // Prevent infinite loops
700
701    loop {
702        let condition_result = interpreter.evaluate_condition(condition)?;
703
704        if !condition_result {
705            break;
706        }
707
708        super::collector::collect_events(interpreter, body)?;
709
710        // Break signalled inside while body -> exit
711        if interpreter.break_flag {
712            interpreter.break_flag = false;
713            break;
714        }
715
716        // Check schedule deadline (from "at X and during Y:")
717        if let Some(deadline) = interpreter.schedule_deadline {
718            if interpreter.cursor_time >= deadline {
719                break;
720            }
721        }
722
723        iter_count = iter_count.saturating_add(1);
724        if iter_count > hard_iter_cap {
725            break;
726        }
727    }
728
729    Ok(())
730}
731
732pub fn execute_event_handlers(interpreter: &mut AudioInterpreter, event_name: &str) -> Result<()> {
733    let handlers = interpreter.event_registry.get_handlers_matching(event_name);
734
735    for (index, handler) in handlers.iter().enumerate() {
736        // If handler has args, allow numeric interval to gate execution for 'beat'/'bar'
737        if let Some(args) = &handler.args {
738            if let Some(num_val) = args.iter().find(|v| matches!(v, Value::Number(_))) {
739                if let Value::Number(n) = num_val {
740                    let interval = (*n as usize).max(1);
741                    if event_name == "beat" {
742                        let cur = interpreter.special_vars.current_beat.floor() as usize;
743                        if cur % interval != 0 {
744                            continue;
745                        }
746                    } else if event_name == "bar" {
747                        let cur = interpreter.special_vars.current_bar.floor() as usize;
748                        if cur % interval != 0 {
749                            continue;
750                        }
751                    }
752                }
753            }
754        }
755
756        if handler.once
757            && !interpreter
758                .event_registry
759                .should_execute_once(event_name, index)
760        {
761            continue;
762        }
763
764        let body_clone = handler.body.clone();
765        super::collector::collect_events(interpreter, &body_clone)?;
766    }
767
768    Ok(())
769}
770
771pub fn handle_assign(
772    interpreter: &mut AudioInterpreter,
773    target: &str,
774    property: &str,
775    value: &Value,
776) -> Result<()> {
777    // Support dotted targets like "myTrigger.effects.reverb" as the target string.
778    // The parser may pass the full path as `target` and the last path segment as `property`.
779    if target.contains('.') {
780        let parts: Vec<&str> = target.split('.').collect();
781        let root = parts[0];
782        if let Some(root_val) = interpreter.variables.get_mut(root) {
783            // Traverse into nested maps to reach the parent map where to insert `property`.
784            let mut current = root_val;
785            for seg in parts.iter().skip(1) {
786                match current {
787                    Value::Map(map) => {
788                        if !map.contains_key(*seg) {
789                            // create nested map if missing
790                            map.insert((*seg).to_string(), Value::Map(HashMap::new()));
791                        }
792                        current = map.get_mut(*seg).unwrap();
793                    }
794                    _ => {
795                        return Err(anyhow::anyhow!(
796                            "Cannot traverse into non-map segment '{}' when assigning to '{}'",
797                            seg,
798                            target
799                        ));
800                    }
801                }
802            }
803
804            // Now `current` should be a Value::Map where we insert `property`.
805            if let Value::Map(map) = current {
806                map.insert(property.to_string(), value.clone());
807
808                // If the root object is a synth, update its synth definition
809                if interpreter.events.synths.contains_key(root) {
810                    if let Some(Value::Map(root_map)) = interpreter.variables.get(root) {
811                        let map_clone = root_map.clone();
812                        let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
813                        interpreter
814                            .events
815                            .synths
816                            .insert(root.to_string(), updated_def);
817                    }
818                }
819            } else {
820                return Err(anyhow::anyhow!(
821                    "Cannot assign property '{}' to non-map target '{}'",
822                    property,
823                    target
824                ));
825            }
826        } else {
827            return Err(anyhow::anyhow!("Variable '{}' not found", root));
828        }
829    } else {
830        if let Some(var) = interpreter.variables.get_mut(target) {
831            if let Value::Map(map) = var {
832                map.insert(property.to_string(), value.clone());
833
834                if interpreter.events.synths.contains_key(target) {
835                    let map_clone = map.clone();
836                    let updated_def = interpreter.extract_synth_def_from_map(&map_clone)?;
837                    interpreter
838                        .events
839                        .synths
840                        .insert(target.to_string(), updated_def);
841                }
842            } else {
843                return Err(anyhow::anyhow!(
844                    "Cannot assign property '{}' to non-map variable '{}'",
845                    property,
846                    target
847                ));
848            }
849        } else {
850            return Err(anyhow::anyhow!("Variable '{}' not found", target));
851        }
852    }
853
854    Ok(())
855}
856
857pub fn extract_synth_def_from_map(
858    _interpreter: &AudioInterpreter,
859    map: &HashMap<String, Value>,
860) -> Result<crate::engine::audio::events::SynthDefinition> {
861    use crate::engine::audio::events::extract_filters;
862    use crate::engine::audio::lfo::{LfoParams, LfoRate, LfoTarget, LfoWaveform};
863
864    let waveform = crate::engine::audio::events::extract_string(map, "waveform", "sine");
865    let attack = crate::engine::audio::events::extract_number(map, "attack", 0.01);
866    let decay = crate::engine::audio::events::extract_number(map, "decay", 0.1);
867    let sustain = crate::engine::audio::events::extract_number(map, "sustain", 0.7);
868    let release = crate::engine::audio::events::extract_number(map, "release", 0.2);
869
870    // Accept both String and Identifier for type (and synth_type alias)
871    let synth_type = if let Some(v) = map.get("type") {
872        match v {
873            Value::String(t) => {
874                let clean = t.trim_matches('"').trim_matches('\'');
875                if clean.is_empty() || clean == "synth" {
876                    None
877                } else {
878                    Some(clean.to_string())
879                }
880            }
881            Value::Identifier(id) => {
882                let clean = id.trim_matches('"').trim_matches('\'');
883                if clean.is_empty() || clean == "synth" {
884                    None
885                } else {
886                    Some(clean.to_string())
887                }
888            }
889            _ => None,
890        }
891    } else if let Some(v2) = map.get("synth_type") {
892        match v2 {
893            Value::String(t2) => {
894                let clean = t2.trim_matches('"').trim_matches('\'');
895                if clean.is_empty() || clean == "synth" {
896                    None
897                } else {
898                    Some(clean.to_string())
899                }
900            }
901            Value::Identifier(id2) => {
902                let clean = id2.trim_matches('"').trim_matches('\'');
903                if clean.is_empty() || clean == "synth" {
904                    None
905                } else {
906                    Some(clean.to_string())
907                }
908            }
909            _ => None,
910        }
911    } else {
912        None
913    };
914
915    let filters = if let Some(Value::Array(filters_arr)) = map.get("filters") {
916        extract_filters(filters_arr)
917    } else {
918        Vec::new()
919    };
920
921    let plugin_author = if let Some(Value::String(s)) = map.get("plugin_author") {
922        Some(s.clone())
923    } else {
924        None
925    };
926    let plugin_name = if let Some(Value::String(s)) = map.get("plugin_name") {
927        Some(s.clone())
928    } else {
929        None
930    };
931    let plugin_export = if let Some(Value::String(s)) = map.get("plugin_export") {
932        Some(s.clone())
933    } else {
934        None
935    };
936
937    // Extract LFO configuration if present
938    let lfo = if let Some(Value::Map(lfo_map)) = map.get("lfo") {
939        // Parse rate (Hz or tempo-synced like "1/4")
940        let rate_str = if let Some(Value::Number(n)) = lfo_map.get("rate") {
941            n.to_string()
942        } else if let Some(Value::String(s)) = lfo_map.get("rate") {
943            s.clone()
944        } else {
945            "5.0".to_string() // Default rate
946        };
947        let rate = LfoRate::from_value(&rate_str);
948
949        // Parse depth (0-1)
950        let depth = if let Some(Value::Number(n)) = lfo_map.get("depth") {
951            (*n).clamp(0.0, 1.0)
952        } else {
953            0.5 // Default depth
954        };
955
956        // Parse waveform (sine, triangle, square, saw)
957        let waveform_str = if let Some(Value::String(s)) = lfo_map.get("shape") {
958            s.clone()
959        } else if let Some(Value::String(s)) = lfo_map.get("waveform") {
960            s.clone()
961        } else {
962            "sine".to_string() // Default waveform
963        };
964        let waveform = LfoWaveform::from_str(&waveform_str);
965
966        // Parse target (volume, pitch, filter, pan)
967        let target = if let Some(Value::String(s)) = lfo_map.get("target") {
968            LfoTarget::from_str(s).unwrap_or(LfoTarget::Volume)
969        } else {
970            LfoTarget::Volume // Default target
971        };
972
973        // Parse initial phase (0-1)
974        let phase = if let Some(Value::Number(n)) = lfo_map.get("phase") {
975            (*n).fract().abs() // Ensure 0-1 range
976        } else {
977            0.0 // Default phase
978        };
979
980        Some(LfoParams {
981            rate,
982            depth,
983            waveform,
984            target,
985            phase,
986        })
987    } else {
988        None
989    };
990
991    let mut options = HashMap::new();
992
993    // List of SYNTH ADSR keys (standard parameters, not plugin options)
994    let synth_keys = [
995        "waveform",
996        "attack",
997        "decay",
998        "sustain",
999        "release",
1000        "type",
1001        "filters",
1002        "plugin_author",
1003        "plugin_name",
1004        "plugin_export",
1005        "lfo",
1006    ];
1007
1008    // List of AUDIO parameters (applied post-render, not plugin options)
1009    let audio_keys = [
1010        "pan",
1011        "gain",
1012        "volume",
1013        "detune",
1014        "spread",
1015        "synth_type", // Alias for type
1016        "chain",      // Effects chain
1017        "effects",    // Effects (deprecated name)
1018    ];
1019
1020    for (key, val) in map.iter() {
1021        // Skip all synth standard keys AND audio parameters
1022        if synth_keys.contains(&key.as_str()) || audio_keys.contains(&key.as_str()) {
1023            continue;
1024        }
1025
1026        // Skip keys starting with underscore (internal keys)
1027        if key.starts_with("_") {
1028            continue;
1029        }
1030
1031        // Now add everything else to options (plugin parameters)
1032        if let Value::Number(n) = val {
1033            options.insert(key.clone(), *n);
1034        } else if let Value::String(s) = val {
1035            // Try to parse string numbers
1036            if let Ok(n) = s.parse::<f32>() {
1037                options.insert(key.clone(), n);
1038            }
1039        }
1040    }
1041
1042    Ok(crate::engine::audio::events::SynthDefinition {
1043        waveform,
1044        attack,
1045        decay,
1046        sustain,
1047        release,
1048        synth_type,
1049        filters,
1050        options,
1051        plugin_author,
1052        plugin_name,
1053        plugin_export,
1054        lfo,
1055    })
1056}
1057
1058pub fn handle_load(interpreter: &mut AudioInterpreter, source: &str, alias: &str) -> Result<()> {
1059    use std::path::Path;
1060
1061    let path = Path::new(source);
1062    // Determine extension
1063    if let Some(ext) = path
1064        .extension()
1065        .and_then(|s| s.to_str())
1066        .map(|s| s.to_lowercase())
1067    {
1068        match ext.as_str() {
1069            "mid" | "midi" => {
1070                use crate::engine::audio::midi::load_midi_file;
1071                let midi_data = load_midi_file(path)?;
1072                interpreter.variables.insert(alias.to_string(), midi_data);
1073                // MIDI file loaded (silent)
1074                Ok(())
1075            }
1076            "wav" | "flac" | "mp3" | "ogg" => {
1077                // For native/CLI builds, register sample via the samples subsystem.
1078                #[cfg(feature = "cli")]
1079                {
1080                    use crate::engine::audio::samples;
1081                    let registered = samples::register_sample_from_path(path)?;
1082                    // Record the sample URI under the alias variable as a string (consistent with triggers)
1083                    interpreter
1084                        .variables
1085                        .insert(alias.to_string(), Value::String(registered.clone()));
1086                    // Sample file loaded (silent)
1087                    return Ok(());
1088                }
1089
1090                // For non-CLI builds (WASM/plugins), fallback to storing the original path as a string.
1091                #[cfg(not(feature = "cli"))]
1092                {
1093                    interpreter
1094                        .variables
1095                        .insert(alias.to_string(), Value::String(source.to_string()));
1096                    return Ok(());
1097                }
1098            }
1099            _ => Err(anyhow::anyhow!("Unsupported file type for @load: {}", ext)),
1100        }
1101    } else {
1102        Err(anyhow::anyhow!(
1103            "Cannot determine file extension for {}",
1104            source
1105        ))
1106    }
1107}
1108
1109pub fn handle_bind(
1110    interpreter: &mut AudioInterpreter,
1111    source: &str,
1112    target: &str,
1113    options: &Value,
1114) -> Result<()> {
1115    use std::collections::HashMap as StdHashMap;
1116
1117    // Support bindings that reference runtime MIDI device mappings like:
1118    //   bind myKickPattern -> mapping.out.myDeviceA with { port: 1, channel: 10 }
1119    //   bind mapping.in.myDeviceB with { port: 2, channel: 10 } -> mySynth
1120    // When a 'mapping.*' path is used, register a lightweight mapping entry in the
1121    // interpreter variables and expose convenience variables:
1122    //   mapping.<in|out>.<device>.<noteOn|noteOff|rest>
1123    if source.starts_with("mapping.") || target.starts_with("mapping.") {
1124        // extract options if present
1125        let opts_map: StdHashMap<String, Value> = if let Value::Map(m) = options {
1126            m.clone()
1127        } else {
1128            StdHashMap::new()
1129        };
1130
1131        // Helper function to create mapping variables and bookkeeping
1132        fn create_and_insert(
1133            path: &str,
1134            opts_map: &StdHashMap<String, Value>,
1135            interpreter: &mut AudioInterpreter,
1136        ) -> Option<(String, String)> {
1137            let parts: Vec<&str> = path.split('.').collect();
1138            if parts.len() >= 3 {
1139                let direction = parts[1]; // "in" or "out"
1140                let device = parts[2];
1141
1142                let mut map = StdHashMap::new();
1143                map.insert(
1144                    "_type".to_string(),
1145                    Value::String("midi_mapping".to_string()),
1146                );
1147                map.insert(
1148                    "direction".to_string(),
1149                    Value::String(direction.to_string()),
1150                );
1151                map.insert("device".to_string(), Value::String(device.to_string()));
1152
1153                // merge provided options
1154                for (k, v) in opts_map.iter() {
1155                    map.insert(k.clone(), v.clone());
1156                }
1157
1158                // Ensure mapping defaults
1159                crate::utils::props::ensure_default_properties(&mut map, Some("mapping"));
1160
1161                interpreter
1162                    .variables
1163                    .insert(path.to_string(), Value::Map(map.clone()));
1164
1165                // Expose event variables for convenience
1166                let note_on = format!("mapping.{}.{}.noteOn", direction, device);
1167                let note_off = format!("mapping.{}.{}.noteOff", direction, device);
1168                let rest = format!("mapping.{}.{}.rest", direction, device);
1169                interpreter
1170                    .variables
1171                    .insert(note_on.clone(), Value::String(note_on.clone()));
1172                interpreter
1173                    .variables
1174                    .insert(note_off.clone(), Value::String(note_off.clone()));
1175                interpreter
1176                    .variables
1177                    .insert(rest.clone(), Value::String(rest.clone()));
1178
1179                return Some((direction.to_string(), device.to_string()));
1180            }
1181            None
1182        }
1183
1184        // If source is mapping.* (incoming mapping binds to target instrument)
1185        if source.starts_with("mapping.") {
1186            if let Some((direction, device)) = create_and_insert(source, &opts_map, interpreter) {
1187                // Record association when target is not also mapping.*
1188                if !target.starts_with("mapping.") {
1189                    let mut bmap = StdHashMap::new();
1190                    bmap.insert("instrument".to_string(), Value::String(target.to_string()));
1191                    bmap.insert("direction".to_string(), Value::String(direction.clone()));
1192                    bmap.insert("device".to_string(), Value::String(device.clone()));
1193                    for (k, v) in opts_map.iter() {
1194                        bmap.insert(k.clone(), v.clone());
1195                    }
1196                    interpreter
1197                        .variables
1198                        .insert(format!("__mapping_bind::{}", source), Value::Map(bmap));
1199
1200                    // If we have a midi_manager, try to open the input port (for incoming mappings)
1201                    #[cfg(feature = "cli")]
1202                    if let Some(manager) = &mut interpreter.midi_manager {
1203                        if let Some(Value::Number(port_num)) = opts_map.get("port") {
1204                            let idx = *port_num as usize;
1205                            if let Ok(mut mgr) = manager.lock() {
1206                                // use device as name for identification
1207                                let _ = mgr.open_input_by_index(idx, &device);
1208                            }
1209                        }
1210                    }
1211                }
1212            }
1213        }
1214
1215        // If target is mapping.* (binding a sequence/instrument to an external device)
1216        if target.starts_with("mapping.") {
1217            if let Some((direction, device)) = create_and_insert(target, &opts_map, interpreter) {
1218                if !source.starts_with("mapping.") {
1219                    let mut bmap = StdHashMap::new();
1220                    bmap.insert("source".to_string(), Value::String(source.to_string()));
1221                    bmap.insert("direction".to_string(), Value::String(direction.clone()));
1222                    bmap.insert("device".to_string(), Value::String(device.clone()));
1223                    for (k, v) in opts_map.iter() {
1224                        bmap.insert(k.clone(), v.clone());
1225                    }
1226                    interpreter
1227                        .variables
1228                        .insert(format!("__mapping_bind::{}", target), Value::Map(bmap));
1229
1230                    // If we have a midi_manager, try to open the output port (for outgoing mappings)
1231                    #[cfg(feature = "cli")]
1232                    if let Some(manager) = &mut interpreter.midi_manager {
1233                        if let Some(Value::Number(port_num)) = opts_map.get("port") {
1234                            let idx = *port_num as usize;
1235                            if let Ok(mut mgr) = manager.lock() {
1236                                let _ = mgr.open_output_by_name(&device, idx);
1237                            }
1238                        }
1239                    }
1240                }
1241            }
1242        }
1243
1244        // Nothing more to schedule here at audio event level; actual MIDI I/O handlers
1245        // will be responsible for reacting to incoming messages and emitting events into
1246        // the interpreter event registry, and for flushing outgoing bound sequences to
1247        // MIDI device ports when appropriate.
1248        return Ok(());
1249    }
1250
1251    // Fallback: existing behaviour (binding MIDI file data to a synth)
1252    let midi_data = interpreter
1253        .variables
1254        .get(source)
1255        .ok_or_else(|| anyhow::anyhow!("MIDI source '{}' not found", source))?
1256        .clone();
1257
1258    if let Value::Map(midi_map) = &midi_data {
1259        let notes = midi_map
1260            .get("notes")
1261            .ok_or_else(|| anyhow::anyhow!("MIDI data has no notes"))?;
1262
1263        if let Value::Array(notes_array) = notes {
1264            let _synth_def = interpreter
1265                .events
1266                .synths
1267                .get(target)
1268                .ok_or_else(|| anyhow::anyhow!("Synth '{}' not found", target))?
1269                .clone();
1270
1271            let default_velocity = 100;
1272            let mut velocity = default_velocity;
1273
1274            if let Value::Map(opts) = options {
1275                if let Some(Value::Number(v)) = opts.get("velocity") {
1276                    velocity = *v as u8;
1277                }
1278            }
1279
1280            // Determine MIDI file BPM (if present) so we can rescale times to interpreter BPM
1281            // Default to interpreter.bpm when the MIDI file has no BPM metadata
1282            let midi_bpm =
1283                crate::engine::audio::events::extract_number(midi_map, "bpm", interpreter.bpm);
1284
1285            for note_val in notes_array {
1286                if let Value::Map(note_map) = note_val {
1287                    let time = crate::engine::audio::events::extract_number(note_map, "time", 0.0);
1288                    let note =
1289                        crate::engine::audio::events::extract_number(note_map, "note", 60.0) as u8;
1290                    let note_velocity = crate::engine::audio::events::extract_number(
1291                        note_map,
1292                        "velocity",
1293                        velocity as f32,
1294                    ) as u8;
1295                    // Duration may be present (ms) from MIDI loader; fallback to 500 ms
1296                    let duration_ms =
1297                        crate::engine::audio::events::extract_number(note_map, "duration", 500.0);
1298
1299                    use crate::engine::audio::events::AudioEvent;
1300                    let synth_def = interpreter
1301                        .events
1302                        .get_synth(target)
1303                        .cloned()
1304                        .unwrap_or_default();
1305                    // Rescale times according to interpreter BPM vs MIDI file BPM.
1306                    // If midi_bpm == interpreter.bpm this is a no-op. We compute factor = midi_bpm / interpreter.bpm
1307                    let interp_bpm = interpreter.bpm;
1308                    let factor = if interp_bpm > 0.0 {
1309                        midi_bpm / interp_bpm
1310                    } else {
1311                        1.0
1312                    };
1313
1314                    let start_time_s = (time / 1000.0) * factor;
1315                    let duration_s = (duration_ms / 1000.0) * factor;
1316
1317                    let event = AudioEvent::Note {
1318                        midi: note,
1319                        start_time: start_time_s,
1320                        duration: duration_s,
1321                        velocity: note_velocity as f32,
1322                        synth_id: target.to_string(),
1323                        synth_def,
1324                        pan: 0.0,
1325                        detune: 0.0,
1326                        gain: 1.0,
1327                        attack: None,
1328                        release: None,
1329                        delay_time: None,
1330                        delay_feedback: None,
1331                        delay_mix: None,
1332                        reverb_amount: None,
1333                        drive_amount: None,
1334                        drive_color: None,
1335                        effects: None,
1336                        use_per_note_automation: false,
1337                    };
1338
1339                    // bound note scheduled
1340                    interpreter.events.events.push(event);
1341                }
1342            }
1343
1344            // Bound notes from source to target
1345        }
1346    }
1347
1348    Ok(())
1349}
1350
1351#[cfg(feature = "cli")]
1352pub fn handle_use_plugin(
1353    interpreter: &mut AudioInterpreter,
1354    author: &str,
1355    name: &str,
1356    alias: &str,
1357) -> Result<()> {
1358    use crate::engine::plugin::loader::load_plugin;
1359
1360    match load_plugin(author, name) {
1361        Ok((info, _wasm_bytes)) => {
1362            let mut plugin_map = HashMap::new();
1363            plugin_map.insert("_type".to_string(), Value::String("plugin".to_string()));
1364            plugin_map.insert("_author".to_string(), Value::String(info.author.clone()));
1365            plugin_map.insert("_name".to_string(), Value::String(info.name.clone()));
1366
1367            if let Some(version) = &info.version {
1368                plugin_map.insert("_version".to_string(), Value::String(version.clone()));
1369            }
1370
1371            for export in &info.exports {
1372                let mut export_map = HashMap::new();
1373                export_map.insert(
1374                    "_plugin_author".to_string(),
1375                    Value::String(info.author.clone()),
1376                );
1377                export_map.insert("_plugin_name".to_string(), Value::String(info.name.clone()));
1378                export_map.insert(
1379                    "_export_name".to_string(),
1380                    Value::String(export.name.clone()),
1381                );
1382                export_map.insert(
1383                    "_export_kind".to_string(),
1384                    Value::String(export.kind.clone()),
1385                );
1386
1387                plugin_map.insert(export.name.clone(), Value::Map(export_map));
1388            }
1389            // Ensure plugin map has default properties available for dotted access
1390            crate::utils::props::ensure_default_properties(&mut plugin_map, Some("plugin"));
1391
1392            interpreter
1393                .variables
1394                .insert(alias.to_string(), Value::Map(plugin_map));
1395        }
1396        Err(e) => {
1397            eprintln!("❌ Failed to load plugin {}.{}: {}", author, name, e);
1398            return Err(anyhow::anyhow!("Failed to load plugin: {}", e));
1399        }
1400    }
1401
1402    Ok(())
1403}
1404
1405#[cfg(not(feature = "cli"))]
1406pub fn handle_use_plugin(
1407    interpreter: &mut AudioInterpreter,
1408    author: &str,
1409    name: &str,
1410    alias: &str,
1411) -> Result<()> {
1412    // Plugin loading not supported in this build (WASM/plugin builds). Insert a minimal placeholder so scripts can still reference the alias.
1413    let mut plugin_map = HashMap::new();
1414    plugin_map.insert(
1415        "_type".to_string(),
1416        Value::String("plugin_stub".to_string()),
1417    );
1418    plugin_map.insert("_author".to_string(), Value::String(author.to_string()));
1419    plugin_map.insert("_name".to_string(), Value::String(name.to_string()));
1420    // Ensure stubs also expose defaults for dotted access
1421    crate::utils::props::ensure_default_properties(&mut plugin_map, Some("plugin"));
1422
1423    interpreter
1424        .variables
1425        .insert(alias.to_string(), Value::Map(plugin_map));
1426    Ok(())
1427}
1428
1429pub fn handle_bank(
1430    interpreter: &mut AudioInterpreter,
1431    name: &str,
1432    alias: &Option<String>,
1433) -> Result<()> {
1434    let target_alias = alias
1435        .clone()
1436        .unwrap_or_else(|| name.split('.').last().unwrap_or(name).to_string());
1437
1438    if let Some(existing_value) = interpreter.variables.get(name) {
1439        interpreter
1440            .variables
1441            .insert(target_alias.clone(), existing_value.clone());
1442    } else {
1443        #[cfg(feature = "wasm")]
1444        {
1445            use crate::web::registry::banks::REGISTERED_BANKS;
1446            REGISTERED_BANKS.with(|banks| {
1447                for bank in banks.borrow().iter() {
1448                    if bank.full_name == *name {
1449                        if let Some(Value::Map(bank_map)) = interpreter.variables.get(&bank.alias) {
1450                            interpreter
1451                                .variables
1452                                .insert(target_alias.clone(), Value::Map(bank_map.clone()));
1453                        }
1454                    }
1455                }
1456            });
1457        }
1458
1459        #[cfg(not(feature = "wasm"))]
1460        {
1461            if let Ok(current_dir) = std::env::current_dir() {
1462                match interpreter.banks.register_bank(
1463                    target_alias.clone(),
1464                    &name,
1465                    &current_dir,
1466                    &current_dir,
1467                ) {
1468                    Ok(_) => {
1469                        let mut bank_map = HashMap::new();
1470                        bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1471                        bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1472                        interpreter
1473                            .variables
1474                            .insert(target_alias.clone(), Value::Map(bank_map));
1475                    }
1476                    Err(e) => {
1477                        eprintln!("⚠️ Failed to register bank '{}': {}", name, e);
1478                        let mut bank_map = HashMap::new();
1479                        bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1480                        bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1481                        interpreter
1482                            .variables
1483                            .insert(target_alias.clone(), Value::Map(bank_map));
1484                    }
1485                }
1486            } else {
1487                let mut bank_map = HashMap::new();
1488                bank_map.insert("_name".to_string(), Value::String(name.to_string()));
1489                bank_map.insert("_alias".to_string(), Value::String(target_alias.clone()));
1490                interpreter
1491                    .variables
1492                    .insert(target_alias.clone(), Value::Map(bank_map));
1493                eprintln!(
1494                    "⚠️ Could not determine cwd to register bank '{}', registered minimal alias.",
1495                    name
1496                );
1497            }
1498        }
1499    }
1500
1501    Ok(())
1502}
1503
1504pub fn handle_trigger(
1505    interpreter: &mut AudioInterpreter,
1506    entity: &str,
1507    effects: Option<&crate::language::syntax::ast::Value>,
1508) -> Result<()> {
1509    let resolved_entity = if entity.starts_with('.') {
1510        &entity[1..]
1511    } else {
1512        entity
1513    };
1514
1515    // If this resolved entity refers to a variable that contains a Trigger statement,
1516    // execute that stored trigger instead (supports: let t = .bank.kick -> reverse(true); .t )
1517    if let Some(var_val) = interpreter.variables.get(resolved_entity).cloned() {
1518        if let crate::language::syntax::ast::Value::Statement(stmt_box) = var_val {
1519            if let crate::language::syntax::ast::StatementKind::Trigger {
1520                entity: inner_entity,
1521                duration: _,
1522                effects: stored_effects,
1523            } = &stmt_box.kind
1524            {
1525                // Avoid direct recursion if someone stored a trigger that points to itself
1526                if inner_entity != resolved_entity {
1527                    // Prefer stored effects when present, otherwise fall back to effects passed in
1528                    let chosen_effects = stored_effects.as_ref().or(effects);
1529                    return handle_trigger(interpreter, inner_entity, chosen_effects);
1530                } else {
1531                    return Ok(());
1532                }
1533            }
1534        }
1535    }
1536
1537    if resolved_entity.contains('.') {
1538        let parts: Vec<&str> = resolved_entity.split('.').collect();
1539        if parts.len() == 2 {
1540            let (var_name, property) = (parts[0], parts[1]);
1541
1542            if let Some(Value::Map(map)) = interpreter.variables.get(var_name) {
1543                if let Some(Value::String(sample_uri)) = map.get(property) {
1544                    let uri = sample_uri.trim_matches('"').trim_matches('\'');
1545                    // scheduling sample at current cursor_time
1546                    interpreter.events.add_sample_event_with_effects(
1547                        uri,
1548                        interpreter.cursor_time,
1549                        1.0,
1550                        effects.cloned(),
1551                    );
1552                    let beat_duration = interpreter.beat_duration();
1553                    interpreter.cursor_time += beat_duration;
1554                } else {
1555                    #[cfg(not(feature = "wasm"))]
1556                    {
1557                        // First try to produce an internal devalang://bank URI (preferred, supports lazy loading)
1558                        let resolved_uri = interpreter.resolve_sample_uri(resolved_entity);
1559                        if resolved_uri != resolved_entity {
1560                            // scheduling resolved sample
1561                            interpreter.events.add_sample_event_with_effects(
1562                                &resolved_uri,
1563                                interpreter.cursor_time,
1564                                1.0,
1565                                effects.cloned(),
1566                            );
1567                            let beat_duration = interpreter.beat_duration();
1568                            interpreter.cursor_time += beat_duration;
1569                        } else if let Some(pathbuf) =
1570                            interpreter.banks.resolve_trigger(var_name, property)
1571                        {
1572                            if let Some(path_str) = pathbuf.to_str() {
1573                                // scheduling sample via bank path
1574                                interpreter.events.add_sample_event_with_effects(
1575                                    path_str,
1576                                    interpreter.cursor_time,
1577                                    1.0,
1578                                    effects.cloned(),
1579                                );
1580                                let beat_duration = interpreter.beat_duration();
1581                                interpreter.cursor_time += beat_duration;
1582                            } else {
1583                                println!(
1584                                    "⚠️ Resolution failed for {}.{} (invalid path)",
1585                                    var_name, property
1586                                );
1587                            }
1588                        } else {
1589                            // no path found in BankRegistry
1590                        }
1591                    }
1592                }
1593            }
1594        }
1595    } else {
1596        if let Some(Value::String(sample_uri)) = interpreter.variables.get(resolved_entity) {
1597            let uri = sample_uri.trim_matches('"').trim_matches('\'');
1598            interpreter.events.add_sample_event_with_effects(
1599                uri,
1600                interpreter.cursor_time,
1601                1.0,
1602                effects.cloned(),
1603            );
1604            let beat_duration = interpreter.beat_duration();
1605            interpreter.cursor_time += beat_duration;
1606        }
1607    }
1608
1609    // Note: do not call interpreter.render_audio() here - rendering is handled by the build pipeline.
1610    // Trigger queued for rendering (events collected)
1611
1612    Ok(())
1613}
1614
1615pub fn extract_pattern_data(
1616    _interpreter: &AudioInterpreter,
1617    value: &Value,
1618) -> (Option<String>, Option<HashMap<String, f32>>) {
1619    match value {
1620        Value::String(pattern) => (Some(pattern.clone()), None),
1621        Value::Map(map) => {
1622            let pattern = map.get("pattern").and_then(|v| {
1623                if let Value::String(s) = v {
1624                    Some(s.clone())
1625                } else {
1626                    None
1627                }
1628            });
1629
1630            let mut options = HashMap::new();
1631            for (key, val) in map.iter() {
1632                if key != "pattern" {
1633                    if let Value::Number(num) = val {
1634                        options.insert(key.clone(), *num);
1635                    }
1636                }
1637            }
1638
1639            let opts = if options.is_empty() {
1640                None
1641            } else {
1642                Some(options)
1643            };
1644            (pattern, opts)
1645        }
1646        _ => (None, None),
1647    }
1648}
1649
1650pub fn execute_pattern(
1651    interpreter: &mut AudioInterpreter,
1652    target: &str,
1653    pattern: &str,
1654    options: Option<HashMap<String, f32>>,
1655) -> Result<()> {
1656    execute_pattern_with_source(interpreter, target, pattern, options, None)
1657}
1658
1659pub fn execute_pattern_with_source(
1660    interpreter: &mut AudioInterpreter,
1661    target: &str,
1662    pattern: &str,
1663    options: Option<HashMap<String, f32>>,
1664    source: Option<&str>,
1665) -> Result<()> {
1666    use crate::engine::audio::events::AudioEvent;
1667
1668    let swing = options
1669        .as_ref()
1670        .and_then(|o| o.get("swing").copied())
1671        .unwrap_or(0.0);
1672    let humanize = options
1673        .as_ref()
1674        .and_then(|o| o.get("humanize").copied())
1675        .unwrap_or(0.0);
1676    let velocity_mult = options
1677        .as_ref()
1678        .and_then(|o| o.get("velocity").copied())
1679        .unwrap_or(1.0);
1680    let tempo_override = options.as_ref().and_then(|o| o.get("tempo").copied());
1681
1682    let effective_bpm = tempo_override.unwrap_or(interpreter.bpm);
1683
1684    let resolved_uri = resolve_sample_uri(interpreter, target);
1685
1686    let pattern_chars: Vec<char> = pattern.chars().filter(|c| !c.is_whitespace()).collect();
1687    let step_count = pattern_chars.len() as f32;
1688    if step_count == 0.0 {
1689        return Ok(());
1690    }
1691
1692    let bar_duration = (60.0 / effective_bpm) * 4.0;
1693    let step_duration = bar_duration / step_count;
1694
1695    for (i, &ch) in pattern_chars.iter().enumerate() {
1696        if ch == 'x' || ch == 'X' {
1697            let mut time = interpreter.cursor_time + (i as f32 * step_duration);
1698            if swing > 0.0 && i % 2 == 1 {
1699                time += step_duration * swing;
1700            }
1701
1702            #[cfg(any(feature = "cli", feature = "wasm"))]
1703            if humanize > 0.0 {
1704                let offset = crate::engine::special_vars::gen_range_f32(-humanize, humanize);
1705                time += offset;
1706            }
1707
1708            let event = AudioEvent::Sample {
1709                uri: resolved_uri.clone(),
1710                start_time: time,
1711                velocity: velocity_mult, // Already in 0-1 range, not MIDI 0-127
1712                effects: None,
1713                source: source.map(|s| s.to_string()),
1714            };
1715            interpreter.events.events.push(event);
1716        }
1717    }
1718
1719    interpreter.cursor_time += bar_duration;
1720    Ok(())
1721}
1722
1723pub fn resolve_sample_uri(interpreter: &AudioInterpreter, target: &str) -> String {
1724    if let Some(dot_pos) = target.find('.') {
1725        let bank_alias = &target[..dot_pos];
1726        let trigger_name = &target[dot_pos + 1..];
1727        if let Some(Value::Map(bank_map)) = interpreter.variables.get(bank_alias) {
1728            if let Some(Value::String(bank_name)) = bank_map.get("_name") {
1729                return format!("devalang://bank/{}/{}", bank_name, trigger_name);
1730            }
1731        }
1732    }
1733    target.to_string()
1734}
1735
1736/// Handle fade statement: automate a parameter in or out over a duration
1737pub fn handle_fade(
1738    interpreter: &mut AudioInterpreter,
1739    direction: &str,
1740    target: &str,
1741    duration: &crate::language::syntax::ast::DurationValue,
1742) -> Result<()> {
1743    // Parse the target (entity.property format, with optional leading dot)
1744    let normalized_target = if target.starts_with('.') {
1745        &target[1..] // Remove leading dot if present
1746    } else {
1747        target
1748    };
1749
1750    let parts: Vec<&str> = normalized_target.split('.').collect();
1751    if parts.len() != 2 {
1752        return Err(anyhow::anyhow!(
1753            "Fade target must be in format 'entity.property', got '{}'",
1754            target
1755        ));
1756    }
1757
1758    let entity = parts[0];
1759    let property = parts[1];
1760
1761    // Convert duration to seconds
1762    let dur_secs = match duration {
1763        crate::language::syntax::ast::DurationValue::Milliseconds(ms) => ms / 1000.0,
1764        crate::language::syntax::ast::DurationValue::Beats(b) => b * (60.0 / interpreter.bpm),
1765        crate::language::syntax::ast::DurationValue::Beat(s) => {
1766            // parse a fraction like "3/4" into beats
1767            if let Some((num, den)) = {
1768                let mut sp = s.split('/');
1769                if let (Some(a), Some(b)) = (sp.next(), sp.next()) {
1770                    if let (Ok(an), Ok(bn)) = (a.trim().parse::<f32>(), b.trim().parse::<f32>()) {
1771                        Some((an, bn))
1772                    } else {
1773                        None
1774                    }
1775                } else {
1776                    None
1777                }
1778            } {
1779                if den.abs() > f32::EPSILON {
1780                    (num / den) * (60.0 / interpreter.bpm)
1781                } else {
1782                    0.0
1783                }
1784            } else {
1785                0.0
1786            }
1787        }
1788        crate::language::syntax::ast::DurationValue::Number(n) => n / 1000.0,
1789        _ => 0.0,
1790    };
1791
1792    // Determine start and end values based on direction
1793    let (from_value, to_value) = match direction.to_lowercase().as_str() {
1794        "in" => (0.0, 1.0),  // Fade in: 0 -> 1
1795        "out" => (1.0, 0.0), // Fade out: 1 -> 0
1796        _ => {
1797            return Err(anyhow::anyhow!(
1798                "Fade direction must be 'in' or 'out', got '{}'",
1799                direction
1800            ));
1801        }
1802    };
1803
1804    // Create an automation parameter that modulates the target property
1805    // We'll add a fade event to the automation registry
1806    let mut fade_envelope = crate::engine::audio::automation::AutomationEnvelope::new(format!(
1807        "{}.{}",
1808        entity, property
1809    ));
1810
1811    // Use Linear curve for fade
1812    use crate::engine::audio::automation::AutomationCurve;
1813    fade_envelope.add_param(crate::engine::audio::automation::AutomationParam {
1814        param_name: property.to_string(),
1815        from_value,
1816        to_value,
1817        start_time: interpreter.cursor_time,
1818        duration: dur_secs,
1819        curve: AutomationCurve::Linear,
1820    });
1821
1822    interpreter.automation_registry.register(fade_envelope);
1823
1824    // Advance cursor time by fade duration
1825    interpreter.cursor_time += dur_secs;
1826
1827    Ok(())
1828}
1829
1830/// Handle stop statement: cease audio playback
1831pub fn handle_stop(
1832    interpreter: &mut AudioInterpreter,
1833    target: Option<&str>,
1834    delay: f32,
1835) -> Result<()> {
1836    // Create a stop event at the absolute time (delay from start, not from cursor_time)
1837    // Stop is a global instruction that applies from absolute position 0
1838    let stop_time = delay;
1839
1840    if let Some(entity_name) = target {
1841        // Stop specific entity: create a filter event that silences this entity
1842        let stop_event = AudioEvent::Stop {
1843            target: Some(entity_name.to_string()),
1844            time: stop_time,
1845        };
1846        interpreter.events.events.push(stop_event);
1847    } else {
1848        // Stop all: create a master stop event
1849        let stop_event = AudioEvent::Stop {
1850            target: None,
1851            time: stop_time,
1852        };
1853        interpreter.events.events.push(stop_event);
1854    }
1855
1856    Ok(())
1857}