use std::mem;
use std::rc::Rc;

use rustc_abi::FieldIdx;
use rustc_data_structures::fx::{FxHashMap, FxIndexMap};
use rustc_hir::def_id::LocalDefId;
use rustc_middle::mir::ConstraintCategory;
use rustc_middle::ty::{self, TyCtxt};
use rustc_span::ErrorGuaranteed;
use smallvec::SmallVec;

use crate::consumers::BorrowckConsumer;
use crate::nll::compute_closure_requirements_modulo_opaques;
use crate::region_infer::opaque_types::{
    apply_computed_concrete_opaque_types, clone_and_resolve_opaque_types,
    compute_concrete_opaque_types, detect_opaque_types_added_while_handling_opaque_types,
};
use crate::type_check::{Locations, constraint_conversion};
use crate::{
    ClosureRegionRequirements, CollectRegionConstraintsResult, ConcreteOpaqueTypes,
    PropagatedBorrowCheckResults, borrowck_check_region_constraints,
    borrowck_collect_region_constraints,
};

/// The shared context used by both the root as well as all its nested
/// items.
pub(super) struct BorrowCheckRootCtxt<'tcx> {
    pub tcx: TyCtxt<'tcx>,
    root_def_id: LocalDefId,
    concrete_opaque_types: ConcreteOpaqueTypes<'tcx>,
    /// The region constraints computed by [borrowck_collect_region_constraints]. This uses
    /// an [FxIndexMap] to guarantee that iterating over it visits nested bodies before
    /// their parents.
    collect_region_constraints_results:
        FxIndexMap<LocalDefId, CollectRegionConstraintsResult<'tcx>>,
    propagated_borrowck_results: FxHashMap<LocalDefId, PropagatedBorrowCheckResults<'tcx>>,
    tainted_by_errors: Option<ErrorGuaranteed>,
    /// This should be `None` during normal compilation. See [`crate::consumers`] for more
    /// information on how this is used.
    pub consumer: Option<BorrowckConsumer<'tcx>>,
}

