use rustc_ast::token::{Delimiter, TokenKind};
use rustc_ast::tokenstream::{DelimSpacing, DelimSpan, Spacing, TokenStream, TokenTree};
use rustc_ast::{
    Attribute, DUMMY_NODE_ID, EiiDecl, EiiImpl, ItemKind, MetaItem, Mutability, Path, StmtKind,
    Visibility, ast,
};
use rustc_ast_pretty::pprust::path_to_string;
use rustc_expand::base::{Annotatable, ExtCtxt};
use rustc_span::{ErrorGuaranteed, Ident, Span, kw, sym};
use thin_vec::{ThinVec, thin_vec};

use crate::errors::{
    EiiAttributeNotSupported, EiiExternTargetExpectedList, EiiExternTargetExpectedMacro,
    EiiExternTargetExpectedUnsafe, EiiMacroExpectedMaxOneArgument, EiiOnlyOnce,
    EiiSharedMacroInStatementPosition, EiiSharedMacroTarget, EiiStaticArgumentRequired,
    EiiStaticDefault, EiiStaticMultipleImplementations, EiiStaticMutable,
};

/// ```rust
/// #[eii]
/// fn panic_handler();
///
/// // or:
///
/// #[eii(panic_handler)]
/// fn panic_handler();
///
/// // expansion:
///
/// extern "Rust" {
///     fn panic_handler();
/// }
///
/// #[rustc_builtin_macro(eii_shared_macro)]
/// #[eii_declaration(panic_handler)]
/// macro panic_handler() {}
/// ```
pub(crate) fn eii(
    ecx: &mut ExtCtxt<'_>,
    span: Span,
    meta_item: &ast::MetaItem,
    item: Annotatable,
) -> Vec<Annotatable> {
    eii_(ecx, span, meta_item, item, false)
}

pub(crate) fn unsafe_eii(
    ecx: &mut ExtCtxt<'_>,
    span: Span,
    meta_item: &ast::MetaItem,
    item: Annotatable,
) -> Vec<Annotatable> {
    eii_(ecx, span, meta_item, item, true)
}

fn eii_(
    ecx: &mut ExtCtxt<'_>,
    eii_attr_span: Span,
    meta_item: &ast::MetaItem,
    orig_item: Annotatable,
    impl_unsafe: bool,
) -> Vec<Annotatable> {
    let eii_attr_span = ecx.with_def_site_ctxt(eii_attr_span);

    let item = if let Annotatable::Item(item) = orig_item {
        item
    } else if let Annotatable::Stmt(ref stmt) = orig_item
        && let StmtKind::Item(ref item) = stmt.kind
        && let ItemKind::Fn(ref f) = item.kind
    {
        ecx.dcx().emit_err(EiiSharedMacroInStatementPosition {
            span: eii_attr_span.to(item.span),
            name: path_to_string(&meta_item.path),
            item_span: f.ident.span,
        });
        return vec![orig_item];
    } else {
        ecx.dcx().emit_err(EiiSharedMacroTarget {
            span: eii_attr_span,
            name: path_to_string(&meta_item.path),
        });
        return vec![orig_item];
    };

    let ast::Item { attrs, id: _, span: _, vis, kind, tokens: _ } = item.as_ref();
    let (item_span, foreign_item_name) = match kind {
        ItemKind::Fn(func) => (func.sig.span, func.ident),
        ItemKind::Static(stat) => {
            // Statics with a default are not supported yet
            if let Some(stat_body) = &stat.expr {
                ecx.dcx().emit_err(EiiStaticDefault {
                    span: stat_body.span,
                    name: path_to_string(&meta_item.path),
                });
                return vec![];
            }
            // Statics must have an explicit name for the eii
            if meta_item.is_word() {
                ecx.dcx().emit_err(EiiStaticArgumentRequired {
                    span: eii_attr_span,
                    name: path_to_string(&meta_item.path),
                });
                return vec![];
            }

            // Mut statics are currently not supported
            if stat.mutability == Mutability::Mut {
                ecx.dcx().emit_err(EiiStaticMutable {
                    span: eii_attr_span,
                    name: path_to_string(&meta_item.path),
                });
            }

            (item.span, stat.ident)
        }
        _ => {
            ecx.dcx().emit_err(EiiSharedMacroTarget {
                span: eii_attr_span,
                name: path_to_string(&meta_item.path),
            });
            return vec![Annotatable::Item(item)];
        }
    };

    // only clone what we need
    let attrs = attrs.clone();
    let vis = vis.clone();

    let attrs_from_decl =
        filter_attrs_for_multiple_eii_attr(ecx, attrs, eii_attr_span, &meta_item.path);
    let (macro_attrs, foreign_item_attrs, default_func_attrs) =
        split_attrs(ecx, item_span, attrs_from_decl);

    let Ok(macro_name) = name_for_impl_macro(ecx, foreign_item_name, &meta_item) else {
        // we don't need to wrap in Annotatable::Stmt conditionally since
        // EII can't be used on items in statement position
        return vec![Annotatable::Item(item)];
    };

    let mut module_items = Vec::new();

    if let ItemKind::Fn(func) = kind
        && func.body.is_some()
    {
        module_items.push(generate_default_func_impl(
            ecx,
            &func,
            impl_unsafe,
            macro_name,
            eii_attr_span,
            item_span,
            foreign_item_name,
            default_func_attrs,
        ))
    }

    module_items.push(generate_foreign_item(
        ecx,
        eii_attr_span,
        item_span,
        kind,
        vis,
        foreign_item_attrs,
    ));
    module_items.push(generate_attribute_macro_to_implement(
        ecx,
        eii_attr_span,
        macro_name,
        foreign_item_name,
        impl_unsafe,
        macro_attrs,
    ));

    // we don't need to wrap in Annotatable::Stmt conditionally since
    // EII can't be used on items in statement position
    module_items.into_iter().map(Annotatable::Item).collect()
}

