//! A rust library to parse the return type generated by [async_trait](https://crates.io/crates/async-trait) in procedural macro.

use proc_macro2::TokenStream;
use quote::quote;
use syn::{
    GenericArgument, PathArguments, ReturnType, TraitBound, Type, TypeParamBound, TypePath,
    TypeTraitObject,
};

const PIN: &str = "Pin";
const BOX: &str = "Box";
const FUTURE: &str = "Future";
const RESULT: &str = "Result";

/// The return type information of a function.
pub struct PinBoxFutRet {
    is_pin_box_fut: bool,
    is_fut_ret_result: bool,
    ret_ty: TokenStream,
}

impl Default for PinBoxFutRet {
    fn default() -> Self {
        PinBoxFutRet {
            is_pin_box_fut: false,
            is_fut_ret_result: false,
            ret_ty: quote! {},
        }
    }
}

impl PinBoxFutRet {
    /// Parse a function return type in proc-macro.
    pub fn parse(ret_ty: &ReturnType) -> PinBoxFutRet {
        let expect_ty = match ret_ty {
            ReturnType::Type(_, ty) => ty,
            _ => return PinBoxFutRet::default(),
        };

        let expect_pin = match *(expect_ty.clone()) {
            Type::Path(TypePath { qself: _, path }) => {
                let last_seg = path.segments.last().cloned();
                match last_seg.map(|ls| (ls.ident.clone(), ls)) {
                    Some((ls_ident, ls)) if ls_ident == PIN => ls,
                    _ => return PinBoxFutRet::default(),
                }
            }
            _ => return PinBoxFutRet::default(),
        };

        let expect_box = match &expect_pin.arguments {
            PathArguments::AngleBracketed(wrapper) => match wrapper.args.last() {
                Some(GenericArgument::Type(Type::Path(TypePath { qself: _, path }))) => {
                    match path.segments.last().map(|ls| (ls.ident.clone(), ls)) {
                        Some((ls_ident, ls)) if ls_ident == BOX => ls,
                        _ => return PinBoxFutRet::default(),
                    }
                }
                _ => return PinBoxFutRet::default(),
            },
            _ => return PinBoxFutRet::default(),
        };

        match &expect_box.arguments {
            PathArguments::AngleBracketed(wrapper) => match wrapper.args.last() {
                Some(GenericArgument::Type(Type::TraitObject(TypeTraitObject {
                    dyn_token: _,
                    bounds,
                }))) => {
                    let mut fut_ret = PinBoxFutRet::default();

                    for bound in bounds.iter() {
                        if let TypeParamBound::Trait(TraitBound { path, .. }) = bound {
                            if let Some(arg) = path.segments.last() {
                                if arg.ident == FUTURE {
                                    fut_ret.is_pin_box_fut = true;
                                    fut_ret.is_fut_ret_result =
                                        is_fut_ret_result(&arg.arguments, &mut fut_ret);
                                    break;
                                }
                            }
                        }
                    }
                    fut_ret
                }
                _ => PinBoxFutRet::default(),
            },
            _ => PinBoxFutRet::default(),
        }
    }

    /// Whether the function return a
    /// [`Pin<Box<dyn Future<Output = RetTy>>>`](https://doc.rust-lang.org/std/boxed/struct.Box.html#method.pin).
    pub fn is_ret_pin_box_fut(&self) -> bool {
        self.is_pin_box_fut
    }

    /// Whether the output type of
    /// [`Future`](https://docs.rs/futures/0.3.21/futures/future/trait.Future.html)
    /// is a [`Result`](https://doc.rust-lang.org/std/result/index.html).
    pub fn is_fut_ret_result(&self) -> bool {
        self.is_fut_ret_result
    }

    /// The token stream of the return type.
    pub fn return_type(&self) -> TokenStream {
        self.ret_ty.clone()
    }
}

fn is_fut_ret_result(input: &PathArguments, fut_ret: &mut PinBoxFutRet) -> bool {
    match input {
        PathArguments::AngleBracketed(angle_arg) => {
            match angle_arg.args.first().expect("future output") {
                GenericArgument::Binding(binding) => match &binding.ty {
                    Type::Path(path) => {
                        fut_ret.ret_ty = quote! { #path };
                        path.path
                            .segments
                            .last()
                            .unwrap()
                            .ident
                            .to_string()
                            .contains(RESULT)
                    }
                    _ => false,
                },
                _ => false,
            }
        }
        _ => false,
    }
}
