use rustc_data_structures::intern::Interned;
use rustc_errors::MultiSpan;
use rustc_hir::def_id::DefId;
use rustc_macros::{StableHash, TyDecodable, TyEncodable};
use rustc_span::{DUMMY_SP, ErrorGuaranteed, Symbol, kw, sym};
use rustc_type_ir::RegionKind as IrRegionKind;
pub use rustc_type_ir::RegionVid;
use tracing::debug;

use crate::ty::{self, BoundVar, TyCtxt, TypeFlags};

pub type RegionKind<'tcx> = IrRegionKind<TyCtxt<'tcx>>;

/// Use this rather than `RegionKind`, whenever possible.
#[derive(Copy, Clone, PartialEq, Eq, Hash, StableHash)]
#[rustc_pass_by_value]
pub struct Region<'tcx>(pub Interned<'tcx, RegionKind<'tcx>>);

impl<'tcx> rustc_type_ir::inherent::IntoKind for Region<'tcx> {
    type Kind = RegionKind<'tcx>;

    fn kind(self) -> RegionKind<'tcx> {
        *self.0.0
    }
}

impl<'tcx> rustc_type_ir::Flags for Region<'tcx> {
    fn flags(&self) -> TypeFlags {
        self.type_flags()
    }

    fn outer_exclusive_binder(&self) -> ty::DebruijnIndex {
        match self.kind() {
            ty::ReBound(ty::BoundVarIndexKind::Bound(debruijn), _) => debruijn.shifted_in(1),
            _ => ty::INNERMOST,
        }
    }
}

impl<'tcx> Region<'tcx> {
    #[inline]
    pub fn new_early_param(
        tcx: TyCtxt<'tcx>,
        early_bound_region: ty::EarlyParamRegion,
    ) -> Region<'tcx> {
        tcx.intern_region(ty::ReEarlyParam(early_bound_region))
    }

    #[inline]
    pub fn new_bound(
        tcx: TyCtxt<'tcx>,
        debruijn: ty::DebruijnIndex,
        bound_region: ty::BoundRegion<'tcx>,
    ) -> Region<'tcx> {
        // Use a pre-interned one when possible.
        if let ty::BoundRegion { var, kind: ty::BoundRegionKind::Anon } = bound_region
            && let Some(inner) = tcx.lifetimes.anon_re_bounds.get(debruijn.as_usize())
            && let Some(re) = inner.get(var.as_usize()).copied()
        {
            re
        } else {
            tcx.intern_region(ty::ReBound(ty::BoundVarIndexKind::Bound(debruijn), bound_region))
        }
    }

    #[inline]
    pub fn new_canonical_bound(tcx: TyCtxt<'tcx>, var: ty::BoundVar) -> Region<'tcx> {
        // Use a pre-interned one when possible.
        if let Some(re) = tcx.lifetimes.anon_re_canonical_bounds.get(var.as_usize()).copied() {
            re
        } else {
            tcx.intern_region(ty::ReBound(
                ty::BoundVarIndexKind::Canonical,
                ty::BoundRegion { var, kind: ty::BoundRegionKind::Anon },
            ))
        }
    }

    #[inline]
    pub fn new_late_param(
        tcx: TyCtxt<'tcx>,
        scope: DefId,
        kind: LateParamRegionKind,
    ) -> Region<'tcx> {
        let data = LateParamRegion { scope, kind };
        tcx.intern_region(ty::ReLateParam(data))
    }

    #[inline]
    pub fn new_var(tcx: TyCtxt<'tcx>, v: ty::RegionVid) -> Region<'tcx> {
        // Use a pre-interned one when possible.
        tcx.lifetimes
            .re_vars
            .get(v.as_usize())
            .copied()
            .unwrap_or_else(|| tcx.intern_region(ty::ReVar(v)))
    }

    #[inline]
    pub fn new_placeholder(
        tcx: TyCtxt<'tcx>,
        placeholder: ty::PlaceholderRegion<'tcx>,
    ) -> Region<'tcx> {
        tcx.intern_region(ty::RePlaceholder(placeholder))
    }

    /// Constructs a `RegionKind::ReError` region.
    #[track_caller]
    pub fn new_error(tcx: TyCtxt<'tcx>, guar: ErrorGuaranteed) -> Region<'tcx> {
        tcx.intern_region(ty::ReError(guar))
    }

    /// Constructs a `RegionKind::ReError` region and registers a delayed bug to ensure it gets
    /// used.
    #[track_caller]
    pub fn new_error_misc(tcx: TyCtxt<'tcx>) -> Region<'tcx> {
        Region::new_error_with_message(
            tcx,
            DUMMY_SP,
            "RegionKind::ReError constructed but no error reported",
        )
    }

    /// Constructs a `RegionKind::ReError` region and registers a delayed bug with the given `msg`
    /// to ensure it gets used.
    #[track_caller]
    pub fn new_error_with_message<S: Into<MultiSpan>>(
        tcx: TyCtxt<'tcx>,
        span: S,
        msg: &'static str,
    ) -> Region<'tcx> {
        let reported = tcx.dcx().span_delayed_bug(span, msg);
        Region::new_error(tcx, reported)
    }

    /// Avoid this in favour of more specific `new_*` methods, where possible,
    /// to avoid the cost of the `match`.
    pub fn new_from_kind(tcx: TyCtxt<'tcx>, kind: RegionKind<'tcx>) -> Region<'tcx> {
        match kind {
            ty::ReEarlyParam(region) => Region::new_early_param(tcx, region),
            ty::ReBound(ty::BoundVarIndexKind::Bound(debruijn), region) => {
                Region::new_bound(tcx, debruijn, region)
            }
            ty::ReBound(ty::BoundVarIndexKind::Canonical, region) => {
                Region::new_canonical_bound(tcx, region.var)
            }
            ty::ReLateParam(ty::LateParamRegion { scope, kind }) => {
                Region::new_late_param(tcx, scope, kind)
            }
            ty::ReStatic => tcx.lifetimes.re_static,
            ty::ReVar(vid) => Region::new_var(tcx, vid),
            ty::RePlaceholder(region) => Region::new_placeholder(tcx, region),
            ty::ReErased => tcx.lifetimes.re_erased,
            ty::ReError(reported) => Region::new_error(tcx, reported),
        }
    }
}

