//! Binary operations, comparison, numeric coercion, range/iterator helpers,
//! `is_truthy` / `materialize_projected` / `value_type_name`, and builtin
//! dispatch (`intrinsic` and the per-intrinsic direct paths).

use std::borrow::Cow;
use std::sync::Arc;

use crate::ast::BinaryOp;

use super::instruction::Name;
use super::*;

pub(crate) fn expect_arg_count(
    name: &str,
    values: &[Value],
    expected: usize,
) -> Result<(), RuntimeError> {
    if values.len() == expected {
        Ok(())
    } else {
        Err(RuntimeError::TypeError {
            message: format!("`{name}` takes {expected} arg(s), got {}", values.len()),
        })
    }
}

pub(crate) async fn execute_intrinsic(
    builtin: IntrinsicOp,
    names: &[Name],
    values: &[Value],
) -> Result<Value, RuntimeError> {
    match builtin {
        IntrinsicOp::Len => {
            expect_arg_count("len", values, 1)?;
            execute_len_builtin(&values[0]).await
        }
        IntrinsicOp::Empty => {
            expect_arg_count("empty", values, 1)?;
            match &values[0] {
                Value::String(value) => Ok(Value::Bool(value.is_empty())),
                Value::List(values) => Ok(Value::Bool(values.is_empty())),
                Value::Record(record) => Ok(Value::Bool(record.is_empty())),
                Value::Projected(value) => {
                    value
                        .empty()
                        .await
                        .map(Value::Bool)
                        .ok_or_else(|| RuntimeError::TypeError {
                            message: "`empty` requires a string, list, record, or null".to_string(),
                        })
                }
                Value::Null => Ok(Value::Bool(true)),
                _ => Err(RuntimeError::TypeError {
                    message: "`empty` requires a string, list, record, or null".to_string(),
                }),
            }
        }
        IntrinsicOp::Keys => {
            expect_arg_count("keys", values, 1)?;
            match &values[0] {
                Value::Record(record) => Ok(Value::List(
                    record
                        .keys()
                        .map(|key| Value::String(key.into()))
                        .collect::<Vec<_>>()
                        .into(),
                )),
                Value::Projected(value) => Ok(Value::List(
                    value
                        .keys()
                        .await
                        .into_iter()
                        .map(|key| Value::String(key.into()))
                        .collect::<Vec<_>>()
                        .into(),
                )),
                Value::Null => Ok(Value::List(Vec::new().into())),
                _ => Err(RuntimeError::TypeError {
                    message: "`keys` requires a record or null".to_string(),
                }),
            }
        }
        IntrinsicOp::Values => {
            expect_arg_count("values", values, 1)?;
            if let Value::Projected(value) = &values[0]
                && let Some(value) = value.values().await
            {
                return Ok(value);
            }
            let value = materialize_projected_async(values[0].clone()).await;
            match &value {
                Value::Record(record) => Ok(Value::List(
                    record.values().cloned().collect::<Vec<_>>().into(),
                )),
                Value::Null => Ok(Value::List(Vec::new().into())),
                _ => Err(RuntimeError::TypeError {
                    message: "`values` requires a record or null".to_string(),
                }),
            }
        }
        IntrinsicOp::Contains => {
            expect_arg_count("contains", values, 2)?;
            execute_contains_builtin(&values[0], &values[1]).await
        }
        IntrinsicOp::Find(_) => execute_find_builtin(values).await,
        IntrinsicOp::GrepText => execute_grep_text_builtin(values).await,
        IntrinsicOp::StartsWith => {
            expect_arg_count("starts_with", values, 2)?;
            let prefix = materialize_projected_async(values[1].clone()).await;
            if let Value::Projected(value) = &values[0]
                && let Some(value) = value.starts_with(prefix.clone()).await
            {
                return Ok(value);
            }
            let value = materialize_projected_async(values[0].clone()).await;
            let value = coerce_string(&value)?;
            let prefix = coerce_string(&prefix)?;
            Ok(Value::Bool(value.starts_with(prefix.as_ref())))
        }
        IntrinsicOp::EndsWith => {
            expect_arg_count("ends_with", values, 2)?;
            let suffix = materialize_projected_async(values[1].clone()).await;
            if let Value::Projected(value) = &values[0]
                && let Some(value) = value.ends_with(suffix.clone()).await
            {
                return Ok(value);
            }
            let value = materialize_projected_async(values[0].clone()).await;
            let value = coerce_string(&value)?;
            let suffix = coerce_string(&suffix)?;
            Ok(Value::Bool(value.ends_with(suffix.as_ref())))
        }
        IntrinsicOp::Split => {
            expect_arg_count("split", values, 2)?;
            let needle = materialize_projected_async(values[1].clone()).await;
            if let Value::Projected(value) = &values[0]
                && let Some(value) = value.split(needle.clone()).await
            {
                return Ok(value);
            }
            let value = materialize_projected_async(values[0].clone()).await;
            let value = coerce_string(&value)?;
            let needle = coerce_string(&needle)?;
            Ok(Value::List(
                value
                    .split(needle.as_ref())
                    .map(|part| Value::String(part.into()))
                    .collect::<Vec<_>>()
                    .into(),
            ))
        }
        IntrinsicOp::Join => {
            expect_arg_count("join", values, 2)?;
            execute_join_builtin_async(&values[0], &values[1]).await
        }
        IntrinsicOp::Trim => {
            expect_arg_count("trim", values, 1)?;
            if let Value::Projected(value) = &values[0]
                && let Some(value) = value.trim().await
            {
                return Ok(value);
            }
            let value = materialize_projected_async(values[0].clone()).await;
            Ok(Value::String(coerce_string(&value)?.trim().into()))
        }
        IntrinsicOp::Slice => {
            expect_arg_count("slice", values, 3)?;
            let start = as_slice_bound_async(&values[1]).await?;
            let end = as_slice_bound_async(&values[2]).await?;
            if let Value::Projected(value) = &values[0]
                && let Some(value) = value.slice(start, end).await
            {
                return Ok(value);
            }
            let target = materialize_projected_async(values[0].clone()).await;
            match &target {
                Value::String(value) => Ok(Value::String(slice_string(value, start, end).into())),
                Value::List(items) => {
                    let Some((start, end)) = clamp_slice_bounds(start, end, items.len()) else {
                        return Ok(Value::List(Vec::new().into()));
                    };
                    Ok(Value::List(items[start..end].to_vec().into()))
                }
                _ => Err(RuntimeError::TypeError {
                    message: "`slice` requires a string or list".to_string(),
                }),
            }
        }
        IntrinsicOp::ToString => {
            expect_arg_count("to_string", values, 1)?;
            let value = if value_contains_projected(&values[0]) {
                stringify_value_async(&values[0]).await?
            } else {
                stringify_value_direct(&values[0])?
            };
            Ok(Value::String(value.into()))
        }
        IntrinsicOp::ToInt => {
            expect_arg_count("to_int", values, 1)?;
            if let Value::Projected(value) = &values[0]
                && let Some(value) = value.to_number().await
            {
                return Ok(Value::Number(as_number(&value)?.trunc()));
            }
            let value = materialize_projected_async(values[0].clone()).await;
            Ok(Value::Number(as_number(&value)?.trunc()))
        }
        IntrinsicOp::ToFloat => {
            expect_arg_count("to_float", values, 1)?;
            if let Value::Projected(value) = &values[0]
                && let Some(value) = value.to_number().await
            {
                return Ok(Value::Number(as_number(&value)?));
            }
            let value = materialize_projected_async(values[0].clone()).await;
            Ok(Value::Number(as_number(&value)?))
        }
        IntrinsicOp::JsonParse => {
            expect_arg_count("json_parse", values, 1)?;
            if let Value::Projected(value) = &values[0]
                && let Some(value) = value.json_parse().await
            {
                return Ok(value);
            }
            let value = materialize_projected_async(values[0].clone()).await;
            let parsed: serde_json::Value =
                serde_json::from_str(&coerce_string(&value)?).map_err(|err| {
                    RuntimeError::ValueError {
                        message: format!("invalid json: {err}"),
                    }
                })?;
            Ok(from_json(parsed))
        }
        IntrinsicOp::Format(_) => {
            if values.is_empty() {
                return Err(RuntimeError::TypeError {
                    message: "`format` requires at least a template string".to_string(),
                });
            }
            let template = match &values[0] {
                Value::String(value) => value.as_str(),
                other => {
                    return Err(RuntimeError::TypeError {
                        message: format!(
                            "`format` template must be a string, got {}",
                            value_type_name(other)
                        ),
                    });
                }
            };
            Ok(Value::String(
                apply_format_async(template, &values[1..]).await?.into(),
            ))
        }
        IntrinsicOp::Validate => {
            expect_arg_count("validate", values, 2)?;
            execute_validate_builtin(
                materialize_projected_async(values[0].clone()).await,
                &materialize_projected_async(values[1].clone()).await,
            )
        }
        IntrinsicOp::Range(_) => execute_range_builtin_async(values).await,
        IntrinsicOp::CeilDiv => {
            execute_integer_div_builtin_async("ceil_div", values, f64::ceil).await
        }
        IntrinsicOp::FloorDiv => {
            execute_integer_div_builtin_async("floor_div", values, f64::floor).await
        }
        IntrinsicOp::Push => {
            expect_arg_count("push", values, 2)?;
            execute_push_builtin_async(values[0].clone(), values[1].clone()).await
        }
        IntrinsicOp::ValidateCompiled(_)
        | IntrinsicOp::PushAssign(_)
        | IntrinsicOp::FormatCompiled(_)
        | IntrinsicOp::FormatCompiledSlotNumber { .. }
        | IntrinsicOp::FormatCompiledSlotNumberBinary { .. } => {
            unreachable!("compiled-only intrinsic reached generic executor")
        }
        IntrinsicOp::InvalidArity { name, argc } => Err(RuntimeError::TypeError {
            message: invalid_arity_message(names[name].text.as_ref(), argc),
        }),
        IntrinsicOp::Unknown { name, .. } => Err(RuntimeError::UnknownBuiltin {
            name: names[name].text.to_string(),
        }),
    }
}

