use crate::base::{self, *};
use crate::proc_macro_server;

use rustc_ast::token;
use rustc_ast::tokenstream::{TokenStream, TokenTree};
use rustc_ast::{self as ast, *};
use rustc_data_structures::sync::Lrc;
use rustc_errors::{Applicability, ErrorReported};
use rustc_parse::nt_to_tokenstream;
use rustc_span::symbol::sym;
use rustc_span::{Span, DUMMY_SP};

const EXEC_STRATEGY: pm::bridge::server::SameThread = pm::bridge::server::SameThread;

pub struct BangProcMacro {
    pub client: pm::bridge::client::Client<fn(pm::TokenStream) -> pm::TokenStream>,
}

impl base::ProcMacro for BangProcMacro {
    fn expand<'cx>(
        &self,
        ecx: &'cx mut ExtCtxt<'_>,
        span: Span,
        input: TokenStream,
    ) -> Result<TokenStream, ErrorReported> {
        let server = proc_macro_server::Rustc::new(ecx);
        self.client.run(&EXEC_STRATEGY, server, input, ecx.ecfg.proc_macro_backtrace).map_err(|e| {
            let mut err = ecx.struct_span_err(span, "proc macro panicked");
            if let Some(s) = e.as_str() {
                err.help(&format!("message: {}", s));
            }
            err.emit();
            ErrorReported
        })
    }
}

pub struct AttrProcMacro {
    pub client: pm::bridge::client::Client<fn(pm::TokenStream, pm::TokenStream) -> pm::TokenStream>,
}

impl base::AttrProcMacro for AttrProcMacro {
    fn expand<'cx>(
        &self,
        ecx: &'cx mut ExtCtxt<'_>,
        span: Span,
        annotation: TokenStream,
        annotated: TokenStream,
    ) -> Result<TokenStream, ErrorReported> {
        let server = proc_macro_server::Rustc::new(ecx);
        self.client
            .run(&EXEC_STRATEGY, server, annotation, annotated, ecx.ecfg.proc_macro_backtrace)
            .map_err(|e| {
                let mut err = ecx.struct_span_err(span, "custom attribute panicked");
                if let Some(s) = e.as_str() {
                    err.help(&format!("message: {}", s));
                }
                err.emit();
                ErrorReported
            })
    }
}

pub struct ProcMacroDerive {
    pub client: pm::bridge::client::Client<fn(pm::TokenStream) -> pm::TokenStream>,
}

impl MultiItemModifier for ProcMacroDerive {
    fn expand(
        &self,
        ecx: &mut ExtCtxt<'_>,
        span: Span,
        _meta_item: &ast::MetaItem,
        item: Annotatable,
    ) -> ExpandResult<Vec<Annotatable>, Annotatable> {
        let item = match item {
            Annotatable::Arm(..)
            | Annotatable::Field(..)
            | Annotatable::FieldPat(..)
            | Annotatable::GenericParam(..)
            | Annotatable::Param(..)
            | Annotatable::StructField(..)
            | Annotatable::Variant(..) => panic!("unexpected annotatable"),
            Annotatable::Item(item) => item,
            Annotatable::ImplItem(_)
            | Annotatable::TraitItem(_)
            | Annotatable::ForeignItem(_)
            | Annotatable::Stmt(_)
            | Annotatable::Expr(_) => {
                ecx.span_err(
                    span,
                    "proc-macro derives may only be applied to a struct, enum, or union",
                );
                return ExpandResult::Ready(Vec::new());
            }
        };
        match item.kind {
            ItemKind::Struct(..) | ItemKind::Enum(..) | ItemKind::Union(..) => {}
            _ => {
                ecx.span_err(
                    span,
                    "proc-macro derives may only be applied to a struct, enum, or union",
                );
                return ExpandResult::Ready(Vec::new());
            }
        }

        let item = token::NtItem(item);
        let input = if item.pretty_printing_compatibility_hack() {
            TokenTree::token(token::Interpolated(Lrc::new(item)), DUMMY_SP).into()
        } else {
            nt_to_tokenstream(&item, &ecx.sess.parse_sess, DUMMY_SP)
        };

        let server = proc_macro_server::Rustc::new(ecx);
        let stream =
            match self.client.run(&EXEC_STRATEGY, server, input, ecx.ecfg.proc_macro_backtrace) {
                Ok(stream) => stream,
                Err(e) => {
                    let mut err = ecx.struct_span_err(span, "proc-macro derive panicked");
                    if let Some(s) = e.as_str() {
                        err.help(&format!("message: {}", s));
                    }
                    err.emit();
                    return ExpandResult::Ready(vec![]);
                }
            };

        let error_count_before = ecx.sess.parse_sess.span_diagnostic.err_count();
        let mut parser =
            rustc_parse::stream_to_parser(&ecx.sess.parse_sess, stream, Some("proc-macro derive"));
        let mut items = vec![];

        loop {
            match parser.parse_item() {
                Ok(None) => break,
                Ok(Some(item)) => items.push(Annotatable::Item(item)),
                Err(mut err) => {
                    err.emit();
                    break;
                }
            }
        }

        // fail if there have been errors emitted
        if ecx.sess.parse_sess.span_diagnostic.err_count() > error_count_before {
            ecx.struct_span_err(span, "proc-macro derive produced unparseable tokens").emit();
        }

        ExpandResult::Ready(items)
    }
}

