// NOTE: the runtime function take a &[ConcreteExpr] instead of BoundArguments
// because they notoriously might want not to bind arguments in the first
// place (e.g. "if"/"unless").
use std::sync::Arc;

use crate::moonblade::error::{ConcretizationError, EvaluationError, SpecifiedEvaluationError};
use crate::moonblade::interpreter::{ConcreteExpr, EvaluationContext};
use crate::moonblade::parser::FunctionCall;
use crate::moonblade::types::{
    Arity, ColumIndexationBy, DynamicValue, EvaluationResult, FunctionArguments, HeadersIndex,
    LambdaArguments,
};

pub type ComptimeFunctionResult = Result<Option<ConcreteExpr>, ConcretizationError>;
pub type ComptimeFunction = fn(&FunctionCall, &HeadersIndex) -> ComptimeFunctionResult;
pub type RuntimeFunction = fn(&EvaluationContext, &[ConcreteExpr]) -> EvaluationResult;

#[derive(Debug, Clone, Copy)]
enum AbstractColReturnValue {
    Cell,
    Index,
    Header,
}

impl AbstractColReturnValue {
    fn possible_concretization(
        &self,
        headers_index: &HeadersIndex,
    ) -> Result<(), ConcretizationError> {
        if headers_index.is_headless() && matches!(self, AbstractColReturnValue::Header) {
            Err(ConcretizationError::Custom(
                "cannot read header from file without one".to_string(),
            ))
        } else {
            Ok(())
        }
    }

    fn possible_evaluation(
        &self,
        headers_index: &HeadersIndex,
    ) -> Result<(), SpecifiedEvaluationError> {
        if headers_index.is_headless() && matches!(self, AbstractColReturnValue::Header) {
            Err(
                EvaluationError::Custom("cannot read header from file without one".to_string())
                    .anonymous(),
            )
        } else {
            Ok(())
        }
    }
}

static COL_INDEX_ERROR_MSG: &str = "this expression does not know current column's index. This is usually only the case with the -C/--along-rows & -M/--along-matrix flags or commands evaluation a same expression over multiple columns like xan transform! Or did you forget to give this function an argument?";