fn invalid_arity_message(name: &str, argc: usize) -> String {
    // A handful of variadic builtins have bespoke prose; the rest derive their
    // message directly from the single arity registry.
    match name {
        "find" => format!("`find` takes 2 or 3 arg(s), got {argc}"),
        "range" => format!("`range` takes 1, 2, or 3 arg(s), got {argc}"),
        "format" => "`format` requires at least a template string".to_string(),
        _ => {
            let expected = match crate::builtins::lookup(name).map(|builtin| builtin.arity) {
                Some(crate::builtins::Arity::Exact(n)) => n,
                _ => 0,
            };
            format!("`{name}` takes {expected} arg(s), got {argc}")
        }
    }
}

pub(crate) async fn execute_len_builtin(value: &Value) -> Result<Value, RuntimeError> {
    if let Value::Projected(value) = value {
        return Ok(Value::Number(value.len().await as f64));
    }
    execute_len_direct(value)
}

pub(crate) fn execute_len_direct(value: &Value) -> Result<Value, RuntimeError> {
    value_len(value)
        .map(|len| Value::Number(len as f64))
        .ok_or_else(|| RuntimeError::TypeError {
            message: "`len` requires a string, list, record, or null; use `.size` for images"
                .to_string(),
        })
}

pub(crate) async fn execute_contains_builtin(
    haystack: &Value,
    needle: &Value,
) -> Result<Value, RuntimeError> {
    let needle = materialize_projected_async(needle.clone()).await;
    if !matches!(haystack, Value::Projected(_)) {
        return execute_contains_direct(haystack, &needle).map(Value::Bool);
    }
    match haystack {
        Value::Projected(value) => Ok(Value::Bool(value.contains(&needle).await?)),
        Value::Null => Ok(Value::Bool(false)),
        _ => Err(RuntimeError::TypeError {
            message:
                "`contains` requires a string/string, list/value, record/key, or null/value pair"
                    .to_string(),
        }),
    }
}

