use std::{
    fmt,
    hash::{Hash, Hasher},
};

use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::snippet_opt;
use if_chain::if_chain;
use rustc_ast::ast;
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_errors::Applicability;
use rustc_hir::def_id::DefId;
use rustc_lint::{EarlyContext, EarlyLintPass};
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::hygiene::{ExpnKind, MacroKind};
use rustc_span::Span;
use serde::{de, Deserialize};

declare_clippy_lint! {
    /// ### What it does
    /// Checks that common macros are used with consistent bracing.
    ///
    /// ### Why is this bad?
    /// This is mostly a consistency lint although using () or []
    /// doesn't give you a semicolon in item position, which can be unexpected.
    ///
    /// ### Example
    /// ```rust
    /// vec!{1, 2, 3};
    /// ```
    /// Use instead:
    /// ```rust
    /// vec![1, 2, 3];
    /// ```
    #[clippy::version = "1.55.0"]
    pub NONSTANDARD_MACRO_BRACES,
    nursery,
    "check consistent use of braces in macro"
}

const BRACES: &[(&str, &str)] = &[("(", ")"), ("{", "}"), ("[", "]")];

/// The (callsite span, (open brace, close brace), source snippet)
type MacroInfo<'a> = (Span, &'a (String, String), String);

#[derive(Clone, Debug, Default)]
pub struct MacroBraces {
    macro_braces: FxHashMap<String, (String, String)>,
    done: FxHashSet<Span>,
}

impl MacroBraces {
    pub fn new(conf: &FxHashSet<MacroMatcher>) -> Self {
        let macro_braces = macro_braces(conf.clone());
        Self {
            macro_braces,
            done: FxHashSet::default(),
        }
    }
}

impl_lint_pass!(MacroBraces => [NONSTANDARD_MACRO_BRACES]);

impl EarlyLintPass for MacroBraces {
    fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) {
        if let Some((span, braces, snip)) = is_offending_macro(cx, item.span, self) {
            emit_help(cx, &snip, braces, span);
            self.done.insert(span);
        }
    }

    fn check_stmt(&mut self, cx: &EarlyContext<'_>, stmt: &ast::Stmt) {
        if let Some((span, braces, snip)) = is_offending_macro(cx, stmt.span, self) {
            emit_help(cx, &snip, braces, span);
            self.done.insert(span);
        }
    }

    fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &ast::Expr) {
        if let Some((span, braces, snip)) = is_offending_macro(cx, expr.span, self) {
            emit_help(cx, &snip, braces, span);
            self.done.insert(span);
        }
    }

    fn check_ty(&mut self, cx: &EarlyContext<'_>, ty: &ast::Ty) {
        if let Some((span, braces, snip)) = is_offending_macro(cx, ty.span, self) {
            emit_help(cx, &snip, braces, span);
            self.done.insert(span);
        }
    }
}

fn is_offending_macro<'a>(cx: &EarlyContext<'_>, span: Span, mac_braces: &'a MacroBraces) -> Option<MacroInfo<'a>> {
    let unnested_or_local = || {
        !span.ctxt().outer_expn_data().call_site.from_expansion()
            || span
                .macro_backtrace()
                .last()
                .map_or(false, |e| e.macro_def_id.map_or(false, DefId::is_local))
    };
    let span_call_site = span.ctxt().outer_expn_data().call_site;
    if_chain! {
        if let ExpnKind::Macro(MacroKind::Bang, mac_name) = span.ctxt().outer_expn_data().kind;
        let name = mac_name.as_str();
        if let Some(braces) = mac_braces.macro_braces.get(name);
        if let Some(snip) = snippet_opt(cx, span_call_site);
        // we must check only invocation sites
        // https://github.com/rust-lang/rust-clippy/issues/7422
        if snip.starts_with(&format!("{name}!"));
        if unnested_or_local();
        // make formatting consistent
        let c = snip.replace(' ', "");
        if !c.starts_with(&format!("{name}!{}", braces.0));
        if !mac_braces.done.contains(&span_call_site);
        then {
            Some((span_call_site, braces, snip))
        } else {
            None
        }
    }
}