crate fn collect_derives(cx: &mut ExtCtxt<'_>, attrs: &mut Vec<ast::Attribute>) -> Vec<ast::Path> {
    let mut result = Vec::new();
    attrs.retain(|attr| {
        if !attr.has_name(sym::derive) {
            return true;
        }

        // 1) First let's ensure that it's a meta item.
        let nmis = match attr.meta_item_list() {
            None => {
                cx.struct_span_err(attr.span, "malformed `derive` attribute input")
                    .span_suggestion(
                        attr.span,
                        "missing traits to be derived",
                        "#[derive(Trait1, Trait2, ...)]".to_owned(),
                        Applicability::HasPlaceholders,
                    )
                    .emit();
                return false;
            }
            Some(x) => x,
        };

        let mut error_reported_filter_map = false;
        let mut error_reported_map = false;
        let traits = nmis
            .into_iter()
            // 2) Moreover, let's ensure we have a path and not `#[derive("foo")]`.
            .filter_map(|nmi| match nmi {
                NestedMetaItem::Literal(lit) => {
                    error_reported_filter_map = true;
                    cx.struct_span_err(lit.span, "expected path to a trait, found literal")
                        .help("for example, write `#[derive(Debug)]` for `Debug`")
                        .emit();
                    None
                }
                NestedMetaItem::MetaItem(mi) => Some(mi),
            })
            // 3) Finally, we only accept `#[derive($path_0, $path_1, ..)]`
            // but not e.g. `#[derive($path_0 = "value", $path_1(abc))]`.
            // In this case we can still at least determine that the user
            // wanted this trait to be derived, so let's keep it.
            .map(|mi| {
                let mut traits_dont_accept = |title, action| {
                    error_reported_map = true;
                    let sp = mi.span.with_lo(mi.path.span.hi());
                    cx.struct_span_err(sp, title)
                        .span_suggestion(
                            sp,
                            action,
                            String::new(),
                            Applicability::MachineApplicable,
                        )
                        .emit();
                };
                match &mi.kind {
                    MetaItemKind::List(..) => traits_dont_accept(
                        "traits in `#[derive(...)]` don't accept arguments",
                        "remove the arguments",
                    ),
                    MetaItemKind::NameValue(..) => traits_dont_accept(
                        "traits in `#[derive(...)]` don't accept values",
                        "remove the value",
                    ),
                    MetaItemKind::Word => {}
                }
                mi.path
            });

        result.extend(traits);
        !error_reported_filter_map && !error_reported_map
    });
    result
}