pub(crate) fn execute_contains_direct(
    haystack: &Value,
    needle: &Value,
) -> Result<bool, RuntimeError> {
    match (haystack, needle) {
        (Value::String(haystack), needle) => Ok(haystack.contains(coerce_string(needle)?.as_ref())),
        (Value::List(items), needle) => Ok(items.contains(needle)),
        (Value::Record(record), needle) => {
            Ok(record.get(coerce_string(needle)?.as_ref()).is_some())
        }
        (Value::Null, _) => Ok(false),
        _ => Err(RuntimeError::TypeError {
            message:
                "`contains` requires a string/string, list/value, record/key, or null/value pair"
                    .to_string(),
        }),
    }
}

pub(crate) async fn execute_find_builtin(values: &[Value]) -> Result<Value, RuntimeError> {
    if !(values.len() == 2 || values.len() == 3) {
        return Err(RuntimeError::TypeError {
            message: format!("`find` takes 2 or 3 arg(s), got {}", values.len()),
        });
    }

    let needle = materialize_projected_async(values[1].clone()).await;
    let start = match values.get(2) {
        Some(value) => {
            let value = materialize_projected_async(value.clone()).await;
            as_non_negative_char_index("find", "start", &value)?
        }
        None => 0,
    };

    if let Value::Projected(value) = &values[0]
        && let Some(value) = value.find(needle.clone(), start).await
    {
        return Ok(value);
    }
    let haystack = materialize_projected_async(values[0].clone()).await;
    execute_find_direct(&haystack, &needle, start)
}

