use arc_swap::ArcSwap;
use cagire::forth::{Dictionary, ExecutionTrace, Forth, Rng, StepContext, Value, Variables};
use parking_lot::Mutex;
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::collections::HashMap;
use std::sync::Arc;

pub fn default_ctx() -> StepContext<'static> {
    StepContext {
        step: 0,
        beat: 0.0,
        bank: 0,
        pattern: 0,
        tempo: 120.0,
        phase: 0.0,
        slot: 0,
        runs: 0,
        iter: 0,
        speed: 1.0,
        fill: false,
        nudge_secs: 0.0,
        cc_access: None,
        speed_key: "__speed_0_0__",
        mouse_x: 0.5,
        mouse_y: 0.5,
        mouse_down: 0.0,
    }
}

pub fn ctx_with(f: impl FnOnce(&mut StepContext<'static>)) -> StepContext<'static> {
    let mut ctx = default_ctx();
    f(&mut ctx);
    ctx
}

pub fn new_vars() -> Variables {
    Arc::new(ArcSwap::from_pointee(HashMap::new()))
}

pub fn new_dict() -> Dictionary {
    Arc::new(Mutex::new(HashMap::new()))
}

pub fn seeded_rng(seed: u64) -> Rng {
    Arc::new(Mutex::new(StdRng::seed_from_u64(seed)))
}

pub fn forth() -> Forth {
    Forth::new(new_vars(), new_dict(), seeded_rng(42))
}

pub fn forth_seeded(seed: u64) -> Forth {
    Forth::new(new_vars(), new_dict(), seeded_rng(seed))
}

pub fn run(script: &str) -> Forth {
    let f = forth();
    f.evaluate(script, &default_ctx()).unwrap();
    f
}

pub fn run_ctx(script: &str, ctx: &StepContext) -> Forth {
    let f = forth();
    f.evaluate(script, ctx).unwrap();
    f
}

pub fn stack_top(f: &Forth) -> Value {
    f.stack().pop().expect("stack empty")
}

pub fn stack_int(f: &Forth) -> i64 {
    match stack_top(f) {
        Value::Int(i, _) => i,
        other => panic!("expected Int, got {:?}", other),
    }
}

pub fn stack_float(f: &Forth) -> f64 {
    match stack_top(f) {
        Value::Float(x, _) => x,
        Value::Int(i, _) => i as f64,
        other => panic!("expected number, got {:?}", other),
    }
}

pub fn expect_stack(script: &str, expected: &[Value]) {
    let f = run(script);
    let stack = f.stack();
    assert_eq!(stack, expected, "script: {}", script);
}

pub fn expect_int(script: &str, expected: i64) {
    expect_stack(script, &[Value::Int(expected, None)]);
}

pub fn expect_str(script: &str, expected: &str) {
    expect_stack(script, &[Value::Str(Arc::from(expected), None)]);
}

pub fn expect_float(script: &str, expected: f64) {
    let f = run(script);
    let stack = f.stack();
    assert_eq!(stack.len(), 1, "expected single value on stack");
    let val = stack_float(&f);
    assert!(
        (val - expected).abs() < 1e-9,
        "expected {}, got {}",
        expected,
        val
    );
}

pub fn expect_floats_close(script: &str, expected: f64, epsilon: f64) {
    let f = run(script);
    let val = stack_float(&f);
    assert!(
        (val - expected).abs() < epsilon,
        "expected ~{}, got {}",
        expected,
        val
    );
}

pub fn expect_error(script: &str, expected_substr: &str) {
    let f = forth();
    let result = f.evaluate(script, &default_ctx());
    assert!(result.is_err(), "expected error for '{}'", script);
    let err = result.unwrap_err();
    assert!(
        err.contains(expected_substr),
        "error '{}' does not contain '{}'",
        err,
        expected_substr
    );
}

pub fn expect_outputs(script: &str, count: usize) -> Vec<String> {
    let f = forth();
    let outputs = f.evaluate(script, &default_ctx()).unwrap();
    assert_eq!(outputs.len(), count, "expected {} outputs", count);
    outputs
}

pub fn run_with_trace(script: &str) -> (Forth, ExecutionTrace) {
    let f = forth();
    let mut trace = ExecutionTrace::default();
    f.evaluate_with_trace(script, &default_ctx(), &mut trace)
        .unwrap();
    (f, trace)
}
