use rustc_feature::AttributeStability;
use rustc_hir::attrs::{CrateType, WindowsSubsystemKind};
use rustc_session::lint::builtin::UNKNOWN_CRATE_TYPES;
use rustc_span::Symbol;
use rustc_span::edit_distance::find_best_match_for_name_with_substrings;

use super::prelude::*;
use crate::diagnostics::{UnknownCrateTypes, UnknownCrateTypesSuggestion};

pub(crate) struct CrateNameParser;

impl SingleAttributeParser for CrateNameParser {
    const PATH: &[Symbol] = &[sym::crate_name];
    const ON_DUPLICATE: OnDuplicate = OnDuplicate::WarnButFutureError;
    const TEMPLATE: AttributeTemplate = template!(NameValueStr: "name");
    const ALLOWED_TARGETS: AllowedTargets<'_> =
        AllowedTargets::AllowListWarnRest(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = AttributeStability::Stable;

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

        let name = cx.expect_string_literal(n)?;

        Some(AttributeKind::CrateName { name, name_span: n.value_span, attr_span: cx.attr_span })
    }
}

pub(crate) struct CrateTypeParser;

impl CombineAttributeParser for CrateTypeParser {
    const PATH: &[Symbol] = &[sym::crate_type];
    type Item = CrateType;
    const CONVERT: ConvertFn<Self::Item> = |items, _| AttributeKind::CrateType(items);
    const ALLOWED_TARGETS: AllowedTargets<'_> =
        AllowedTargets::AllowListWarnRest(&[Allow(Target::Crate)]);
    const TEMPLATE: AttributeTemplate =
        template!(NameValueStr: "crate type", "https://doc.rust-lang.org/reference/linkage.html");
    const STABILITY: AttributeStability = AttributeStability::Stable;

    fn extend(
        cx: &mut AcceptContext<'_, '_>,
        args: &ArgParser,
    ) -> impl IntoIterator<Item = Self::Item> {
        let n = cx.expect_name_value(args, cx.attr_span, None)?;

        let crate_type = cx.expect_string_literal(n)?;

        let Ok(crate_type) = crate_type.try_into() else {
            // We don't error on invalid `#![crate_type]` when not applied to a crate
            if cx.shared.target == Target::Crate {
                let candidate = find_best_match_for_name_with_substrings(
                    &CrateType::all_stable().iter().map(|(name, _)| *name).collect::<Vec<_>>(),
                    crate_type,
                    Some(5),
                );
                let span = n.value_span;
                cx.emit_lint(
                    UNKNOWN_CRATE_TYPES,
                    UnknownCrateTypes {
                        sugg: candidate.map(|s| UnknownCrateTypesSuggestion { span, snippet: s }),
                    },
                    span,
                );
            }
            return None;
        };

        Some(crate_type)
    }
}

pub(crate) struct RecursionLimitParser;

impl SingleAttributeParser for RecursionLimitParser {
    const PATH: &[Symbol] = &[sym::recursion_limit];
    const ON_DUPLICATE: OnDuplicate = OnDuplicate::WarnButFutureError;
    const TEMPLATE: AttributeTemplate = template!(NameValueStr: "N", "https://doc.rust-lang.org/reference/attributes/limits.html#the-recursion_limit-attribute");
    const ALLOWED_TARGETS: AllowedTargets<'_> =
        AllowedTargets::AllowListWarnRest(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = AttributeStability::Stable;

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

        Some(AttributeKind::RecursionLimit { limit: cx.parse_limit_int(nv)? })
    }
}

pub(crate) struct MoveSizeLimitParser;

impl SingleAttributeParser for MoveSizeLimitParser {
    const PATH: &[Symbol] = &[sym::move_size_limit];
    const TEMPLATE: AttributeTemplate = template!(NameValueStr: "N");
    const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = unstable!(large_assignments);

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

        Some(AttributeKind::MoveSizeLimit { limit: cx.parse_limit_int(nv)? })
    }
}

pub(crate) struct TypeLengthLimitParser;

impl SingleAttributeParser for TypeLengthLimitParser {
    const PATH: &[Symbol] = &[sym::type_length_limit];
    const ON_DUPLICATE: OnDuplicate = OnDuplicate::WarnButFutureError;
    const TEMPLATE: AttributeTemplate = template!(NameValueStr: "N");
    const ALLOWED_TARGETS: AllowedTargets<'_> =
        AllowedTargets::AllowListWarnRest(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = AttributeStability::Stable;

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

        Some(AttributeKind::TypeLengthLimit { limit: cx.parse_limit_int(nv)? })
    }
}