pub(crate) async fn execute_grep_text_builtin(values: &[Value]) -> Result<Value, RuntimeError> {
    expect_arg_count("grep_text", values, 2)?;
    let needle = materialize_projected_async(values[1].clone()).await;
    if let Value::Projected(value) = &values[0]
        && let Some(value) = value.grep_text(needle.clone()).await
    {
        return Ok(value);
    }
    let text = materialize_projected_async(values[0].clone()).await;
    execute_grep_text_direct(&text, &needle)
}

pub(crate) fn execute_find_direct(
    text: &Value,
    needle: &Value,
    start: usize,
) -> Result<Value, RuntimeError> {
    let text = coerce_string(text)?;
    let needle = coerce_string(needle)?;
    Ok(match find_text(text.as_ref(), needle.as_ref(), start) {
        Some(index) => Value::Number(index as f64),
        None => Value::Null,
    })
}

pub(crate) fn execute_grep_text_direct(
    text: &Value,
    needle: &Value,
) -> Result<Value, RuntimeError> {
    let text = coerce_string(text)?;
    let needle = coerce_string(needle)?;
    grep_text_strings(text.as_ref(), needle.as_ref())
}

fn grep_text_strings(text: &str, needle: &str) -> Result<Value, RuntimeError> {
    if needle.is_empty() {
        return Err(RuntimeError::ValueError {
            message: "`grep_text` needle must not be empty".to_string(),
        });
    }

    let needle_len = needle.chars().count();
    let needle_value = Value::String(needle.into());
    let mut matches = Vec::new();
    for (line_index, line) in text.lines().enumerate() {
        let Some(start) = find_text(line, needle, 0) else {
            continue;
        };
        let mut record = record_with_capacity(5);
        record.insert_str("line", Value::Number((line_index + 1) as f64));
        record.insert_str("text", Value::String(line.into()));
        record.insert_str("match", needle_value.clone());
        record.insert_str("start", Value::Number(start as f64));
        record.insert_str("end", Value::Number((start + needle_len) as f64));
        matches.push(Value::Record(Arc::new(record)));
    }

    Ok(Value::List(matches.into()))
}

fn find_text(text: &str, needle: &str, start: usize) -> Option<usize> {
    let start_byte = if start == 0 {
        0
    } else {
        byte_index_for_char(text, start)?
    };
    if needle.is_empty() {
        return Some(start);
    }
    let tail = &text[start_byte..];
    let match_byte = tail.find(needle)?;
    Some(start + tail[..match_byte].chars().count())
}

fn byte_index_for_char(text: &str, target: usize) -> Option<usize> {
    let mut char_count = 0;
    for (byte_index, _) in text.char_indices() {
        if char_count == target {
            return Some(byte_index);
        }
        char_count += 1;
    }
    if char_count == target {
        Some(text.len())
    } else {
        None
    }
}

pub(crate) fn value_len(value: &Value) -> Option<usize> {
    match value {
        Value::String(value) => Some(value.chars().count()),
        Value::List(values) => Some(values.len()),
        Value::Record(record) => Some(record.len()),
        Value::Null => Some(0),
        _ => None,
    }
}

pub(crate) async fn iterable_values(value: Value) -> Result<ListValue, RuntimeError> {
    match value {
        Value::List(values) => Ok(values),
        Value::Projected(value) => match value.materialize_async().await {
            Value::List(values) => Ok(values),
            _ => Err(RuntimeError::NonListIteration),
        },
        _ => Err(RuntimeError::NonListIteration),
    }
}

pub(crate) async fn execute_join_builtin_async(
    items: &Value,
    sep: &Value,
) -> Result<Value, RuntimeError> {
    let sep = materialize_projected_async(sep.clone()).await;
    if let Value::Projected(value) = items
        && let Some(value) = value.join(sep.clone()).await
    {
        return Ok(value);
    }
    let items = materialize_projected_async(items.clone()).await;
    let Value::List(items) = &items else {
        return Err(RuntimeError::TypeError {
            message: "`join` requires a list as the first argument".to_string(),
        });
    };
    let sep = coerce_string(&sep)?;
    let mut joined = String::new();
    for (index, item) in items.iter().enumerate() {
        if index > 0 {
            joined.push_str(sep.as_ref());
        }
        let item = materialize_projected_async(item.clone()).await;
        joined.push_str(coerce_string(&item)?.as_ref());
    }
    Ok(Value::String(joined.into()))
}

#[cfg(test)]
pub(crate) fn execute_join_builtin(items: &Value, sep: &Value) -> Result<Value, RuntimeError> {
    futures_executor::block_on(execute_join_builtin_async(items, sep))
}

pub(crate) fn execute_range_builtin(values: &[Value]) -> Result<Value, RuntimeError> {
    let (start, end, step) = range_bounds(values)?;
    build_range(start, end, step)
}

