use crate::FILE_EXTENSION;
use crate::prelude::*;
use std::fs::{self, DirEntry};
use std::path::Path;
use std::rc::Rc;

fn define_function(body: &Argument, fn_args: &[Argument]) -> Result<Atom> {
    let body = body.function_call("Error during definition: no valid function body was given!")?;
    let function_arg_names = fn_args
        .iter()
        .map(|fn_arg| fn_arg.variable("Error during definition: invalid args were given!"))
        .collect::<Result<Vec<String>>>()?;

    let function = Function {
        doc: String::new(),
        argc: Some(function_arg_names.len()),
        callback: Rc::new(move |state, args| {
            // a function call should have its own scope and not leak variables
            let old_storage = state.storage.clone();

            for (idx, arg) in function_arg_names.iter().enumerate() {
                let arg_result = args[idx].eval(state)?;
                state.storage.insert(arg.clone(), arg_result);
            }

            let function_result = body.eval(state);
            state.storage = old_storage;
            function_result
        }),
    };

    Ok(Atom::Function(function))
}

functions! {
    /// Evaluates all given arguments and returns the atom the last argument evaluated to.
    /// If no arguments are given, `null` is returned.
    ///
    /// Every program is implicitly wrapped in a call to this function.
    ///
    /// This function has an alias: `run`.
    "_"(_) => |state, args| {
        if args.is_empty() {
            Ok(Atom::Null)
        } else {
            for arg in &args[0..args.len() - 1] {
                arg.eval(state)?;
            }
            args[args.len() - 1].eval(state)
        }
    }
    /// Assigns the second argument to a variable named like the first argument.
    ///
    /// This function has an alias: `assign`.
    "="(2) => |state, args| {
        let var = args[0].variable("Error during assignment: no variable was given to assign to!")?;
        let value = args[1].eval(state)?;
        state.storage.insert(var, value);
        Ok(Atom::Null)
    }
    /// Evaluates the first argument as a boolean.
    /// If it evaluates to true, the second argument is evaluated and returned.
    /// If it evaluates to false, the second argument is ignored and `null` is returned.
    "if"(2) => |state, args| {
        Ok(if args[0].eval(state)?.bool()? {
            args[1].eval(state)?
        } else {
            Atom::Null
        })
    }
    /// Evaluates the first argument as a boolean.
    /// If it evaluates to true, the second argument is evaluated and returned.
    /// If it evaluates to false, the third argument is evaluated and returned instead.
    "ifelse"(3) => |state, args| {
        Ok(if args[0].eval(state)?.bool()? {
            args[1].eval(state)?
        } else {
            args[2].eval(state)?
        })
    }
    /// Repeatedly evaluates the first argument as a boolean.
    /// If it evaluates to true, the second argument is evaluated and the same steps begin again.
    /// If it evaluates to false, the loop ends and `null` is returned.
    "while"(2) => |state, args| {
        while args[0].eval(state)?.bool()? {
            args[1].eval(state)?;
        }
        Ok(Atom::Null)
    }
    /// Defines a new function.
    /// The first argument is the function identifier and the last argument is the function body.
    /// All arguments in between are the names of the function arguments that can be accessed in
    /// the function body.
    /// Values defined in the function are scoped and cannot be accessed outside of the function body.
    "def"(_) => |state, args| {
        let [var, fn_args @ .., body] = args else {
            return raise!(
                Error::Argument,
                "too few arguments passed to `def`: expected at least 2, found {}", args.len()
            );
        };
        let var = var.variable("Error during function definition: no valid variable was given to define to!")?;

        state.storage.insert(var, define_function(body, fn_args)?);
        Ok(Atom::Null)
    }
    /// Creates a new function and returns it.
    ///
    /// The last argument is the function body.
    /// All arguments before are the names of the function arguments that can be accessed in
    /// the function body.
    /// Values defined in the function are scoped and cannot be accessed outside of the function body.
    "fn"(_) => |_, args| {
        let Some((body, fn_args)) = args.split_last() else {
            return raise!(Error::Argument, "`fn` invocation is missing body");
        };
        define_function(body, fn_args)
    }
    /// Imports a file, either from the stl or the local directory.
    /// TODO document the exact algorithm and hierarchy more clearly, also the behavior of `=`
    "import"(1) => |state, args| {
        let name = args[0].variable("`import` argument must be a variable, string syntax was removed")?;
        if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
            return raise!(
                Error::Import,
                "invalid characters in import name `{name}`",
            );
        }

        // lookup order:
        // 1. look inside the programs current directory
        // 2. look in the global stl directory
        let mut source = None;

        'outer: for dir_path in [&state.file_directory, &state.stl_path] {
            for item in read_dir_files(dir_path) {
                if *item.file_name() == *format!("{name}.{FILE_EXTENSION}") {
                    if let Ok(file_content) = fs::read_to_string(item.path()) {
                        source = Some((file_content, dir_path));
                        break 'outer;
                    }
                }
            }
        }
        let Some((code, source_dir)) = source else {
            return raise!(
                Error::Import,
                "failed to find file for importing `{name}`",
            );
        };

        // TODO: consider using `.file()` here instead
        let (atom, imported_state) = Runner::new()
            .code(code)
            .current_dir(source_dir)
            .stl_dir(&state.stl_path)
            .run();

        let atom = atom?;

        for (k, v) in imported_state.storage {
            state.storage.insert(k, v);
        }
        Ok(atom)
    }
    /// Raises an exception of the kind `UserRaised` with the given string message.
    "error"(1) => |state, args| {
        Err(Exception::new(args[0].eval(state)?.string()?, Error::UserRaised))
    }
    /// Evaluates the given value and returns it.
    /// If an exception occurs while evaluating the argument, the exception is converted into a
    /// string and returned instead.
    "catch"(1) => |state, args| {
        Ok(args[0]
            .eval(state)
            .unwrap_or_else(|exc| Atom::String(exc.to_string())))
    }
    /// Evaluates both arguments and returns whether they are equal.
    /// TODO: define this behavior in edge cases and document it
    "=="(2) => |state, args| {
        Ok(Atom::Bool(args[0].eval(state)? == args[1].eval(state)?))
    }
    /// Evaluates the argument as a boolean and returns `null` if it is true.
    /// If it is false, raise an exception of the `Assertion` kind.
    "assert"(1) => |state, args| {
        if args[0].eval(state)?.bool()? {
            Ok(Atom::Null)
        } else {
            raise!(Error::Assertion, "Assertion failed!")
        }
    }
    /// Evaluates both arguments and compares then, returning `null` if they are equal.
    /// If not, raise an exception of the `Assertion` kind with a message containing both values.
    "assert_eq"(2) => |state, args| {
        let lhs = args[0].eval(state)?;
        let rhs = args[1].eval(state)?;
        if lhs == rhs {
            Ok(Atom::Null)
        } else {
            raise!(
                Error::Assertion,
                "Equality assertion failed! lhs: `{lhs}`, rhs: `{rhs}`!"
            )
        }
    }
    /// Defines a new type.
    /// The first argument must be given and is the ident of the type.
    /// All further arguments are its fields.
    "type"(_) => |state, args| {
        let Some((ident, fields)) = args.split_first() else {
            return raise!(Error::Argument, "`type` takes at least one argument");
        };
        let var = ident.variable("`type` must take a variable as first argument")?;
        let fields = fields
            .iter()
            .map(|field| field.variable("`type` field arguments should be variables"))
            .collect::<Result<Vec<_>>>()?;

        let function = Function {
            argc: Some(fields.len()),
            doc: String::new(),
            callback: Rc::new(move |state, args| {
                Ok(Atom::Object(
                    fields
                        .iter()
                        .zip(args)
                        .map(|(field, arg)| Ok((field.clone(), arg.eval(state)?)))
                        .collect::<Result<_>>()?,
                ))
            }),
        };

        state.storage.insert(var, Atom::Function(function));
        Ok(Atom::Null)
    }
    /// Get the value of a field of an object.
    ///
    /// The first argument is the object, the second is its name as a variable.
    /// TODO: consider allowing the second to also be an arg that evals to Atom::String?
    ///
    /// If the field does not exist on the object, an exception is raised.
    ///
    /// This function has an alias: `getattr`.
    "."(2) => |state, args| {
        let obj = args[0].eval(state)?.object()?;
        let field = args[1].variable("`.` takes a field identifier as second argument")?;
        obj.get(&field).cloned().ok_or_else(|| Exception::new(format!("object has no field named `{field}`"), Error::Name))
    }
    /// Set the value of a field of an object to a new value and returns the updated object.
    ///
    /// The first argument is the object, the second is its name as a variable and the third is the new value.
    ///
    /// If the field does not exist on the object, an exception is raised.
    /// TODO: think if it should be allowed to add fields with this that did not exist before or not
    /// TODO: consider allowing the second to also be an arg that evals to Atom::String?
    ///
    /// This function has an alias: `setattr`.
    "->"(3) => |state, args| {
        let mut obj = args[0].eval(state)?.object()?;
        let field = args[1].variable("`.` takes a field identifier as second argument")?;
        let value = args[2].eval(state)?;
        *obj.get_mut(&field).ok_or_else(|| Exception::new(format!("object has no field named `{field}`"), Error::Name))? = value;
        Ok(Atom::Object(obj))
    }
    /// Evaluates the given argument and terminates the program directly.
    /// The program will return the given value as its final result.
    /// 
    /// Even if the argument causes an exception, it is returned directly too.
    "exit"(1) => |state, args| {
        let value = args[0].eval(state);
        state.exit_unwind_value = Some(value);
        Ok(Atom::Null)
    }
}

fn read_dir_files(path: impl AsRef<Path>) -> impl Iterator<Item = DirEntry> {
    fs::read_dir(&path)
        .unwrap_or_else(|err| {
            panic!(
                "error when reading directory `{}`: {err}",
                path.as_ref().display()
            )
        })
        .flatten()
}
