//! Diagnostics related methods for `Ty`.

use std::borrow::Cow;
use std::ops::ControlFlow;

use crate::ty::{
    AliasTy, Const, ConstKind, FallibleTypeFolder, InferConst, InferTy, Opaque, PolyTraitPredicate,
    Projection, Ty, TyCtxt, TypeFoldable, TypeSuperFoldable, TypeSuperVisitable, TypeVisitable,
    TypeVisitor,
};

use rustc_data_structures::fx::FxHashMap;
use rustc_errors::{Applicability, Diagnostic, DiagnosticArgValue, IntoDiagnosticArg};
use rustc_hir as hir;
use rustc_hir::def::DefKind;
use rustc_hir::def_id::DefId;
use rustc_hir::{PredicateOrigin, WherePredicate};
use rustc_span::{BytePos, Span};
use rustc_type_ir::sty::TyKind::*;

impl<'tcx> IntoDiagnosticArg for Ty<'tcx> {
    fn into_diagnostic_arg(self) -> DiagnosticArgValue<'static> {
        self.to_string().into_diagnostic_arg()
    }
}

impl<'tcx> Ty<'tcx> {
    /// Similar to `Ty::is_primitive`, but also considers inferred numeric values to be primitive.
    pub fn is_primitive_ty(self) -> bool {
        matches!(
            self.kind(),
            Bool | Char
                | Str
                | Int(_)
                | Uint(_)
                | Float(_)
                | Infer(
                    InferTy::IntVar(_)
                        | InferTy::FloatVar(_)
                        | InferTy::FreshIntTy(_)
                        | InferTy::FreshFloatTy(_)
                )
        )
    }

    /// Whether the type is succinctly representable as a type instead of just referred to with a
    /// description in error messages. This is used in the main error message.
    pub fn is_simple_ty(self) -> bool {
        match self.kind() {
            Bool
            | Char
            | Str
            | Int(_)
            | Uint(_)
            | Float(_)
            | Infer(
                InferTy::IntVar(_)
                | InferTy::FloatVar(_)
                | InferTy::FreshIntTy(_)
                | InferTy::FreshFloatTy(_),
            ) => true,
            Ref(_, x, _) | Array(x, _) | Slice(x) => x.peel_refs().is_simple_ty(),
            Tuple(tys) if tys.is_empty() => true,
            _ => false,
        }
    }

    /// Whether the type is succinctly representable as a type instead of just referred to with a
    /// description in error messages. This is used in the primary span label. Beyond what
    /// `is_simple_ty` includes, it also accepts ADTs with no type arguments and references to
    /// ADTs with no type arguments.
    pub fn is_simple_text(self) -> bool {
        match self.kind() {
            Adt(_, args) => args.non_erasable_generics().next().is_none(),
            Ref(_, ty, _) => ty.is_simple_text(),
            _ => self.is_simple_ty(),
        }
    }
}

pub trait IsSuggestable<'tcx>: Sized {
    /// Whether this makes sense to suggest in a diagnostic.
    ///
    /// We filter out certain types and constants since they don't provide
    /// meaningful rendered suggestions when pretty-printed. We leave some
    /// nonsense, such as region vars, since those render as `'_` and are
    /// usually okay to reinterpret as elided lifetimes.
    ///
    /// Only if `infer_suggestable` is true, we consider type and const
    /// inference variables to be suggestable.
    fn is_suggestable(self, tcx: TyCtxt<'tcx>, infer_suggestable: bool) -> bool;

    fn make_suggestable(self, tcx: TyCtxt<'tcx>, infer_suggestable: bool) -> Option<Self>;
}

impl<'tcx, T> IsSuggestable<'tcx> for T
where
    T: TypeVisitable<TyCtxt<'tcx>> + TypeFoldable<TyCtxt<'tcx>>,
{
    fn is_suggestable(self, tcx: TyCtxt<'tcx>, infer_suggestable: bool) -> bool {
        self.visit_with(&mut IsSuggestableVisitor { tcx, infer_suggestable }).is_continue()
    }

    fn make_suggestable(self, tcx: TyCtxt<'tcx>, infer_suggestable: bool) -> Option<T> {
        self.try_fold_with(&mut MakeSuggestableFolder { tcx, infer_suggestable }).ok()
    }
}

pub fn suggest_arbitrary_trait_bound<'tcx>(
    tcx: TyCtxt<'tcx>,
    generics: &hir::Generics<'_>,
    err: &mut Diagnostic,
    trait_pred: PolyTraitPredicate<'tcx>,
    associated_ty: Option<(&'static str, Ty<'tcx>)>,
) -> bool {
    if !trait_pred.is_suggestable(tcx, false) {
        return false;
    }

    let param_name = trait_pred.skip_binder().self_ty().to_string();
    let mut constraint = trait_pred.to_string();

    if let Some((name, term)) = associated_ty {
        // FIXME: this case overlaps with code in TyCtxt::note_and_explain_type_err.
        // That should be extracted into a helper function.
        if constraint.ends_with('>') {
            constraint = format!("{}, {} = {}>", &constraint[..constraint.len() - 1], name, term);
        } else {
            constraint.push_str(&format!("<{name} = {term}>"));
        }
    }

    let param = generics.params.iter().find(|p| p.name.ident().as_str() == param_name);

    // Skip, there is a param named Self
    if param.is_some() && param_name == "Self" {
        return false;
    }

    // Suggest a where clause bound for a non-type parameter.
    err.span_suggestion_verbose(
        generics.tail_span_for_predicate_suggestion(),
        format!(
            "consider {} `where` clause, but there might be an alternative better way to express \
             this requirement",
            if generics.where_clause_span.is_empty() { "introducing a" } else { "extending the" },
        ),
        format!("{} {constraint}", generics.add_where_or_trailing_comma()),
        Applicability::MaybeIncorrect,
    );
    true
}