pub(crate) async fn execute_range_builtin_async(values: &[Value]) -> Result<Value, RuntimeError> {
    let (start, end, step) = range_bounds_async(values).await?;
    build_range(start, end, step)
}

pub(crate) async fn range_bounds_async(values: &[Value]) -> Result<(i64, i64, i64), RuntimeError> {
    let mut materialized = Vec::with_capacity(values.len());
    for value in values {
        let value = match value {
            Value::Projected(projected) => match projected.range_bound().await {
                Some(value) => value,
                None => projected.materialize_async().await,
            },
            other => other.clone(),
        };
        materialized.push(value);
    }
    range_bounds(&materialized)
}

pub(crate) fn range_bounds(values: &[Value]) -> Result<(i64, i64, i64), RuntimeError> {
    let (start, end, step) = match values {
        [end] => (0, as_range_bound(end)?, 1),
        [start, end] => (as_range_bound(start)?, as_range_bound(end)?, 1),
        [start, end, step] => (
            as_range_bound(start)?,
            as_range_bound(end)?,
            as_range_bound(step)?,
        ),
        _ => {
            return Err(RuntimeError::TypeError {
                message: format!("`range` takes 1, 2, or 3 arg(s), got {}", values.len()),
            });
        }
    };
    if step == 0 {
        return Err(RuntimeError::ValueError {
            message: "`range` step must not be 0".to_string(),
        });
    }
    validate_range_len(start, end, step)?;
    Ok((start, end, step))
}

pub(crate) async fn execute_push_builtin_async(
    list: Value,
    item: Value,
) -> Result<Value, RuntimeError> {
    let item = materialize_projected_async(item).await;
    if let Value::Projected(value) = &list
        && let Some(value) = value.push(item.clone()).await
    {
        return Ok(value);
    }
    let list = materialize_projected_async(list).await;
    let Value::List(items) = list else {
        return Err(RuntimeError::TypeError {
            message: "`push` requires a list as the first argument".to_string(),
        });
    };
    let mut values = items.into_vec();
    if values.len() == values.capacity() {
        values.reserve(1);
    }
    values.push(item);
    Ok(Value::List(values.into()))
}

#[cfg(test)]
pub(crate) fn execute_push_builtin(list: &Value, item: Value) -> Result<Value, RuntimeError> {
    futures_executor::block_on(execute_push_builtin_async(list.clone(), item))
}

pub(crate) fn as_range_bound(value: &Value) -> Result<i64, RuntimeError> {
    let Value::Number(number) = value else {
        return Err(RuntimeError::TypeError {
            message: format!(
                "`range` bounds must be finite integers, got {}",
                value_type_name(value)
            ),
        });
    };
    if !number.is_finite()
        || number.fract() != 0.0
        || *number < i64::MIN as f64
        || *number > i64::MAX as f64
    {
        return Err(RuntimeError::TypeError {
            message: "`range` bounds must be finite integers".to_string(),
        });
    }
    Ok(*number as i64)
}

pub(crate) fn build_range(start: i64, end: i64, step: i64) -> Result<Value, RuntimeError> {
    if range_len(start, end, step)? == 0 {
        return Ok(Value::List(Vec::new().into()));
    }
    let mut items = Vec::new();
    let mut value = start;
    if step > 0 {
        while value < end {
            items.push(Value::Number(value as f64));
            value = value.saturating_add(step);
        }
    } else {
        while value > end {
            items.push(Value::Number(value as f64));
            value = value.saturating_add(step);
        }
    }
    Ok(Value::List(items.into()))
}

pub(crate) fn validate_range_len(start: i64, end: i64, step: i64) -> Result<(), RuntimeError> {
    const MAX_RANGE_ITEMS: i128 = 1_000_000;
    if range_len(start, end, step)? > MAX_RANGE_ITEMS {
        return Err(RuntimeError::ValueError {
            message: format!("`range` would create more than {MAX_RANGE_ITEMS} items"),
        });
    }
    Ok(())
}

fn range_len(start: i64, end: i64, step: i64) -> Result<i128, RuntimeError> {
    if step == 0 {
        return Err(RuntimeError::ValueError {
            message: "`range` step must not be 0".to_string(),
        });
    }
    if (step > 0 && start >= end) || (step < 0 && start <= end) {
        return Ok(0);
    }
    let distance = if step > 0 {
        end as i128 - start as i128
    } else {
        start as i128 - end as i128
    };
    let step = (step as i128).abs();
    Ok((distance + step - 1) / step)
}

