//! Detecting whether a type is infinitely-sized.

use hir_def::{AdtId, VariantId, hir::generics::GenericParams};
use rustc_type_ir::inherent::IntoKind;

use crate::{
    db::HirDatabase,
    next_solver::{GenericArgKind, GenericArgs, Ty, TyKind},
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Representability {
    Representable,
    Infinite,
}

macro_rules! rtry {
    ($e:expr) => {
        match $e {
            e @ Representability::Infinite => return e,
            Representability::Representable => {}
        }
    };
}

#[salsa::tracked(cycle_result = representability_cycle)]
pub(crate) fn representability(db: &dyn HirDatabase, id: AdtId) -> Representability {
    match id {
        AdtId::StructId(id) => variant_representability(db, id.into()),
        AdtId::UnionId(id) => variant_representability(db, id.into()),
        AdtId::EnumId(id) => {
            for &(variant, ..) in id.enum_variants(db).variants.values() {
                rtry!(variant_representability(db, variant.into()));
            }
            Representability::Representable
        }
    }
}

pub(crate) fn representability_cycle(
    _db: &dyn HirDatabase,
    _: salsa::Id,
    _id: AdtId,
) -> Representability {
    Representability::Infinite
}

fn variant_representability(db: &dyn HirDatabase, id: VariantId) -> Representability {
    for ty in db.field_types(id).values() {
        rtry!(representability_ty(db, ty.get().instantiate_identity().skip_norm_wip()));
    }
    Representability::Representable
}

fn representability_ty<'db>(db: &'db dyn HirDatabase, ty: Ty<'db>) -> Representability {
    match ty.kind() {
        TyKind::Adt(adt_id, args) => representability_adt_ty(db, adt_id.def_id(), args),
        // FIXME(#11924) allow zero-length arrays?
        TyKind::Array(ty, _) => representability_ty(db, ty),
        TyKind::Tuple(tys) => {
            for ty in tys {
                rtry!(representability_ty(db, ty));
            }
            Representability::Representable
        }
        _ => Representability::Representable,
    }
}

fn representability_adt_ty<'db>(
    db: &'db dyn HirDatabase,
    def_id: AdtId,
    args: GenericArgs<'db>,
) -> Representability {
    rtry!(representability(db, def_id));

    // At this point, we know that the item of the ADT type is representable;
    // but the type parameters may cause a cycle with an upstream type
    let params_in_repr = params_in_repr(db, def_id);
    for (i, arg) in args.iter().enumerate() {
        if let GenericArgKind::Type(ty) = arg.kind()
            && params_in_repr[i]
        {
            rtry!(representability_ty(db, ty));
        }
    }
    Representability::Representable
}

fn params_in_repr(db: &dyn HirDatabase, def_id: AdtId) -> Box<[bool]> {
    let generics = GenericParams::of(db, def_id.into());
    let mut params_in_repr = (0..generics.len_lifetimes() + generics.len_type_or_consts())
        .map(|_| false)
        .collect::<Box<[bool]>>();
    let mut handle_variant = |variant| {
        for field in db.field_types(variant).values() {
            params_in_repr_ty(
                db,
                field.get().instantiate_identity().skip_norm_wip(),
                &mut params_in_repr,
            );
        }
    };
    match def_id {
        AdtId::StructId(def_id) => handle_variant(def_id.into()),
        AdtId::UnionId(def_id) => handle_variant(def_id.into()),
        AdtId::EnumId(def_id) => {
            for &(variant, ..) in def_id.enum_variants(db).variants.values() {
                handle_variant(variant.into());
            }
        }
    }
    params_in_repr
}

fn params_in_repr_ty<'db>(db: &'db dyn HirDatabase, ty: Ty<'db>, params_in_repr: &mut [bool]) {
    match ty.kind() {
        TyKind::Adt(adt, args) => {
            let inner_params_in_repr = self::params_in_repr(db, adt.def_id());
            for (i, arg) in args.iter().enumerate() {
                if let GenericArgKind::Type(ty) = arg.kind()
                    && inner_params_in_repr[i]
                {
                    params_in_repr_ty(db, ty, params_in_repr);
                }
            }
        }
        TyKind::Array(ty, _) => params_in_repr_ty(db, ty, params_in_repr),
        TyKind::Tuple(tys) => tys.iter().for_each(|ty| params_in_repr_ty(db, ty, params_in_repr)),
        TyKind::Param(param) => {
            params_in_repr[param.index as usize] = true;
        }
        _ => {}
    }
}
