Documentation
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use std::ops::Deref;
use syn::spanned::Spanned;
use syn::{Error, FnArg, ItemFn, PatType, Path, ReturnType, Type};

pub fn expand_ffi(args: TokenStream, input: TokenStream) -> Result<TokenStream, Error> {
    let abi_fn: ItemFn = syn::parse2(input)?;

    let crate_name = format_ident!("abi");

    let fn_name = abi_fn.sig.ident.clone();
    let raw_rtn_ty = rtn_type(&abi_fn)?;
    let rtn_matched = rtn_is_ptr(args);

    let (ffi_rtn_ty, ffi_null, ffi_rtn_trans) = {
        if rtn_matched {
            (
                quote! { abi::preclude::AbiPtr<#raw_rtn_ty> },
                quote! { AbiPtr::null() },
                quote! { into_ptr(result) },
            )
        } else {
            (
                quote! { abi::preclude::AbiString },
                quote! { AbiString::null() },
                quote! {
                    match into_json(result) {
                        Ok(val) => val,
                        Err(err) => {
                            print_error(err.to_string());
                            return AbiString::null();
                        }
                    }
                },
            )
        }
    };

    let mut ffi_params = Vec::new();
    let mut ffi_values = Vec::new();
    let mut ffi_trans = Vec::new();

    for arg in &abi_fn.sig.inputs {
        let pat_ty = pat_type(&arg)?;
        let arg_name = pat_ty.pat.clone();
        let (is_reference, arg_ty) = match pat_ty.ty.deref() {
            Type::Reference(reference) => (true, reference.elem.clone()),
            _ => (false, pat_ty.ty.clone()),
        };

        let (param, value) = {
            if is_reference {
                (
                    quote! { #arg_name : abi::preclude::AbiPtr<#arg_ty>  },
                    quote! {
                        let #arg_name = {
                            match unsafe {from_ptr(#arg_name)} {
                                Ok(val) => val,
                                Err(err) => {
                                    print_error(err.to_string());
                                    return #ffi_null;
                                }
                            }
                        };
                    },
                )
            } else {
                (
                    quote! { #arg_name : abi::preclude::AbiString },
                    quote! {
                        let #arg_name = {
                            match unsafe {from_json(#arg_name)} {
                                Ok(val) => val,
                                Err(err) => {
                                    print_error(err.to_string());
                                    return #ffi_null;
                                }
                            }
                        };
                    },
                )
            }
        };
        ffi_params.push(param);
        ffi_values.push(value);
        ffi_trans.push(arg_name);
    }

    let expanded = quote! {
        #[unsafe(no_mangle)]
        pub extern "C" fn #fn_name ( #(#ffi_params),*) -> #ffi_rtn_ty {
            use #crate_name::preclude::*;

            #(#ffi_values)*

            #abi_fn

            let result = #fn_name(#(#ffi_trans),*);

            #ffi_rtn_trans
        }
    };

    Ok(TokenStream::from(expanded))
}

fn rtn_type(abi_fn: &ItemFn) -> Result<Box<Type>, Error> {
    let output = &abi_fn.sig.output;
    Ok(match output {
        ReturnType::Default => return Err(Error::new(output.span(), "invalid return")),
        ReturnType::Type(_, ty) => ty.clone(),
    })
}

fn rtn_is_ptr(args: TokenStream) -> bool {
    let abi_args: Option<Path> = syn::parse2(args).ok();
    abi_args.map(|e| e.to_token_stream().to_string() == "ptr").unwrap_or(false)
}

fn pat_type(arg: &FnArg) -> Result<PatType, Error> {
    Ok(match arg {
        FnArg::Receiver(_) => return Err(Error::new(arg.span(), "invalid self")),
        FnArg::Typed(pat) => pat.clone(),
    })
}