pub fn get_special_function(
    name: &str,
) -> Option<(
    Option<ComptimeFunction>,
    Option<RuntimeFunction>,
    FunctionArguments,
)> {
    macro_rules! higher_order_fn {
        ($name:expr, $variant:ident) => {
            (
                None,
                Some(|context: &EvaluationContext, args: &[ConcreteExpr]| {
                    runtime_higher_order(context, args, $name, HigherOrderOperation::$variant)
                }),
                FunctionArguments::binary(),
            )
        };
    }

    Some(match name {
        // NOTE: col, cols and headers need a comptime version because static evaluation
        // is not an option for them. What's more they rely on the headers index which
        // is not available to normal functions.
        "col" => (
            Some(|call: &FunctionCall, headers_index: &HeadersIndex| {
                if call.args.is_empty() {
                    return Ok(None);
                }

                abstract_comptime_col(false, AbstractColReturnValue::Cell, call, headers_index)
            }),
            Some(|context: &EvaluationContext, args: &[ConcreteExpr]| {
                if args.is_empty() {
                    return match context.col_index() {
                        Some(i) => Ok(context.record[i].into()),
                        None => Err(EvaluationError::from(COL_INDEX_ERROR_MSG).specify("col")),
                    };
                }

                abstract_runtime_col("col", false, AbstractColReturnValue::Cell, context, args)
            }),
            FunctionArguments::with_range(0..=2),
        ),
        "header" => (
            Some(|call: &FunctionCall, headers_index: &HeadersIndex| {
                if call.args.is_empty() {
                    return Ok(None);
                }

                abstract_comptime_col(false, AbstractColReturnValue::Header, call, headers_index)
            }),
            Some(|context: &EvaluationContext, args: &[ConcreteExpr]| {
                if args.is_empty() {
                    return match context.col_index() {
                        Some(i) => Ok(context.headers_index[i].into()),
                        None => Err(EvaluationError::from(COL_INDEX_ERROR_MSG).specify("header")),
                    };
                }

                abstract_runtime_col(
                    "header",
                    false,
                    AbstractColReturnValue::Header,
                    context,
                    args,
                )
            }),
            FunctionArguments::with_range(0..=2),
        ),
        "col_index" => (
            Some(|call: &FunctionCall, headers_index: &HeadersIndex| {
                if call.args.is_empty() {
                    return Ok(None);
                }

                abstract_comptime_col(false, AbstractColReturnValue::Index, call, headers_index)
            }),
            Some(|context: &EvaluationContext, args: &[ConcreteExpr]| {
                if args.is_empty() {
                    return match context.col_index() {
                        Some(i) => Ok(i.into()),
                        None => {
                            Err(EvaluationError::from(COL_INDEX_ERROR_MSG).specify("col_index"))
                        }
                    };
                }

                abstract_runtime_col(
                    "col_index",
                    false,
                    AbstractColReturnValue::Index,
                    context,
                    args,
                )
            }),
            FunctionArguments::with_range(0..=2),
        ),
        "col?" => (
            Some(|call: &FunctionCall, headers_index: &HeadersIndex| {
                abstract_comptime_col(true, AbstractColReturnValue::Cell, call, headers_index)
            }),
            Some(|context: &EvaluationContext, args: &[ConcreteExpr]| {
                abstract_runtime_col("col?", true, AbstractColReturnValue::Cell, context, args)
            }),
            FunctionArguments::with_range(1..=2),
        ),
        "header?" => (
            Some(|call: &FunctionCall, headers_index: &HeadersIndex| {
                abstract_comptime_col(true, AbstractColReturnValue::Header, call, headers_index)
            }),
            Some(|context: &EvaluationContext, args: &[ConcreteExpr]| {
                abstract_runtime_col(
                    "header?",
                    true,
                    AbstractColReturnValue::Header,
                    context,
                    args,
                )
            }),
            FunctionArguments::with_range(1..=2),
        ),
        "col_index?" => (
            Some(|call: &FunctionCall, headers_index: &HeadersIndex| {
                abstract_comptime_col(true, AbstractColReturnValue::Index, call, headers_index)
            }),
            Some(|context: &EvaluationContext, args: &[ConcreteExpr]| {
                abstract_runtime_col(
                    "col_index?",
                    true,
                    AbstractColReturnValue::Index,
                    context,
                    args,
                )
            }),
            FunctionArguments::with_range(1..=2),
        ),
        "cols" => (
            Some(|call: &FunctionCall, headers_index: &HeadersIndex| {
                abstract_comptime_cols(
                    AbstractColReturnValue::Cell,
                    call,
                    headers_index,
                    ConcreteExpr::Column,
                )
            }),
            Some(|context: &EvaluationContext, args: &[ConcreteExpr]| {
                abstract_runtime_cols("cols", AbstractColReturnValue::Cell, context, args, |i| {
                    DynamicValue::from(&context.record[i])
                })
            }),
            FunctionArguments::with_range(0..=2),
        ),
        "headers" => (
            Some(|call: &FunctionCall, headers_index: &HeadersIndex| {
                abstract_comptime_cols(AbstractColReturnValue::Header, call, headers_index, |i| {
                    ConcreteExpr::Value(DynamicValue::from(&headers_index[i]))
                })
            }),
            Some(|context: &EvaluationContext, args: &[ConcreteExpr]| {
                abstract_runtime_cols(
                    "headers",
                    AbstractColReturnValue::Header,
                    context,
                    args,
                    |i| DynamicValue::from(&context.headers_index[i]),
                )
            }),
            FunctionArguments::with_range(0..=2),
        ),
        "prev_col" => (
            None,
            Some(|context: &EvaluationContext, args: &[ConcreteExpr]| {
                let offset = if args.is_empty() {
                    1
                } else {
                    args.first()
                        .unwrap()
                        .evaluate(context)?
                        .try_as_usize()
                        .map_err(|err| err.specify("prev_col"))?
                };

                match context.col_index() {
                    Some(i) => {
                        if offset > i {
                            return Err(EvaluationError::RelativeColumnNotFound(
                                i,
                                -(offset as isize),
                            )
                            .specify("prev_col"));
                        }

                        let j = i - offset;

                        match context.record.get(j) {
                            Some(cell) => Ok(cell.into()),
                            None => Err(EvaluationError::RelativeColumnNotFound(
                                i,
                                -(offset as isize),
                            )
                            .specify("prev_col")),
                        }
                    }
                    None => Err(EvaluationError::from(COL_INDEX_ERROR_MSG).specify("prev_col")),
                }
            }),
            FunctionArguments::with_range(0..=1),
        ),
        "next_col" => (
            None,
            Some(|context: &EvaluationContext, args: &[ConcreteExpr]| {
                let offset = if args.is_empty() {
                    1
                } else {
                    args.first()
                        .unwrap()
                        .evaluate(context)?
                        .try_as_usize()
                        .map_err(|err| err.specify("next_col"))?
                };

                match context.col_index() {
                    Some(i) => match context.record.get(i + offset) {
                        Some(cell) => Ok(cell.into()),
                        None => Err(EvaluationError::RelativeColumnNotFound(i, offset as isize)
                            .specify("next_col")),
                    },
                    None => Err(EvaluationError::from(COL_INDEX_ERROR_MSG).specify("next_col")),
                }
            }),
            FunctionArguments::with_range(0..=1),
        ),
        // NOTE: index needs to be a special function because it relies on external
        // data that cannot be accessed by normal functions.
        "row_index" => (None, Some(runtime_row_index), FunctionArguments::nullary()),

        // NOTE: if and unless need to be special functions because they short-circuit
        // underlying evaluation and circumvent the typical DFS evaluation scheme.
        // NOTE: if and unless don't require a comptime version because static evaluation
        // will work just fine here.
        "if" => (None, Some(runtime_if), FunctionArguments::with_range(2..=3)),
        "unless" => (
            None,
            Some(runtime_unless),
            FunctionArguments::with_range(2..=3),
        ),
        "and" => (None, Some(runtime_and), FunctionArguments::variadic(2)),
        "or" => (None, Some(runtime_or), FunctionArguments::variadic(2)),

        // NOTE: try is special because you need to suppress the error if any
        "try" => (None, Some(runtime_try), FunctionArguments::unary()),

        // NOTE: warn must know row index
        "warn" => (None, Some(runtime_warn), FunctionArguments::unary()),

        // NOTE: lambda evaluation need to be a special function because, like
        // if and unless, they cannot work in DFS fashion unless you
        // bind some values ahead of time.
        // NOTE: higher-order functions work fine with static evaluation
        "map" => higher_order_fn!("map", Map),
        "flat_map" => higher_order_fn!("flat_map", FlatMap),
        "filter" => higher_order_fn!("filter", Filter),
        "find" => higher_order_fn!("find", Find),
        "find_index" => higher_order_fn!("find_index", FindIndex),
        "all" => higher_order_fn!("all", All),
        "any" => higher_order_fn!("any", Any),

        _ => return None,
    })
}

