//! Print diagnostics to explain why values are borrowed.

use std::collections::VecDeque;

use rustc_data_structures::fx::FxHashSet;
use rustc_errors::{Applicability, DiagnosticBuilder};
use rustc_index::vec::IndexVec;
use rustc_infer::infer::NLLRegionVariableOrigin;
use rustc_middle::mir::{
    Body, CastKind, ConstraintCategory, FakeReadCause, Local, Location, Operand, Place, Rvalue,
    Statement, StatementKind, TerminatorKind,
};
use rustc_middle::ty::adjustment::PointerCast;
use rustc_middle::ty::{self, RegionVid, TyCtxt};
use rustc_span::symbol::Symbol;
use rustc_span::Span;

use crate::borrow_check::{
    borrow_set::BorrowData, nll::ConstraintDescription, region_infer::Cause, MirBorrowckCtxt,
    WriteKind,
};

use super::{find_use, RegionName, UseSpans};

#[derive(Debug)]
pub(in crate::borrow_check) enum BorrowExplanation {
    UsedLater(LaterUseKind, Span),
    UsedLaterInLoop(LaterUseKind, Span),
    UsedLaterWhenDropped {
        drop_loc: Location,
        dropped_local: Local,
        should_note_order: bool,
    },
    MustBeValidFor {
        category: ConstraintCategory,
        from_closure: bool,
        span: Span,
        region_name: RegionName,
        opt_place_desc: Option<String>,
    },
    Unexplained,
}

#[derive(Clone, Copy, Debug)]
pub(in crate::borrow_check) enum LaterUseKind {
    TraitCapture,
    ClosureCapture,
    Call,
    FakeLetRead,
    Other,
}

