//! Automatic migration of Rust 2021 patterns to a form valid in both Editions 2021 and 2024.

use rustc_data_structures::fx::FxIndexMap;
use rustc_errors::{Applicability, Diag, EmissionGuarantee, MultiSpan, pluralize};
use rustc_hir::{BindingMode, ByRef, HirId, Mutability};
use rustc_lint_defs::builtin::RUST_2024_INCOMPATIBLE_PAT;
use rustc_middle::ty::{self, Rust2024IncompatiblePatInfo, TyCtxt};
use rustc_span::{Ident, Span};

/// For patterns flagged for migration during HIR typeck, this handles constructing and emitting
/// a diagnostic suggestion.
pub(super) struct PatMigration<'a> {
    suggestion: Vec<(Span, String)>,
    ref_pattern_count: usize,
    binding_mode_count: usize,
    /// Internal state: the ref-mutability of the default binding mode at the subpattern being
    /// lowered, with the span where it was introduced. `None` for a by-value default mode.
    default_mode_span: Option<(Span, ty::Mutability)>,
    /// Labels for where incompatibility-causing by-ref default binding modes were introduced.
    // FIXME(ref_pat_eat_one_layer_2024_structural): To track the default binding mode, we duplicate
    // logic from HIR typeck (in order to avoid needing to store all changes to the dbm in
    // TypeckResults). Since the default binding mode acts differently under this feature gate, the
    // labels will be wrong.
    default_mode_labels: FxIndexMap<Span, Mutability>,
    /// Information collected from typeck, including spans for subpatterns invalid in Rust 2024.
    info: &'a Rust2024IncompatiblePatInfo,
}

impl<'a> PatMigration<'a> {
    pub(super) fn new(info: &'a Rust2024IncompatiblePatInfo) -> Self {
        PatMigration {
            suggestion: Vec::new(),
            ref_pattern_count: 0,
            binding_mode_count: 0,
            default_mode_span: None,
            default_mode_labels: Default::default(),
            info,
        }
    }