#[derive(Debug)]
enum SuggestChangingConstraintsMessage<'a> {
    RestrictBoundFurther,
    RestrictType { ty: &'a str },
    RestrictTypeFurther { ty: &'a str },
    RemoveMaybeUnsized,
    ReplaceMaybeUnsizedWithSized,
}

fn suggest_changing_unsized_bound(
    generics: &hir::Generics<'_>,
    suggestions: &mut Vec<(Span, String, SuggestChangingConstraintsMessage<'_>)>,
    param: &hir::GenericParam<'_>,
    def_id: Option<DefId>,
) {
    // See if there's a `?Sized` bound that can be removed to suggest that.
    // First look at the `where` clause because we can have `where T: ?Sized`,
    // then look at params.
    for (where_pos, predicate) in generics.predicates.iter().enumerate() {
        let WherePredicate::BoundPredicate(predicate) = predicate else {
            continue;
        };
        if !predicate.is_param_bound(param.def_id.to_def_id()) {
            continue;
        };

        for (pos, bound) in predicate.bounds.iter().enumerate() {
            let hir::GenericBound::Trait(poly, hir::TraitBoundModifier::Maybe) = bound else {
                continue;
            };
            if poly.trait_ref.trait_def_id() != def_id {
                continue;
            }
            if predicate.origin == PredicateOrigin::ImplTrait && predicate.bounds.len() == 1 {
                // For `impl ?Sized` with no other bounds, suggest `impl Sized` instead.
                let bound_span = bound.span();
                if bound_span.can_be_used_for_suggestions() {
                    let question_span = bound_span.with_hi(bound_span.lo() + BytePos(1));
                    suggestions.push((
                        question_span,
                        String::new(),
                        SuggestChangingConstraintsMessage::ReplaceMaybeUnsizedWithSized,
                    ));
                }
            } else {
                let sp = generics.span_for_bound_removal(where_pos, pos);
                suggestions.push((
                    sp,
                    String::new(),
                    SuggestChangingConstraintsMessage::RemoveMaybeUnsized,
                ));
            }
        }
    }
}

/// Suggest restricting a type param with a new bound.
///
/// If `span_to_replace` is provided, then that span will be replaced with the
/// `constraint`. If one wasn't provided, then the full bound will be suggested.
pub fn suggest_constraining_type_param(
    tcx: TyCtxt<'_>,
    generics: &hir::Generics<'_>,
    err: &mut Diagnostic,
    param_name: &str,
    constraint: &str,
    def_id: Option<DefId>,
    span_to_replace: Option<Span>,
) -> bool {
    suggest_constraining_type_params(
        tcx,
        generics,
        err,
        [(param_name, constraint, def_id)].into_iter(),
        span_to_replace,
    )
}

/// Suggest restricting a type param with a new bound.
pub fn suggest_constraining_type_params<'a>(
    tcx: TyCtxt<'_>,
    generics: &hir::Generics<'_>,
    err: &mut Diagnostic,
    param_names_and_constraints: impl Iterator<Item = (&'a str, &'a str, Option<DefId>)>,
    span_to_replace: Option<Span>,
) -> bool {
    let mut grouped = FxHashMap::default();
    param_names_and_constraints.for_each(|(param_name, constraint, def_id)| {
        grouped.entry(param_name).or_insert(Vec::new()).push((constraint, def_id))
    });

    let mut applicability = Applicability::MachineApplicable;
    let mut suggestions = Vec::new();

    for (param_name, mut constraints) in grouped {
        let param = generics.params.iter().find(|p| p.name.ident().as_str() == param_name);
        let Some(param) = param else { return false };

        {
            let mut sized_constraints =
                constraints.extract_if(|(_, def_id)| *def_id == tcx.lang_items().sized_trait());
            if let Some((_, def_id)) = sized_constraints.next() {
                applicability = Applicability::MaybeIncorrect;

                err.span_label(param.span, "this type parameter needs to be `Sized`");
                suggest_changing_unsized_bound(generics, &mut suggestions, param, def_id);
            }
        }

        if constraints.is_empty() {
            continue;
        }

        let mut constraint = constraints.iter().map(|&(c, _)| c).collect::<Vec<_>>();
        constraint.sort();
        constraint.dedup();
        let constraint = constraint.join(" + ");
        let mut suggest_restrict = |span, bound_list_non_empty| {
            suggestions.push((
                span,
                if span_to_replace.is_some() {
                    constraint.clone()
                } else if bound_list_non_empty {
                    format!(" + {constraint}")
                } else {
                    format!(" {constraint}")
                },
                SuggestChangingConstraintsMessage::RestrictBoundFurther,
            ))
        };

        if let Some(span) = span_to_replace {
            suggest_restrict(span, true);
            continue;
        }

        // When the type parameter has been provided bounds
        //
        //    Message:
        //      fn foo<T>(t: T) where T: Foo { ... }
        //                            ^^^^^^
        //                            |
        //                            help: consider further restricting this bound with `+ Bar`
        //
        //    Suggestion:
        //      fn foo<T>(t: T) where T: Foo { ... }
        //                                  ^
        //                                  |
        //                                  replace with: ` + Bar`
        //
        // Or, if user has provided some bounds, suggest restricting them:
        //
        //   fn foo<T: Foo>(t: T) { ... }
        //             ---
        //             |
        //             help: consider further restricting this bound with `+ Bar`
        //
        // Suggestion for tools in this case is:
        //
        //   fn foo<T: Foo>(t: T) { ... }
        //          --
        //          |
        //          replace with: `T: Bar +`
        if let Some(span) = generics.bounds_span_for_suggestions(param.def_id) {
            suggest_restrict(span, true);
            continue;
        }

        if generics.has_where_clause_predicates {
            // This part is a bit tricky, because using the `where` clause user can
            // provide zero, one or many bounds for the same type parameter, so we
            // have following cases to consider:
            //
            // When the type parameter has been provided zero bounds
            //
            //    Message:
            //      fn foo<X, Y>(x: X, y: Y) where Y: Foo { ... }
            //             - help: consider restricting this type parameter with `where X: Bar`
            //
            //    Suggestion:
            //      fn foo<X, Y>(x: X, y: Y) where Y: Foo { ... }
            //                                           - insert: `, X: Bar`
            suggestions.push((
                generics.tail_span_for_predicate_suggestion(),
                constraints
                    .iter()
                    .map(|&(constraint, _)| format!(", {param_name}: {constraint}"))
                    .collect::<String>(),
                SuggestChangingConstraintsMessage::RestrictTypeFurther { ty: param_name },
            ));
            continue;
        }

        // Additionally, there may be no `where` clause but the generic parameter has a default:
        //
        //    Message:
        //      trait Foo<T=()> {... }
        //                - help: consider further restricting this type parameter with `where T: Zar`
        //
        //    Suggestion:
        //      trait Foo<T=()> {... }
        //                     - insert: `where T: Zar`
        if matches!(param.kind, hir::GenericParamKind::Type { default: Some(_), .. }) {
            // Suggest a bound, but there is no existing `where` clause *and* the type param has a
            // default (`<T=Foo>`), so we suggest adding `where T: Bar`.
            suggestions.push((
                generics.tail_span_for_predicate_suggestion(),
                format!(" where {param_name}: {constraint}"),
                SuggestChangingConstraintsMessage::RestrictTypeFurther { ty: param_name },
            ));
            continue;
        }

        // If user has provided a colon, don't suggest adding another:
        //
        //   fn foo<T:>(t: T) { ... }
        //            - insert: consider restricting this type parameter with `T: Foo`
        if let Some(colon_span) = param.colon_span {
            suggestions.push((
                colon_span.shrink_to_hi(),
                format!(" {constraint}"),
                SuggestChangingConstraintsMessage::RestrictType { ty: param_name },
            ));
            continue;
        }

        // If user hasn't provided any bounds, suggest adding a new one:
        //
        //   fn foo<T>(t: T) { ... }
        //          - help: consider restricting this type parameter with `T: Foo`
        suggestions.push((
            param.span.shrink_to_hi(),
            format!(": {constraint}"),
            SuggestChangingConstraintsMessage::RestrictType { ty: param_name },
        ));
    }

    // FIXME: remove the suggestions that are from derive, as the span is not correct
    suggestions = suggestions
        .into_iter()
        .filter(|(span, _, _)| !span.in_derive_expansion())
        .collect::<Vec<_>>();

    if suggestions.len() == 1 {
        let (span, suggestion, msg) = suggestions.pop().unwrap();
        let msg = match msg {
            SuggestChangingConstraintsMessage::RestrictBoundFurther => {
                Cow::from("consider further restricting this bound")
            }
            SuggestChangingConstraintsMessage::RestrictType { ty } => {
                Cow::from(format!("consider restricting type parameter `{ty}`"))
            }
            SuggestChangingConstraintsMessage::RestrictTypeFurther { ty } => {
                Cow::from(format!("consider further restricting type parameter `{ty}`"))
            }
            SuggestChangingConstraintsMessage::RemoveMaybeUnsized => {
                Cow::from("consider removing the `?Sized` bound to make the type parameter `Sized`")
            }
            SuggestChangingConstraintsMessage::ReplaceMaybeUnsizedWithSized => {
                Cow::from("consider replacing `?Sized` with `Sized`")
            }
        };

        err.span_suggestion_verbose(span, msg, suggestion, applicability);
    } else if suggestions.len() > 1 {
        err.multipart_suggestion_verbose(
            "consider restricting type parameters",
            suggestions.into_iter().map(|(span, suggestion, _)| (span, suggestion)).collect(),
            applicability,
        );
    }

    true
}

/// Collect al types that have an implicit `'static` obligation that we could suggest `'_` for.
pub struct TraitObjectVisitor<'tcx>(pub Vec<&'tcx hir::Ty<'tcx>>, pub crate::hir::map::Map<'tcx>);