fn split_attrs(
    ecx: &mut ExtCtxt<'_>,
    span: Span,
    attrs: ThinVec<Attribute>,
) -> (ThinVec<Attribute>, ThinVec<Attribute>, ThinVec<Attribute>) {
    let mut macro_attributes = ThinVec::new();
    let mut foreign_item_attributes = ThinVec::new();
    let mut default_attributes = ThinVec::new();

    for attr in attrs {
        match attr.name() {
            // Inline only matters for the default function being inlined into callsites
            Some(sym::inline) => default_attributes.push(attr),
            // If an eii is marked a lang item, that's because we want to call its declaration, so
            // mark the foreign item as the lang item
            Some(sym::lang) => foreign_item_attributes.push(attr),
            // Deprecating an eii means deprecating the macro and the foreign item
            Some(sym::deprecated) => {
                foreign_item_attributes.push(attr.clone());
                macro_attributes.push(attr);
            }
            // The stability of an EII affects the usage of the macro and calling the foreign item
            Some(sym::stable) | Some(sym::unstable) => {
                foreign_item_attributes.push(attr.clone());
                macro_attributes.push(attr);
            }
            // Doc attributes should be forwarded to the macro and the foreign item, since those are
            // the two items you interact with as a user.
            // FIXME: idk yet how EIIs show up in docs, might want to customize
            _ if attr.is_doc_comment() => {
                foreign_item_attributes.push(attr.clone());
                macro_attributes.push(attr);
            }
            Some(sym::eii) => unreachable!("should already be filtered out"),
            _ => {
                ecx.dcx().emit_err(EiiAttributeNotSupported { span, attr_span: attr.span() });
            }
        }
    }

    (macro_attributes, foreign_item_attributes, default_attributes)
}

/// Decide on the name of the macro that can be used to implement the EII.
/// This is either an explicitly given name, or the name of the item in the
/// declaration of the EII.
fn name_for_impl_macro(
    ecx: &mut ExtCtxt<'_>,
    item_ident: Ident,
    meta_item: &MetaItem,
) -> Result<Ident, ErrorGuaranteed> {
    if meta_item.is_word() {
        Ok(item_ident)
    } else if let Some([first]) = meta_item.meta_item_list()
        && let Some(m) = first.meta_item()
        && m.path.segments.len() == 1
    {
        Ok(m.path.segments[0].ident)
    } else {
        Err(ecx.dcx().emit_err(EiiMacroExpectedMaxOneArgument {
            span: meta_item.span,
            name: path_to_string(&meta_item.path),
        }))
    }
}

