use rustc_ast::LitKind;
use rustc_feature::AttributeStability;
use rustc_hir::attrs::{DeprecatedSince, Deprecation};
use rustc_hir::{RustcVersion, VERSION_PLACEHOLDER};

use super::prelude::*;
use super::util::parse_version;
use crate::session_diagnostics::{
    DeprecatedItemSuggestion, InvalidSince, MissingNote, MissingSince,
};

fn get(
    cx: &mut AcceptContext<'_, '_>,
    name: Symbol,
    param_span: Span,
    arg: &ArgParser,
    item: Option<Symbol>,
) -> Option<Ident> {
    if item.is_some() {
        cx.adcx().duplicate_key(param_span, name);
        return None;
    }
    let v = cx.expect_name_value(arg, param_span, Some(name))?;
    if let Some(value_str) = v.value_as_ident() {
        Some(value_str)
    } else {
        cx.adcx().expected_string_literal(v.value_span, Some(&v.value_as_lit()));
        None
    }
}

pub(crate) struct DeprecatedParser;
impl SingleAttributeParser for DeprecatedParser {
    const PATH: &[Symbol] = &[sym::deprecated];
    const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowListWarnRest(&[
        Allow(Target::Fn),
        Allow(Target::Mod),
        Allow(Target::Struct),
        Allow(Target::Enum),
        Allow(Target::Union),
        Allow(Target::Const),
        Allow(Target::Static),
        Allow(Target::MacroDef),
        Allow(Target::Method(MethodKind::Inherent)),
        Allow(Target::Method(MethodKind::Trait { body: false })),
        Allow(Target::Method(MethodKind::Trait { body: true })),
        Allow(Target::TyAlias),
        Allow(Target::Use),
        Allow(Target::ForeignFn),
        Allow(Target::ForeignStatic),
        Allow(Target::ForeignTy),
        Allow(Target::Field),
        Allow(Target::Trait),
        Allow(Target::AssocTy),
        Allow(Target::AssocConst),
        Allow(Target::Variant),
        Allow(Target::Impl { of_trait: false }),
        Allow(Target::Crate),
        Error(Target::WherePredicate),
    ]);
    const TEMPLATE: AttributeTemplate = template!(
        Word,
        List: &[r#"since = "version""#, r#"note = "reason""#, r#"since = "version", note = "reason""#],
        NameValueStr: "reason"
    );
    const STABILITY: AttributeStability = AttributeStability::Stable;

    fn convert(cx: &mut AcceptContext<'_, '_>, args: &ArgParser) -> Option<AttributeKind> {
        let features = cx.features();

        let mut since = None;
        let mut note: Option<Ident> = None;
        let mut suggestion = None;

        let is_rustc = features.staged_api();

        match args {
            ArgParser::NoArgs => {
                // ok
            }
            ArgParser::List(list) => {
                // If the argument list contains a single string literal:
                // check whether it may be a version and suggest since field
                // otherwise, suggest using NameValue syntax
                if let Some(elem) = list.as_single()
                    && let Some(lit) = elem.as_lit()
                    && let LitKind::Str(text, _) = lit.kind
                {
                    let mut adcx = cx.adcx();

                    match parse_since(text, true) {
                        DeprecatedSince::Future | DeprecatedSince::RustcVersion(_) => {
                            adcx.push_suggestion(
                                String::from("try specifying a deprecated since version"),
                                elem.span(),
                                format!("since = {}", lit.kind),
                            );
                        }
                        _ => {
                            if let Some(span) = args.span() {
                                adcx.push_suggestion(
                                    String::from("try using `=` instead"),
                                    span,
                                    format!(" = {}", lit.kind),
                                );
                            }
                        }
                    };

                    adcx.expected_not_literal(elem.span());
                    return None;
                }

                for param in list.mixed() {
                    let Some(param) = param.meta_item() else {
                        cx.adcx().expected_not_literal(param.span());
                        return None;
                    };

                    let ident_name = param.path().word_sym();

                    match ident_name {
                        Some(name @ sym::since) => {
                            since = Some(get(cx, name, param.span(), param.args(), since)?.name);
                        }
                        Some(name @ sym::note) => {
                            note = Some(get(
                                cx,
                                name,
                                param.span(),
                                param.args(),
                                note.map(|ident| ident.name),
                            )?);
                        }
                        Some(name @ sym::suggestion) => {
                            if !features.deprecated_suggestion() {
                                cx.emit_err(DeprecatedItemSuggestion {
                                    span: param.span(),
                                    is_nightly: cx.sess().is_nightly_build(),
                                    details: (),
                                });
                            }

                            suggestion =
                                Some(get(cx, name, param.span(), param.args(), suggestion)?.name);
                        }
                        _ => {
                            cx.adcx().expected_specific_argument(
                                param.span(),
                                if features.deprecated_suggestion() {
                                    &[sym::since, sym::note, sym::suggestion]
                                } else {
                                    &[sym::since, sym::note]
                                },
                            );
                            return None;
                        }
                    }
                }
            }
            ArgParser::NameValue(v) => {
                let Some(value) = v.value_as_ident() else {
                    cx.adcx().expected_string_literal(v.value_span, Some(v.value_as_lit()));
                    return None;
                };
                note = Some(value);
            }
        }

        let since = if let Some(since) = since {
            let since = parse_since(since, is_rustc);
            if matches!(since, DeprecatedSince::Err) {
                cx.emit_err(InvalidSince { span: cx.attr_span });
            }
            since
        } else if is_rustc {
            cx.emit_err(MissingSince { span: cx.attr_span });
            DeprecatedSince::Err
        } else {
            DeprecatedSince::Unspecified
        };

        if is_rustc && note.is_none() {
            cx.emit_err(MissingNote { span: cx.attr_span });
            return None;
        }

        Some(AttributeKind::Deprecated {
            deprecation: Deprecation { since, note, suggestion },
            span: cx.attr_span,
        })
    }
}

fn parse_since(since: Symbol, is_rustc: bool) -> DeprecatedSince {
    if since.as_str() == "TBD" {
        DeprecatedSince::Future
    } else if !is_rustc {
        DeprecatedSince::NonStandard(since)
    } else if since.as_str() == VERSION_PLACEHOLDER {
        DeprecatedSince::RustcVersion(RustcVersion::CURRENT)
    } else if let Some(version) = parse_version(since) {
        DeprecatedSince::RustcVersion(version)
    } else {
        DeprecatedSince::Err
    }
}