impl<'tcx> rustc_type_ir::inherent::Region<TyCtxt<'tcx>> for Region<'tcx> {
    fn new_bound(
        interner: TyCtxt<'tcx>,
        debruijn: ty::DebruijnIndex,
        var: ty::BoundRegion<'tcx>,
    ) -> Self {
        Region::new_bound(interner, debruijn, var)
    }

    fn new_anon_bound(tcx: TyCtxt<'tcx>, debruijn: ty::DebruijnIndex, var: ty::BoundVar) -> Self {
        Region::new_bound(tcx, debruijn, ty::BoundRegion { var, kind: ty::BoundRegionKind::Anon })
    }

    fn new_canonical_bound(tcx: TyCtxt<'tcx>, var: rustc_type_ir::BoundVar) -> Self {
        Region::new_canonical_bound(tcx, var)
    }

    fn new_placeholder(tcx: TyCtxt<'tcx>, placeholder: ty::PlaceholderRegion<'tcx>) -> Self {
        Region::new_placeholder(tcx, placeholder)
    }

    fn new_static(tcx: TyCtxt<'tcx>) -> Self {
        tcx.lifetimes.re_static
    }
}

/// Region utilities
impl<'tcx> Region<'tcx> {
    pub fn kind(self) -> RegionKind<'tcx> {
        *self.0.0
    }

    pub fn get_name(self, tcx: TyCtxt<'tcx>) -> Option<Symbol> {
        match self.kind() {
            ty::ReEarlyParam(ebr) => ebr.is_named().then_some(ebr.name),
            ty::ReBound(_, br) => br.kind.get_name(tcx),
            ty::ReLateParam(fr) => fr.kind.get_name(tcx),
            ty::ReStatic => Some(kw::StaticLifetime),
            ty::RePlaceholder(placeholder) => placeholder.bound.kind.get_name(tcx),
            _ => None,
        }
    }

    pub fn get_name_or_anon(self, tcx: TyCtxt<'tcx>) -> Symbol {
        match self.get_name(tcx) {
            Some(name) => name,
            None => sym::anon,
        }
    }

    /// Is this region named by the user?
    pub fn is_named(self, tcx: TyCtxt<'tcx>) -> bool {
        match self.kind() {
            ty::ReEarlyParam(ebr) => ebr.is_named(),
            ty::ReBound(_, br) => br.kind.is_named(tcx),
            ty::ReLateParam(fr) => fr.kind.is_named(tcx),
            ty::ReStatic => true,
            ty::ReVar(..) => false,
            ty::RePlaceholder(placeholder) => placeholder.bound.kind.is_named(tcx),
            ty::ReErased => false,
            ty::ReError(_) => false,
        }
    }

    #[inline]
    pub fn is_error(self) -> bool {
        matches!(self.kind(), ty::ReError(_))
    }

    #[inline]
    pub fn is_static(self) -> bool {
        matches!(self.kind(), ty::ReStatic)
    }

    #[inline]
    pub fn is_erased(self) -> bool {
        matches!(self.kind(), ty::ReErased)
    }

    #[inline]
    pub fn is_bound(self) -> bool {
        matches!(self.kind(), ty::ReBound(..))
    }

    #[inline]
    pub fn is_placeholder(self) -> bool {
        matches!(self.kind(), ty::RePlaceholder(..))
    }

    #[inline]
    pub fn bound_at_or_above_binder(self, index: ty::DebruijnIndex) -> bool {
        match self.kind() {
            ty::ReBound(ty::BoundVarIndexKind::Bound(debruijn), _) => debruijn >= index,
            _ => false,
        }
    }

    pub fn type_flags(self) -> TypeFlags {
        let mut flags = TypeFlags::empty();

        match self.kind() {
            ty::ReVar(..) => {
                flags = flags | TypeFlags::HAS_FREE_REGIONS;
                flags = flags | TypeFlags::HAS_FREE_LOCAL_REGIONS;
                flags = flags | TypeFlags::HAS_RE_INFER;
            }
            ty::RePlaceholder(..) => {
                flags = flags | TypeFlags::HAS_FREE_REGIONS;
                flags = flags | TypeFlags::HAS_FREE_LOCAL_REGIONS;
                flags = flags | TypeFlags::HAS_RE_PLACEHOLDER;
            }
            ty::ReEarlyParam(..) => {
                flags = flags | TypeFlags::HAS_FREE_REGIONS;
                flags = flags | TypeFlags::HAS_FREE_LOCAL_REGIONS;
                flags = flags | TypeFlags::HAS_RE_PARAM;
            }
            ty::ReLateParam { .. } => {
                flags = flags | TypeFlags::HAS_FREE_REGIONS;
                flags = flags | TypeFlags::HAS_FREE_LOCAL_REGIONS;
            }
            ty::ReStatic => {
                flags = flags | TypeFlags::HAS_FREE_REGIONS;
            }
            ty::ReBound(ty::BoundVarIndexKind::Canonical, _) => {
                flags = flags | TypeFlags::HAS_RE_BOUND;
                flags = flags | TypeFlags::HAS_CANONICAL_BOUND;
            }
            ty::ReBound(ty::BoundVarIndexKind::Bound(..), _) => {
                flags = flags | TypeFlags::HAS_RE_BOUND;
            }
            ty::ReErased => {
                flags = flags | TypeFlags::HAS_RE_ERASED;
            }
            ty::ReError(_) => {
                flags = flags | TypeFlags::HAS_FREE_REGIONS;
                flags = flags | TypeFlags::HAS_RE_ERROR;
            }
        }

        debug!("type_flags({:?}) = {:?}", self, flags);

        flags
    }

    /// True for free regions other than `'static`.
    pub fn is_param(self) -> bool {
        matches!(self.kind(), ty::ReEarlyParam(_) | ty::ReLateParam(_))
    }

    /// True for free region in the current context.
    ///
    /// This is the case for `'static` and param regions.
    pub fn is_free(self) -> bool {
        match self.kind() {
            ty::ReStatic | ty::ReEarlyParam(..) | ty::ReLateParam(..) => true,
            ty::ReVar(..)
            | ty::RePlaceholder(..)
            | ty::ReBound(..)
            | ty::ReErased
            | ty::ReError(..) => false,
        }
    }

    pub fn is_var(self) -> bool {
        matches!(self.kind(), ty::ReVar(_))
    }

    pub fn as_var(self) -> RegionVid {
        match self.kind() {
            ty::ReVar(vid) => vid,
            _ => bug!("expected region {:?} to be of kind ReVar", self),
        }
    }

    /// Given some item `binding_item`, check if this region is a generic parameter introduced by it
    /// or one of the parent generics. Returns the `DefId` of the parameter definition if so.
    pub fn opt_param_def_id(self, tcx: TyCtxt<'tcx>, binding_item: DefId) -> Option<DefId> {
        match self.kind() {
            ty::ReEarlyParam(ebr) => {
                Some(tcx.generics_of(binding_item).region_param(ebr, tcx).def_id)
            }
            ty::ReLateParam(ty::LateParamRegion {
                kind: ty::LateParamRegionKind::Named(def_id),
                ..
            }) => Some(def_id),
            _ => None,
        }
    }
}