/// Ensure that in the list of attrs, there's only a single `eii` attribute.
fn filter_attrs_for_multiple_eii_attr(
    ecx: &mut ExtCtxt<'_>,
    attrs: ThinVec<Attribute>,
    eii_attr_span: Span,
    eii_attr_path: &Path,
) -> ThinVec<Attribute> {
    attrs
        .into_iter()
        .filter(|i| {
            if i.has_name(sym::eii) {
                ecx.dcx().emit_err(EiiOnlyOnce {
                    span: i.span,
                    first_span: eii_attr_span,
                    name: path_to_string(eii_attr_path),
                });
                false
            } else {
                true
            }
        })
        .collect()
}

fn generate_default_func_impl(
    ecx: &mut ExtCtxt<'_>,
    func: &ast::Fn,
    impl_unsafe: bool,
    macro_name: Ident,
    eii_attr_span: Span,
    item_span: Span,
    foreign_item_name: Ident,
    attrs: ThinVec<Attribute>,
) -> Box<ast::Item> {
    let mut default_func = func.clone();
    default_func.eii_impls.push(EiiImpl {
        node_id: DUMMY_NODE_ID,
        inner_span: macro_name.span,
        eii_macro_path: ast::Path::from_ident(macro_name),
        impl_safety: if impl_unsafe {
            ast::Safety::Unsafe(eii_attr_span)
        } else {
            ast::Safety::Default
        },
        span: eii_attr_span,
        is_default: true,
        known_eii_macro_resolution: Some(ast::EiiDecl {
            foreign_item: ecx.path(
                foreign_item_name.span,
                // prefix self to explicitly escape the const block generated below
                // NOTE: this is why EIIs can't be used on statements
                vec![Ident::from_str_and_span("self", foreign_item_name.span), foreign_item_name],
            ),
            impl_unsafe,
        }),
    });

    let anon_mod = |span: Span, stmts: ThinVec<ast::Stmt>| {
        let unit = ecx.ty(item_span, ast::TyKind::Tup(ThinVec::new()));
        let underscore = Ident::new(kw::Underscore, item_span);
        ecx.item_const(
            span,
            underscore,
            unit,
            ast::ConstItemRhsKind::new_body(ecx.expr_block(ecx.block(span, stmts))),
        )
    };

    // const _: () = {
    //     <orig fn>
    // }
    anon_mod(
        item_span,
        thin_vec![ecx.stmt_item(
            item_span,
            ecx.item(item_span, attrs, ItemKind::Fn(Box::new(default_func)))
        ),],
    )
}

/// Generates a foreign item, like
///
/// ```rust, ignore
/// extern "…" { safe fn item(); }
/// ```
fn generate_foreign_item(
    ecx: &mut ExtCtxt<'_>,
    eii_attr_span: Span,
    item_span: Span,
    item_kind: &ItemKind,
    vis: Visibility,
    attrs_from_decl: ThinVec<Attribute>,
) -> Box<ast::Item> {
    let mut foreign_item_attrs = attrs_from_decl;

    // Add the rustc_eii_foreign_item on the foreign item. Usually, foreign items are mangled.
    // This attribute makes sure that we later know that this foreign item's symbol should not be.
    foreign_item_attrs.push(ecx.attr_word(sym::rustc_eii_foreign_item, eii_attr_span));

    // We set the abi to the default "rust" abi, which can be overridden by `generate_foreign_func`,
    // if a specific abi was specified on the EII function
    let mut abi = Some(ast::StrLit {
        symbol: sym::Rust,
        suffix: None,
        symbol_unescaped: sym::Rust,
        style: ast::StrStyle::Cooked,
        span: eii_attr_span,
    });
    let foreign_kind = match item_kind {
        ItemKind::Fn(func) => generate_foreign_func(func.clone(), &mut abi),
        ItemKind::Static(stat) => generate_foreign_static(stat.clone()),
        _ => unreachable!("Target was checked earlier"),
    };

    ecx.item(
        eii_attr_span,
        ThinVec::new(),
        ast::ItemKind::ForeignMod(ast::ForeignMod {
            extern_span: eii_attr_span,
            safety: ast::Safety::Unsafe(eii_attr_span),
            abi,
            items: From::from([Box::new(ast::ForeignItem {
                attrs: foreign_item_attrs,
                id: ast::DUMMY_NODE_ID,
                span: item_span,
                vis,
                kind: foreign_kind,
                tokens: None,
            })]),
        }),
    )
}

