use super::value::Value;

/// Error type for built-in method dispatch, decoupled from interpreter errors.
pub enum MethodError {
    ArityMismatch {
        type_name: String,
        method: String,
        expected: usize,
        got: usize,
    },
    UndefinedMethod {
        type_name: String,
        method: String,
    },
}

/// Check that the number of arguments matches the expected arity.
fn check_arity(
    type_name: &str,
    method: &str,
    expected: usize,
    args: &[Value],
) -> Result<(), MethodError> {
    if args.len() != expected {
        return Err(MethodError::ArityMismatch {
            type_name: type_name.to_owned(),
            method: method.to_owned(),
            expected,
            got: args.len(),
        });
    }

    Ok(())
}

/// Dispatch a built-in method call on a receiver value.
#[allow(clippy::cast_precision_loss)]
pub fn dispatch(receiver: &Value, method: &str, args: &[Value]) -> Result<Value, MethodError> {
    match receiver {
        Value::Array(elements) => dispatch_array(elements, method, args),
        Value::String(s) => dispatch_string(s, method, args),
        _ => Err(MethodError::UndefinedMethod {
            type_name: receiver.type_name().to_owned(),
            method: method.to_owned(),
        }),
    }
}

/// Dispatch a method call on an Array value.
#[allow(clippy::cast_precision_loss)]
fn dispatch_array(elements: &[Value], method: &str, args: &[Value]) -> Result<Value, MethodError> {
    match method {
        "len" => {
            check_arity("Array", method, 0, args)?;
            Ok(Value::Number(elements.len() as f64))
        }
        _ => Err(MethodError::UndefinedMethod {
            type_name: "Array".to_owned(),
            method: method.to_owned(),
        }),
    }
}

/// Dispatch a method call on a String value.
#[allow(clippy::cast_precision_loss)]
fn dispatch_string(s: &str, method: &str, args: &[Value]) -> Result<Value, MethodError> {
    match method {
        "len" => {
            check_arity("String", method, 0, args)?;
            Ok(Value::Number(s.len() as f64))
        }
        _ => Err(MethodError::UndefinedMethod {
            type_name: "String".to_owned(),
            method: method.to_owned(),
        }),
    }
}
