use crate::{
    Error, TulispContext, TulispObject, destruct_eval_bind,
    object::wrappers::generic::{Shared, SharedMut},
};
use std::collections::HashMap;

struct TulispObjectEql(TulispObject);

impl std::hash::Hash for TulispObjectEql {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        if self.0.integerp() {
            self.0.as_int().unwrap().hash(state);
        } else if self.0.floatp() {
            self.0.as_float().unwrap().to_bits().hash(state);
        } else {
            state.write_usize(self.0.addr_as_usize());
        }
    }
}

impl PartialEq for TulispObjectEql {
    fn eq(&self, other: &Self) -> bool {
        self.0.eql(&other.0)
    }
}
impl Eq for TulispObjectEql {}

impl From<TulispObject> for TulispObjectEql {
    fn from(obj: TulispObject) -> Self {
        Self(obj)
    }
}

pub(crate) struct HashTable {
    inner: SharedMut<HashMap<TulispObjectEql, TulispObject>>,
}

impl std::fmt::Display for HashTable {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "#<hash-table>")
    }
}

pub(crate) fn add(ctx: &mut TulispContext) {
    ctx.defspecial("make-hash-table", |_ctx, args| {
        if !args.null() {
            return Err(Error::invalid_argument(
                "make-hash-table: expected no arguments.".to_string(),
            )
            .with_trace(args.clone()));
        }
        let table = Shared::new(HashTable {
            inner: SharedMut::new(HashMap::new()),
        });
        Ok(table.into())
    });

    ctx.defspecial("gethash", |ctx, args| {
        destruct_eval_bind!(ctx, (key table) = args);
        let binding = table.as_any()?;
        let table = binding.downcast_ref::<HashTable>().unwrap();
        let value = table
            .inner
            .borrow_mut()
            .get(&key.into())
            .cloned()
            .unwrap_or(TulispObject::nil());

        Ok(value)
    });

    ctx.defspecial("puthash", |ctx, args| {
        destruct_eval_bind!(ctx, (key value table) = args);

        let binding = table.as_any()?;
        let table = binding.downcast_ref::<HashTable>().unwrap();
        table.inner.borrow_mut().insert(key.into(), value);

        Ok(TulispObject::nil())
    });
}