fn abstract_comptime_col(
    unsure: bool,
    return_value: AbstractColReturnValue,
    call: &FunctionCall,
    headers_index: &HeadersIndex,
) -> ComptimeFunctionResult {
    return_value.possible_concretization(headers_index)?;

    if let Some(column_indexation) = ColumIndexationBy::from_arguments(&call.raw_args_as_ref()) {
        if column_indexation.has_name() && headers_index.is_headless() {
            return Err(ConcretizationError::ColumnNotFound(column_indexation, true));
        }

        // NOTE: if file is empty, we don't raise on concretization to permit
        // commands working with headless files to remain no-ops
        if headers_index.is_headless() && headers_index.is_empty() {
            return Ok(Some(ConcreteExpr::Value(DynamicValue::None)));
        }

        match headers_index.get(&column_indexation) {
            Some(index) => {
                return Ok(Some(match return_value {
                    AbstractColReturnValue::Cell => ConcreteExpr::Column(index),
                    AbstractColReturnValue::Index => ConcreteExpr::Value(DynamicValue::from(index)),
                    AbstractColReturnValue::Header => {
                        ConcreteExpr::Value(DynamicValue::from(&headers_index[index]))
                    }
                }));
            }
            None => {
                return if unsure {
                    Ok(Some(ConcreteExpr::Value(DynamicValue::None)))
                } else {
                    Err(ConcretizationError::ColumnNotFound(
                        column_indexation,
                        headers_index.is_headless(),
                    ))
                };
            }
        };
    }

    Ok(None)
}

