use std::{
    path::{Path, PathBuf},
    rc::Rc,
};

use crate::{
    error::MachineError,
    vm::{
        Value,
        env::Environment,
        value::{Keyword, StringValue, convert::TryFromValue},
    },
};

pub fn load(env: &Rc<Environment>) {
    env.defun_native(
        "import",
        "Evaluates the given script file",
        |vm, env, args| {
            if args.is_empty() {
                return Err(MachineError::InvalidArgumentCount);
            }

            let caller = vm
                .read_call_stack(0)
                .ok_or(MachineError::CallStackUnderflow)?;
            let caller_directory = caller
                .closure
                .function
                .script
                .as_ref()
                .and_then(|path| path.parent())
                .map(Path::to_owned)
                .unwrap_or_else(|| PathBuf::from("."));

            for arg in args {
                let path_str = StringValue::try_from_value(arg)?;
                let mut path = PathBuf::from(&*path_str);

                if path.is_relative() {
                    let in_caller_directory = caller_directory.join(&path);
                    if in_caller_directory.exists() {
                        path = in_caller_directory;
                    }
                }

                vm.load_file(Default::default(), env, &path)
                    .map_err(|error| MachineError::LoadError(path_str, error.into()))?;
            }

            Ok(Value::Nil)
        },
    );

    env.defun_native("gensym", "Provides an unique symbol name", |_, env, _| {
        Ok(env.gensym().into())
    });
    env.defun_native(
        "explain",
        "Provides an explanation for a given value",
        |_, _, args| {
            let [argument] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let explanation = match argument {
                Value::Nil => "an empty list".into(),
                Value::Cons(_) => "a cons-cell".into(),
                Value::Number(_) => "a number".into(),
                Value::Boolean(_) => "a boolean value".into(),
                Value::Quasi(_) => "a quasi-quoted value".into(),
                Value::Quote(_) => "a quoted value".into(),
                Value::Unquote(_) => "an unquoted value".into(),
                Value::UnquoteSplice(_) => "an unquote-spliced value".into(),
                Value::Identifier(identifier) => format!("an identifier {:?}", identifier.as_ref()),
                Value::Vector(_) => "a vector".into(),
                Value::String(_) => "a string".into(),
                Value::Keyword(_) => "a keyword".into(),
                Value::Closure(closure) => {
                    format!(
                        "function {}: {}",
                        closure.function.name(),
                        closure.function.docstring()
                    )
                }
                Value::Function(function) => {
                    format!("function {}: {}", function.name(), function.docstring())
                }
                Value::NativeFunction(function) => {
                    format!(
                        "built-in function {}: {}",
                        function.name(),
                        function.docstring()
                    )
                }
                Value::NativeValue(_) => "a native value".into(),
            };
            Ok(Value::String(explanation.into()))
        },
    );
    env.defun_native(
        "type",
        "Returns the data type of the argument",
        |_, _, args| {
            let [argument] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            Ok(argument.type_id())
        },
    );
    env.defmacro_native(
        "assert",
        "Checks that the condition is true, otherwise aborts the execution",
        |vm, _, args| match args {
            [] => Err(MachineError::InvalidArgumentCount),
            [cond] => {
                let assertion_failure_msg = match vm.current_location() {
                    Some(ip) => format!("{ip}: assertion failed: {cond}"),
                    None => format!("<undefined>: assertion failed: {cond}"),
                };
                let assertion_failure = Value::list_or_nil([
                    Value::Identifier("abort".into()),
                    Value::String(assertion_failure_msg.into()),
                ]);
                Ok(Value::list_or_nil([
                    Value::Keyword(Keyword::If),
                    cond.clone(),
                    Value::Nil,
                    assertion_failure,
                ]))
            }
            _ => Err(MachineError::InvalidArgumentCount),
        },
    );
    env.defun_native(
        "abort",
        "Aborts the execution with the given reason message",
        |_, _, args| {
            let mut message = String::new();
            for (i, arg) in args.iter().enumerate() {
                if i != 0 {
                    message.push(' ');
                }
                message.push_str(&format!("{arg}"));
            }
            Err(MachineError::Abort(message.into()))
        },
    );
}
