//! Compute the binary representation of a type

use base_db::CrateId;
use chalk_ir::{AdtId, TyKind};
use hir_def::{
    layout::{
        Abi, FieldsShape, Integer, Layout, LayoutCalculator, LayoutError, Primitive, ReprOptions,
        RustcEnumVariantIdx, Scalar, Size, StructKind, TargetDataLayout, Variants, WrappingRange,
    },
    LocalFieldId,
};
use stdx::never;

use crate::{consteval::try_const_usize, db::HirDatabase, Interner, Substitution, Ty};

use self::adt::struct_variant_idx;
pub use self::{
    adt::{layout_of_adt_query, layout_of_adt_recover},
    target::target_data_layout_query,
};

macro_rules! user_error {
    ($x: expr) => {
        return Err(LayoutError::UserError(format!($x)))
    };
}

mod adt;
mod target;

struct LayoutCx<'a> {
    krate: CrateId,
    target: &'a TargetDataLayout,
}

impl<'a> LayoutCalculator for LayoutCx<'a> {
    type TargetDataLayoutRef = &'a TargetDataLayout;

    fn delay_bug(&self, txt: &str) {
        never!("{}", txt);
    }

    fn current_data_layout(&self) -> &'a TargetDataLayout {
        self.target
    }
}

fn scalar_unit(dl: &TargetDataLayout, value: Primitive) -> Scalar {
    Scalar::Initialized { value, valid_range: WrappingRange::full(value.size(dl)) }
}

fn scalar(dl: &TargetDataLayout, value: Primitive) -> Layout {
    Layout::scalar(dl, scalar_unit(dl, value))
}

