use super::context::*;
use super::engine::*;
use super::fli::*;
use super::init::*;
use super::result::*;
use super::term::*;
use crate::{term_getable, term_putable, unifiable};
use std::convert::TryInto;
use std::os::raw::c_char;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(PartialEq, Eq, Hash, Debug)]
pub struct Atom {
    atom: atom_t,
}
impl Atom {
    pub unsafe fn wrap(atom: atom_t) -> Atom {
        Atom { atom }
    }
    pub fn new(name: &str) -> Atom {
        assert_swipl_is_initialized();
        const S_USIZE: usize = std::mem::size_of::<usize>();
        let atom = if name.len() == S_USIZE - 1 {
            let mut buf: [u8; S_USIZE] = [0; S_USIZE];
            buf[..name.len()].clone_from_slice(name.as_bytes());
            unsafe {
                PL_new_atom_mbchars(
                    REP_UTF8.try_into().unwrap(),
                    name.len(),
                    buf.as_ptr() as *const c_char,
                )
            }
        } else {
            unsafe {
                PL_new_atom_mbchars(
                    REP_UTF8.try_into().unwrap(),
                    name.len(),
                    name.as_ptr() as *const c_char,
                )
            }
        };
        unsafe { Atom::wrap(atom) }
    }
    pub fn atom_ptr(&self) -> atom_t {
        self.atom
    }
    pub fn name(&self) -> String {
        assert_some_engine_is_active();
        let name;
        unsafe {
            let temp_term_ref = PL_new_term_ref();
            let unsafe_engine = unmanaged_engine_context();
            let temp_term = Term::new(temp_term_ref, unsafe_engine.as_term_origin());
            temp_term.put(self).unwrap();
            name = temp_term.get_atom_name(|name| name.unwrap().to_string());
            temp_term.reset();
        }
        name.unwrap()
    }
    pub(crate) fn increment_refcount(&self) {
        unsafe { PL_register_atom(self.atom) }
    }
}
impl ToString for Atom {
    fn to_string(&self) -> String {
        self.name()
    }
}
impl From<Atom> for String {
    fn from(atom: Atom) -> String {
        atom.to_string()
    }
}
impl Clone for Atom {
    fn clone(&self) -> Self {
        assert_some_engine_is_active();
        unsafe { PL_register_atom(self.atom) };
        Atom { atom: self.atom }
    }
}
impl Drop for Atom {
    fn drop(&mut self) {
        assert_some_engine_is_active();
        unsafe {
            PL_unregister_atom(self.atom);
        }
    }
}
unifiable! {
    (self:Atom, term) => {
        let result = unsafe { PL_unify_atom(term.term_ptr(), self.atom) };
        result != 0
    }
}
#[allow(unused_unsafe)]
pub unsafe fn get_atom<F, R>(term: &Term, func: F) -> PrologResult<R>
where
    F: Fn(Option<&Atom>) -> R,
{
    let mut atom = 0;
    let result = unsafe { PL_get_atom(term.term_ptr(), &mut atom) };
    if unsafe { pl_default_exception() != 0 } {
        return Err(PrologError::Exception);
    }
    let arg = if result == 0 {
        None
    } else {
        let atom = unsafe { Atom::wrap(atom) };
        Some(atom)
    };
    let result = func(arg.as_ref());
    std::mem::forget(arg);
    Ok(result)
}
term_getable! {
    (Atom, "atom", term) => {
        match term.get_atom(|a| a.cloned()) {
            Ok(r) => r,
            Err(_) => None
        }
    }
}
term_putable! {
    (self:Atom, term) => {
        unsafe { PL_put_atom(term.term_ptr(), self.atom); }
    }
}
pub enum Atomable<'a> {
    Str(&'a str),
    String(String),
}
impl<'a> From<&'a str> for Atomable<'a> {
    fn from(s: &str) -> Atomable {
        Atomable::Str(s)
    }
}
impl<'a> From<String> for Atomable<'a> {
    fn from(s: String) -> Atomable<'static> {
        Atomable::String(s)
    }
}
impl<'a> Atomable<'a> {
    pub fn new<T: Into<Atomable<'a>>>(s: T) -> Self {
        s.into()
    }
    pub fn name(&self) -> &str {
        match self {
            Self::Str(s) => s,
            Self::String(s) => s,
        }
    }
    pub fn owned(&self) -> Atomable<'static> {
        match self {
            Self::Str(s) => Atomable::String(s.to_string()),
            Self::String(s) => Atomable::String(s.clone()),
        }
    }
}
pub fn atomable<'a, T: Into<Atomable<'a>>>(s: T) -> Atomable<'a> {
    Atomable::new(s)
}
pub trait IntoAtom {
    fn into_atom(self) -> Atom;
}
impl IntoAtom for Atom {
    fn into_atom(self) -> Atom {
        self
    }
}
impl IntoAtom for &Atom {
    fn into_atom(self) -> Atom {
        self.clone()
    }
}
impl<'a> IntoAtom for &Atomable<'a> {
    fn into_atom(self) -> Atom {
        Atom::new(self.as_ref())
    }
}
impl<'a> IntoAtom for Atomable<'a> {
    fn into_atom(self) -> Atom {
        (&self).into_atom()
    }
}
impl<'a> IntoAtom for &'a str {
    fn into_atom(self) -> Atom {
        Atom::new(self)
    }
}
pub trait AsAtom {
    fn as_atom(&self) -> Atom;
    fn as_atom_ptr(&self) -> (atom_t, Option<Atom>) {
        let atom = self.as_atom();
        (atom.atom_ptr(), Some(atom))
    }
}
impl AsAtom for Atom {
    fn as_atom(&self) -> Atom {
        self.clone()
    }
    fn as_atom_ptr(&self) -> (atom_t, Option<Atom>) {
        (self.atom_ptr(), None)
    }
}
impl AsAtom for &Atom {
    fn as_atom(&self) -> Atom {
        (*self).clone()
    }
    fn as_atom_ptr(&self) -> (atom_t, Option<Atom>) {
        (self.atom_ptr(), None)
    }
}
impl<'a> AsAtom for Atomable<'a> {
    fn as_atom(&self) -> Atom {
        self.into_atom()
    }
}
impl<'a> AsAtom for &'a str {
    fn as_atom(&self) -> Atom {
        self.into_atom()
    }
}
impl AsAtom for str {
    fn as_atom(&self) -> Atom {
        self.into_atom()
    }
}
unifiable! {
    (self:Atomable<'a>, term) => {
        let result = unsafe {
            PL_unify_chars(
                term.term_ptr(),
                (PL_ATOM | REP_UTF8).try_into().unwrap(),
                self.name().len(),
                self.name().as_bytes().as_ptr() as *const c_char,
            )
        };
        result != 0
    }
}
pub fn get_atomable<F, R>(term: &Term, func: F) -> PrologResult<R>
where
    F: Fn(Option<&Atomable>) -> R,
{
    assert_some_engine_is_active();
    let mut ptr = std::ptr::null_mut();
    let mut len = 0;
    let result = unsafe {
        PL_get_nchars(
            term.term_ptr(),
            &mut len,
            &mut ptr,
            CVT_ATOM | REP_UTF8 | BUF_DISCARDABLE,
        )
    };
    if unsafe { pl_default_exception() != 0 } {
        return Err(PrologError::Exception);
    }
    let arg = if result == 0 {
        None
    } else {
        let swipl_string_ref = unsafe { std::slice::from_raw_parts(ptr as *const u8, len) };
        let swipl_string = std::str::from_utf8(swipl_string_ref).unwrap();
        let atomable = Atomable::new(swipl_string);
        Some(atomable)
    };
    let result = func(arg.as_ref());
    std::mem::forget(arg);
    Ok(result)
}
term_getable! {
    (Atomable<'static>, "atom", term) => {
        match get_atomable(term, |a|a.map(|a|a.owned())) {
            Ok(r) => r,
            Err(_) => None
        }
    }
}
term_putable! {
    (self:Atomable<'a>, term) => {
        unsafe {
            PL_put_chars(
                term.term_ptr(),
                (PL_ATOM | REP_UTF8).try_into().unwrap(),
                self.name().len(),
                self.name().as_bytes().as_ptr() as *const c_char,
            );
        }
    }
}
impl<'a> AsRef<str> for Atomable<'a> {
    fn as_ref(&self) -> &str {
        self.name()
    }
}
pub struct LazyAtom {
    s: &'static str,
    a: AtomicUsize,
}
impl LazyAtom {
    pub const fn new(s: &'static str) -> Self {
        Self {
            s,
            a: AtomicUsize::new(0),
        }
    }
    pub fn as_atom_t(&self) -> atom_t {
        let ptr = self.a.load(Ordering::Relaxed);
        if ptr == 0 {
            let atom = Atom::new(self.s);
            let atom_ptr = atom.atom_ptr();
            let swapped = self.a.swap(atom_ptr, Ordering::Relaxed);
            if swapped == 0 {
                std::mem::forget(atom);
            }
            atom_ptr
        } else {
            ptr
        }
    }
}
impl AsAtom for LazyAtom {
    fn as_atom(&self) -> Atom {
        let ptr = self.as_atom_t();
        let atom = unsafe { Atom::wrap(ptr) };
        atom.increment_refcount();
        atom
    }
    fn as_atom_ptr(&self) -> (atom_t, Option<Atom>) {
        let ptr = self.as_atom_t();
        (ptr, None)
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn create_atom_and_retrieve_name() {
        let engine = Engine::new();
        let _activation = engine.activate();
        let atom = Atom::new("the cow says moo");
        let name = atom.name();
        assert_eq!(name, "the cow says moo");
    }
    #[test]
    fn create_and_compare_some_atoms() {
        let engine = Engine::new();
        let _activation = engine.activate();
        let a1 = Atom::new("foo");
        let a2 = Atom::new("bar");
        assert!(a1 != a2);
        let a3 = Atom::new("foo");
        assert_eq!(a1, a3);
    }
    #[test]
    fn clone_atom() {
        let engine = Engine::new();
        let _activation = engine.activate();
        let a1 = Atom::new("foo");
        let a2 = a1.clone();
        assert_eq!(a1, a2);
    }
    #[test]
    fn create_atom_of_magic_length() {
        let engine = Engine::new();
        let _activation = engine.activate();
        let len = std::mem::size_of::<usize>() - 1;
        let name = (0..len).map(|_| "a").collect::<String>();
        let _atom = Atom::new(&name);
    }
    #[test]
    fn unify_atoms() {
        let engine = Engine::new();
        let activation = engine.activate();
        let context: Context<_> = activation.into();
        let a1 = Atom::new("foo");
        let a2 = Atom::new("bar");
        let term = context.new_term_ref();
        assert!(term.unify(&a1).is_ok());
        assert!(term.unify(a1).is_ok());
        assert!(term.unify(a2).is_err());
    }
    #[test]
    fn unify_atoms_from_string() {
        let engine = Engine::new();
        let activation = engine.activate();
        let context: Context<_> = activation.into();
        let a1 = Atom::new("foo");
        let a2 = Atom::new("bar");
        let term = context.new_term_ref();
        assert!(term.unify(atomable("foo")).is_ok());
        assert!(term.unify(atomable("foo")).is_ok());
        assert!(term.unify(a1).is_ok());
        assert!(term.unify(atomable("bar")).is_err());
        assert!(term.unify(a2).is_err());
    }
    #[test]
    fn unify_from_atomable_turned_atom() {
        let engine = Engine::new();
        let activation = engine.activate();
        let context: Context<_> = activation.into();
        let a1 = atomable("foo").as_atom();
        let a2 = atomable("bar").as_atom();
        assert_eq!("foo", a1.name());
        let term = context.new_term_ref();
        assert!(term.unify(&a1).is_ok());
        assert!(term.unify(&a1).is_ok());
        assert!(term.unify(&a2).is_err());
    }
    #[test]
    fn retrieve_atom_temporarily() {
        let engine = Engine::new();
        let activation = engine.activate();
        let context: Context<_> = activation.into();
        let a1 = "foo".as_atom();
        let term = context.new_term_ref();
        term.unify(&a1).unwrap();
        term.get_atom(|a2| assert_eq!(&a1, a2.unwrap())).unwrap();
    }
    #[test]
    fn retrieve_atom() {
        let engine = Engine::new();
        let activation = engine.activate();
        let context: Context<_> = activation.into();
        let a1 = "foo".as_atom();
        let term = context.new_term_ref();
        term.unify(&a1).unwrap();
        let a2: Atom = term.get().unwrap();
        assert_eq!(a1, a2);
    }
    #[test]
    fn retrieve_atomable_temporarily() {
        let engine = Engine::new();
        let activation = engine.activate();
        let context: Context<_> = activation.into();
        let a1 = "foo".as_atom();
        let term = context.new_term_ref();
        term.unify(&a1).unwrap();
        term.get_atom_name(|a2| assert_eq!("foo", a2.unwrap()))
            .unwrap();
    }
    #[test]
    fn retrieve_atomable() {
        let engine = Engine::new();
        let activation = engine.activate();
        let context: Context<_> = activation.into();
        let a1 = "foo".as_atom();
        let term = context.new_term_ref();
        term.unify(&a1).unwrap();
        let a2: Atomable = term.get().unwrap();
        assert_eq!("foo", a2.name());
    }
    #[test]
    fn lazy_atom_to_atom() {
        let engine = Engine::new();
        let _activation = engine.activate();
        let lazy = LazyAtom::new("moo");
        let a1 = lazy.as_atom();
        let a2 = lazy.as_atom();
        assert_eq!(a1, a2);
        let a3 = "moo".as_atom();
        assert_eq!(a1, a3);
    }
    use swipl_macros::atom;
    #[test]
    fn inline_atom_through_macro_ident() {
        let engine = Engine::new();
        let _activation = engine.activate();
        let a1 = atom!(foo);
        let a2 = "foo".as_atom();
        assert_eq!(a1, a2);
    }
    #[test]
    fn inline_atom_through_macro_str() {
        let engine = Engine::new();
        let _activation = engine.activate();
        let a1 = atom!("bar");
        let a2 = "bar".as_atom();
        assert_eq!(a1, a2);
    }
}