use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::spanned::Spanned;
use syn::token::{Dot, Paren};
use syn::{Block, Expr, ExprLit, ExprMethodCall, ItemFn, Lit, Stmt};

pub(crate) fn derive_message_formats(func: &ItemFn) -> TokenStream {
    let mut strings = quote!();

    if let Err(err) = parse_block(&func.block, &mut strings) {
        return err;
    }

    quote! {
        #func
        fn message_formats() -> &'static [&'static str] {
            &[#strings]
        }
    }
}

fn parse_block(block: &Block, strings: &mut TokenStream) -> Result<(), TokenStream> {
    let Some(Stmt::Expr(last, _)) = block.stmts.last() else {
        panic!("expected last statement in block to be an expression")
    };
    parse_expr(last, strings)?;
    Ok(())
}

fn parse_expr(expr: &Expr, strings: &mut TokenStream) -> Result<(), TokenStream> {
    match expr {
        Expr::Macro(mac) if mac.mac.path.is_ident("format") => {
            let mut tokens = mac.mac.tokens.to_token_stream().into_iter();
            let Some(first_token) = tokens.next() else {
                return Err(
                    quote_spanned!(expr.span() => compile_error!("expected `format!` to have an argument")),
                );
            };
            // do not throw an error if the `format!` argument contains a formatting argument

            if !first_token.to_string().contains('{') {
                // comma and string
                if tokens.next().is_none() || tokens.next().is_none() {
                    return Err(
                        quote_spanned!(expr.span() => compile_error!("prefer `String::to_string` over `format!` without arguments")),
                    );
                };
            };
            strings.extend(quote! {#first_token,});
            Ok(())
        }
        Expr::Block(block) => parse_block(&block.block, strings),
        Expr::If(expr) => {
            parse_block(&expr.then_branch, strings)?;
            if let Some((_, then)) = &expr.else_branch {
                parse_expr(then, strings)?;
            }
            Ok(())
        }
        Expr::MethodCall(method_call) => match method_call {
            ExprMethodCall {
                method,
                receiver,
                attrs,
                dot_token,
                turbofish: None,
                paren_token,
                args,
            } if *method == *"to_string"
                && attrs.is_empty()
                && args.is_empty()
                && *paren_token == Paren::default()
                && *dot_token == Dot::default() =>
            {
                let Expr::Lit(ExprLit {
                    lit: Lit::Str(ref literal_string),
                    ..
                }) = **receiver
                else {
                    return Err(
                        quote_spanned!(expr.span() => compile_error!("expected `String::to_string` method on str literal")),
                    );
                };

                let str_token = literal_string.token();

                strings.extend(quote! {#str_token,});
                Ok(())
            }
            _ => Err(
                quote_spanned!(expr.span() => compile_error!("expected `String::to_string` method on str literal")),
            ),
        },
        Expr::Match(block) => {
            for arm in &block.arms {
                parse_expr(&arm.body, strings)?;
            }
            Ok(())
        }
        _ => Err(quote_spanned!(
            expr.span() =>
            compile_error!("expected last expression to be a `format!` macro, a static String or a match block")
        )),
    }
}
