use proc_macro2::{Span, TokenStream};
use std::mem;
use syn::visit_mut::{self, VisitMut};
use syn::{
    parse_quote_spanned, token, Expr, GenericArgument, Lifetime, Receiver, ReturnType, Token, Type,
    TypeBareFn, TypeImplTrait, TypeParen, TypePtr, TypeReference,
};

pub struct CollectLifetimes {
    pub elided: Vec<Lifetime>,
    pub explicit: Vec<Lifetime>,
    pub name: &'static str,
}

impl CollectLifetimes {
    pub fn new(name: &'static str) -> Self {
        CollectLifetimes {
            elided: Vec::new(),
            explicit: Vec::new(),
            name,
        }
    }

    fn visit_opt_lifetime(&mut self, reference: Token![&], lifetime: &mut Option<Lifetime>) {
        match lifetime {
            None => *lifetime = Some(self.next_lifetime(reference.span)),
            Some(lifetime) => self.visit_lifetime(lifetime),
        }
    }

    fn visit_lifetime(&mut self, lifetime: &mut Lifetime) {
        if lifetime.ident == "_" {
            *lifetime = self.next_lifetime(lifetime.span());
        } else {
            self.explicit.push(lifetime.clone());
        }
    }

    fn next_lifetime(&mut self, span: Span) -> Lifetime {
        let name = format!("{}{}", self.name, self.elided.len());
        let life = Lifetime::new(&name, span);
        self.elided.push(life.clone());
        life
    }
}

impl VisitMut for CollectLifetimes {
    fn visit_receiver_mut(&mut self, arg: &mut Receiver) {
        if let Some((reference, lifetime)) = &mut arg.reference {
            self.visit_opt_lifetime(*reference, lifetime);
        }
    }

    fn visit_type_reference_mut(&mut self, ty: &mut TypeReference) {
        self.visit_opt_lifetime(ty.and_token, &mut ty.lifetime);
        visit_mut::visit_type_reference_mut(self, ty);
    }

    fn visit_generic_argument_mut(&mut self, gen: &mut GenericArgument) {
        if let GenericArgument::Lifetime(lifetime) = gen {
            self.visit_lifetime(lifetime);
        }
        visit_mut::visit_generic_argument_mut(self, gen);
    }
}

pub struct AddLifetimeToImplTrait;

impl VisitMut for AddLifetimeToImplTrait {
    fn visit_type_impl_trait_mut(&mut self, ty: &mut TypeImplTrait) {
        let span = ty.impl_token.span;
        let lifetime = parse_quote_spanned!(span=> 'async_trait);
        ty.bounds.insert(0, lifetime);
        if let Some(punct) = ty.bounds.pairs_mut().next().unwrap().punct_mut() {
            punct.span = span;
        }
        visit_mut::visit_type_impl_trait_mut(self, ty);
    }

    fn visit_type_reference_mut(&mut self, ty: &mut TypeReference) {
        parenthesize_impl_trait(&mut ty.elem, ty.and_token.span);
        visit_mut::visit_type_reference_mut(self, ty);
    }

    fn visit_type_ptr_mut(&mut self, ty: &mut TypePtr) {
        parenthesize_impl_trait(&mut ty.elem, ty.star_token.span);
        visit_mut::visit_type_ptr_mut(self, ty);
    }

    fn visit_type_bare_fn_mut(&mut self, ty: &mut TypeBareFn) {
        if let ReturnType::Type(arrow, return_type) = &mut ty.output {
            parenthesize_impl_trait(return_type, arrow.spans[0]);
        }
        visit_mut::visit_type_bare_fn_mut(self, ty);
    }

    fn visit_expr_mut(&mut self, _e: &mut Expr) {
        // Do not recurse into impl Traits inside of an array length expression.
        //
        //    fn outer(arg: [u8; { fn inner(_: impl Trait) {}; 0 }]);
    }
}

fn parenthesize_impl_trait(elem: &mut Type, paren_span: Span) {
    if let Type::ImplTrait(_) = *elem {
        let placeholder = Type::Verbatim(TokenStream::new());
        *elem = Type::Paren(TypeParen {
            paren_token: token::Paren(paren_span),
            elem: Box::new(mem::replace(elem, placeholder)),
        });
    }
}
