use crate::{attr, private, ty};
use proc_macro2::{Span, TokenStream, TokenTree};
use quote::{format_ident, quote, quote_spanned};
use syn::parse::{Error, Parse, ParseStream, Result};
use syn::punctuated::Punctuated;
use syn::{
    braced, parenthesized, parse_quote, Abi, Attribute, BareFnArg, BoundLifetimes, GenericParam,
    Generics, Ident, Path, ReturnType, Token, Type, TypeBareFn, Visibility, WhereClause,
};

pub struct Element {
    attrs: Vec<Attribute>,
    vis: Visibility,
    ident: Ident,
    ty: Type,
    expr: TokenStream,
    orig_item: Option<TokenStream>,
    start_span: Span,
    end_span: Span,
}

impl Parse for Element {
    fn parse(input: ParseStream) -> Result<Self> {
        let attrs = input.call(Attribute::parse_outer)?;
        let item = input.cursor();
        let vis: Visibility = input.parse()?;
        let static_token: Option<Token![static]> = input.parse()?;
        if static_token.is_some() {
            let mut_token: Option<Token![mut]> = input.parse()?;
            if let Some(mut_token) = mut_token {
                return Err(Error::new_spanned(
                    mut_token,
                    "static mut is not supported by distributed_slice",
                ));
            }
            let ident: Ident = input.parse()?;
            input.parse::<Token![:]>()?;
            let start_span = input.span();
            let ty: Type = input.parse()?;
            let end_span = quote!(#ty).into_iter().last().unwrap().span();
            input.parse::<Token![=]>()?;
            let mut expr_semi = Vec::from_iter(input.parse::<TokenStream>()?);
            if let Some(tail) = expr_semi.pop() {
                syn::parse2::<Token![;]>(TokenStream::from(tail))?;
            }
            let expr = TokenStream::from_iter(expr_semi);
            Ok(Element {
                attrs,
                vis,
                ident,
                ty,
                expr,
                orig_item: None,
                start_span,
                end_span,
            })
        } else {
            let constness: Option<Token![const]> = input.parse()?;
            let asyncness: Option<Token![async]> = input.parse()?;
            let unsafety: Option<Token![unsafe]> = input.parse()?;
            let abi: Option<Abi> = input.parse()?;
            let fn_token: Token![fn] = input.parse().map_err(|_| {
                Error::new_spanned(
                    item.token_stream(),
                    "distributed element must be either static or function item",
                )
            })?;
            let ident: Ident = input.parse()?;
            let generics: Generics = input.parse()?;

            let content;
            let paren_token = parenthesized!(content in input);
            let mut inputs = Punctuated::new();
            while !content.is_empty() {
                content.parse::<Option<Token![mut]>>()?;
                let ident = if let Some(wild) = content.parse::<Option<Token![_]>>()? {
                    Ident::from(wild)
                } else {
                    content.parse()?
                };
                let colon_token: Token![:] = content.parse()?;
                let ty: Type = content.parse()?;
                inputs.push_value(BareFnArg {
                    attrs: Vec::new(),
                    name: Some((ident, colon_token)),
                    ty,
                });
                if !content.is_empty() {
                    let comma: Token![,] = content.parse()?;
                    inputs.push_punct(comma);
                }
            }

            let output: ReturnType = input.parse()?;
            let where_clause: Option<WhereClause> = input.parse()?;

            let content;
            braced!(content in input);
            content.parse::<TokenStream>()?;

            if let Some(constness) = constness {
                return Err(Error::new_spanned(
                    constness,
                    "const fn distributed slice element is not supported",
                ));
            }

            if let Some(asyncness) = asyncness {
                return Err(Error::new_spanned(
                    asyncness,
                    "async fn distributed slice element is not supported",
                ));
            }

            let lifetimes = if generics.params.is_empty() {
                None
            } else {
                let mut bound = BoundLifetimes {
                    for_token: Token![for](generics.lt_token.unwrap().span),
                    lt_token: generics.lt_token.unwrap(),
                    lifetimes: Punctuated::new(),
                    gt_token: generics.gt_token.unwrap(),
                };
                for param in generics.params.into_pairs() {
                    let (param, punct) = param.into_tuple();
                    if let GenericParam::Lifetime(_) = param {
                        bound.lifetimes.push_value(param);
                        if let Some(punct) = punct {
                            bound.lifetimes.push_punct(punct);
                        }
                    } else {
                        return Err(Error::new_spanned(
                            param,
                            "cannot have generic parameters on distributed slice element",
                        ));
                    }
                }
                Some(bound)
            };

            if let Some(where_clause) = where_clause {
                return Err(Error::new_spanned(
                    where_clause,
                    "where-clause is not allowed on distributed slice elements",
                ));
            }

            let start_span = item.span();
            let end_span = quote!(#output)
                .into_iter()
                .last()
                .as_ref()
                .map_or(paren_token.span.close(), TokenTree::span);
            let mut original_attrs = attrs;
            let linkme_path = attr::linkme_path(&mut original_attrs)?;

            let attrs = vec![
                parse_quote! {
                    #[allow(non_upper_case_globals)]
                },
                parse_quote! {
                    #[linkme(crate = #linkme_path)]
                },
            ];
            let vis = Visibility::Inherited;
            let expr = parse_quote!(#ident);
            let ty = Type::BareFn(TypeBareFn {
                lifetimes,
                unsafety,
                abi,
                fn_token,
                paren_token,
                inputs,
                variadic: None,
                output,
            });
            let ident = format_ident!("_LINKME_ELEMENT_{}", ident);
            let item = item.token_stream();
            let orig_item = Some(quote!(
                #(#original_attrs)*
                #item
            ));

            Ok(Element {
                attrs,
                vis,
                ident,
                ty,
                expr,
                orig_item,
                start_span,
                end_span,
            })
        }
    }
}

pub fn expand(path: Path, pos: impl Into<Option<usize>>, input: Element) -> TokenStream {
    let pos = pos.into();
    do_expand(path, pos, input)
}

fn do_expand(path: Path, pos: Option<usize>, input: Element) -> TokenStream {
    let mut attrs = input.attrs;
    let vis = input.vis;
    let ident = input.ident;
    let mut ty = input.ty;
    let expr = input.expr;
    let orig_item = input.orig_item;

    ty::populate_static_lifetimes(&mut ty);

    let linkme_path = match attr::linkme_path(&mut attrs) {
        Ok(path) => path,
        Err(err) => return err.to_compile_error(),
    };

    let sort_key = pos.into_iter().map(|pos| format!("{:04}", pos));

    let factory = quote_spanned!(input.start_span=> __new);
    let get = quote_spanned!(input.end_span=> #factory());

    quote! {
        #path ! {
            #![linkme_macro = #path]
            #(
                #![linkme_sort_key = #sort_key]
            )*
            #(#attrs)*
            #vis static #ident : #ty = {
                #[allow(clippy::no_effect_underscore_binding)]
                unsafe fn __typecheck(_: #linkme_path::#private::Void) {
                    #[allow(clippy::ref_option_ref)]
                    let #factory = || -> fn() -> &'static #ty { || &#ident };
                    unsafe {
                        #linkme_path::DistributedSlice::private_typecheck(#path, #get);
                    }
                }

                #expr
            };
        }

        #orig_item
    }
}