impl<'v> hir::intravisit::Visitor<'v> for TraitObjectVisitor<'v> {
    fn visit_ty(&mut self, ty: &'v hir::Ty<'v>) {
        match ty.kind {
            hir::TyKind::TraitObject(
                _,
                hir::Lifetime {
                    res:
                        hir::LifetimeName::ImplicitObjectLifetimeDefault | hir::LifetimeName::Static,
                    ..
                },
                _,
            ) => {
                self.0.push(ty);
            }
            hir::TyKind::OpaqueDef(item_id, _, _) => {
                self.0.push(ty);
                let item = self.1.item(item_id);
                hir::intravisit::walk_item(self, item);
            }
            _ => {}
        }
        hir::intravisit::walk_ty(self, ty);
    }
}

/// Collect al types that have an implicit `'static` obligation that we could suggest `'_` for.
pub struct StaticLifetimeVisitor<'tcx>(pub Vec<Span>, pub crate::hir::map::Map<'tcx>);

impl<'v> hir::intravisit::Visitor<'v> for StaticLifetimeVisitor<'v> {
    fn visit_lifetime(&mut self, lt: &'v hir::Lifetime) {
        if let hir::LifetimeName::ImplicitObjectLifetimeDefault | hir::LifetimeName::Static = lt.res
        {
            self.0.push(lt.ident.span);
        }
    }
}