fn abstract_comptime_cols<F>(
    return_value: AbstractColReturnValue,
    call: &FunctionCall,
    headers_index: &HeadersIndex,
    map: F,
) -> ComptimeFunctionResult
where
    F: Fn(usize) -> ConcreteExpr,
{
    return_value.possible_concretization(headers_index)?;

    if call.args.is_empty() {
        return Ok(Some(ConcreteExpr::List(
            (0..headers_index.len()).map(map).collect(),
        )));
    }

    match ColumIndexationBy::from_argument(&call.args[0].1) {
        None => Ok(None),
        Some(first_column_indexation) => {
            if first_column_indexation.has_name() && headers_index.is_headless() {
                return Err(ConcretizationError::ColumnNotFound(
                    first_column_indexation,
                    true,
                ));
            }

            // NOTE: if file is empty, we don't raise on concretization to permit
            // commands working with headless files to remain no-ops
            // NOTE: here we don't wait to make sure second indexation is valid
            // wrt headless but I guess we really don't care...
            if headers_index.is_headless() && headers_index.is_empty() {
                return Ok(Some(ConcreteExpr::Value(DynamicValue::from(vec![]))));
            }

            match headers_index.get(&first_column_indexation) {
                Some(first_index) => {
                    if call.args.len() < 2 {
                        Ok(Some(ConcreteExpr::List(
                            (first_index..headers_index.len()).map(map).collect(),
                        )))
                    } else {
                        match ColumIndexationBy::from_argument(&call.args[1].1) {
                            None => Ok(None),
                            Some(second_column_indexation) => {
                                if second_column_indexation.has_name()
                                    && headers_index.is_headless()
                                {
                                    return Err(ConcretizationError::ColumnNotFound(
                                        second_column_indexation,
                                        true,
                                    ));
                                }

                                match headers_index.get(&second_column_indexation) {
                                    Some(second_index) => {
                                        let range: Vec<_> = if first_index > second_index {
                                            (second_index..=first_index).map(map).rev().collect()
                                        } else {
                                            (first_index..=second_index).map(map).collect()
                                        };

                                        Ok(Some(ConcreteExpr::List(range)))
                                    }
                                    None => Err(ConcretizationError::ColumnNotFound(
                                        second_column_indexation,
                                        headers_index.is_headless(),
                                    )),
                                }
                            }
                        }
                    }
                }
                None => Err(ConcretizationError::ColumnNotFound(
                    first_column_indexation,
                    headers_index.is_headless(),
                )),
            }
        }
    }
}