    /// On Rust 2024, this emits a hard error. On earlier Editions, this emits the
    /// future-incompatibility lint `rust_2024_incompatible_pat`.
    pub(super) fn emit<'tcx>(self, tcx: TyCtxt<'tcx>, pat_id: HirId) {
        let mut spans =
            MultiSpan::from_spans(self.info.primary_labels.iter().map(|(span, _)| *span).collect());
        for (span, label) in self.info.primary_labels.iter() {
            spans.push_span_label(*span, label.clone());
        }
        // If a relevant span is from at least edition 2024, this is a hard error.
        let is_hard_error = spans.primary_spans().iter().any(|span| span.at_least_rust_2024());
        let primary_message = self.primary_message(is_hard_error);
        if is_hard_error {
            let mut err = tcx.dcx().struct_span_err(spans, primary_message);
            err.note("for more information, see <https://doc.rust-lang.org/reference/patterns.html#binding-modes>");
            self.format_subdiagnostics(&mut err);
            err.emit();
        } else {
            tcx.emit_node_span_lint(
                RUST_2024_INCOMPATIBLE_PAT,
                pat_id,
                spans,
                rustc_errors::DiagDecorator(|diag| {
                    diag.primary_message(primary_message);
                    self.format_subdiagnostics(diag);
                }),
            );
        }
    }

    fn primary_message(&self, is_hard_error: bool) -> String {
        let verb1 = match (self.info.bad_mut_modifiers, self.info.bad_ref_modifiers) {
            (true, true) => "write explicit binding modifiers",
            (true, false) => "mutably bind by value",
            (false, true) => "explicitly borrow",
            (false, false) => "explicitly dereference",
        };
        let or_verb2 = match (
            self.info.bad_mut_modifiers,
            self.info.bad_ref_modifiers,
            self.info.bad_ref_pats,
        ) {
            // We only need two verb phrases if mentioning both modifiers and reference patterns.
            (false, false, _) | (_, _, false) => "",
            // If mentioning `mut`, we don't have an "explicitly" yet.
            (true, _, true) => " or explicitly dereference",
            // If mentioning `ref`/`ref mut` but not `mut`, we already have an "explicitly".
            (false, true, true) => " or dereference",
        };
        let in_rust_2024 = if is_hard_error { "" } else { " in Rust 2024" };
        format!("cannot {verb1}{or_verb2} within an implicitly-borrowing pattern{in_rust_2024}")
    }

    fn format_subdiagnostics(self, diag: &mut Diag<'_, impl EmissionGuarantee>) {
        // Format and emit explanatory notes about default binding modes. Reversing the spans' order
        // means if we have nested spans, the innermost ones will be visited first.
        for (span, def_br_mutbl) in self.default_mode_labels.into_iter().rev() {
            // Don't point to a macro call site.
            if !span.from_expansion() {
                let note_msg = "matching on a reference type with a non-reference pattern implicitly borrows the contents";
                let label_msg = format!(
                    "this non-reference pattern matches on a reference type `{}_`",
                    def_br_mutbl.ref_prefix_str()
                );
                let mut label = MultiSpan::from(span);
                label.push_span_label(span, label_msg);
                diag.span_note(label, note_msg);
            }
        }

        // Format and emit the suggestion.
        let applicability =
            if self.suggestion.iter().all(|(span, _)| span.can_be_used_for_suggestions()) {
                Applicability::MachineApplicable
            } else {
                Applicability::MaybeIncorrect
            };
        let plural_modes = pluralize!(self.binding_mode_count);
        let msg = if self.info.suggest_eliding_modes {
            format!("remove the unnecessary binding modifier{plural_modes}")
        } else {
            let match_on_these_references = if self.ref_pattern_count == 1 {
                "match on the reference with a reference pattern"
            } else {
                "match on these references with reference patterns"
            };
            let and_explain_modes = if self.binding_mode_count > 0 {
                let a = if self.binding_mode_count == 1 { "a " } else { "" };
                format!(" and borrow explicitly using {a}variable binding mode{plural_modes}")
            } else {
                " to avoid implicitly borrowing".to_owned()
            };
            format!("{match_on_these_references}{and_explain_modes}")
        };
        // FIXME(dianne): for peace of mind, don't risk emitting a 0-part suggestion (that panics!)
        debug_assert!(!self.suggestion.is_empty());
        if !self.suggestion.is_empty() {
            diag.multipart_suggestion(msg, self.suggestion, applicability);
        }
    }

    /// Tracks when we're lowering a pattern that implicitly dereferences the scrutinee.
    /// This should only be called when the pattern type adjustments list `adjustments` contains an
    /// implicit deref of a reference type. Returns the prior default binding mode; this should be
    /// followed by a call to [`PatMigration::leave_ref`] to restore it when we leave the pattern.
    pub(super) fn visit_implicit_derefs<'tcx>(
        &mut self,
        pat_span: Span,
        adjustments: &[ty::adjustment::PatAdjustment<'tcx>],
    ) -> Option<(Span, Mutability)> {
        // Implicitly dereferencing references changes the default binding mode, but implicit derefs
        // of smart pointers do not. Thus, we only consider implicit derefs of reference types.
        let implicit_deref_mutbls = adjustments.iter().filter_map(|adjust| {
            if let &ty::Ref(_, _, mutbl) = adjust.source.kind() { Some(mutbl) } else { None }
        });

        if !self.info.suggest_eliding_modes {
            // If we can't fix the pattern by eliding modifiers, we'll need to make the pattern
            // fully explicit. i.e. we'll need to suggest reference patterns for this.
            let suggestion_str: String =
                implicit_deref_mutbls.clone().map(|mutbl| mutbl.ref_prefix_str()).collect();
            self.suggestion.push((pat_span.shrink_to_lo(), suggestion_str));
            self.ref_pattern_count += adjustments.len();
        }

        // Remember if this changed the default binding mode, in case we want to label it.
        let min_mutbl = implicit_deref_mutbls.min().unwrap();
        if self.default_mode_span.is_none_or(|(_, old_mutbl)| min_mutbl < old_mutbl) {
            // This changes the default binding mode to `ref` or `ref mut`. Return the old mode so
            // it can be reinstated when we leave the pattern.
            self.default_mode_span.replace((pat_span, min_mutbl))
        } else {
            // This does not change the default binding mode; it was already `ref` or `ref mut`.
            self.default_mode_span
        }
    }

    /// Tracks the default binding mode when we're lowering a `&` or `&mut` pattern.
    /// Returns the prior default binding mode; this should be followed by a call to
    /// [`PatMigration::leave_ref`] to restore it when we leave the pattern.
    pub(super) fn visit_explicit_deref(&mut self) -> Option<(Span, Mutability)> {
        if let Some((default_mode_span, default_ref_mutbl)) = self.default_mode_span {
            // If this eats a by-ref default binding mode, label the binding mode.
            self.default_mode_labels.insert(default_mode_span, default_ref_mutbl);
        }
        // Set the default binding mode to by-value and return the old default binding mode so it
        // can be reinstated when we leave the pattern.
        self.default_mode_span.take()
    }

    /// Restores the default binding mode after lowering a pattern that could change it.
    /// This should follow a call to either [`PatMigration::visit_explicit_deref`] or
    /// [`PatMigration::visit_implicit_derefs`].
    pub(super) fn leave_ref(&mut self, old_mode_span: Option<(Span, Mutability)>) {
        self.default_mode_span = old_mode_span
    }

    /// Determines if a binding is relevant to the diagnostic and adjusts the notes/suggestion if
    /// so. Bindings are relevant if they have a modifier under a by-ref default mode (invalid in
    /// Rust 2024) or if we need to suggest a binding modifier for them.
    pub(super) fn visit_binding(
        &mut self,
        pat_span: Span,
        mode: BindingMode,
        explicit_ba: BindingMode,
        ident: Ident,
    ) {
        if explicit_ba != BindingMode::NONE
            && let Some((default_mode_span, default_ref_mutbl)) = self.default_mode_span
        {
            // If this overrides a by-ref default binding mode, label the binding mode.
            self.default_mode_labels.insert(default_mode_span, default_ref_mutbl);
            // If our suggestion is to elide redundnt modes, this will be one of them.
            if self.info.suggest_eliding_modes {
                self.suggestion.push((pat_span.with_hi(ident.span.lo()), String::new()));
                self.binding_mode_count += 1;
            }
        }
        if !self.info.suggest_eliding_modes
            && explicit_ba.0 == ByRef::No
            && let ByRef::Yes(_, mutbl) = mode.0
        {
            // If we can't fix the pattern by eliding modifiers, we'll need to make the pattern
            // fully explicit. i.e. we'll need to suggest reference patterns for this.
            let sugg_str = match mutbl {
                Mutability::Not => "ref ",
                Mutability::Mut => "ref mut ",
            };
            self.suggestion
                .push((pat_span.with_lo(ident.span.lo()).shrink_to_lo(), sugg_str.to_owned()));
            self.binding_mode_count += 1;
        }
    }
}