impl<'tcx> BorrowCheckRootCtxt<'tcx> {
    pub(super) fn new(
        tcx: TyCtxt<'tcx>,
        root_def_id: LocalDefId,
        consumer: Option<BorrowckConsumer<'tcx>>,
    ) -> BorrowCheckRootCtxt<'tcx> {
        BorrowCheckRootCtxt {
            tcx,
            root_def_id,
            concrete_opaque_types: Default::default(),
            collect_region_constraints_results: Default::default(),
            propagated_borrowck_results: Default::default(),
            tainted_by_errors: None,
            consumer,
        }
    }

    pub(super) fn root_def_id(&self) -> LocalDefId {
        self.root_def_id
    }

    pub(super) fn set_tainted_by_errors(&mut self, guar: ErrorGuaranteed) {
        self.tainted_by_errors = Some(guar);
    }

    pub(super) fn used_mut_upvars(
        &mut self,
        nested_body_def_id: LocalDefId,
    ) -> &SmallVec<[FieldIdx; 8]> {
        &self.propagated_borrowck_results[&nested_body_def_id].used_mut_upvars
    }

    pub(super) fn finalize(self) -> Result<&'tcx ConcreteOpaqueTypes<'tcx>, ErrorGuaranteed> {
        if let Some(guar) = self.tainted_by_errors {
            Err(guar)
        } else {
            Ok(self.tcx.arena.alloc(self.concrete_opaque_types))
        }
    }

    fn handle_opaque_type_uses(&mut self) {
        let mut per_body_info = Vec::new();
        for input in self.collect_region_constraints_results.values_mut() {
            let (num_entries, opaque_types) = clone_and_resolve_opaque_types(
                &input.infcx,
                &input.universal_region_relations,
                &mut input.constraints,
            );
            input.deferred_opaque_type_errors = compute_concrete_opaque_types(
                &input.infcx,
                &input.universal_region_relations,
                &input.constraints,
                Rc::clone(&input.location_map),
                &mut self.concrete_opaque_types,
                &opaque_types,
            );
            per_body_info.push((num_entries, opaque_types));
        }

        for (input, (opaque_types_storage_num_entries, opaque_types)) in
            self.collect_region_constraints_results.values_mut().zip(per_body_info)
        {
            if input.deferred_opaque_type_errors.is_empty() {
                input.deferred_opaque_type_errors = apply_computed_concrete_opaque_types(
                    &input.infcx,
                    &input.body_owned,
                    &input.universal_region_relations.universal_regions,
                    &input.region_bound_pairs,
                    &input.known_type_outlives_obligations,
                    &mut input.constraints,
                    &mut self.concrete_opaque_types,
                    &opaque_types,
                );
            }

            detect_opaque_types_added_while_handling_opaque_types(
                &input.infcx,
                opaque_types_storage_num_entries,
            )
        }
    }

    /// Computing defining uses of opaques may depend on the propagated region
    /// requirements of nested bodies, while applying defining uses may introduce
    /// additional region requirements we need to propagate.
    ///
    /// This results in cyclic dependency. To compute the defining uses in parent
    /// bodies, we need the closure requirements of its nested bodies, but to check
    /// non-defining uses in nested bodies, we may rely on the defining uses in the
    /// parent.
    ///
    /// We handle this issue by applying closure requirements twice. Once using the
    /// region constraints from before we've handled opaque types in the nested body
    /// - which is used by the parent to handle its defining uses - and once after.
    ///
    /// As a performance optimization, we also eagerly finish borrowck for bodies
    /// which don't depend on opaque types. In this case they get removed from
    /// `collect_region_constraints_results` and the final result gets put into
    /// `propagated_borrowck_results`.
    fn apply_closure_requirements_modulo_opaques(&mut self) {
        let mut closure_requirements_modulo_opaques = FxHashMap::default();
        // We need to `mem::take` both `self.collect_region_constraints_results` and
        // `input.deferred_closure_requirements` as we otherwise can't iterate over
        // them while mutably using the containing struct.
        let collect_region_constraints_results =
            mem::take(&mut self.collect_region_constraints_results);
        // We iterate over all bodies here, visiting nested bodies before their parent.
        for (def_id, mut input) in collect_region_constraints_results {
            // A body depends on opaque types if it either has any opaque type uses itself,
            // or it has a nested body which does.
            //
            // If the current body does not depend on any opaque types, we eagerly compute
            // its final result and write it into `self.propagated_borrowck_results`. This
            // avoids having to compute its closure requirements modulo regions, as they
            // are just the same as its final closure requirements.
            let mut depends_on_opaques = input.infcx.has_opaque_types_in_storage();

            // Iterate over all nested bodies of `input`. If that nested body depends on
            // opaque types, we apply its closure requirements modulo opaques. Otherwise
            // we use the closure requirements from its final borrowck result.
            //
            // In case we've only applied the closure requirements modulo opaques, we have
            // to later apply its closure requirements considering opaques, so we put that
            // nested body back into `deferred_closure_requirements`.
            for (def_id, args, locations) in mem::take(&mut input.deferred_closure_requirements) {
                let closure_requirements = match self.propagated_borrowck_results.get(&def_id) {
                    None => {
                        depends_on_opaques = true;
                        input.deferred_closure_requirements.push((def_id, args, locations));
                        &closure_requirements_modulo_opaques[&def_id]
                    }
                    Some(result) => &result.closure_requirements,
                };

                Self::apply_closure_requirements(
                    &mut input,
                    closure_requirements,
                    def_id,
                    args,
                    locations,
                );
            }

            // In case the current body does depend on opaques and is a nested body,
            // we need to compute its closure requirements modulo opaques so that
            // we're able to use it when visiting its parent later in this function.
            //
            // If the current body does not depend on opaque types, we finish borrowck
            // and write its result into `propagated_borrowck_results`.
            if depends_on_opaques {
                if def_id != self.root_def_id {
                    let req = Self::compute_closure_requirements_modulo_opaques(&input);
                    closure_requirements_modulo_opaques.insert(def_id, req);
                }
                self.collect_region_constraints_results.insert(def_id, input);
            } else {
                assert!(input.deferred_closure_requirements.is_empty());
                let result = borrowck_check_region_constraints(self, input);
                self.propagated_borrowck_results.insert(def_id, result);
            }
        }
    }

    fn compute_closure_requirements_modulo_opaques(
        input: &CollectRegionConstraintsResult<'tcx>,
    ) -> Option<ClosureRegionRequirements<'tcx>> {
        compute_closure_requirements_modulo_opaques(
            &input.infcx,
            &input.body_owned,
            Rc::clone(&input.location_map),
            &input.universal_region_relations,
            &input.constraints,
        )
    }

    fn apply_closure_requirements(
        input: &mut CollectRegionConstraintsResult<'tcx>,
        closure_requirements: &Option<ClosureRegionRequirements<'tcx>>,
        closure_def_id: LocalDefId,
        args: ty::GenericArgsRef<'tcx>,
        locations: Locations,
    ) {
        if let Some(closure_requirements) = closure_requirements {
            constraint_conversion::ConstraintConversion::new(
                &input.infcx,
                &input.universal_region_relations.universal_regions,
                &input.region_bound_pairs,
                &input.known_type_outlives_obligations,
                locations,
                input.body_owned.span,      // irrelevant; will be overridden.
                ConstraintCategory::Boring, // same as above.
                &mut input.constraints,
            )
            .apply_closure_requirements(closure_requirements, closure_def_id, args);
        }
    }

    pub(super) fn do_mir_borrowck(&mut self) {
        // The list of all bodies we need to borrowck. This first looks at
        // nested bodies, and then their parents. This means accessing e.g.
        // `used_mut_upvars` for a closure can assume that we've already
        // checked that closure.
        let all_bodies = self
            .tcx
            .nested_bodies_within(self.root_def_id)
            .iter()
            .chain(std::iter::once(self.root_def_id));
        for def_id in all_bodies {
            let result = borrowck_collect_region_constraints(self, def_id);
            self.collect_region_constraints_results.insert(def_id, result);
        }

        // We now apply the closure requirements of nested bodies modulo
        // regions. In case a body does not depend on opaque types, we
        // eagerly check its region constraints and use the final closure
        // requirements.
        //
        // We eagerly finish borrowck for bodies which don't depend on
        // opaques.
        self.apply_closure_requirements_modulo_opaques();

        // We handle opaque type uses for all bodies together.
        self.handle_opaque_type_uses();

        // Now walk over all bodies which depend on opaque types and finish borrowck.
        //
        // We first apply the final closure requirements from nested bodies which also
        // depend on opaque types and then finish borrow checking the parent. Bodies
        // which don't depend on opaques have already been fully borrowchecked in
        // `apply_closure_requirements_modulo_opaques` as an optimization.
        for (def_id, mut input) in mem::take(&mut self.collect_region_constraints_results) {
            for (def_id, args, locations) in mem::take(&mut input.deferred_closure_requirements) {
                // We visit nested bodies before their parent, so we're already
                // done with nested bodies at this point.
                let closure_requirements =
                    &self.propagated_borrowck_results[&def_id].closure_requirements;
                Self::apply_closure_requirements(
                    &mut input,
                    closure_requirements,
                    def_id,
                    args,
                    locations,
                );
            }

            let result = borrowck_check_region_constraints(self, input);
            self.propagated_borrowck_results.insert(def_id, result);
        }
    }
}
