use rustc_index::IndexSlice;
use rustc_middle::mir::visit::{PlaceContext, VisitPlacesWith, Visitor};
use rustc_middle::mir::{Body, Local, Place};
use rustc_middle::ty::{self, RegionVid, TyCtxt};
use rustc_span::{Span, Symbol};
use tracing::debug;

use crate::region_infer::RegionInferenceContext;

impl<'tcx> RegionInferenceContext<'tcx> {
    /// Find the name and span of the variable corresponding to the given region.
    /// The returned var will also be ensured to actually be used in `body`.
    pub(crate) fn get_var_name_and_span_for_region(
        &self,
        tcx: TyCtxt<'tcx>,
        body: &Body<'tcx>,
        local_names: &IndexSlice<Local, Option<Symbol>>,
        upvars: &[&ty::CapturedPlace<'tcx>],
        fr: RegionVid,
    ) -> Option<(Option<Symbol>, Span)> {
        debug!("get_var_name_and_span_for_region(fr={fr:?})");
        assert!(self.universal_regions().is_universal_region(fr));

        debug!("get_var_name_and_span_for_region: attempting upvar");
        self.get_upvar_index_for_region(tcx, fr)
            .map(|index| {
                // FIXME(project-rfc-2229#8): Use place span for diagnostics
                // We know our upvars are used thanks to `fn compute_min_captures()` in `upvar.rs`.
                let (name, span) = self.get_upvar_name_and_span_for_region(tcx, upvars, index);
                (Some(name), span)
            })
            .or_else(|| {
                debug!("get_var_name_and_span_for_region: attempting argument");
                self.get_user_arg_index_for_region(tcx, fr).and_then(|index| {
                    let local = self.user_arg_index_to_local(body, index);
                    if body_uses_local(body, local) {
                        Some(self.get_argument_name_and_span_for_region(body, local_names, index))
                    } else {
                        debug!("get_var_name_and_span_for_region: skipping unused local {local:?}");
                        None
                    }
                })
            })
    }

    /// Search the upvars (if any) to find one that references fr. Return its index.
    pub(crate) fn get_upvar_index_for_region(
        &self,
        tcx: TyCtxt<'tcx>,
        fr: RegionVid,
    ) -> Option<usize> {
        let upvar_index =
            self.universal_regions().defining_ty.upvar_tys().iter().position(|upvar_ty| {
                debug!("get_upvar_index_for_region: upvar_ty={upvar_ty:?}");
                tcx.any_free_region_meets(&upvar_ty, |r| {
                    let r = r.as_var();
                    debug!("get_upvar_index_for_region: r={r:?} fr={fr:?}");
                    r == fr
                })
            })?;

        let upvar_ty = self.universal_regions().defining_ty.upvar_tys().get(upvar_index);

        debug!(
            "get_upvar_index_for_region: found {fr:?} in upvar {upvar_index} which has type {upvar_ty:?}",
        );

        Some(upvar_index)
    }

    /// Given the index of an upvar, finds its name and the span from where it was
    /// declared.
    pub(crate) fn get_upvar_name_and_span_for_region(
        &self,
        tcx: TyCtxt<'tcx>,
        upvars: &[&ty::CapturedPlace<'tcx>],
        upvar_index: usize,
    ) -> (Symbol, Span) {
        let upvar_hir_id = upvars[upvar_index].get_root_variable();
        debug!("get_upvar_name_and_span_for_region: upvar_hir_id={upvar_hir_id:?}");

        let upvar_name = tcx.hir_name(upvar_hir_id);
        let upvar_span = tcx.hir_span(upvar_hir_id);
        debug!(
            "get_upvar_name_and_span_for_region: upvar_name={upvar_name:?} upvar_span={upvar_span:?}",
        );

        (upvar_name, upvar_span)
    }

    /// Search the argument types for one that references fr (which should be a free region).
    /// Returns Some(_) with the index of the input if one is found.
    ///
    /// N.B., in the case of a closure, the index is indexing into the signature as seen by the
    /// user - in particular, index 0 is not the implicit self parameter.
    pub(crate) fn get_user_arg_index_for_region(
        &self,
        tcx: TyCtxt<'tcx>,
        fr: RegionVid,
    ) -> Option<usize> {
        let implicit_inputs = self.universal_regions().defining_ty.implicit_inputs();
        let user_arg_index =
            self.universal_regions().unnormalized_input_tys.iter().skip(implicit_inputs).position(
                |arg_ty| {
                    debug!("get_user_arg_index_for_region: arg_ty = {arg_ty:?}");
                    tcx.any_free_region_meets(arg_ty, |r| r.as_var() == fr)
                },
            )?;

        debug!(
            "get_user_arg_index_for_region: found {fr:?} in argument {user_arg_index} which has type {:?}",
            self.universal_regions().unnormalized_input_tys[user_arg_index],
        );

        Some(user_arg_index)
    }

    /// Given the index of an argument as seen from the user (i.e. excluding
    /// implicit inputs), returns the corresponding MIR local.
    fn user_arg_index_to_local(&self, body: &Body<'tcx>, user_arg_index: usize) -> Local {
        let implicit_inputs = self.universal_regions().defining_ty.implicit_inputs();
        body.args_iter().nth(implicit_inputs + user_arg_index).unwrap()
    }

    /// Given the index of an argument, finds its name (if any) and the span from where it was
    /// declared.
    pub(crate) fn get_argument_name_and_span_for_region(
        &self,
        body: &Body<'tcx>,
        local_names: &IndexSlice<Local, Option<Symbol>>,
        user_arg_index: usize,
    ) -> (Option<Symbol>, Span) {
        let argument_local = self.user_arg_index_to_local(body, user_arg_index);
        debug!("get_argument_name_and_span_for_region: argument_local={argument_local:?}");

        let argument_name = local_names[argument_local];
        let argument_span = body.local_decls[argument_local].source_info.span;
        debug!(
            "get_argument_name_and_span_for_region: argument_name={argument_name:?} argument_span={argument_span:?}",
        );

        (argument_name, argument_span)
    }
}

fn body_uses_local<'tcx>(body: &Body<'tcx>, target: Local) -> bool {
    let mut used = false;
    VisitPlacesWith(|place: Place<'_>, context: PlaceContext| {
        if !matches!(context, PlaceContext::NonUse(_)) && place.local == target {
            used = true;
        }
    })
    .visit_body(body);
    used
}