pub(crate) fn execute_integer_div_builtin(
    name: &'static str,
    values: &[Value],
    round: impl FnOnce(f64) -> f64,
) -> Result<Value, RuntimeError> {
    expect_arg_count(name, values, 2)?;
    let dividend = as_integer_div_arg(name, "dividend", &values[0])?;
    let divisor = as_integer_div_arg(name, "divisor", &values[1])?;
    if divisor == 0.0 {
        return Err(RuntimeError::ValueError {
            message: format!("`{name}` divisor must not be 0"),
        });
    }
    Ok(Value::Number(round(dividend / divisor)))
}

async fn execute_integer_div_builtin_async(
    name: &'static str,
    values: &[Value],
    round: impl FnOnce(f64) -> f64,
) -> Result<Value, RuntimeError> {
    expect_arg_count(name, values, 2)?;
    let dividend = materialize_projected_async(values[0].clone()).await;
    let divisor = materialize_projected_async(values[1].clone()).await;
    execute_integer_div_builtin(name, &[dividend, divisor], round)
}

fn as_integer_div_arg(
    builtin: &'static str,
    arg_name: &'static str,
    value: &Value,
) -> Result<f64, RuntimeError> {
    let Value::Number(number) = value else {
        return Err(RuntimeError::TypeError {
            message: format!(
                "`{builtin}` {arg_name} must be a finite integer, got {}",
                value_type_name(value)
            ),
        });
    };
    if !number.is_finite() || number.fract() != 0.0 {
        return Err(RuntimeError::TypeError {
            message: format!("`{builtin}` {arg_name} must be a finite integer"),
        });
    }
    Ok(*number)
}

pub(crate) fn eval_binary_values(
    left: Value,
    op: BinaryOp,
    right: Value,
) -> Result<Value, RuntimeError> {
    match op {
        BinaryOp::Add => add_values(left, right),
        BinaryOp::Subtract => numeric_binary_values(left, right, |a, b| a - b),
        BinaryOp::Multiply => numeric_binary_values(left, right, |a, b| a * b),
        BinaryOp::Divide => numeric_binary_values(left, right, |a, b| a / b),
        BinaryOp::Modulo => numeric_binary_values(left, right, |a, b| a % b),
        BinaryOp::Equal => Ok(Value::Bool(left == right)),
        BinaryOp::NotEqual => Ok(Value::Bool(left != right)),
        BinaryOp::Less => compare_ordered(left, right, |a, b| a < b, |a, b| a < b),
        BinaryOp::LessEqual => compare_ordered(left, right, |a, b| a <= b, |a, b| a <= b),
        BinaryOp::Greater => compare_ordered(left, right, |a, b| a > b, |a, b| a > b),
        BinaryOp::GreaterEqual => compare_ordered(left, right, |a, b| a >= b, |a, b| a >= b),
        BinaryOp::And | BinaryOp::Or => unreachable!("logical ops are compiled with jumps"),
    }
}

pub(crate) fn eval_number_binary_values(left: f64, op: BinaryOp, right: f64) -> Value {
    match op {
        BinaryOp::Add
        | BinaryOp::Subtract
        | BinaryOp::Multiply
        | BinaryOp::Divide
        | BinaryOp::Modulo => Value::Number(eval_number_numeric_binary_value(left, op, right)),
        BinaryOp::Equal => Value::Bool(left == right),
        BinaryOp::NotEqual => Value::Bool(left != right),
        BinaryOp::Less => Value::Bool(left < right),
        BinaryOp::LessEqual => Value::Bool(left <= right),
        BinaryOp::Greater => Value::Bool(left > right),
        BinaryOp::GreaterEqual => Value::Bool(left >= right),
        BinaryOp::And | BinaryOp::Or => unreachable!("logical ops are compiled with jumps"),
    }
}

pub(crate) fn eval_number_numeric_binary_value(left: f64, op: BinaryOp, right: f64) -> f64 {
    match op {
        BinaryOp::Add => left + right,
        BinaryOp::Subtract => left - right,
        BinaryOp::Multiply => left * right,
        BinaryOp::Divide => left / right,
        BinaryOp::Modulo => left % right,
        _ => unreachable!("non-numeric op in fused numeric branch"),
    }
}

pub(crate) fn eval_number_compare_values(left: f64, op: BinaryOp, right: f64) -> bool {
    match op {
        BinaryOp::Equal => left == right,
        BinaryOp::NotEqual => left != right,
        BinaryOp::Less => left < right,
        BinaryOp::LessEqual => left <= right,
        BinaryOp::Greater => left > right,
        BinaryOp::GreaterEqual => left >= right,
        _ => unreachable!("non-comparison op in fused slot branch"),
    }
}

pub(crate) fn is_comparison_binary_op(op: BinaryOp) -> bool {
    matches!(
        op,
        BinaryOp::Equal
            | BinaryOp::NotEqual
            | BinaryOp::Less
            | BinaryOp::LessEqual
            | BinaryOp::Greater
            | BinaryOp::GreaterEqual
    )
}