#[derive(Copy, Clone, PartialEq, Eq, Hash, TyEncodable, TyDecodable)]
#[derive(StableHash)]
pub struct EarlyParamRegion {
    pub index: u32,
    pub name: Symbol,
}

impl EarlyParamRegion {
    /// Does this early bound region have a name? Early bound regions normally
    /// always have names except when using anonymous lifetimes (`'_`).
    pub fn is_named(&self) -> bool {
        self.name != kw::UnderscoreLifetime
    }
}

impl rustc_type_ir::inherent::ParamLike for EarlyParamRegion {
    fn index(self) -> u32 {
        self.index
    }
}

impl std::fmt::Debug for EarlyParamRegion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}/#{}", self.name, self.index)
    }
}

#[derive(Clone, PartialEq, Eq, Hash, TyEncodable, TyDecodable, Copy)]
#[derive(StableHash)]
/// The parameter representation of late-bound function parameters, "some region
/// at least as big as the scope `fr.scope`".
///
/// Similar to a placeholder region as we create `LateParam` regions when entering a binder
/// except they are always in the root universe and instead of using a boundvar to distinguish
/// between others we use the `DefId` of the parameter. For this reason the `bound_region` field
/// should basically always be `BoundRegionKind::Named` as otherwise there is no way of telling
/// different parameters apart.
pub struct LateParamRegion {
    pub scope: DefId,
    pub kind: LateParamRegionKind,
}