fn emit_help(cx: &EarlyContext<'_>, snip: &str, braces: &(String, String), span: Span) {
    if let Some((macro_name, macro_args_str)) = snip.split_once('!') {
        let mut macro_args = macro_args_str.trim().to_string();
        // now remove the wrong braces
        macro_args.remove(0);
        macro_args.pop();
        span_lint_and_sugg(
            cx,
            NONSTANDARD_MACRO_BRACES,
            span,
            &format!("use of irregular braces for `{macro_name}!` macro"),
            "consider writing",
            format!("{macro_name}!{}{macro_args}{}", braces.0, braces.1),
            Applicability::MachineApplicable,
        );
    }
}

fn macro_braces(conf: FxHashSet<MacroMatcher>) -> FxHashMap<String, (String, String)> {
    let mut braces = vec![
        macro_matcher!(
            name: "print",
            braces: ("(", ")"),
        ),
        macro_matcher!(
            name: "println",
            braces: ("(", ")"),
        ),
        macro_matcher!(
            name: "eprint",
            braces: ("(", ")"),
        ),
        macro_matcher!(
            name: "eprintln",
            braces: ("(", ")"),
        ),
        macro_matcher!(
            name: "write",
            braces: ("(", ")"),
        ),
        macro_matcher!(
            name: "writeln",
            braces: ("(", ")"),
        ),
        macro_matcher!(
            name: "format",
            braces: ("(", ")"),
        ),
        macro_matcher!(
            name: "format_args",
            braces: ("(", ")"),
        ),
        macro_matcher!(
            name: "vec",
            braces: ("[", "]"),
        ),
        macro_matcher!(
            name: "matches",
            braces: ("(", ")"),
        ),
    ]
    .into_iter()
    .collect::<FxHashMap<_, _>>();
    // We want users items to override any existing items
    for it in conf {
        braces.insert(it.name, it.braces);
    }
    braces
}

macro_rules! macro_matcher {
    (name: $name:expr, braces: ($open:expr, $close:expr) $(,)?) => {
        ($name.to_owned(), ($open.to_owned(), $close.to_owned()))
    };
}
pub(crate) use macro_matcher;

#[derive(Clone, Debug)]
pub struct MacroMatcher {
    name: String,
    braces: (String, String),
}

impl Hash for MacroMatcher {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.name.hash(state);
    }
}

impl PartialEq for MacroMatcher {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name
    }
}
impl Eq for MacroMatcher {}

impl<'de> Deserialize<'de> for MacroMatcher {
    fn deserialize<D>(deser: D) -> Result<Self, D::Error>
    where
        D: de::Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(field_identifier, rename_all = "lowercase")]
        enum Field {
            Name,
            Brace,
        }
        struct MacVisitor;
        impl<'de> de::Visitor<'de> for MacVisitor {
            type Value = MacroMatcher;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str("struct MacroMatcher")
            }

            fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
            where
                V: de::MapAccess<'de>,
            {
                let mut name = None;
                let mut brace: Option<&str> = None;
                while let Some(key) = map.next_key()? {
                    match key {
                        Field::Name => {
                            if name.is_some() {
                                return Err(de::Error::duplicate_field("name"));
                            }
                            name = Some(map.next_value()?);
                        },
                        Field::Brace => {
                            if brace.is_some() {
                                return Err(de::Error::duplicate_field("brace"));
                            }
                            brace = Some(map.next_value()?);
                        },
                    }
                }
                let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
                let brace = brace.ok_or_else(|| de::Error::missing_field("brace"))?;
                Ok(MacroMatcher {
                    name,
                    braces: BRACES
                        .iter()
                        .find(|b| b.0 == brace)
                        .map(|(o, c)| ((*o).to_owned(), (*c).to_owned()))
                        .ok_or_else(|| de::Error::custom(format!("expected one of `(`, `{{`, `[` found `{brace}`")))?,
                })
            }
        }

        const FIELDS: &[&str] = &["name", "brace"];
        deser.deserialize_struct("MacroMatcher", FIELDS, MacVisitor)
    }
}