fn generate_foreign_func(
    mut func: Box<ast::Fn>,
    abi: &mut Option<ast::StrLit>,
) -> ast::ForeignItemKind {
    match func.sig.header.ext {
        // extern "X" fn  =>  extern "X" {}
        ast::Extern::Explicit(lit, _) => *abi = Some(lit),
        // extern fn  =>  extern {}
        ast::Extern::Implicit(_) => *abi = None,
        // no abi was specified, so we keep the default
        ast::Extern::None => {}
    };

    // ABI has been moved to the extern {} block, so we remove it from the fn item.
    func.sig.header.ext = ast::Extern::None;
    func.body = None;

    // And mark safe functions explicitly as `safe fn`.
    if func.sig.header.safety == ast::Safety::Default {
        func.sig.header.safety = ast::Safety::Safe(func.sig.span);
    }

    ast::ForeignItemKind::Fn(func)
}

fn generate_foreign_static(mut stat: Box<ast::StaticItem>) -> ast::ForeignItemKind {
    if stat.safety == ast::Safety::Default {
        stat.safety = ast::Safety::Safe(stat.ident.span);
    }

    ast::ForeignItemKind::Static(stat)
}

/// Generate a stub macro (a bit like in core) that will roughly look like:
///
/// ```rust, ignore, example
/// // Since this a stub macro, the actual code that expands it lives in the compiler.
/// // This attribute tells the compiler that
/// #[builtin_macro(eii_shared_macro)]
/// // the metadata to link this macro to the generated foreign item.
/// #[eii_declaration(<related_foreign_item>)]
/// macro macro_name { () => {} }
/// ```
fn generate_attribute_macro_to_implement(
    ecx: &mut ExtCtxt<'_>,
    span: Span,
    macro_name: Ident,
    foreign_item_name: Ident,
    impl_unsafe: bool,
    attrs_from_decl: ThinVec<Attribute>,
) -> Box<ast::Item> {
    let mut macro_attrs = attrs_from_decl;

    // Avoid "missing stability attribute" errors for eiis in std. See #146993.
    macro_attrs.push(ecx.attr_name_value_str(sym::rustc_macro_transparency, sym::semiopaque, span));

    // #[builtin_macro(eii_shared_macro)]
    macro_attrs.push(ecx.attr_nested_word(sym::rustc_builtin_macro, sym::eii_shared_macro, span));

    // cant use ecx methods here to construct item since we need it to be public
    Box::new(ast::Item {
        attrs: macro_attrs,
        id: ast::DUMMY_NODE_ID,
        span,
        // pub
        vis: ast::Visibility { span, kind: ast::VisibilityKind::Public, tokens: None },
        kind: ast::ItemKind::MacroDef(
            // macro macro_name
            macro_name,
            ast::MacroDef {
                // { () => {} }
                body: Box::new(ast::DelimArgs {
                    dspan: DelimSpan::from_single(span),
                    delim: Delimiter::Brace,
                    tokens: TokenStream::from_iter([
                        TokenTree::Delimited(
                            DelimSpan::from_single(span),
                            DelimSpacing::new(Spacing::Alone, Spacing::Alone),
                            Delimiter::Parenthesis,
                            TokenStream::default(),
                        ),
                        TokenTree::token_alone(TokenKind::FatArrow, span),
                        TokenTree::Delimited(
                            DelimSpan::from_single(span),
                            DelimSpacing::new(Spacing::Alone, Spacing::Alone),
                            Delimiter::Brace,
                            TokenStream::default(),
                        ),
                    ]),
                }),
                macro_rules: false,
                // #[eii_declaration(foreign_item_ident)]
                eii_declaration: Some(ast::EiiDecl {
                    foreign_item: ast::Path::from_ident(foreign_item_name),
                    impl_unsafe,
                }),
            },
        ),
        tokens: None,
    })
}