/// When liberating bound regions, we map their [`ty::BoundRegionKind`]
/// to this as we need to track the index of anonymous regions. We
/// otherwise end up liberating multiple bound regions to the same
/// late-bound region.
#[derive(Clone, PartialEq, Eq, Hash, TyEncodable, TyDecodable, Copy)]
#[derive(StableHash)]
pub enum LateParamRegionKind {
    /// An anonymous region parameter for a given fn (&T)
    ///
    /// Unlike [`ty::BoundRegionKind::Anon`], this tracks the index of the
    /// liberated bound region.
    ///
    /// We should ideally never liberate anonymous regions, but do so for the
    /// sake of diagnostics in `FnCtxt::sig_of_closure_with_expectation`.
    Anon(u32),

    /// An anonymous region parameter with a `Symbol` name.
    ///
    /// Used to give late-bound regions names for things like pretty printing.
    NamedAnon(u32, Symbol),

    /// Late-bound regions that appear in the AST.
    Named(DefId),

    /// Anonymous region for the implicit env pointer parameter
    /// to a closure
    ClosureEnv,
}

impl LateParamRegionKind {
    pub fn from_bound(var: BoundVar, br: ty::BoundRegionKind<'_>) -> LateParamRegionKind {
        match br {
            ty::BoundRegionKind::Anon => LateParamRegionKind::Anon(var.as_u32()),
            ty::BoundRegionKind::Named(def_id) => LateParamRegionKind::Named(def_id),
            ty::BoundRegionKind::ClosureEnv => LateParamRegionKind::ClosureEnv,
            ty::BoundRegionKind::NamedForPrinting(name) => {
                LateParamRegionKind::NamedAnon(var.as_u32(), name)
            }
        }
    }

    pub fn is_named(&self, tcx: TyCtxt<'_>) -> bool {
        self.get_name(tcx).is_some()
    }

    pub fn get_name(&self, tcx: TyCtxt<'_>) -> Option<Symbol> {
        match *self {
            LateParamRegionKind::Named(def_id) => {
                let name = tcx.item_name(def_id);
                if name != kw::UnderscoreLifetime { Some(name) } else { None }
            }
            LateParamRegionKind::NamedAnon(_, name) => Some(name),
            _ => None,
        }
    }

    pub fn get_id(&self) -> Option<DefId> {
        match *self {
            LateParamRegionKind::Named(id) => Some(id),
            _ => None,
        }
    }
}

// Some types are used a lot. Make sure they don't unintentionally get bigger.
#[cfg(target_pointer_width = "64")]
mod size_asserts {
    use rustc_data_structures::static_assert_size;

    use super::*;
    // tidy-alphabetical-start
    static_assert_size!(RegionKind<'_>, 20);
    static_assert_size!(ty::WithCachedTypeInfo<RegionKind<'_>>, 28);
    // tidy-alphabetical-end
}