fn runtime_if(context: &EvaluationContext, args: &[ConcreteExpr]) -> EvaluationResult {
    let arity = args.len();

    let condition = &args[0];
    let result = condition.evaluate(context)?;

    let mut branch: Option<&ConcreteExpr> = None;

    if result.is_truthy() {
        branch = Some(&args[1]);
    } else if arity == 3 {
        branch = Some(&args[2]);
    }

    match branch {
        None => Ok(DynamicValue::None),
        Some(arg) => arg.evaluate(context),
    }
}

fn runtime_unless(context: &EvaluationContext, args: &[ConcreteExpr]) -> EvaluationResult {
    let arity = args.len();

    let condition = &args[0];
    let result = condition.evaluate(context)?;

    let mut branch: Option<&ConcreteExpr> = None;

    if result.is_falsey() {
        branch = Some(&args[1]);
    } else if arity == 3 {
        branch = Some(&args[2]);
    }

    match branch {
        None => Ok(DynamicValue::None),
        Some(arg) => arg.evaluate(context),
    }
}

fn runtime_or(context: &EvaluationContext, args: &[ConcreteExpr]) -> EvaluationResult {
    debug_assert!(args.len() >= 2);

    let mut last: Option<DynamicValue> = None;

    for arg in args {
        let value = arg.evaluate(context)?;

        if value.is_truthy() {
            return Ok(value);
        }

        last.replace(value);
    }

    Ok(last.unwrap())
}

fn runtime_and(context: &EvaluationContext, args: &[ConcreteExpr]) -> EvaluationResult {
    debug_assert!(args.len() >= 2);

    let mut last: Option<DynamicValue> = None;

    for arg in args {
        let value = arg.evaluate(context)?;

        if value.is_falsey() {
            return Ok(value);
        }

        last.replace(value);
    }

    Ok(last.unwrap())
}

fn runtime_row_index(context: &EvaluationContext, _args: &[ConcreteExpr]) -> EvaluationResult {
    match context.row_index() {
        Some(i) => Ok(i.into()),
        None => Err(
            EvaluationError::from("this expression does not know a row's index")
                .specify("row_index"),
        ),
    }
}

fn abstract_runtime_col(
    fn_name: &'static str,
    unsure: bool,
    return_value: AbstractColReturnValue,
    context: &EvaluationContext,
    args: &[ConcreteExpr],
) -> EvaluationResult {
    let headers_index = &context.headers_index;

    return_value.possible_evaluation(headers_index)?;

    let name_or_pos = args.first().unwrap().evaluate(context)?;

    let pos = match args.get(1) {
        Some(p) => Some(p.evaluate(context)?),
        None => None,
    };

    match ColumIndexationBy::from_bound_arguments(name_or_pos, pos) {
        None => Err(EvaluationError::Custom("invalid arguments".to_string()).specify(fn_name)),
        Some(indexation) => {
            if indexation.has_name() && headers_index.is_headless() {
                return Err(EvaluationError::ColumnNotFound(indexation, true).specify("col"));
            }

            match headers_index.get(&indexation) {
                None => {
                    if unsure {
                        Ok(DynamicValue::None)
                    } else {
                        Err(EvaluationError::ColumnNotFound(
                            indexation,
                            headers_index.is_headless(),
                        )
                        .specify(fn_name))
                    }
                }
                Some(index) => Ok(match return_value {
                    AbstractColReturnValue::Index => DynamicValue::from(index),
                    AbstractColReturnValue::Cell => DynamicValue::from(&context.record[index]),
                    AbstractColReturnValue::Header => DynamicValue::from(&headers_index[index]),
                }),
            }
        }
    }
}