pub fn layout_of_ty(db: &dyn HirDatabase, ty: &Ty, krate: CrateId) -> Result<Layout, LayoutError> {
    let Some(target) = db.target_data_layout(krate) else { return Err(LayoutError::TargetLayoutNotAvailable) };
    let cx = LayoutCx { krate, target: &target };
    let dl = &*cx.current_data_layout();
    Ok(match ty.kind(Interner) {
        TyKind::Adt(AdtId(def), subst) => db.layout_of_adt(*def, subst.clone())?,
        TyKind::Scalar(s) => match s {
            chalk_ir::Scalar::Bool => Layout::scalar(
                dl,
                Scalar::Initialized {
                    value: Primitive::Int(Integer::I8, false),
                    valid_range: WrappingRange { start: 0, end: 1 },
                },
            ),
            chalk_ir::Scalar::Char => Layout::scalar(
                dl,
                Scalar::Initialized {
                    value: Primitive::Int(Integer::I32, false),
                    valid_range: WrappingRange { start: 0, end: 0x10FFFF },
                },
            ),
            chalk_ir::Scalar::Int(i) => scalar(
                dl,
                Primitive::Int(
                    match i {
                        chalk_ir::IntTy::Isize => dl.ptr_sized_integer(),
                        chalk_ir::IntTy::I8 => Integer::I8,
                        chalk_ir::IntTy::I16 => Integer::I16,
                        chalk_ir::IntTy::I32 => Integer::I32,
                        chalk_ir::IntTy::I64 => Integer::I64,
                        chalk_ir::IntTy::I128 => Integer::I128,
                    },
                    true,
                ),
            ),
            chalk_ir::Scalar::Uint(i) => scalar(
                dl,
                Primitive::Int(
                    match i {
                        chalk_ir::UintTy::Usize => dl.ptr_sized_integer(),
                        chalk_ir::UintTy::U8 => Integer::I8,
                        chalk_ir::UintTy::U16 => Integer::I16,
                        chalk_ir::UintTy::U32 => Integer::I32,
                        chalk_ir::UintTy::U64 => Integer::I64,
                        chalk_ir::UintTy::U128 => Integer::I128,
                    },
                    false,
                ),
            ),
            chalk_ir::Scalar::Float(f) => scalar(
                dl,
                match f {
                    chalk_ir::FloatTy::F32 => Primitive::F32,
                    chalk_ir::FloatTy::F64 => Primitive::F64,
                },
            ),
        },
        TyKind::Tuple(len, tys) => {
            let kind = if *len == 0 { StructKind::AlwaysSized } else { StructKind::MaybeUnsized };

            let fields = tys
                .iter(Interner)
                .map(|k| layout_of_ty(db, k.assert_ty_ref(Interner), krate))
                .collect::<Result<Vec<_>, _>>()?;
            let fields = fields.iter().collect::<Vec<_>>();
            let fields = fields.iter().collect::<Vec<_>>();
            cx.univariant(dl, &fields, &ReprOptions::default(), kind).ok_or(LayoutError::Unknown)?
        }
        TyKind::Array(element, count) => {
            let count = try_const_usize(&count).ok_or(LayoutError::UserError(
                "mismatched type of const generic parameter".to_string(),
            ))? as u64;
            let element = layout_of_ty(db, element, krate)?;
            let size = element.size.checked_mul(count, dl).ok_or(LayoutError::SizeOverflow)?;

            let abi = if count != 0 && matches!(element.abi, Abi::Uninhabited) {
                Abi::Uninhabited
            } else {
                Abi::Aggregate { sized: true }
            };

            let largest_niche = if count != 0 { element.largest_niche } else { None };

            Layout {
                variants: Variants::Single { index: struct_variant_idx() },
                fields: FieldsShape::Array { stride: element.size, count },
                abi,
                largest_niche,
                align: element.align,
                size,
            }
        }
        TyKind::Slice(element) => {
            let element = layout_of_ty(db, element, krate)?;
            Layout {
                variants: Variants::Single { index: struct_variant_idx() },
                fields: FieldsShape::Array { stride: element.size, count: 0 },
                abi: Abi::Aggregate { sized: false },
                largest_niche: None,
                align: element.align,
                size: Size::ZERO,
            }
        }
        // Potentially-wide pointers.
        TyKind::Ref(_, _, pointee) | TyKind::Raw(_, pointee) => {
            let mut data_ptr = scalar_unit(dl, Primitive::Pointer);
            if matches!(ty.kind(Interner), TyKind::Ref(..)) {
                data_ptr.valid_range_mut().start = 1;
            }

            // let pointee = tcx.normalize_erasing_regions(param_env, pointee);
            // if pointee.is_sized(tcx.at(DUMMY_SP), param_env) {
            //     return Ok(tcx.mk_layout(LayoutS::scalar(cx, data_ptr)));
            // }

            let unsized_part = struct_tail_erasing_lifetimes(db, pointee.clone());
            let metadata = match unsized_part.kind(Interner) {
                TyKind::Slice(_) | TyKind::Str => {
                    scalar_unit(dl, Primitive::Int(dl.ptr_sized_integer(), false))
                }
                TyKind::Dyn(..) => {
                    let mut vtable = scalar_unit(dl, Primitive::Pointer);
                    vtable.valid_range_mut().start = 1;
                    vtable
                }
                _ => {
                    // pointee is sized
                    return Ok(Layout::scalar(dl, data_ptr));
                }
            };

            // Effectively a (ptr, meta) tuple.
            cx.scalar_pair(data_ptr, metadata)
        }
        TyKind::FnDef(_, _) => layout_of_unit(&cx, dl)?,
        TyKind::Str => Layout {
            variants: Variants::Single { index: struct_variant_idx() },
            fields: FieldsShape::Array { stride: Size::from_bytes(1), count: 0 },
            abi: Abi::Aggregate { sized: false },
            largest_niche: None,
            align: dl.i8_align,
            size: Size::ZERO,
        },
        TyKind::Never => Layout {
            variants: Variants::Single { index: struct_variant_idx() },
            fields: FieldsShape::Primitive,
            abi: Abi::Uninhabited,
            largest_niche: None,
            align: dl.i8_align,
            size: Size::ZERO,
        },
        TyKind::Dyn(_) | TyKind::Foreign(_) => {
            let mut unit = layout_of_unit(&cx, dl)?;
            match unit.abi {
                Abi::Aggregate { ref mut sized } => *sized = false,
                _ => user_error!("bug"),
            }
            unit
        }
        TyKind::Function(_) => {
            let mut ptr = scalar_unit(dl, Primitive::Pointer);
            ptr.valid_range_mut().start = 1;
            Layout::scalar(dl, ptr)
        }
        TyKind::OpaqueType(opaque_ty_id, _) => {
            let impl_trait_id = db.lookup_intern_impl_trait_id((*opaque_ty_id).into());
            match impl_trait_id {
                crate::ImplTraitId::ReturnTypeImplTrait(func, idx) => {
                    let infer = db.infer(func.into());
                    layout_of_ty(db, &infer.type_of_rpit[idx], krate)?
                }
                crate::ImplTraitId::AsyncBlockTypeImplTrait(_, _) => {
                    return Err(LayoutError::NotImplemented)
                }
            }
        }
        TyKind::Closure(_, _) | TyKind::Generator(_, _) | TyKind::GeneratorWitness(_, _) => {
            return Err(LayoutError::NotImplemented)
        }
        TyKind::AssociatedType(_, _)
        | TyKind::Error
        | TyKind::Alias(_)
        | TyKind::Placeholder(_)
        | TyKind::BoundVar(_)
        | TyKind::InferenceVar(_, _) => return Err(LayoutError::HasPlaceholder),
    })
}

fn layout_of_unit(cx: &LayoutCx<'_>, dl: &TargetDataLayout) -> Result<Layout, LayoutError> {
    cx.univariant::<RustcEnumVariantIdx, &&Layout>(
        dl,
        &[],
        &ReprOptions::default(),
        StructKind::AlwaysSized,
    )
    .ok_or(LayoutError::Unknown)
}

fn struct_tail_erasing_lifetimes(db: &dyn HirDatabase, pointee: Ty) -> Ty {
    match pointee.kind(Interner) {
        TyKind::Adt(AdtId(hir_def::AdtId::StructId(i)), subst) => {
            let data = db.struct_data(*i);
            let mut it = data.variant_data.fields().iter().rev();
            match it.next() {
                Some((f, _)) => field_ty(db, (*i).into(), f, subst),
                None => pointee,
            }
        }
        _ => pointee,
    }
}

fn field_ty(
    db: &dyn HirDatabase,
    def: hir_def::VariantId,
    fd: LocalFieldId,
    subst: &Substitution,
) -> Ty {
    db.field_types(def)[fd].clone().substitute(Interner, subst)
}

#[cfg(test)]
mod tests;