impl BorrowExplanation {
    pub(in crate::borrow_check) fn is_explained(&self) -> bool {
        match self {
            BorrowExplanation::Unexplained => false,
            _ => true,
        }
    }
    pub(in crate::borrow_check) fn add_explanation_to_diagnostic<'tcx>(
        &self,
        tcx: TyCtxt<'tcx>,
        body: &Body<'tcx>,
        local_names: &IndexVec<Local, Option<Symbol>>,
        err: &mut DiagnosticBuilder<'_>,
        borrow_desc: &str,
        borrow_span: Option<Span>,
    ) {
        match *self {
            BorrowExplanation::UsedLater(later_use_kind, var_or_use_span) => {
                let message = match later_use_kind {
                    LaterUseKind::TraitCapture => "captured here by trait object",
                    LaterUseKind::ClosureCapture => "captured here by closure",
                    LaterUseKind::Call => "used by call",
                    LaterUseKind::FakeLetRead => "stored here",
                    LaterUseKind::Other => "used here",
                };
                if !borrow_span.map(|sp| sp.overlaps(var_or_use_span)).unwrap_or(false) {
                    err.span_label(
                        var_or_use_span,
                        format!("{}borrow later {}", borrow_desc, message),
                    );
                }
            }
            BorrowExplanation::UsedLaterInLoop(later_use_kind, var_or_use_span) => {
                let message = match later_use_kind {
                    LaterUseKind::TraitCapture => {
                        "borrow captured here by trait object, in later iteration of loop"
                    }
                    LaterUseKind::ClosureCapture => {
                        "borrow captured here by closure, in later iteration of loop"
                    }
                    LaterUseKind::Call => "borrow used by call, in later iteration of loop",
                    LaterUseKind::FakeLetRead => "borrow later stored here",
                    LaterUseKind::Other => "borrow used here, in later iteration of loop",
                };
                err.span_label(var_or_use_span, format!("{}{}", borrow_desc, message));
            }
            BorrowExplanation::UsedLaterWhenDropped {
                drop_loc,
                dropped_local,
                should_note_order,
            } => {
                let local_decl = &body.local_decls[dropped_local];
                let (dtor_desc, type_desc) = match local_decl.ty.kind() {
                    // If type is an ADT that implements Drop, then
                    // simplify output by reporting just the ADT name.
                    ty::Adt(adt, _substs) if adt.has_dtor(tcx) && !adt.is_box() => {
                        ("`Drop` code", format!("type `{}`", tcx.def_path_str(adt.did)))
                    }

                    // Otherwise, just report the whole type (and use
                    // the intentionally fuzzy phrase "destructor")
                    ty::Closure(..) => ("destructor", "closure".to_owned()),
                    ty::Generator(..) => ("destructor", "generator".to_owned()),

                    _ => ("destructor", format!("type `{}`", local_decl.ty)),
                };

                match local_names[dropped_local] {
                    Some(local_name) if !local_decl.from_compiler_desugaring() => {
                        let message = format!(
                            "{B}borrow might be used here, when `{LOC}` is dropped \
                             and runs the {DTOR} for {TYPE}",
                            B = borrow_desc,
                            LOC = local_name,
                            TYPE = type_desc,
                            DTOR = dtor_desc
                        );
                        err.span_label(body.source_info(drop_loc).span, message);

                        if should_note_order {
                            err.note(
                                "values in a scope are dropped \
                                 in the opposite order they are defined",
                            );
                        }
                    }
                    _ => {
                        err.span_label(
                            local_decl.source_info.span,
                            format!(
                                "a temporary with access to the {B}borrow \
                                 is created here ...",
                                B = borrow_desc
                            ),
                        );
                        let message = format!(
                            "... and the {B}borrow might be used here, \
                             when that temporary is dropped \
                             and runs the {DTOR} for {TYPE}",
                            B = borrow_desc,
                            TYPE = type_desc,
                            DTOR = dtor_desc
                        );
                        err.span_label(body.source_info(drop_loc).span, message);

                        if let Some(info) = &local_decl.is_block_tail {
                            if info.tail_result_is_ignored {
                                err.span_suggestion_verbose(
                                    info.span.shrink_to_hi(),
                                    "consider adding semicolon after the expression so its \
                                     temporaries are dropped sooner, before the local variables \
                                     declared by the block are dropped",
                                    ";".to_string(),
                                    Applicability::MaybeIncorrect,
                                );
                            } else {
                                err.note(
                                    "the temporary is part of an expression at the end of a \
                                     block;\nconsider forcing this temporary to be dropped sooner, \
                                     before the block's local variables are dropped",
                                );
                                err.multipart_suggestion(
                                    "for example, you could save the expression's value in a new \
                                     local variable `x` and then make `x` be the expression at the \
                                     end of the block",
                                    vec![
                                        (info.span.shrink_to_lo(), "let x = ".to_string()),
                                        (info.span.shrink_to_hi(), "; x".to_string()),
                                    ],
                                    Applicability::MaybeIncorrect,
                                );
                            };
                        }
                    }
                }
            }
            BorrowExplanation::MustBeValidFor {
                category,
                span,
                ref region_name,
                ref opt_place_desc,
                from_closure: _,
            } => {
                region_name.highlight_region_name(err);

                if let Some(desc) = opt_place_desc {
                    err.span_label(
                        span,
                        format!(
                            "{}requires that `{}` is borrowed for `{}`",
                            category.description(),
                            desc,
                            region_name,
                        ),
                    );
                } else {
                    err.span_label(
                        span,
                        format!(
                            "{}requires that {}borrow lasts for `{}`",
                            category.description(),
                            borrow_desc,
                            region_name,
                        ),
                    );
                };

                self.add_lifetime_bound_suggestion_to_diagnostic(err, &category, span, region_name);
            }
            _ => {}
        }
    }
    pub(in crate::borrow_check) fn add_lifetime_bound_suggestion_to_diagnostic(
        &self,
        err: &mut DiagnosticBuilder<'_>,
        category: &ConstraintCategory,
        span: Span,
        region_name: &RegionName,
    ) {
        if let ConstraintCategory::OpaqueType = category {
            let suggestable_name =
                if region_name.was_named() { region_name.to_string() } else { "'_".to_string() };

            let msg = format!(
                "you can add a bound to the {}to make it last less than `'static` and match `{}`",
                category.description(),
                region_name,
            );

            err.span_suggestion_verbose(
                span.shrink_to_hi(),
                &msg,
                format!(" + {}", suggestable_name),
                Applicability::Unspecified,
            );
        }
    }
}

impl<'cx, 'tcx> MirBorrowckCtxt<'cx, 'tcx> {
    fn free_region_constraint_info(
        &self,
        borrow_region: RegionVid,
        outlived_region: RegionVid,
    ) -> (ConstraintCategory, bool, Span, Option<RegionName>) {
        let (category, from_closure, span) = self.regioncx.best_blame_constraint(
            &self.body,
            borrow_region,
            NLLRegionVariableOrigin::FreeRegion,
            |r| self.regioncx.provides_universal_region(r, borrow_region, outlived_region),
        );

        let outlived_fr_name = self.give_region_a_name(outlived_region);

        (category, from_closure, span, outlived_fr_name)
    }

