use proc_macro2::TokenStream;
use quote::quote;
use syn::Ident;

use crate::TargetEnv;

pub mod complex;
pub mod primitive;
pub mod simple;

pub use complex::ComplexType;
pub use primitive::AbiPrimitive;
pub use simple::SimpleType;

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum TypeKind {
    ThinPointer,
    FatPointer,
    Other,
}

pub trait Type {
    fn width(&self, env: &TargetEnv) -> TypeWidth;
    fn kind(&self) -> TypeKind;
    fn as_rust_type(&self) -> TokenStream;

    fn emit_to_syscall_arguments(&self, env: &TargetEnv, value: &Ident) -> TokenStream;
    fn emit_from_syscall_arguments(
        &self,
        env: &TargetEnv,
        args: &Ident,
        index: usize,
    ) -> (TokenStream, usize);

    fn as_rust_ref_type(&self, mutable: bool) -> TokenStream {
        let inner = self.as_rust_type();
        match mutable {
            true => quote!(&mut #inner),
            false => quote!(& #inner),
        }
    }

    fn as_rust_slice_type(&self, mutable: bool) -> TokenStream {
        let inner = self.as_rust_type();
        match mutable {
            true => quote!(&mut [#inner]),
            false => quote!(&[#inner]),
        }
    }

    fn as_rust_array_type(&self, mutable: bool, length: usize) -> TokenStream {
        let inner = self.as_rust_type();
        match mutable {
            true => quote!(&mut [#inner; #length]),
            false => quote!(&[#inner; #length]),
        }
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
pub enum TypeWidth {
    Zero,
    U8,
    U16,
    U31,
    U32,
    U64,
    U128,
    Unknown,
}

impl TypeWidth {
    pub fn double(&self) -> Self {
        match self {
            Self::Zero => todo!(),
            Self::U8 => Self::U16,
            Self::U16 => Self::U32,
            Self::U31 => Self::U64,
            Self::U32 => Self::U64,
            Self::U64 => Self::U128,
            Self::U128 => todo!(),
            Self::Unknown => Self::Unknown,
        }
    }
}

#[cfg(test)]
mod tests {
    use std::rc::Rc;

    use proc_macro2::Span;
    use quote::quote;
    use syn::Ident;

    use crate::abi::ty::Type;

    use super::{AbiPrimitive, ComplexType, SimpleType, TargetEnv, TypeWidth};

    const TARGET_64: TargetEnv = TargetEnv {
        thin_pointer_width: TypeWidth::U64,
        fat_pointer_width: TypeWidth::U128,
    };
    const TARGET_32: TargetEnv = TargetEnv {
        thin_pointer_width: TypeWidth::U32,
        fat_pointer_width: TypeWidth::U64,
    };

    #[test]
    fn type_width() {
        let ty = SimpleType::Reference {
            mutable: false,
            pointee: Rc::new(ComplexType::Simple(SimpleType::Primitive(
                AbiPrimitive::U32,
            ))),
        };
        assert_eq!(ty.width(&TARGET_64), TypeWidth::U64);

        let ty = SimpleType::Primitive(AbiPrimitive::U64);
        assert_eq!(ty.width(&TARGET_64), TypeWidth::U64);
    }

    #[test]
    fn option_width() {
        let ty = ComplexType::Option(Rc::new(ComplexType::primitive(AbiPrimitive::U64)));
        assert_eq!(ty.width(&TARGET_64), TypeWidth::U128);

        let ty = ComplexType::Option(Rc::new(ComplexType::Simple(SimpleType::Reference {
            mutable: true,
            pointee: Rc::new(ComplexType::Simple(SimpleType::Primitive(
                AbiPrimitive::U64,
            ))),
        })));
        assert_eq!(ty.width(&TARGET_64), TypeWidth::U64);
    }

    #[test]
    fn simple_rust_types() {
        let ty = ComplexType::Option(Rc::new(ComplexType::primitive(AbiPrimitive::U32)));
        assert_eq!(
            quote!(Option<u32>).to_string(),
            ty.as_rust_type().to_string()
        );

        let ty = SimpleType::Reference {
            mutable: true,
            pointee: Rc::new(ComplexType::Simple(SimpleType::Primitive(
                AbiPrimitive::U64,
            ))),
        };
        assert_eq!(quote!(&mut u64).to_string(), ty.as_rust_type().to_string());

        let ty = SimpleType::Slice {
            mutable: false,
            element: Rc::new(ComplexType::Simple(SimpleType::Primitive(AbiPrimitive::U8))),
        };
        assert_eq!(quote!(&[u8]).to_string(), ty.as_rust_type().to_string());
    }

    #[test]
    fn u31_type() {
        let ty = SimpleType::Primitive(AbiPrimitive::U31);
        let option_ty = ComplexType::Option(Rc::new(ComplexType::Simple(ty.clone())));

        assert_eq!(ty.width(&TARGET_32), TypeWidth::U31);
        assert_eq!(ty.width(&TARGET_64), TypeWidth::U31);
        assert_eq!(ty.width(&TARGET_32).double(), TypeWidth::U64);
        assert_eq!(ty.width(&TARGET_64).double(), TypeWidth::U64);

        assert_eq!(option_ty.width(&TARGET_32), TypeWidth::U32);
        assert_eq!(option_ty.width(&TARGET_64), TypeWidth::U32);

        let val = Ident::new("val0", Span::call_site());
        assert_eq!(
            ty.emit_to_syscall_arguments(&TARGET_64, &val).to_string(),
            quote!(val0 as usize).to_string()
        );
        assert_eq!(
            ty.emit_to_syscall_arguments(&TARGET_32, &val).to_string(),
            quote!(val0 as usize).to_string()
        );
    }

    #[test]
    fn emit_to_syscall_arguments() {
        let ty = SimpleType::Reference {
            mutable: false,
            pointee: Rc::new(ComplexType::Simple(SimpleType::Primitive(
                AbiPrimitive::U64,
            ))),
        };

        let val = Ident::new("val0", Span::call_site());
        assert_eq!(
            ty.emit_to_syscall_arguments(&TARGET_64, &val).to_string(),
            quote!((val0 as *const u64).addr()).to_string()
        );

        let val = Ident::new("val0", Span::call_site());
        assert_eq!(
            ty.emit_to_syscall_arguments(&TARGET_32, &val).to_string(),
            quote!((val0 as *const u64).addr()).to_string()
        );

        let ty = SimpleType::Primitive(AbiPrimitive::U64);

        let val = Ident::new("val0", Span::call_site());
        assert_eq!(
            ty.emit_to_syscall_arguments(&TARGET_64, &val).to_string(),
            quote!(val0 as usize).to_string()
        );

        let val = Ident::new("val0", Span::call_site());
        assert_eq!(
            ty.emit_to_syscall_arguments(&TARGET_32, &val).to_string(),
            quote!(val0 as usize, (val0 >> 32) as usize).to_string()
        );
    }
}
