mumu-event 0.1.1

event plugin for the mumu ecosystem
Documentation
// src/interval.rs

// event/src/interval.rs

use core_mumu::{
    parser::interpreter::apply::apply_n_ary_function_value,
    parser::types::Value,
    Interpreter,
};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use lazy_static::lazy_static;

/// Represents one repeating interval task.
pub struct IntervalTask {
    pub id: i32,          // unique handle
    pub period: Duration, // how long between callbacks
    pub next_tick: Instant,
    pub callback: Value,
    pub canceled: bool,
}

lazy_static! {
    pub static ref INTERVAL_TASKS: Mutex<Vec<IntervalTask>> = Mutex::new(Vec::new());
}

lazy_static! {
    static ref NEXT_INTERVAL_ID: Mutex<i32> = Mutex::new(1000);
}

/// `event:interval(ms, callback)` bridging function with placeholder-based partial usage.
///
/// Returns an integer handle (Value::Int) so you can call `event:stop(handle)` later.
pub fn event_interval_bridge(
    interp: &mut Interpreter,
    args: Vec<Value>
) -> Result<Value, String> {
    match args.len() {
        0 => {
            // No args => partial => (ms=?, cb=?)
            Ok(make_interval_partial(None, None))
        }
        1 => {
            let first = &args[0];
            match first {
                Value::Placeholder => Ok(make_interval_partial(None, None)),
                Value::Int(_) | Value::IntArray(_) => {
                    Ok(make_interval_partial(Some(first.clone()), None))
                }
                Value::Function(_) => {
                    Ok(make_interval_partial(None, Some(first.clone())))
                }
                other => Err(format!(
                    "event:interval => single arg must be int, function, or '_', got {:?}",
                    other
                )),
            }
        }
        2 => {
            let ms_val = args[0].clone();
            let cb_val = args[1].clone();
            let ms_is_pl = matches!(ms_val, Value::Placeholder);
            let cb_is_pl = matches!(cb_val, Value::Placeholder);

            if !ms_is_pl && !cb_is_pl {
                // both real => schedule
                schedule_interval(interp, ms_val, cb_val)
            } else {
                // partial usage
                let maybe_ms = if ms_is_pl { None } else { Some(ms_val) };
                let maybe_cb = if cb_is_pl { None } else { Some(cb_val) };
                Ok(make_interval_partial(maybe_ms, maybe_cb))
            }
        }
        n => Err(format!(
            "event:interval => expected up to 2 arguments, got {}",
            n
        )),
    }
}

/// Build a partial closure capturing ms & callback until both are known.
fn make_interval_partial(ms_opt: Option<Value>, cb_opt: Option<Value>) -> Value {
    use core_mumu::parser::types::FunctionValue::RustClosure;

    let closure = move |interp: &mut Interpreter, new_args: Vec<Value>| {
        let mut current_ms = ms_opt.clone();
        let mut current_cb = cb_opt.clone();

        for arg in new_args {
            if current_ms.is_none() {
                if matches!(arg, Value::Placeholder) {
                    // remain None
                } else {
                    current_ms = Some(arg);
                }
                continue;
            }
            if current_cb.is_none() {
                if matches!(arg, Value::Placeholder) {
                    // remain None
                } else {
                    current_cb = Some(arg);
                }
                continue;
            }
            return Err("event:interval => partial => too many arguments".to_string());
        }

        if current_ms.is_some() && current_cb.is_some() {
            schedule_interval(interp, current_ms.as_ref().unwrap().clone(), current_cb.as_ref().unwrap().clone())
        } else {
            Ok(make_interval_partial(current_ms, current_cb))
        }
    };

    Value::Function(Box::new(RustClosure(
        "event:interval-partial".to_string(),
        Arc::new(Mutex::new(closure)),
        0,
    )))
}

/// Actually create the IntervalTask, store it, and return the handle as an Int(...).
fn schedule_interval(
    interp: &mut Interpreter,
    ms_val: Value,
    cb_val: Value
) -> Result<Value, String> {
    // parse ms
    let ms_i32 = match ms_val {
        Value::Int(n) => n,
        Value::IntArray(ref arr) if arr.len() == 1 => arr[0],
        other => {
            return Err(format!(
                "event:interval => 'ms' must be int or single-element IntArray, got {:?}",
                other
            ));
        }
    };
    if ms_i32 < 0 {
        return Err("event:interval => 'ms' must be >= 0".to_string());
    }

    // parse callback
    if let Value::Function(_) = cb_val {
        // ok
    } else {
        return Err(format!(
            "event:interval => second argument must be Function(...), got {:?}",
            cb_val
        ));
    }

    // get a unique ID
    let mut lock_id = NEXT_INTERVAL_ID.lock().unwrap();
    let new_id = *lock_id;
    *lock_id += 1;
    drop(lock_id);

    let period = Duration::from_millis(ms_i32 as u64);
    let now = Instant::now();
    let new_task = IntervalTask {
        id: new_id,
        period,
        next_tick: now + period,
        callback: cb_val,
        canceled: false,
    };

    // store it
    {
        let mut lock = INTERVAL_TASKS.lock().unwrap();
        lock.push(new_task);
    }

    if interp.is_verbose() {
        eprintln!(
            "[event:interval] => created new IntervalTask => id={}, ms={}, next_tick={:?}",
            new_id, ms_i32, now + period
        );
    }

    // Return the handle as an Int
    Ok(Value::Int(new_id))
}

/// Called by the poller each time an interval triggers. We call the callback with zero arguments.
pub fn execute_interval_callback(
    interp: &mut Interpreter,
    cb_val: &Value
) -> Result<(), String> {
    if let Value::Function(fb) = cb_val {
        apply_n_ary_function_value(interp, fb.clone(), vec![])?;
        Ok(())
    } else {
        Err("execute_interval_callback => not a function".to_string())
    }
}

/// `event:stop(handle)` => stops the recurring interval or loop with that ID, removing it from the loop.
/// If you pass a handle that does not exist or was already canceled, no error occurs. 
pub fn event_stop_bridge(
    _interp: &mut Interpreter,
    mut args: Vec<Value>
) -> Result<Value, String> {
    if args.len() != 1 {
        return Err(format!("event:stop => expected 1 argument => the handle, got {}", args.len()));
    }
    let handle_val = args.remove(0);

    // parse it as an integer ID
    let id_i32 = match handle_val {
        Value::Int(i) => i,
        Value::IntArray(ref arr) if arr.len() == 1 => arr[0],
        other => {
            return Err(format!(
                "event:stop => handle must be an int or single-element IntArray, got {:?}",
                other
            ));
        }
    };

    // Try to cancel interval
    let mut _canceled = false;
    {
        let mut lock = INTERVAL_TASKS.lock().unwrap();
        for task in lock.iter_mut() {
            if task.id == id_i32 {
                task.canceled = true;
                _canceled = true;
                break;
            }
        }
    }

    // Try to cancel loop (regardless of interval match)
    crate::r#loop::stop_loop_task_by_id(id_i32);

    Ok(Value::Bool(true))
}