#![feature(let_chains)]
#![feature(proc_macro_span)]
#![allow(clippy::needless_if, dead_code)]

extern crate proc_macro;

use core::mem;
use proc_macro::token_stream::IntoIter;
use proc_macro::Delimiter::{self, Brace, Parenthesis};
use proc_macro::Spacing::{self, Alone, Joint};
use proc_macro::{Group, Ident, Literal, Punct, Span, TokenStream, TokenTree as TT};
use syn::spanned::Spanned;

type Result<T> = core::result::Result<T, TokenStream>;

/// Make a `compile_error!` pointing to the given span.
fn make_error(msg: &str, span: Span) -> TokenStream {
    TokenStream::from_iter([
        TT::Ident(Ident::new("compile_error", span)),
        TT::Punct(punct_with_span('!', Alone, span)),
        TT::Group({
            let mut msg = Literal::string(msg);
            msg.set_span(span);
            group_with_span(Parenthesis, TokenStream::from_iter([TT::Literal(msg)]), span)
        }),
    ])
}

fn expect_tt<T>(tt: Option<TT>, f: impl FnOnce(TT) -> Option<T>, expected: &str, span: Span) -> Result<T> {
    match tt {
        None => Err(make_error(
            &format!("unexpected end of input, expected {expected}"),
            span,
        )),
        Some(tt) => {
            let span = tt.span();
            match f(tt) {
                Some(x) => Ok(x),
                None => Err(make_error(&format!("unexpected token, expected {expected}"), span)),
            }
        },
    }
}

fn punct_with_span(c: char, spacing: Spacing, span: Span) -> Punct {
    let mut p = Punct::new(c, spacing);
    p.set_span(span);
    p
}

fn group_with_span(delimiter: Delimiter, stream: TokenStream, span: Span) -> Group {
    let mut g = Group::new(delimiter, stream);
    g.set_span(span);
    g
}

/// Token used to escape the following token from the macro's span rules.
const ESCAPE_CHAR: char = '$';

/// Takes a single token followed by a sequence of tokens. Returns the sequence of tokens with their
/// span set to that of the first token. Tokens may be escaped with either `#ident` or `#(tokens)`.
#[proc_macro]
pub fn with_span(input: TokenStream) -> TokenStream {
    let mut iter = input.into_iter();
    let span = iter.next().unwrap().span();
    let mut res = TokenStream::new();
    if let Err(e) = write_with_span(span, iter, &mut res) {
        e
    } else {
        res
    }
}

/// Takes a sequence of tokens and return the tokens with the span set such that they appear to be
/// from an external macro. Tokens may be escaped with either `#ident` or `#(tokens)`.
#[proc_macro]
pub fn external(input: TokenStream) -> TokenStream {
    let mut res = TokenStream::new();
    if let Err(e) = write_with_span(Span::mixed_site(), input.into_iter(), &mut res) {
        e
    } else {
        res
    }
}

/// Copies all the tokens, replacing all their spans with the given span. Tokens can be escaped
/// either by `#ident` or `#(tokens)`.
fn write_with_span(s: Span, mut input: IntoIter, out: &mut TokenStream) -> Result<()> {
    while let Some(tt) = input.next() {
        match tt {
            TT::Punct(p) if p.as_char() == ESCAPE_CHAR => {
                expect_tt(
                    input.next(),
                    |tt| match tt {
                        tt @ (TT::Ident(_) | TT::Literal(_)) => {
                            out.extend([tt]);
                            Some(())
                        },
                        TT::Punct(mut p) if p.as_char() == ESCAPE_CHAR => {
                            p.set_span(s);
                            out.extend([TT::Punct(p)]);
                            Some(())
                        },
                        TT::Group(g) if g.delimiter() == Parenthesis => {
                            out.extend([TT::Group(group_with_span(Delimiter::None, g.stream(), g.span()))]);
                            Some(())
                        },
                        _ => None,
                    },
                    "an ident, a literal, or parenthesized tokens",
                    p.span(),
                )?;
            },
            TT::Group(g) => {
                let mut stream = TokenStream::new();
                write_with_span(s, g.stream().into_iter(), &mut stream)?;
                out.extend([TT::Group(group_with_span(g.delimiter(), stream, s))]);
            },
            mut tt => {
                tt.set_span(s);
                out.extend([tt]);
            },
        }
    }
    Ok(())
}