    /// Returns structured explanation for *why* the borrow contains the
    /// point from `location`. This is key for the "3-point errors"
    /// [described in the NLL RFC][d].
    ///
    /// # Parameters
    ///
    /// - `borrow`: the borrow in question
    /// - `location`: where the borrow occurs
    /// - `kind_place`: if Some, this describes the statement that triggered the error.
    ///   - first half is the kind of write, if any, being performed
    ///   - second half is the place being accessed
    ///
    /// [d]: https://rust-lang.github.io/rfcs/2094-nll.html#leveraging-intuition-framing-errors-in-terms-of-points
    pub(in crate::borrow_check) fn explain_why_borrow_contains_point(
        &self,
        location: Location,
        borrow: &BorrowData<'tcx>,
        kind_place: Option<(WriteKind, Place<'tcx>)>,
    ) -> BorrowExplanation {
        debug!(
            "explain_why_borrow_contains_point(location={:?}, borrow={:?}, kind_place={:?})",
            location, borrow, kind_place
        );

        let regioncx = &self.regioncx;
        let body: &Body<'_> = &self.body;
        let tcx = self.infcx.tcx;

        let borrow_region_vid = borrow.region;
        debug!("explain_why_borrow_contains_point: borrow_region_vid={:?}", borrow_region_vid);

        let region_sub = self.regioncx.find_sub_region_live_at(borrow_region_vid, location);
        debug!("explain_why_borrow_contains_point: region_sub={:?}", region_sub);

        match find_use::find(body, regioncx, tcx, region_sub, location) {
            Some(Cause::LiveVar(local, location)) => {
                let span = body.source_info(location).span;
                let spans = self
                    .move_spans(Place::from(local).as_ref(), location)
                    .or_else(|| self.borrow_spans(span, location));

                let borrow_location = location;
                if self.is_use_in_later_iteration_of_loop(borrow_location, location) {
                    let later_use = self.later_use_kind(borrow, spans, location);
                    BorrowExplanation::UsedLaterInLoop(later_use.0, later_use.1)
                } else {
                    // Check if the location represents a `FakeRead`, and adapt the error
                    // message to the `FakeReadCause` it is from: in particular,
                    // the ones inserted in optimized `let var = <expr>` patterns.
                    let later_use = self.later_use_kind(borrow, spans, location);
                    BorrowExplanation::UsedLater(later_use.0, later_use.1)
                }
            }

            Some(Cause::DropVar(local, location)) => {
                let mut should_note_order = false;
                if self.local_names[local].is_some() {
                    if let Some((WriteKind::StorageDeadOrDrop, place)) = kind_place {
                        if let Some(borrowed_local) = place.as_local() {
                            if self.local_names[borrowed_local].is_some() && local != borrowed_local
                            {
                                should_note_order = true;
                            }
                        }
                    }
                }

                BorrowExplanation::UsedLaterWhenDropped {
                    drop_loc: location,
                    dropped_local: local,
                    should_note_order,
                }
            }

            None => {
                if let Some(region) = self.to_error_region_vid(borrow_region_vid) {
                    let (category, from_closure, span, region_name) =
                        self.free_region_constraint_info(borrow_region_vid, region);
                    if let Some(region_name) = region_name {
                        let opt_place_desc = self.describe_place(borrow.borrowed_place.as_ref());
                        BorrowExplanation::MustBeValidFor {
                            category,
                            from_closure,
                            span,
                            region_name,
                            opt_place_desc,
                        }
                    } else {
                        debug!(
                            "explain_why_borrow_contains_point: \
                             Could not generate a region name"
                        );
                        BorrowExplanation::Unexplained
                    }
                } else {
                    debug!(
                        "explain_why_borrow_contains_point: \
                         Could not generate an error region vid"
                    );
                    BorrowExplanation::Unexplained
                }
            }
        }
    }

    /// true if `borrow_location` can reach `use_location` by going through a loop and
    /// `use_location` is also inside of that loop
    fn is_use_in_later_iteration_of_loop(
        &self,
        borrow_location: Location,
        use_location: Location,
    ) -> bool {
        let back_edge = self.reach_through_backedge(borrow_location, use_location);
        back_edge.map_or(false, |back_edge| self.can_reach_head_of_loop(use_location, back_edge))
    }

    /// Returns the outmost back edge if `from` location can reach `to` location passing through
    /// that back edge
    fn reach_through_backedge(&self, from: Location, to: Location) -> Option<Location> {
        let mut visited_locations = FxHashSet::default();
        let mut pending_locations = VecDeque::new();
        visited_locations.insert(from);
        pending_locations.push_back(from);
        debug!("reach_through_backedge: from={:?} to={:?}", from, to,);

        let mut outmost_back_edge = None;
        while let Some(location) = pending_locations.pop_front() {
            debug!(
                "reach_through_backedge: location={:?} outmost_back_edge={:?}
                   pending_locations={:?} visited_locations={:?}",
                location, outmost_back_edge, pending_locations, visited_locations
            );

            if location == to && outmost_back_edge.is_some() {
                // We've managed to reach the use location
                debug!("reach_through_backedge: found!");
                return outmost_back_edge;
            }

            let block = &self.body.basic_blocks()[location.block];

            if location.statement_index < block.statements.len() {
                let successor = location.successor_within_block();
                if visited_locations.insert(successor) {
                    pending_locations.push_back(successor);
                }
            } else {
                pending_locations.extend(
                    block
                        .terminator()
                        .successors()
                        .map(|bb| Location { statement_index: 0, block: *bb })
                        .filter(|s| visited_locations.insert(*s))
                        .map(|s| {
                            if self.is_back_edge(location, s) {
                                match outmost_back_edge {
                                    None => {
                                        outmost_back_edge = Some(location);
                                    }

                                    Some(back_edge)
                                        if location.dominates(back_edge, &self.dominators) =>
                                    {
                                        outmost_back_edge = Some(location);
                                    }

                                    Some(_) => {}
                                }
                            }

                            s
                        }),
                );
            }
        }

        None
    }

    /// true if `from` location can reach `loop_head` location and `loop_head` dominates all the
    /// intermediate nodes
    fn can_reach_head_of_loop(&self, from: Location, loop_head: Location) -> bool {
        self.find_loop_head_dfs(from, loop_head, &mut FxHashSet::default())
    }

    fn find_loop_head_dfs(
        &self,
        from: Location,
        loop_head: Location,
        visited_locations: &mut FxHashSet<Location>,
    ) -> bool {
        visited_locations.insert(from);

        if from == loop_head {
            return true;
        }

        if loop_head.dominates(from, &self.dominators) {
            let block = &self.body.basic_blocks()[from.block];

            if from.statement_index < block.statements.len() {
                let successor = from.successor_within_block();

                if !visited_locations.contains(&successor)
                    && self.find_loop_head_dfs(successor, loop_head, visited_locations)
                {
                    return true;
                }
            } else {
                for bb in block.terminator().successors() {
                    let successor = Location { statement_index: 0, block: *bb };

                    if !visited_locations.contains(&successor)
                        && self.find_loop_head_dfs(successor, loop_head, visited_locations)
                    {
                        return true;
                    }
                }
            }
        }

        false
    }

    /// True if an edge `source -> target` is a backedge -- in other words, if the target
    /// dominates the source.
    fn is_back_edge(&self, source: Location, target: Location) -> bool {
        target.dominates(source, &self.dominators)
    }

    /// Determine how the borrow was later used.
    fn later_use_kind(
        &self,
        borrow: &BorrowData<'tcx>,
        use_spans: UseSpans<'tcx>,
        location: Location,
    ) -> (LaterUseKind, Span) {
        match use_spans {
            UseSpans::ClosureUse { var_span, .. } => {
                // Used in a closure.
                (LaterUseKind::ClosureCapture, var_span)
            }
            UseSpans::PatUse(span)
            | UseSpans::OtherUse(span)
            | UseSpans::FnSelfUse { var_span: span, .. } => {
                let block = &self.body.basic_blocks()[location.block];

                let kind = if let Some(&Statement {
                    kind: StatementKind::FakeRead(FakeReadCause::ForLet, _),
                    ..
                }) = block.statements.get(location.statement_index)
                {
                    LaterUseKind::FakeLetRead
                } else if self.was_captured_by_trait_object(borrow) {
                    LaterUseKind::TraitCapture
                } else if location.statement_index == block.statements.len() {
                    if let TerminatorKind::Call { ref func, from_hir_call: true, .. } =
                        block.terminator().kind
                    {
                        // Just point to the function, to reduce the chance of overlapping spans.
                        let function_span = match func {
                            Operand::Constant(c) => c.span,
                            Operand::Copy(place) | Operand::Move(place) => {
                                if let Some(l) = place.as_local() {
                                    let local_decl = &self.body.local_decls[l];
                                    if self.local_names[l].is_none() {
                                        local_decl.source_info.span
                                    } else {
                                        span
                                    }
                                } else {
                                    span
                                }
                            }
                        };
                        return (LaterUseKind::Call, function_span);
                    } else {
                        LaterUseKind::Other
                    }
                } else {
                    LaterUseKind::Other
                };

                (kind, span)
            }
        }
    }

    /// Checks if a borrowed value was captured by a trait object. We do this by
    /// looking forward in the MIR from the reserve location and checking if we see
    /// a unsized cast to a trait object on our data.
    fn was_captured_by_trait_object(&self, borrow: &BorrowData<'tcx>) -> bool {
        // Start at the reserve location, find the place that we want to see cast to a trait object.
        let location = borrow.reserve_location;
        let block = &self.body[location.block];
        let stmt = block.statements.get(location.statement_index);
        debug!("was_captured_by_trait_object: location={:?} stmt={:?}", location, stmt);

        // We make a `queue` vector that has the locations we want to visit. As of writing, this
        // will only ever have one item at any given time, but by using a vector, we can pop from
        // it which simplifies the termination logic.
        let mut queue = vec![location];
        let mut target = if let Some(&Statement {
            kind: StatementKind::Assign(box (ref place, _)),
            ..
        }) = stmt
        {
            if let Some(local) = place.as_local() {
                local
            } else {
                return false;
            }
        } else {
            return false;
        };

        debug!("was_captured_by_trait: target={:?} queue={:?}", target, queue);
        while let Some(current_location) = queue.pop() {
            debug!("was_captured_by_trait: target={:?}", target);
            let block = &self.body[current_location.block];
            // We need to check the current location to find out if it is a terminator.
            let is_terminator = current_location.statement_index == block.statements.len();
            if !is_terminator {
                let stmt = &block.statements[current_location.statement_index];
                debug!("was_captured_by_trait_object: stmt={:?}", stmt);

                // The only kind of statement that we care about is assignments...
                if let StatementKind::Assign(box (place, rvalue)) = &stmt.kind {
                    let into = match place.local_or_deref_local() {
                        Some(into) => into,
                        None => {
                            // Continue at the next location.
                            queue.push(current_location.successor_within_block());
                            continue;
                        }
                    };

                    match rvalue {
                        // If we see a use, we should check whether it is our data, and if so
                        // update the place that we're looking for to that new place.
                        Rvalue::Use(operand) => match operand {
                            Operand::Copy(place) | Operand::Move(place) => {
                                if let Some(from) = place.as_local() {
                                    if from == target {
                                        target = into;
                                    }
                                }
                            }
                            _ => {}
                        },
                        // If we see a unsized cast, then if it is our data we should check
                        // whether it is being cast to a trait object.
                        Rvalue::Cast(CastKind::Pointer(PointerCast::Unsize), operand, ty) => {
                            match operand {
                                Operand::Copy(place) | Operand::Move(place) => {
                                    if let Some(from) = place.as_local() {
                                        if from == target {
                                            debug!("was_captured_by_trait_object: ty={:?}", ty);
                                            // Check the type for a trait object.
                                            return match ty.kind() {
                                                // `&dyn Trait`
                                                ty::Ref(_, ty, _) if ty.is_trait() => true,
                                                // `Box<dyn Trait>`
                                                _ if ty.is_box() && ty.boxed_ty().is_trait() => {
                                                    true
                                                }
                                                // `dyn Trait`
                                                _ if ty.is_trait() => true,
                                                // Anything else.
                                                _ => false,
                                            };
                                        }
                                    }
                                    return false;
                                }
                                _ => return false,
                            }
                        }
                        _ => {}
                    }
                }

                // Continue at the next location.
                queue.push(current_location.successor_within_block());
            } else {
                // The only thing we need to do for terminators is progress to the next block.
                let terminator = block.terminator();
                debug!("was_captured_by_trait_object: terminator={:?}", terminator);

                if let TerminatorKind::Call { destination: Some((place, block)), args, .. } =
                    &terminator.kind
                {
                    if let Some(dest) = place.as_local() {
                        debug!(
                            "was_captured_by_trait_object: target={:?} dest={:?} args={:?}",
                            target, dest, args
                        );
                        // Check if one of the arguments to this function is the target place.
                        let found_target = args.iter().any(|arg| {
                            if let Operand::Move(place) = arg {
                                if let Some(potential) = place.as_local() {
                                    potential == target
                                } else {
                                    false
                                }
                            } else {
                                false
                            }
                        });

                        // If it is, follow this to the next block and update the target.
                        if found_target {
                            target = dest;
                            queue.push(block.start_location());
                        }
                    }
                }
            }

            debug!("was_captured_by_trait: queue={:?}", queue);
        }

        // We didn't find anything and ran out of locations to check.
        false
    }
}