pub(crate) fn is_numeric_binary_op(op: BinaryOp) -> bool {
    matches!(
        op,
        BinaryOp::Add
            | BinaryOp::Subtract
            | BinaryOp::Multiply
            | BinaryOp::Divide
            | BinaryOp::Modulo
    )
}

pub(crate) fn eval_compare_values(
    left: Value,
    op: BinaryOp,
    right: Value,
) -> Result<bool, RuntimeError> {
    match (left, right) {
        (Value::Number(left), Value::Number(right)) => {
            Ok(eval_number_compare_values(left, op, right))
        }
        (left, right) => match op {
            BinaryOp::Equal => Ok(left == right),
            BinaryOp::NotEqual => Ok(left != right),
            BinaryOp::Less => {
                compare_ordered(left, right, |a, b| a < b, |a, b| a < b).map(expect_bool_value)
            }
            BinaryOp::LessEqual => {
                compare_ordered(left, right, |a, b| a <= b, |a, b| a <= b).map(expect_bool_value)
            }
            BinaryOp::Greater => {
                compare_ordered(left, right, |a, b| a > b, |a, b| a > b).map(expect_bool_value)
            }
            BinaryOp::GreaterEqual => {
                compare_ordered(left, right, |a, b| a >= b, |a, b| a >= b).map(expect_bool_value)
            }
            _ => unreachable!("non-comparison op in fused branch"),
        },
    }
}

pub(crate) fn expect_bool_value(value: Value) -> bool {
    match value {
        Value::Bool(value) => value,
        _ => unreachable!("comparison produced non-bool value"),
    }
}

pub(crate) async fn materialize_projected_async(value: Value) -> Value {
    match value {
        Value::Projected(projected) => projected.materialize_async().await,
        other => other,
    }
}

pub(crate) fn numeric_binary_values(
    left: Value,
    right: Value,
    op: impl FnOnce(f64, f64) -> f64,
) -> Result<Value, RuntimeError> {
    match (left, right) {
        (Value::Number(left), Value::Number(right)) => Ok(Value::Number(op(left, right))),
        (left, right) => Ok(Value::Number(op(as_number(&left)?, as_number(&right)?))),
    }
}

pub(crate) fn as_number(value: &Value) -> Result<f64, RuntimeError> {
    match value {
        Value::Number(value) => Ok(*value),
        Value::Bool(value) => Ok(if *value { 1.0 } else { 0.0 }),
        Value::Null => Ok(0.0),
        Value::String(value) => {
            let value = value.trim();
            if value.is_empty() {
                return Ok(0.0);
            }
            value.parse::<f64>().map_err(|_| RuntimeError::TypeError {
                message: "expected a number".to_string(),
            })
        }
        _ => Err(RuntimeError::TypeError {
            message: format!("expected a number, got {}", value_type_name(value)),
        }),
    }
}

pub(crate) fn coerce_string(value: &Value) -> Result<Cow<'_, str>, RuntimeError> {
    match value {
        Value::String(value) => Ok(Cow::Borrowed(value)),
        Value::Null => Ok(Cow::Borrowed("null")),
        Value::Bool(value) => Ok(Cow::Owned(value.to_string())),
        Value::Number(value) => Ok(Cow::Owned(value.to_string())),
        Value::Image(_)
        | Value::Resource(_)
        | Value::List(_)
        | Value::Record(_)
        | Value::Projected(_) => Err(RuntimeError::TypeError {
            message: format!("expected text, got {}", value_type_name(value)),
        }),
    }
}

pub(crate) fn as_offset(value: &Value) -> Result<isize, RuntimeError> {
    let number = as_number(value)?;
    if !number.is_finite() || number.fract() != 0.0 {
        return Err(RuntimeError::TypeError {
            message: "index must be an integer".to_string(),
        });
    }
    Ok(number as isize)
}

pub(crate) fn as_slice_bound(value: &Value) -> Result<Option<isize>, RuntimeError> {
    match value {
        Value::Null => Ok(None),
        other => as_offset(other).map(Some),
    }
}

fn as_non_negative_char_index(
    builtin: &str,
    arg_name: &str,
    value: &Value,
) -> Result<usize, RuntimeError> {
    let number = as_number(value)?;
    if !number.is_finite() || number.fract() != 0.0 || number < 0.0 || number > usize::MAX as f64 {
        return Err(RuntimeError::TypeError {
            message: format!("`{builtin}` {arg_name} must be a non-negative integer"),
        });
    }
    Ok(number as usize)
}