/// Takes an array repeat expression such as `[0_u32; 2]`, and return the tokens with 10 times the
/// original size, which turns to `[0_u32; 20]`.
#[proc_macro]
pub fn make_it_big(input: TokenStream) -> TokenStream {
    let mut expr_repeat = syn::parse_macro_input!(input as syn::ExprRepeat);
    let len_span = expr_repeat.len.span();
    if let syn::Expr::Lit(expr_lit) = &mut *expr_repeat.len {
        if let syn::Lit::Int(lit_int) = &expr_lit.lit {
            let orig_val = lit_int.base10_parse::<usize>().expect("not a valid length parameter");
            let new_val = orig_val.saturating_mul(10);
            expr_lit.lit = syn::parse_quote_spanned!( len_span => #new_val);
        }
    }
    quote::quote!(#expr_repeat).into()
}

/// Within the item this attribute is attached to, an `inline!` macro is available which expands the
/// contained tokens as though they came from a macro expansion.
///
/// Within the `inline!` macro, any token preceded by `$` is passed as though it were an argument
/// with an automatically chosen fragment specifier. `$ident` will be passed as `ident`, `$1` or
/// `$"literal"` will be passed as `literal`, `$'lt` will be passed as `lifetime`, and `$(...)` will
/// pass the contained tokens as a `tt` sequence (the wrapping parenthesis are removed). If another
/// specifier is required it can be specified within parenthesis like `$(@expr ...)`. This will
/// expand the remaining tokens as a single argument.
///
/// Multiple `inline!` macros may be nested within each other. This will expand as nested macro
/// calls. However, any arguments will be passed as though they came from the outermost context.
#[proc_macro_attribute]
pub fn inline_macros(args: TokenStream, input: TokenStream) -> TokenStream {
    let mut args = args.into_iter();
    let mac_name = match args.next() {
        Some(TT::Ident(name)) => Some(name),
        Some(tt) => {
            return make_error(
                "unexpected argument, expected either an ident or no arguments",
                tt.span(),
            );
        },
        None => None,
    };
    if let Some(tt) = args.next() {
        return make_error(
            "unexpected argument, expected either an ident or no arguments",
            tt.span(),
        );
    };

    let mac_name = if let Some(mac_name) = mac_name {
        Ident::new(&format!("__inline_mac_{mac_name}"), Span::call_site())
    } else {
        let mut input = match LookaheadIter::new(input.clone().into_iter()) {
            Some(x) => x,
            None => return input,
        };
        loop {
            match input.next() {
                None => break Ident::new("__inline_mac", Span::call_site()),
                Some(TT::Ident(kind)) => match &*kind.to_string() {
                    "impl" => break Ident::new("__inline_mac_impl", Span::call_site()),
                    kind @ ("struct" | "enum" | "union" | "fn" | "mod" | "trait" | "type" | "const" | "static") => {
                        if let TT::Ident(name) = &input.tt {
                            break Ident::new(&format!("__inline_mac_{kind}_{name}"), Span::call_site());
                        } else {
                            break Ident::new(&format!("__inline_mac_{kind}"), Span::call_site());
                        }
                    },
                    _ => {},
                },
                _ => {},
            }
        }
    };

    let mut expander = Expander::default();
    let mut mac = MacWriter::new(mac_name);
    if let Err(e) = expander.expand(input.into_iter(), &mut mac) {
        return e;
    }
    let mut out = TokenStream::new();
    mac.finish(&mut out);
    out.extend(expander.expn);
    out
}

/// Wraps a `TokenStream` iterator with a single token lookahead.
struct LookaheadIter {
    tt: TT,
    iter: IntoIter,
}
impl LookaheadIter {
    fn new(mut iter: IntoIter) -> Option<Self> {
        iter.next().map(|tt| Self { tt, iter })
    }

    /// Get's the lookahead token, replacing it with the next token in the stream.
    /// Note: If there isn't a next token, this will not return the lookahead token.
    fn next(&mut self) -> Option<TT> {
        self.iter.next().map(|tt| mem::replace(&mut self.tt, tt))
    }
}

/// Builds the macro used to implement all the `inline!` macro calls.
struct MacWriter {
    name: Ident,
    macros: TokenStream,
    next_idx: usize,
}
impl MacWriter {
    fn new(name: Ident) -> Self {
        Self {
            name,
            macros: TokenStream::new(),
            next_idx: 0,
        }
    }

    /// Inserts a new `inline!` call.
    fn insert(&mut self, name_span: Span, bang_span: Span, body: Group, expander: &mut Expander) -> Result<()> {
        let idx = self.next_idx;
        self.next_idx += 1;

        let mut inner = Expander::for_arm(idx);
        inner.expand(body.stream().into_iter(), self)?;
        let new_arm = inner.arm.unwrap();

        self.macros.extend([
            TT::Group(Group::new(Parenthesis, new_arm.args_def)),
            TT::Punct(Punct::new('=', Joint)),
            TT::Punct(Punct::new('>', Alone)),
            TT::Group(Group::new(Parenthesis, inner.expn)),
            TT::Punct(Punct::new(';', Alone)),
        ]);

        expander.expn.extend([
            TT::Ident({
                let mut name = self.name.clone();
                name.set_span(name_span);
                name
            }),
            TT::Punct(punct_with_span('!', Alone, bang_span)),
        ]);
        let mut call_body = TokenStream::from_iter([TT::Literal(Literal::usize_unsuffixed(idx))]);
        if let Some(arm) = expander.arm.as_mut() {
            if !new_arm.args.is_empty() {
                arm.add_sub_args(new_arm.args, &mut call_body);
            }
        } else {
            call_body.extend(new_arm.args);
        }
        let mut g = Group::new(body.delimiter(), call_body);
        g.set_span(body.span());
        expander.expn.extend([TT::Group(g)]);
        Ok(())
    }

    /// Creates the macro definition.
    fn finish(self, out: &mut TokenStream) {
        if self.next_idx != 0 {
            out.extend([
                TT::Ident(Ident::new("macro_rules", Span::call_site())),
                TT::Punct(Punct::new('!', Alone)),
                TT::Ident(self.name),
                TT::Group(Group::new(Brace, self.macros)),
            ])
        }
    }
}

struct MacroArm {
    args_def: TokenStream,
    args: Vec<TT>,
}
impl MacroArm {
    fn add_single_arg_def(&mut self, kind: &str, dollar_span: Span, arg_span: Span, out: &mut TokenStream) {
        let mut name = Ident::new(&format!("_{}", self.args.len()), Span::call_site());
        self.args_def.extend([
            TT::Punct(Punct::new('$', Alone)),
            TT::Ident(name.clone()),
            TT::Punct(Punct::new(':', Alone)),
            TT::Ident(Ident::new(kind, Span::call_site())),
        ]);
        name.set_span(arg_span);
        out.extend([TT::Punct(punct_with_span('$', Alone, dollar_span)), TT::Ident(name)]);
    }

    fn add_parenthesized_arg_def(&mut self, kind: Ident, dollar_span: Span, arg_span: Span, out: &mut TokenStream) {
        let mut name = Ident::new(&format!("_{}", self.args.len()), Span::call_site());
        self.args_def.extend([TT::Group(Group::new(
            Parenthesis,
            TokenStream::from_iter([
                TT::Punct(Punct::new('$', Alone)),
                TT::Ident(name.clone()),
                TT::Punct(Punct::new(':', Alone)),
                TT::Ident(kind),
            ]),
        ))]);
        name.set_span(arg_span);
        out.extend([TT::Punct(punct_with_span('$', Alone, dollar_span)), TT::Ident(name)]);
    }

    fn add_multi_arg_def(&mut self, dollar_span: Span, arg_span: Span, out: &mut TokenStream) {
        let mut name = Ident::new(&format!("_{}", self.args.len()), Span::call_site());
        self.args_def.extend([TT::Group(Group::new(
            Parenthesis,
            TokenStream::from_iter([
                TT::Punct(Punct::new('$', Alone)),
                TT::Group(Group::new(
                    Parenthesis,
                    TokenStream::from_iter([
                        TT::Punct(Punct::new('$', Alone)),
                        TT::Ident(name.clone()),
                        TT::Punct(Punct::new(':', Alone)),
                        TT::Ident(Ident::new("tt", Span::call_site())),
                    ]),
                )),
                TT::Punct(Punct::new('*', Alone)),
            ]),
        ))]);
        name.set_span(arg_span);
        out.extend([
            TT::Punct(punct_with_span('$', Alone, dollar_span)),
            TT::Group(group_with_span(
                Parenthesis,
                TokenStream::from_iter([TT::Punct(punct_with_span('$', Alone, dollar_span)), TT::Ident(name)]),
                dollar_span,
            )),
            TT::Punct(punct_with_span('*', Alone, dollar_span)),
        ]);
    }

    fn add_arg(&mut self, dollar_span: Span, tt: TT, input: &mut IntoIter, out: &mut TokenStream) -> Result<()> {
        match tt {
            TT::Punct(p) if p.as_char() == ESCAPE_CHAR => out.extend([TT::Punct(p)]),
            TT::Punct(p) if p.as_char() == '\'' && p.spacing() == Joint => {
                let lt_name = expect_tt(
                    input.next(),
                    |tt| match tt {
                        TT::Ident(x) => Some(x),
                        _ => None,
                    },
                    "lifetime name",
                    p.span(),
                )?;
                let arg_span = p.span().join(lt_name.span()).unwrap_or(p.span());
                self.add_single_arg_def("lifetime", dollar_span, arg_span, out);
                self.args.extend([TT::Punct(p), TT::Ident(lt_name)]);
            },
            TT::Ident(x) => {
                self.add_single_arg_def("ident", dollar_span, x.span(), out);
                self.args.push(TT::Ident(x));
            },
            TT::Literal(x) => {
                self.add_single_arg_def("literal", dollar_span, x.span(), out);
                self.args.push(TT::Literal(x));
            },
            TT::Group(g) if g.delimiter() == Parenthesis => {
                let mut inner = g.stream().into_iter();
                if let Some(TT::Punct(p)) = inner.next()
                    && p.as_char() == '@'
                {
                    let kind = expect_tt(
                        inner.next(),
                        |tt| match tt {
                            TT::Ident(kind) => Some(kind),
                            _ => None,
                        },
                        "a macro fragment specifier",
                        p.span(),
                    )?;
                    self.add_parenthesized_arg_def(kind, dollar_span, g.span(), out);
                    self.args
                        .push(TT::Group(group_with_span(Parenthesis, inner.collect(), g.span())))
                } else {
                    self.add_multi_arg_def(dollar_span, g.span(), out);
                    self.args.push(TT::Group(g));
                }
            },
            tt => return Err(make_error("unsupported escape", tt.span())),
        };
        Ok(())
    }

    fn add_sub_args(&mut self, args: Vec<TT>, out: &mut TokenStream) {
        self.add_multi_arg_def(Span::call_site(), Span::call_site(), out);
        self.args
            .extend([TT::Group(Group::new(Parenthesis, TokenStream::from_iter(args)))]);
    }
}

#[derive(Default)]
struct Expander {
    arm: Option<MacroArm>,
    expn: TokenStream,
}
impl Expander {
    fn for_arm(idx: usize) -> Self {
        Self {
            arm: Some(MacroArm {
                args_def: TokenStream::from_iter([TT::Literal(Literal::usize_unsuffixed(idx))]),
                args: Vec::new(),
            }),
            expn: TokenStream::new(),
        }
    }

    fn write_tt(&mut self, tt: TT, mac: &mut MacWriter) -> Result<()> {
        match tt {
            TT::Group(g) => {
                let outer = mem::take(&mut self.expn);
                self.expand(g.stream().into_iter(), mac)?;
                let inner = mem::replace(&mut self.expn, outer);
                self.expn
                    .extend([TT::Group(group_with_span(g.delimiter(), inner, g.span()))]);
            },
            tt => self.expn.extend([tt]),
        }
        Ok(())
    }

    fn expand(&mut self, input: IntoIter, mac: &mut MacWriter) -> Result<()> {
        let Some(mut input) = LookaheadIter::new(input) else {
            return Ok(());
        };
        while let Some(tt) = input.next() {
            if let TT::Punct(p) = &tt
                && p.as_char() == ESCAPE_CHAR
                && let Some(arm) = self.arm.as_mut()
            {
                arm.add_arg(
                    p.span(),
                    mem::replace(&mut input.tt, tt),
                    &mut input.iter,
                    &mut self.expn,
                )?;
                if input.next().is_none() {
                    return Ok(());
                }
            } else if let TT::Punct(p) = &input.tt
                && p.as_char() == '!'
                && let TT::Ident(name) = &tt
                && name.to_string() == "inline"
            {
                let g = expect_tt(
                    input.iter.next(),
                    |tt| match tt {
                        TT::Group(g) => Some(g),
                        _ => None,
                    },
                    "macro arguments",
                    p.span(),
                )?;
                mac.insert(name.span(), p.span(), g, self)?;
                if input.next().is_none() {
                    return Ok(());
                }
            } else {
                self.write_tt(tt, mac)?;
            }
        }
        self.write_tt(input.tt, mac)
    }
}