fn abstract_runtime_cols<F>(
    fn_name: &'static str,
    return_value: AbstractColReturnValue,
    context: &EvaluationContext,
    args: &[ConcreteExpr],
    map: F,
) -> EvaluationResult
where
    F: Fn(usize) -> DynamicValue,
{
    let headers_index = &context.headers_index;

    return_value.possible_evaluation(headers_index)?;

    // NOTE: 0 is not reachable because it can be resolved at comptime by definition
    match args.len() {
        1 => {
            let start_index_arg = args.first().unwrap().evaluate(context)?;

            match ColumIndexationBy::from_bound_arguments(start_index_arg, None) {
                None => {
                    Err(EvaluationError::Custom("invalid arguments".to_string()).specify(fn_name))
                }
                Some(indexation) => {
                    if indexation.has_name() && headers_index.is_headless() {
                        return Err(
                            EvaluationError::ColumnNotFound(indexation, true).specify(fn_name)
                        );
                    }

                    match headers_index.get(&indexation) {
                        None => Err(EvaluationError::ColumnNotFound(
                            indexation,
                            headers_index.is_headless(),
                        )
                        .specify(fn_name)),
                        Some(index) => Ok(DynamicValue::from(
                            (index..headers_index.len()).map(map).collect::<Vec<_>>(),
                        )),
                    }
                }
            }
        }
        2 => {
            let start_index_arg = args.first().unwrap().evaluate(context)?;

            match ColumIndexationBy::from_bound_arguments(start_index_arg, None) {
                None => {
                    Err(EvaluationError::Custom("invalid arguments".to_string()).specify(fn_name))
                }
                Some(start_indexation) => {
                    if start_indexation.has_name() && headers_index.is_headless() {
                        return Err(EvaluationError::ColumnNotFound(start_indexation, true)
                            .specify(fn_name));
                    }

                    match headers_index.get(&start_indexation) {
                        None => Err(EvaluationError::ColumnNotFound(
                            start_indexation,
                            headers_index.is_headless(),
                        )
                        .specify(fn_name)),
                        Some(start_index) => {
                            let end_index_arg = args.last().unwrap().evaluate(context)?;

                            match ColumIndexationBy::from_bound_arguments(end_index_arg, None) {
                                None => {
                                    Err(EvaluationError::Custom("invalid arguments".to_string())
                                        .specify(fn_name))
                                }
                                Some(end_indexation) => {
                                    if end_indexation.has_name() && headers_index.is_headless() {
                                        return Err(EvaluationError::ColumnNotFound(
                                            end_indexation,
                                            true,
                                        )
                                        .specify(fn_name));
                                    }

                                    match headers_index.get(&end_indexation) {
                                        None => Err(EvaluationError::ColumnNotFound(
                                            end_indexation,
                                            headers_index.is_headless(),
                                        )
                                        .specify(fn_name)),
                                        Some(end_index) => {
                                            let range: Vec<_> = if start_index > end_index {
                                                (end_index..=start_index).map(map).rev().collect()
                                            } else {
                                                (start_index..=end_index).map(map).collect()
                                            };

                                            Ok(DynamicValue::from(range))
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        _ => unreachable!(),
    }
}

fn runtime_try(context: &EvaluationContext, args: &[ConcreteExpr]) -> EvaluationResult {
    let result = args.first().unwrap().evaluate(context);

    Ok(result.unwrap_or(DynamicValue::None))
}

fn runtime_warn(context: &EvaluationContext, args: &[ConcreteExpr]) -> EvaluationResult {
    let msg_arg = args.first().unwrap().evaluate(context)?;
    let msg = msg_arg.try_as_str().map_err(|err| err.specify("warn"))?;

    match context.row_index() {
        Some(i) => eprintln!("Row index {}: {}", i, msg),
        None => eprintln!("{}", msg),
    };

    Ok(DynamicValue::None)
}

#[derive(Clone, Copy)]
enum HigherOrderOperation {
    Filter,
    Map,
    FlatMap,
    Find,
    FindIndex,
    All,
    Any,
}

fn runtime_higher_order(
    context: &EvaluationContext,
    args: &[ConcreteExpr],
    name: &str,
    op: HigherOrderOperation,
) -> EvaluationResult {
    let list = args
        .first()
        .unwrap()
        .evaluate(context)?
        .try_into_arc_list()
        .map_err(|err| err.specify(name))?;

    let (names, lambda) = args
        .get(1)
        .unwrap()
        .try_as_lambda()
        .map_err(|err| err.specify(name))?;

    // Validating arity
    Arity::Strict(1)
        .validate(names.len())
        .map_err(|invalid_arity| {
            EvaluationError::InvalidArity(invalid_arity).specify("<lambda>")
        })?;

    let arg_name = names.first().unwrap();

    let mut variables = match context.lambda_variables {
        None => LambdaArguments::new(),
        Some(v) => v.clone(),
    };

    let item_arg_index = variables.register(arg_name);

    match op {
        HigherOrderOperation::Map => {
            let mut new_list = Vec::with_capacity(list.len());

            match Arc::try_unwrap(list) {
                Ok(owned_list) => {
                    for item in owned_list {
                        variables.set(item_arg_index, item);

                        let result = lambda.evaluate(&context.with_lambda_variables(&variables))?;
                        new_list.push(result);
                    }
                }
                Err(borrowed_list) => {
                    for item in borrowed_list.iter() {
                        variables.set(item_arg_index, item.clone());

                        let result = lambda.evaluate(&context.with_lambda_variables(&variables))?;
                        new_list.push(result);
                    }
                }
            }

            Ok(DynamicValue::from(new_list))
        }
        HigherOrderOperation::FlatMap => {
            let mut new_list = Vec::with_capacity(list.len());

            match Arc::try_unwrap(list) {
                Ok(owned_list) => {
                    for item in owned_list {
                        variables.set(item_arg_index, item);

                        let result = lambda.evaluate(&context.with_lambda_variables(&variables))?;

                        for item in result.into_flat_iter() {
                            new_list.push(item);
                        }
                    }
                }
                Err(borrowed_list) => {
                    for item in borrowed_list.iter() {
                        variables.set(item_arg_index, item.clone());

                        let result = lambda.evaluate(&context.with_lambda_variables(&variables))?;

                        for item in result.into_flat_iter() {
                            new_list.push(item);
                        }
                    }
                }
            }

            Ok(DynamicValue::from(new_list))
        }
        HigherOrderOperation::Filter => {
            let mut new_list = Vec::new();

            for item in list.iter() {
                variables.set(item_arg_index, item.clone());

                let result = lambda.evaluate(&context.with_lambda_variables(&variables))?;

                if result.is_truthy() {
                    new_list.push(item.clone());
                }
            }

            Ok(DynamicValue::from(new_list))
        }
        HigherOrderOperation::Find => {
            for item in list.iter() {
                variables.set(item_arg_index, item.clone());

                let result = lambda.evaluate(&context.with_lambda_variables(&variables))?;

                if result.is_truthy() {
                    return Ok(item.clone());
                }
            }

            Ok(DynamicValue::None)
        }
        HigherOrderOperation::FindIndex => {
            for (i, item) in list.iter().enumerate() {
                variables.set(item_arg_index, item.clone());

                let result = lambda.evaluate(&context.with_lambda_variables(&variables))?;

                if result.is_truthy() {
                    return Ok(DynamicValue::from(i));
                }
            }

            Ok(DynamicValue::None)
        }
        HigherOrderOperation::All => {
            for item in list.iter() {
                variables.set(item_arg_index, item.clone());

                let result = lambda.evaluate(&context.with_lambda_variables(&variables))?;

                if result.is_falsey() {
                    return Ok(DynamicValue::Boolean(false));
                }
            }

            Ok(DynamicValue::Boolean(true))
        }
        HigherOrderOperation::Any => {
            for item in list.iter() {
                variables.set(item_arg_index, item.clone());

                let result = lambda.evaluate(&context.with_lambda_variables(&variables))?;

                if result.is_truthy() {
                    return Ok(DynamicValue::Boolean(true));
                }
            }

            Ok(DynamicValue::Boolean(false))
        }
    }
}