pub struct IsSuggestableVisitor<'tcx> {
    tcx: TyCtxt<'tcx>,
    infer_suggestable: bool,
}

impl<'tcx> TypeVisitor<TyCtxt<'tcx>> for IsSuggestableVisitor<'tcx> {
    type BreakTy = ();

    fn visit_ty(&mut self, t: Ty<'tcx>) -> ControlFlow<Self::BreakTy> {
        match *t.kind() {
            Infer(InferTy::TyVar(_)) if self.infer_suggestable => {}

            FnDef(..)
            | Closure(..)
            | Infer(..)
            | Generator(..)
            | GeneratorWitness(..)
            | Bound(_, _)
            | Placeholder(_)
            | Error(_) => {
                return ControlFlow::Break(());
            }

            Alias(Opaque, AliasTy { def_id, .. }) => {
                let parent = self.tcx.parent(def_id);
                let parent_ty = self.tcx.type_of(parent).instantiate_identity();
                if let DefKind::TyAlias { .. } | DefKind::AssocTy = self.tcx.def_kind(parent)
                    && let Alias(Opaque, AliasTy { def_id: parent_opaque_def_id, .. }) = *parent_ty.kind()
                    && parent_opaque_def_id == def_id
                {
                    // Okay
                } else {
                    return ControlFlow::Break(());
                }
            }

            Alias(Projection, AliasTy { def_id, .. }) => {
                if self.tcx.def_kind(def_id) != DefKind::AssocTy {
                    return ControlFlow::Break(());
                }
            }

            Param(param) => {
                // FIXME: It would be nice to make this not use string manipulation,
                // but it's pretty hard to do this, since `ty::ParamTy` is missing
                // sufficient info to determine if it is synthetic, and we don't
                // always have a convenient way of getting `ty::Generics` at the call
                // sites we invoke `IsSuggestable::is_suggestable`.
                if param.name.as_str().starts_with("impl ") {
                    return ControlFlow::Break(());
                }
            }

            _ => {}
        }

        t.super_visit_with(self)
    }

    fn visit_const(&mut self, c: Const<'tcx>) -> ControlFlow<Self::BreakTy> {
        match c.kind() {
            ConstKind::Infer(InferConst::Var(_)) if self.infer_suggestable => {}

            ConstKind::Infer(..)
            | ConstKind::Bound(..)
            | ConstKind::Placeholder(..)
            | ConstKind::Error(..) => {
                return ControlFlow::Break(());
            }
            _ => {}
        }

        c.super_visit_with(self)
    }
}