pub(crate) struct PatternComplexityLimitParser;

impl SingleAttributeParser for PatternComplexityLimitParser {
    const PATH: &[Symbol] = &[sym::pattern_complexity_limit];
    const TEMPLATE: AttributeTemplate = template!(NameValueStr: "N");
    const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = unstable!(
        rustc_attrs,
        "the `#[pattern_complexity_limit]` attribute is used for rustc unit tests"
    );

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

        Some(AttributeKind::PatternComplexityLimit { limit: cx.parse_limit_int(nv)? })
    }
}

pub(crate) struct NoCoreParser;

impl NoArgsAttributeParser for NoCoreParser {
    const PATH: &[Symbol] = &[sym::no_core];
    const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = unstable!(no_core);
    const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::NoCore;
}

pub(crate) struct NoStdParser;

impl NoArgsAttributeParser for NoStdParser {
    const PATH: &[Symbol] = &[sym::no_std];
    const ON_DUPLICATE: OnDuplicate = OnDuplicate::Warn;
    const ALLOWED_TARGETS: AllowedTargets<'_> =
        AllowedTargets::AllowListWarnRest(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = AttributeStability::Stable;
    const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::NoStd;
}

pub(crate) struct NoMainParser;

impl NoArgsAttributeParser for NoMainParser {
    const PATH: &[Symbol] = &[sym::no_main];
    const ON_DUPLICATE: OnDuplicate = OnDuplicate::Warn;
    const ALLOWED_TARGETS: AllowedTargets<'_> =
        AllowedTargets::AllowListWarnRest(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = AttributeStability::Stable;
    const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::NoMain;
}

pub(crate) struct RustcCoherenceIsCoreParser;

impl NoArgsAttributeParser for RustcCoherenceIsCoreParser {
    const PATH: &[Symbol] = &[sym::rustc_coherence_is_core];
    const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = unstable!(rustc_attrs);
    const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::RustcCoherenceIsCore;
}

pub(crate) struct WindowsSubsystemParser;

impl SingleAttributeParser for WindowsSubsystemParser {
    const PATH: &[Symbol] = &[sym::windows_subsystem];
    const ON_DUPLICATE: OnDuplicate = OnDuplicate::WarnButFutureError;
    const ALLOWED_TARGETS: AllowedTargets<'_> =
        AllowedTargets::AllowListWarnRest(&[Allow(Target::Crate)]);
    const TEMPLATE: AttributeTemplate = template!(NameValueStr: ["windows", "console"], "https://doc.rust-lang.org/reference/runtime.html#the-windows_subsystem-attribute");
    const STABILITY: AttributeStability = AttributeStability::Stable;

