//! Things related to regions.

use hir_def::LifetimeParamId;
use intern::{Interned, Symbol};
use rustc_type_ir::{
    BoundVar, Flags, INNERMOST, RegionVid, TypeFlags, TypeFoldable, TypeVisitable, VisitorResult,
    inherent::{IntoKind, PlaceholderLike, SliceLike},
    relate::Relate,
};

use crate::next_solver::{GenericArg, OutlivesPredicate};

use super::{
    ErrorGuaranteed, SolverDefId, interned_vec_db,
    interner::{BoundVarKind, DbInterner, Placeholder},
};

type RegionKind<'db> = rustc_type_ir::RegionKind<DbInterner<'db>>;

#[salsa::interned(constructor = new_, debug)]
pub struct Region<'db> {
    #[returns(ref)]
    kind_: RegionKind<'db>,
}

impl<'db> Region<'db> {
    pub fn new(interner: DbInterner<'db>, kind: RegionKind<'db>) -> Self {
        Region::new_(interner.db(), kind)
    }

    pub fn inner(&self) -> &RegionKind<'db> {
        salsa::with_attached_database(|db| {
            let inner = self.kind_(db);
            // SAFETY: The caller already has access to a `Region<'db>`, so borrowchecking will
            // make sure that our returned value is valid for the lifetime `'db`.
            unsafe { std::mem::transmute::<&RegionKind<'_>, &RegionKind<'db>>(inner) }
        })
        .unwrap()
    }

    pub fn new_early_param(
        interner: DbInterner<'db>,
        early_bound_region: EarlyParamRegion,
    ) -> Self {
        Region::new(interner, RegionKind::ReEarlyParam(early_bound_region))
    }

    pub fn new_placeholder(interner: DbInterner<'db>, placeholder: PlaceholderRegion) -> Self {
        Region::new(interner, RegionKind::RePlaceholder(placeholder))
    }

    pub fn new_var(interner: DbInterner<'db>, v: RegionVid) -> Region<'db> {
        Region::new(interner, RegionKind::ReVar(v))
    }

    pub fn is_placeholder(&self) -> bool {
        matches!(self.inner(), RegionKind::RePlaceholder(..))
    }

    pub fn is_static(&self) -> bool {
        matches!(self.inner(), RegionKind::ReStatic)
    }

    pub fn error(interner: DbInterner<'db>) -> Self {
        Region::new(interner, RegionKind::ReError(ErrorGuaranteed))
    }

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

        match &self.inner() {
            RegionKind::ReVar(..) => {
                flags |= TypeFlags::HAS_FREE_REGIONS;
                flags |= TypeFlags::HAS_FREE_LOCAL_REGIONS;
                flags |= TypeFlags::HAS_RE_INFER;
            }
            RegionKind::RePlaceholder(..) => {
                flags |= TypeFlags::HAS_FREE_REGIONS;
                flags |= TypeFlags::HAS_FREE_LOCAL_REGIONS;
                flags |= TypeFlags::HAS_RE_PLACEHOLDER;
            }
            RegionKind::ReEarlyParam(..) => {
                flags |= TypeFlags::HAS_FREE_REGIONS;
                flags |= TypeFlags::HAS_FREE_LOCAL_REGIONS;
                flags |= TypeFlags::HAS_RE_PARAM;
            }
            RegionKind::ReLateParam(..) => {
                flags |= TypeFlags::HAS_FREE_REGIONS;
                flags |= TypeFlags::HAS_FREE_LOCAL_REGIONS;
            }
            RegionKind::ReStatic => {
                flags |= TypeFlags::HAS_FREE_REGIONS;
            }
            RegionKind::ReBound(..) => {
                flags |= TypeFlags::HAS_RE_BOUND;
            }
            RegionKind::ReErased => {
                flags |= TypeFlags::HAS_RE_ERASED;
            }
            RegionKind::ReError(..) => {
                flags |= TypeFlags::HAS_FREE_REGIONS;
                flags |= TypeFlags::HAS_ERROR;
            }
        }

        flags
    }
}

pub type PlaceholderRegion = Placeholder<BoundRegion>;

#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct EarlyParamRegion {
    // FIXME: See `ParamTy`.
    pub id: LifetimeParamId,
    pub index: u32,
}

#[derive(Copy, Clone, PartialEq, Eq, Hash)]
/// 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: SolverDefId,
    pub bound_region: BoundRegionKind,
}

impl std::fmt::Debug for LateParamRegion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "ReLateParam({:?}, {:?})", self.scope, self.bound_region)
    }
}

#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub enum BoundRegionKind {
    /// An anonymous region parameter for a given fn (&T)
    Anon,

    /// Named region parameters for functions (a in &'a T)
    ///
    /// The `DefId` is needed to distinguish free regions in
    /// the event of shadowing.
    Named(SolverDefId),

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

impl std::fmt::Debug for BoundRegionKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match *self {
            BoundRegionKind::Anon => write!(f, "BrAnon"),
            BoundRegionKind::Named(did) => {
                write!(f, "BrNamed({did:?})")
            }
            BoundRegionKind::ClosureEnv => write!(f, "BrEnv"),
        }
    }
}

