use std::{rc::Rc, slice};

use crate::{
    error::{MachineError, ValueConversionError},
    vm::{
        Value,
        env::Environment,
        value::{
            ConsCell, HashTable, HashTableData,
            convert::{AnyFunction, TryFromValue},
        },
    },
};

pub fn load(env: &Rc<Environment>) {
    // // vectors
    // env.defun_native("getv", |vm, _, args| {
    //     let [vec, index] = args else {
    //         return Err(vm.error_at_ip(MachineErrorKind::InvalidArgument));
    //     };
    //     let Value::Vector(vec) = vec else {
    //         return Err(vm.error_at_ip(MachineErrorKind::InvalidArgument));
    //     };
    //     let index = i64::try_from_value(index).map_err(|e| vm.error_at_ip(e))? as isize;
    //     let value = if index < 0 {
    //         todo!()
    //     } else {
    //         vec.value_at(index as usize).unwrap_or(Value::Nil)
    //     };
    //     Ok(value)
    // });
    // env.defun_native("setv", |vm, _, args| {
    //     let [vec, index, value] = args else {
    //         return Err(vm.error_at_ip(MachineErrorKind::InvalidArgument));
    //     };
    //     let Value::Vector(vec) = vec else {
    //         return Err(vm.error_at_ip(MachineErrorKind::InvalidArgument));
    //     };
    //     let index = i64::try_from_value(index).map_err(|e| vm.error_at_ip(e))? as isize;
    //     if index < 0 {
    //         todo!()
    //     } else {
    //         vec.set_value_at(index as usize, value.clone());
    //     }
    //     Ok(Value::Nil)
    // });

    // lists
    env.defun_native(
        "append",
        "Concatenates the lists into one list",
        |_, _, args| match args {
            [] => Ok(Value::Nil),
            [xs] => Ok(xs.clone()),
            [head @ .., tail] => {
                let mut elements = vec![];
                for arg in head {
                    let iter = arg.proper_iter(ValueConversionError {
                        expected: "proper list".into(),
                        got: arg.clone(),
                    });
                    for element in iter {
                        elements.push(element?.clone());
                    }
                }
                let mut output = tail.clone();
                for element in elements.into_iter().rev() {
                    output = element.cons(output);
                }
                Ok(output)
            }
        },
    );
    env.defun_native(
        "car",
        "Returns the CAR value of a cons-cell",
        |_, _, args| {
            let [x] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let cons: &ConsCell = TryFromValue::try_from_value(x)?;
            Ok(cons.0.clone())
        },
    );
    env.defun_native(
        "cdr",
        "Returns the CDR value of a cons-cell",
        |_, _, args| {
            let [x] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let cons: &ConsCell = TryFromValue::try_from_value(x)?;
            Ok(cons.1.clone())
        },
    );
    env.defun_native(
        "cons",
        "Constructs a cons-cell from CAR and CDR arguments",
        |_, _, args| {
            let [car, cdr] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            Ok(Value::Cons(Rc::new(ConsCell(car.clone(), cdr.clone()))))
        },
    );
    env.defun_native(
        "list",
        "Constructs a list from the arguments",
        |_, _, args| {
            let out = Value::list_or_nil(args.iter().cloned());
            Ok(out)
        },
    );
    env.defun_native(
        "length",
        "Returns the length of the given list",
        |_, _, args| {
            let [xs] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };

            let mut xs = xs;
            let mut count = 0;
            while !xs.is_nil() {
                let Value::Cons(cons) = xs else {
                    break;
                };
                let ConsCell(_, cdr) = cons.as_ref();
                count += 1;
                xs = cdr;
            }
            Ok(count.into())
        },
    );
    env.defun_native(
        "find",
        "Finds an entry in the list matching given predicate",
        |vm, env, args| {
            let [predicate, list] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let predicate = AnyFunction::try_from_value(predicate)?;
            let mut list = list;
            while !list.is_nil() {
                let Value::Cons(cons) = list else {
                    break;
                };
                let pred_value = predicate.invoke(vm, env, slice::from_ref(&cons.0))?;
                if pred_value.is_trueish() {
                    return Ok(cons.0.clone());
                }
                list = &cons.1;
            }
            Ok(Value::Nil)
        },
    );

    // Hash table
    env.defun_native(
        "hash/new",
        "Creates a hash table from the list of pairs",
        |_, _, args| {
            let mut hash = HashTableData::new(16);
            for arg in args {
                let Value::Cons(cons) = arg else {
                    return Err(MachineError::ValueConversion(ValueConversionError {
                        expected: "a pair".into(),
                        got: arg.clone(),
                    }));
                };
                let ConsCell(car, cdr) = cons.as_ref();
                hash.insert(car.clone(), cdr.clone())?;
            }
            let hash = HashTable::from(hash);
            Ok(hash.into())
        },
    );
    env.defun_native(
        "hash/length",
        "Returns the number of associations in the hashtable",
        |_, _, args| {
            let [table] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
            let len = table.borrow().len();
            Ok(len.into())
        },
    );
    env.defun_native(
        "hash->list",
        "Converts a hashtable into a list of pairs",
        |_, _, args| {
            let [table] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
            let mut list = Value::Nil;
            for (key, value) in table.borrow().iter() {
                list = key.clone().cons(value.clone()).cons(list);
            }
            Ok(list)
        },
    );
    env.defun_native(
        "list->hash",
        "Converts a list of pairs into a hashtable",
        |_, _, args| {
            let [pairs] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let mut hash = HashTableData::new(16);
            let pair_iter =
                pairs.proper_iter(MachineError::ValueConversion(ValueConversionError {
                    expected: "a list of pairs".into(),
                    got: pairs.clone(),
                }));
            for pair in pair_iter {
                let pair = pair?;
                let Value::Cons(cons) = pair else {
                    return Err(MachineError::ValueConversion(ValueConversionError {
                        expected: "a pair".into(),
                        got: pair.clone(),
                    }));
                };
                let ConsCell(car, cdr) = cons.as_ref();

                hash.insert(car.clone(), cdr.clone())?;
            }
            let hash = HashTable::from(hash);
            Ok(hash.into())
        },
    );
    env.defun_native(
        "hash/put!",
        "Inserts an association into the hashtable",
        |_, _, args| {
            let [table, key, value] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
            let value = table.borrow_mut().insert(key.clone(), value.clone())?;
            Ok(value)
        },
    );
    env.defun_native(
        "hash/update!",
        "Applies a v -> v' function to the value referencing the key",
        |vm, env, args| {
            let [function, table, key] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
            let function = AnyFunction::try_from_value(function)?;
            let value = {
                let mut borrow = table.borrow_mut();
                match borrow.get_mut(key) {
                    Some(old_value) => {
                        let new_value = function.invoke(vm, env, slice::from_ref(old_value))?;
                        *old_value = new_value.clone();
                        new_value
                    }
                    None => {
                        let new_value = function.invoke(vm, env, &[Value::Nil])?;
                        borrow.insert(key.clone(), new_value.clone())?;
                        new_value
                    }
                }
            };
            Ok(value)
        },
    );
    env.defun_native(
        "hash/remove!",
        "Removes an association from the hashtable",
        |_, _, args| {
            let [table, key] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
            let value = table.borrow_mut().remove(key).unwrap_or(Value::Nil);
            Ok(value)
        },
    );
    env.defun_native(
        "hash/for-each",
        "Applies a (k, v) -> void function to all associations in the hashtable",
        |vm, env, args| {
            let [function, table] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
            let function = AnyFunction::try_from_value(function)?;
            for (key, value) in table.borrow().iter() {
                function.invoke(vm, env, &[key.clone(), value.clone()])?;
            }
            Ok(Value::Nil)
        },
    );
    env.defun_native(
        "hash/fold",
        "Applies a (acc, k, v) -> acc' function to all associations in the hashtable",
        |vm, env, args| {
            let [acc, function, table] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
            let function = AnyFunction::try_from_value(function)?;
            let mut acc = acc.clone();
            for (key, value) in table.borrow().iter() {
                acc = function.invoke(vm, env, &[acc, key.clone(), value.clone()])?;
            }
            Ok(acc)
        },
    );
    env.defun_native(
        "hash/filter!",
        "Removes associations not matching the predicate from the hashtable",
        |vm, env, args| {
            let [predicate, table] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
            let predicate = AnyFunction::try_from_value(predicate)?;
            table.borrow_mut().retain_invoke(vm, env, &predicate)?;
            Ok(Value::Nil)
        },
    );
    env.defun_native(
        "hash/map!",
        "Applies a (k, v) -> v' transform on the hashtable",
        |vm, env, args| {
            let [transform, table] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
            let transform = AnyFunction::try_from_value(transform)?;
            {
                let mut borrow = table.borrow_mut();
                borrow.iter_mut().try_for_each(|(key, value)| {
                    let output = transform.invoke(vm, env, &[key.clone(), value.clone()])?;
                    *value = output;
                    Ok::<_, MachineError>(())
                })?;
            }
            Ok(Value::Nil)
        },
    );
    env.defun_native(
        "hash/get",
        "Retrieves a value from the hashtable associated with given key",
        |_, _, args| {
            let (table, key, default) = match args {
                [table, key] => (table, key, &Value::Nil),
                [table, key, default] => (table, key, default),
                _ => return Err(MachineError::InvalidArgumentCount),
            };
            let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
            let value = table.borrow().get(key).unwrap_or(default).clone();
            Ok(value)
        },
    );
    env.defun_native(
        "hash/has?",
        "Returns #t if the hashtable contains the key",
        |_, _, args| {
            let [table, key] = args else {
                return Err(MachineError::InvalidArgumentCount);
            };
            let table: Rc<HashTable> = TryFromValue::try_from_value(table)?;
            Ok(table.borrow().contains_key(key).into())
        },
    );
}