pub(crate) fn eii_declaration(
    ecx: &mut ExtCtxt<'_>,
    span: Span,
    meta_item: &ast::MetaItem,
    mut item: Annotatable,
) -> Vec<Annotatable> {
    let i = if let Annotatable::Item(ref mut item) = item {
        item
    } else if let Annotatable::Stmt(ref mut stmt) = item
        && let StmtKind::Item(ref mut item) = stmt.kind
    {
        item
    } else {
        ecx.dcx().emit_err(EiiExternTargetExpectedMacro { span });
        return vec![item];
    };

    let ItemKind::MacroDef(_, d) = &mut i.kind else {
        ecx.dcx().emit_err(EiiExternTargetExpectedMacro { span });
        return vec![item];
    };

    let Some(list) = meta_item.meta_item_list() else {
        ecx.dcx().emit_err(EiiExternTargetExpectedList { span: meta_item.span });
        return vec![item];
    };

    if list.len() > 2 {
        ecx.dcx().emit_err(EiiExternTargetExpectedList { span: meta_item.span });
        return vec![item];
    }

    let Some(extern_item_path) = list.get(0).and_then(|i| i.meta_item()).map(|i| i.path.clone())
    else {
        ecx.dcx().emit_err(EiiExternTargetExpectedList { span: meta_item.span });
        return vec![item];
    };

    let impl_unsafe = if let Some(i) = list.get(1) {
        if i.lit().and_then(|i| i.kind.str()).is_some_and(|i| i == kw::Unsafe) {
            true
        } else {
            ecx.dcx().emit_err(EiiExternTargetExpectedUnsafe { span: i.span() });
            return vec![item];
        }
    } else {
        false
    };

    d.eii_declaration = Some(EiiDecl { foreign_item: extern_item_path, impl_unsafe });

    // Return the original item and the new methods.
    vec![item]
}

/// all Eiis share this function as the implementation for their attribute.
pub(crate) fn eii_shared_macro(
    ecx: &mut ExtCtxt<'_>,
    span: Span,
    meta_item: &ast::MetaItem,
    mut item: Annotatable,
) -> Vec<Annotatable> {
    let i = if let Annotatable::Item(ref mut item) = item {
        item
    } else if let Annotatable::Stmt(ref mut stmt) = item
        && let StmtKind::Item(ref mut item) = stmt.kind
    {
        item
    } else {
        ecx.dcx().emit_err(EiiSharedMacroTarget { span, name: path_to_string(&meta_item.path) });
        return vec![item];
    };

    let eii_impls = match &mut i.kind {
        ItemKind::Fn(func) => &mut func.eii_impls,
        ItemKind::Static(stat) => {
            if !stat.eii_impls.is_empty() {
                // Reject multiple implementations on one static item
                // because it might be unintuitive for libraries defining statics the defined statics may alias
                ecx.dcx().emit_err(EiiStaticMultipleImplementations { span });
            }
            &mut stat.eii_impls
        }
        _ => {
            ecx.dcx()
                .emit_err(EiiSharedMacroTarget { span, name: path_to_string(&meta_item.path) });
            return vec![item];
        }
    };

    let is_default = if meta_item.is_word() {
        false
    } else if let Some([first]) = meta_item.meta_item_list()
        && let Some(m) = first.meta_item()
        && m.path.segments.len() == 1
    {
        m.path.segments[0].ident.name == kw::Default
    } else {
        ecx.dcx().emit_err(EiiMacroExpectedMaxOneArgument {
            span: meta_item.span,
            name: path_to_string(&meta_item.path),
        });
        return vec![item];
    };

    eii_impls.push(EiiImpl {
        node_id: DUMMY_NODE_ID,
        inner_span: meta_item.path.span,
        eii_macro_path: meta_item.path.clone(),
        impl_safety: meta_item.unsafety,
        span,
        is_default,
        known_eii_macro_resolution: None,
    });

    vec![item]
}