pub struct MakeSuggestableFolder<'tcx> {
    tcx: TyCtxt<'tcx>,
    infer_suggestable: bool,
}

impl<'tcx> FallibleTypeFolder<TyCtxt<'tcx>> for MakeSuggestableFolder<'tcx> {
    type Error = ();

    fn interner(&self) -> TyCtxt<'tcx> {
        self.tcx
    }

    fn try_fold_ty(&mut self, t: Ty<'tcx>) -> Result<Ty<'tcx>, Self::Error> {
        let t = match *t.kind() {
            Infer(InferTy::TyVar(_)) if self.infer_suggestable => t,

            FnDef(def_id, args) => {
                Ty::new_fn_ptr(self.tcx, self.tcx.fn_sig(def_id).instantiate(self.tcx, args))
            }

            // FIXME(compiler-errors): We could replace these with infer, I guess.
            Closure(..)
            | Infer(..)
            | Generator(..)
            | GeneratorWitness(..)
            | Bound(_, _)
            | Placeholder(_)
            | Error(_) => {
                return Err(());
            }

            Alias(Opaque, AliasTy { def_id, .. }) => {
                let parent = self.tcx.parent(def_id);
                let parent_ty = self.tcx.type_of(parent).instantiate_identity();
                if let hir::def::DefKind::TyAlias { .. } | hir::def::DefKind::AssocTy = self.tcx.def_kind(parent)
                    && let Alias(Opaque, AliasTy { def_id: parent_opaque_def_id, .. }) = *parent_ty.kind()
                    && parent_opaque_def_id == def_id
                {
                    t
                } else {
                    return Err(());
                }
            }

            Param(param) => {
                // FIXME: It would be nice to make this not use string manipulation,
                // but it's pretty hard to do this, since `ty::ParamTy` is missing
                // sufficient info to determine if it is synthetic, and we don't
                // always have a convenient way of getting `ty::Generics` at the call
                // sites we invoke `IsSuggestable::is_suggestable`.
                if param.name.as_str().starts_with("impl ") {
                    return Err(());
                }

                t
            }

            _ => t,
        };

        t.try_super_fold_with(self)
    }

    fn try_fold_const(&mut self, c: Const<'tcx>) -> Result<Const<'tcx>, ()> {
        let c = match c.kind() {
            ConstKind::Infer(InferConst::Var(_)) if self.infer_suggestable => c,

            ConstKind::Infer(..)
            | ConstKind::Bound(..)
            | ConstKind::Placeholder(..)
            | ConstKind::Error(..) => {
                return Err(());
            }

            _ => c,
        };

        c.try_super_fold_with(self)
    }
}

#[derive(Diagnostic)]
#[diag(middle_const_not_used_in_type_alias)]
pub(super) struct ConstNotUsedTraitAlias {
    pub ct: String,
    #[primary_span]
    pub span: Span,
}