    fn convert(cx: &mut AcceptContext<'_, '_>, args: &ArgParser) -> Option<AttributeKind> {
        let nv = cx.expect_name_value(args, cx.inner_span, Some(sym::windows_subsystem))?;

        let kind = match nv.value_as_str() {
            Some(sym::console) => WindowsSubsystemKind::Console,
            Some(sym::windows) => WindowsSubsystemKind::Windows,
            Some(_) | None => {
                cx.adcx().expected_specific_argument_strings(
                    nv.value_span,
                    &[sym::console, sym::windows],
                );
                return None;
            }
        };

        Some(AttributeKind::WindowsSubsystem(kind))
    }
}

pub(crate) struct PanicRuntimeParser;

impl NoArgsAttributeParser for PanicRuntimeParser {
    const PATH: &[Symbol] = &[sym::panic_runtime];
    const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = unstable!(panic_runtime);
    const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::PanicRuntime;
}

pub(crate) struct NeedsPanicRuntimeParser;

impl NoArgsAttributeParser for NeedsPanicRuntimeParser {
    const PATH: &[Symbol] = &[sym::needs_panic_runtime];
    const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = unstable!(needs_panic_runtime);
    const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::NeedsPanicRuntime;
}

pub(crate) struct ProfilerRuntimeParser;

impl NoArgsAttributeParser for ProfilerRuntimeParser {
    const PATH: &[Symbol] = &[sym::profiler_runtime];
    const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = unstable!(profiler_runtime);
    const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::ProfilerRuntime;
}

pub(crate) struct NoBuiltinsParser;

impl NoArgsAttributeParser for NoBuiltinsParser {
    const PATH: &[Symbol] = &[sym::no_builtins];
    const ON_DUPLICATE: OnDuplicate = OnDuplicate::Warn;
    const ALLOWED_TARGETS: AllowedTargets<'_> =
        AllowedTargets::AllowListWarnRest(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = AttributeStability::Stable;
    const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::NoBuiltins;
}

pub(crate) struct RustcPreserveUbChecksParser;

impl NoArgsAttributeParser for RustcPreserveUbChecksParser {
    const PATH: &[Symbol] = &[sym::rustc_preserve_ub_checks];
    const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = unstable!(rustc_attrs);
    const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::RustcPreserveUbChecks;
}

pub(crate) struct RustcNoImplicitBoundsParser;

impl NoArgsAttributeParser for RustcNoImplicitBoundsParser {
    const PATH: &[Symbol] = &[sym::rustc_no_implicit_bounds];
    const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = unstable!(rustc_attrs);
    const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::RustcNoImplicitBounds;
}

pub(crate) struct DefaultLibAllocatorParser;

impl NoArgsAttributeParser for DefaultLibAllocatorParser {
    const PATH: &[Symbol] = &[sym::default_lib_allocator];
    const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[Allow(Target::Crate)]);
    const STABILITY: AttributeStability = unstable!(allocator_internals);
    const CREATE: fn(Span) -> AttributeKind = |_| AttributeKind::DefaultLibAllocator;
}

pub(crate) struct FeatureParser;

impl CombineAttributeParser for FeatureParser {
    const PATH: &[Symbol] = &[sym::feature];
    type Item = Ident;
    const CONVERT: ConvertFn<Self::Item> = AttributeKind::Feature;
    const ALLOWED_TARGETS: AllowedTargets<'_> =
        AllowedTargets::AllowListWarnRest(&[Allow(Target::Crate)]);
    const TEMPLATE: AttributeTemplate = template!(List: &["feature1, feature2, ..."]);
    const STABILITY: AttributeStability = AttributeStability::Stable;

    fn extend(
        cx: &mut AcceptContext<'_, '_>,
        args: &ArgParser,
    ) -> impl IntoIterator<Item = Self::Item> {
        let Some(list) = cx.expect_list(args, cx.attr_span) else {
            return Vec::new();
        };

        if list.is_empty() {
            let attr_span = cx.attr_span;
            cx.adcx().warn_empty_attribute(attr_span);
        }

        let mut res = Vec::new();

        for elem in list.mixed() {
            let Some(elem) = elem.meta_item() else {
                cx.adcx().expected_identifier(elem.span());
                continue;
            };
            let Some(()) = cx.expect_no_args(elem.args()) else {
                continue;
            };
            let path = elem.path();
            let Some(ident) = path.word() else {
                cx.adcx().expected_identifier(path.span());
                continue;
            };
            res.push(ident);
        }

        res
    }
}

pub(crate) struct RegisterToolParser;

impl CombineAttributeParser for RegisterToolParser {
    const PATH: &[Symbol] = &[sym::register_tool];
    type Item = Ident;
    const CONVERT: ConvertFn<Self::Item> = |tools, _span| AttributeKind::RegisterTool(tools);
    const ALLOWED_TARGETS: AllowedTargets<'_> = AllowedTargets::AllowList(&[Allow(Target::Crate)]);
    const TEMPLATE: AttributeTemplate = template!(List: &["tool1, tool2, ..."]);
    const STABILITY: AttributeStability = unstable!(register_tool);

    fn extend(
        cx: &mut AcceptContext<'_, '_>,
        args: &ArgParser,
    ) -> impl IntoIterator<Item = Self::Item> {
        let Some(list) = cx.expect_list(args, cx.attr_span) else {
            return Vec::new();
        };

        if list.is_empty() {
            let attr_span = cx.attr_span;
            cx.adcx().warn_empty_attribute(attr_span);
        }

        let mut res = Vec::new();

        for elem in list.mixed() {
            let Some(elem) = elem.meta_item() else {
                cx.adcx().expected_identifier(elem.span());
                continue;
            };
            let Some(()) = cx.expect_no_args(elem.args()) else {
                continue;
            };

            let path = elem.path();
            let Some(ident) = path.word() else {
                cx.adcx().expected_identifier(path.span());
                continue;
            };

            res.push(ident);
        }

        res
    }
}