#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct BoundRegion {
    pub var: BoundVar,
    pub kind: BoundRegionKind,
}

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.index)
        // write!(f, "{}/#{}", self.name, self.index)
    }
}

impl<'db> rustc_type_ir::inherent::BoundVarLike<DbInterner<'db>> for BoundRegion {
    fn var(self) -> BoundVar {
        self.var
    }

    fn assert_eq(self, var: BoundVarKind) {
        assert_eq!(self.kind, var.expect_region())
    }
}

impl core::fmt::Debug for BoundRegion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.kind {
            BoundRegionKind::Anon => write!(f, "{:?}", self.var),
            BoundRegionKind::ClosureEnv => write!(f, "{:?}.Env", self.var),
            BoundRegionKind::Named(def) => {
                write!(f, "{:?}.Named({:?})", self.var, def)
            }
        }
    }
}

impl BoundRegionKind {
    pub fn is_named(&self) -> bool {
        matches!(self, BoundRegionKind::Named(_))
    }

    pub fn get_name(&self) -> Option<Symbol> {
        None
    }

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

impl<'db> IntoKind for Region<'db> {
    type Kind = RegionKind<'db>;

    fn kind(self) -> Self::Kind {
        *self.inner()
    }
}

impl<'db> TypeVisitable<DbInterner<'db>> for Region<'db> {
    fn visit_with<V: rustc_type_ir::TypeVisitor<DbInterner<'db>>>(
        &self,
        visitor: &mut V,
    ) -> V::Result {
        visitor.visit_region(*self)
    }
}

impl<'db> TypeFoldable<DbInterner<'db>> for Region<'db> {
    fn try_fold_with<F: rustc_type_ir::FallibleTypeFolder<DbInterner<'db>>>(
        self,
        folder: &mut F,
    ) -> Result<Self, F::Error> {
        folder.try_fold_region(self)
    }
    fn fold_with<F: rustc_type_ir::TypeFolder<DbInterner<'db>>>(self, folder: &mut F) -> Self {
        folder.fold_region(self)
    }
}

impl<'db> Relate<DbInterner<'db>> for Region<'db> {
    fn relate<R: rustc_type_ir::relate::TypeRelation<DbInterner<'db>>>(
        relation: &mut R,
        a: Self,
        b: Self,
    ) -> rustc_type_ir::relate::RelateResult<DbInterner<'db>, Self> {
        relation.regions(a, b)
    }
}

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

    fn outer_exclusive_binder(&self) -> rustc_type_ir::DebruijnIndex {
        match &self.inner() {
            RegionKind::ReBound(debruijn, _) => debruijn.shifted_in(1),
            _ => INNERMOST,
        }
    }
}

impl<'db> rustc_type_ir::inherent::Region<DbInterner<'db>> for Region<'db> {
    fn new_bound(
        interner: DbInterner<'db>,
        debruijn: rustc_type_ir::DebruijnIndex,
        var: BoundRegion,
    ) -> Self {
        Region::new(interner, RegionKind::ReBound(debruijn, var))
    }

    fn new_anon_bound(
        interner: DbInterner<'db>,
        debruijn: rustc_type_ir::DebruijnIndex,
        var: rustc_type_ir::BoundVar,
    ) -> Self {
        Region::new(
            interner,
            RegionKind::ReBound(debruijn, BoundRegion { var, kind: BoundRegionKind::Anon }),
        )
    }

    fn new_static(interner: DbInterner<'db>) -> Self {
        Region::new(interner, RegionKind::ReStatic)
    }

    fn new_placeholder(
        interner: DbInterner<'db>,
        var: <DbInterner<'db> as rustc_type_ir::Interner>::PlaceholderRegion,
    ) -> Self {
        Region::new(interner, RegionKind::RePlaceholder(var))
    }
}

impl<'db> PlaceholderLike<DbInterner<'db>> for PlaceholderRegion {
    type Bound = BoundRegion;

    fn universe(self) -> rustc_type_ir::UniverseIndex {
        self.universe
    }

    fn var(self) -> rustc_type_ir::BoundVar {
        self.bound.var
    }

    fn with_updated_universe(self, ui: rustc_type_ir::UniverseIndex) -> Self {
        Placeholder { universe: ui, bound: self.bound }
    }

    fn new(ui: rustc_type_ir::UniverseIndex, bound: Self::Bound) -> Self {
        Placeholder { universe: ui, bound }
    }

    fn new_anon(ui: rustc_type_ir::UniverseIndex, var: rustc_type_ir::BoundVar) -> Self {
        Placeholder { universe: ui, bound: BoundRegion { var, kind: BoundRegionKind::Anon } }
    }
}

type GenericArgOutlivesPredicate<'db> = OutlivesPredicate<'db, GenericArg<'db>>;

interned_vec_db!(RegionAssumptions, GenericArgOutlivesPredicate);
