use std::rc::Rc;

use crate::{environment::Environment, node::function::FunctionType};

use super::{Node, NodeEnum, Precedence, symbol::Symbol};

#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub struct Call {
    function: Rc<NodeEnum>,
    arguments: Vec<Rc<NodeEnum>>,
}

impl Node for Call {
    fn evaluate(&self, env: &mut Environment) -> Result<Rc<NodeEnum>, String> {
        // Evaluate callee and error if its not a function
        let evaluated = self.function.evaluate(env)?;
        let func = if let NodeEnum::Function(func) = evaluated.as_ref() {
            func
        } else {
            return Err(format!(
                "Cannot call {} as a function",
                evaluated.as_string(Some(env))
            ));
        };

        // Check if argument counts match
        let fargs = func.get_arguments();
        if fargs.len() != self.arguments.len() {
            return Err(format!(
                "Error calling function. Expected {} arguments, but got {}",
                func.get_arguments().len(),
                self.arguments.len()
            ));
        }

        // Call function body with arguments
        match func.get_body() {
            // Pass arguments to native function
            FunctionType::Native(_name, native_function) => native_function(&self.arguments),
            FunctionType::UserFunction(node_enum) => {
                // TODO: Push scope
                // Define variables
                fargs
                    .iter()
                    .zip(&self.arguments)
                    .for_each(|(symbol, value)| {
                        env.insert(symbol.get_value(), value.clone());
                    });
                let ev = node_enum.evaluate(env);
                // TODO: Pop scope
                // Return evaluated return value for function
                ev
            }
        }
    }

    fn as_string(&self, env: Option<&Environment>) -> String {
        let arguments = self
            .arguments
            .iter()
            .map(|x| x.as_string(env))
            .reduce(|a, b| a + ", " + &b)
            .unwrap();
        format!("{}({})", self.function.as_string(env), arguments)
    }

    fn precedence(&self) -> Precedence {
        Precedence::Call
    }
}