pub(crate) async fn as_slice_bound_async(value: &Value) -> Result<Option<isize>, RuntimeError> {
    let value = match value {
        Value::Projected(projected) => match projected.slice_bound().await {
            Some(value) => value,
            None => projected.materialize_async().await,
        },
        other => other.clone(),
    };
    as_slice_bound(&value)
}

pub(crate) fn compare_numbers(
    left: Value,
    right: Value,
    cmp: impl FnOnce(f64, f64) -> bool,
) -> Result<Value, RuntimeError> {
    match (left, right) {
        (Value::Number(left), Value::Number(right)) => Ok(Value::Bool(cmp(left, right))),
        (left, right) => Ok(Value::Bool(cmp(as_number(&left)?, as_number(&right)?))),
    }
}

pub(crate) fn compare_ordered(
    left: Value,
    right: Value,
    number_cmp: impl FnOnce(f64, f64) -> bool,
    string_cmp: impl FnOnce(&str, &str) -> bool,
) -> Result<Value, RuntimeError> {
    match (&left, &right) {
        (Value::String(left), Value::String(right)) => Ok(Value::Bool(string_cmp(left, right))),
        _ => compare_numbers(left, right, number_cmp),
    }
}

pub(crate) fn add_values(left: Value, right: Value) -> Result<Value, RuntimeError> {
    match (left, right) {
        (Value::Number(a), Value::Number(b)) => Ok(Value::Number(a + b)),
        (Value::String(a), Value::String(b)) => Ok(Value::String(a + &b)),
        (Value::String(mut a), other) => {
            a.push_str(&stringify_value_blocking(&other)?);
            Ok(Value::String(a))
        }
        (other, Value::String(b)) => {
            let mut text = stringify_value_blocking(&other)?;
            text.push_str(&b);
            Ok(Value::String(text.into()))
        }
        (Value::List(a), Value::List(b)) => {
            let mut values = Vec::with_capacity(a.len() + b.len());
            values.extend(a.iter().cloned());
            values.extend(b.iter().cloned());
            Ok(Value::List(values.into()))
        }
        (left, right) => Ok(Value::Number(as_number(&left)? + as_number(&right)?)),
    }
}

pub(crate) fn is_truthy(value: &Value) -> bool {
    match value {
        Value::Null => false,
        Value::Bool(value) => *value,
        Value::Number(value) => *value != 0.0 && !value.is_nan(),
        Value::String(value) => !value.is_empty(),
        Value::Image(_) | Value::Resource(_) | Value::List(_) | Value::Record(_) => true,
        Value::Projected(value) => futures_executor::block_on(value.truthy()),
    }
}

pub(crate) async fn is_truthy_async(value: &Value) -> bool {
    match value {
        Value::Projected(value) => value.truthy().await,
        other => is_truthy(other),
    }
}

pub(crate) fn success(value: Value) -> Value {
    let result_names = result_wrapper_names();
    let mut record = record_with_capacity(2);
    record.insert_symbolized(
        result_names.ok.symbol,
        result_names.ok.text.clone(),
        Value::Bool(true),
    );
    record.insert_symbolized(
        result_names.value.symbol,
        result_names.value.text.clone(),
        value,
    );
    Value::Record(Arc::new(record))
}

pub(crate) fn error_value(message: String) -> Value {
    let result_names = result_wrapper_names();
    let mut record = record_with_capacity(2);
    record.insert_symbolized(
        result_names.ok.symbol,
        result_names.ok.text.clone(),
        Value::Bool(false),
    );
    record.insert_symbolized(
        result_names.error.symbol,
        result_names.error.text.clone(),
        Value::String(message.into()),
    );
    Value::Record(Arc::new(record))
}

pub(crate) fn value_type_name(value: &Value) -> &str {
    match value {
        Value::Null => "null",
        Value::Bool(_) => "bool",
        Value::Number(_) => "number",
        Value::String(_) => "string",
        Value::Image(_) => "image",
        Value::Resource(_) => "resource",
        Value::List(_) => "list",
        Value::Record(_) => "record",
        Value::Projected(value) => value.value_type_name(),
    }
}

pub(crate) fn value_contains_projected(value: &Value) -> bool {
    match value {
        Value::Projected(_) => true,
        Value::List(values) => values.iter().any(value_contains_projected),
        Value::Record(record) => record.values().any(value_contains_projected),
        Value::Null
        | Value::Bool(_)
        | Value::Number(_)
        | Value::String(_)
        | Value::Image(_)
        | Value::Resource(_) => false,
    }
}

pub(crate) fn materialize_value(value: Value) -> Value {
    match value {
        Value::Projected(projected) => projected.materialize(),
        other => other,
    }
}
