use rustc_abi::{BackendRepr, FieldsShape, Scalar, Variants};
use rustc_middle::ty::layout::{
    HasTyCtxt, LayoutCx, LayoutError, LayoutOf, TyAndLayout, ValidityRequirement,
};
use rustc_middle::ty::print::with_no_trimmed_paths;
use rustc_middle::ty::{PseudoCanonicalInput, ScalarInt, Ty, TyCtxt};
use rustc_middle::{bug, ty};
use rustc_span::DUMMY_SP;

use crate::const_eval::{CanAccessMutGlobal, CheckAlignment, CompileTimeMachine};
use crate::interpret::{InterpCx, MemoryKind};

/// Determines if this type permits "raw" initialization by just transmuting some memory into an
/// instance of `T`.
///
/// `init_kind` indicates if the memory is zero-initialized or left uninitialized. We assume
/// uninitialized memory is mitigated by filling it with 0x01, which reduces the chance of causing
/// LLVM UB.
///
/// By default we check whether that operation would cause *LLVM UB*, i.e., whether the LLVM IR we
/// generate has UB or not. This is a mitigation strategy, which is why we are okay with accepting
/// Rust UB as long as there is no risk of miscompilations. The `strict_init_checks` can be set to
/// do a full check against Rust UB instead (in which case we will also ignore the 0x01-filling and
/// to the full uninit check).
pub fn check_validity_requirement<'tcx>(
    tcx: TyCtxt<'tcx>,
    kind: ValidityRequirement,
    input: PseudoCanonicalInput<'tcx, Ty<'tcx>>,
) -> Result<bool, &'tcx LayoutError<'tcx>> {
    let layout = tcx.layout_of(input)?;

    // There is nothing strict or lax about inhabitedness.
    if kind == ValidityRequirement::Inhabited {
        return Ok(!layout.is_uninhabited());
    }

    let layout_cx = LayoutCx::new(tcx, input.typing_env);
    if kind == ValidityRequirement::Uninit || tcx.sess.opts.unstable_opts.strict_init_checks {
        Ok(check_validity_requirement_strict(layout, &layout_cx, kind))
    } else {
        check_validity_requirement_lax(layout, &layout_cx, kind)
    }
}

/// Implements the 'strict' version of the [`check_validity_requirement`] checks; see that function
/// for details.
fn check_validity_requirement_strict<'tcx>(
    ty: TyAndLayout<'tcx>,
    cx: &LayoutCx<'tcx>,
    kind: ValidityRequirement,
) -> bool {
    let machine = CompileTimeMachine::new(CanAccessMutGlobal::No, CheckAlignment::Error);

    let mut cx = InterpCx::new(cx.tcx(), DUMMY_SP, cx.typing_env, machine);

    // It doesn't really matter which `MemoryKind` we use here, `Stack` is the least wrong.
    let allocated =
        cx.allocate(ty, MemoryKind::Stack).expect("OOM: failed to allocate for uninit check");

    if kind == ValidityRequirement::Zero {
        cx.write_bytes_ptr(
            allocated.ptr(),
            std::iter::repeat_n(0_u8, ty.layout.size().bytes_usize()),
        )
        .expect("failed to write bytes for zero valid check");
    }

    // Assume that if it failed, it's a validation failure.
    // This does *not* actually check that references are dereferenceable, but since all types that
    // require dereferenceability also require non-null, we don't actually get any false negatives
    // due to this.
    // The value we are validating is temporary and discarded at the end of this function, so
    // there is no point in resetting provenance and padding.
    // This is pretty inefficient: we do the full path tracking and even format an error message
    // in case there is a problem, only to entirely throw that away again. For a nightly-only
    // option this is fine, but if this is ever meant to be stable we should probably add
    // a "fast mode" to validation.
    with_no_trimmed_paths!(
        cx.validate_operand(
            &allocated.into(),
            /*recursive*/ false,
            /*reset_provenance_and_padding*/ false,
        )
        .discard_err()
        .is_some()
    )
}

/// Implements the 'lax' (default) version of the [`check_validity_requirement`] checks; see that
/// function for details.
fn check_validity_requirement_lax<'tcx>(
    this: TyAndLayout<'tcx>,
    cx: &LayoutCx<'tcx>,
    init_kind: ValidityRequirement,
) -> Result<bool, &'tcx LayoutError<'tcx>> {
    let scalar_allows_raw_init = move |s: Scalar| -> bool {
        match init_kind {
            ValidityRequirement::Inhabited => {
                bug!("ValidityRequirement::Inhabited should have been handled above")
            }
            ValidityRequirement::Zero => {
                // The range must contain 0.
                s.valid_range(cx).contains(0)
            }
            ValidityRequirement::UninitMitigated0x01Fill => {
                // The range must include an 0x01-filled buffer.
                let mut val: u128 = 0x01;
                for _ in 1..s.size(cx).bytes() {
                    // For sizes >1, repeat the 0x01.
                    val = (val << 8) | 0x01;
                }
                s.valid_range(cx).contains(val)
            }
            ValidityRequirement::Uninit => {
                bug!("ValidityRequirement::Uninit should have been handled above")
            }
        }
    };

    // Check the ABI.
    let valid = !this.is_uninhabited() // definitely UB if uninhabited
        && match this.backend_repr {
            BackendRepr::Scalar(s) => scalar_allows_raw_init(s),
            BackendRepr::ScalarPair(s1, s2) => {
                scalar_allows_raw_init(s1) && scalar_allows_raw_init(s2)
            }
            BackendRepr::SimdVector { element: s, count } => count == 0 || scalar_allows_raw_init(s),
            BackendRepr::Memory { .. } => true, // Fields are checked below.
            BackendRepr::SimdScalableVector { element, .. } => scalar_allows_raw_init(element),
        };

    if !valid {
        // This is definitely not okay.
        return Ok(false);
    }

    // Special magic check for references and boxes (i.e., special pointer types).
    if let Some(pointee) = this.ty.builtin_deref(false) {
        let pointee = cx.layout_of(pointee)?;
        // We need to ensure that the LLVM attributes `aligned` and `dereferenceable(size)` are satisfied.
        if pointee.align.bytes() > 1 {
            // 0x01-filling is not aligned.
            return Ok(false);
        }
        if pointee.size.bytes() > 0 {
            // A 'fake' integer pointer is not sufficiently dereferenceable.
            return Ok(false);
        }
    }

    // If we have not found an error yet, we need to recursively descend into fields.
    match &this.fields {
        FieldsShape::Primitive | FieldsShape::Union { .. } => {}
        FieldsShape::Array { .. } => {
            // Arrays never have scalar layout in LLVM, so if the array is not actually
            // accessed, there is no LLVM UB -- therefore we can skip this.
        }
        FieldsShape::Arbitrary { offsets, .. } => {
            for idx in 0..offsets.len() {
                if !check_validity_requirement_lax(this.field(cx, idx), cx, init_kind)? {
                    // We found a field that is unhappy with this kind of initialization.
                    return Ok(false);
                }
            }
        }
    }

    match &this.variants {
        Variants::Empty => return Ok(false),
        Variants::Single { .. } => {
            // All fields of this single variant have already been checked above, there is nothing
            // else to do.
        }
        Variants::Multiple { .. } => {
            // We cannot tell LLVM anything about the details of this multi-variant layout, so
            // invalid values "hidden" inside the variant cannot cause LLVM trouble.
        }
    }

    Ok(true)
}

pub(crate) fn validate_scalar_in_layout<'tcx>(
    tcx: TyCtxt<'tcx>,
    scalar: ScalarInt,
    ty: Ty<'tcx>,
) -> bool {
    let machine = CompileTimeMachine::new(CanAccessMutGlobal::No, CheckAlignment::Error);

    let typing_env = ty::TypingEnv::fully_monomorphized();
    let mut cx = InterpCx::new(tcx, DUMMY_SP, typing_env, machine);

    let Ok(layout) = cx.layout_of(ty) else {
        bug!("could not compute layout of {scalar:?}:{ty:?}")
    };

    // It doesn't really matter which `MemoryKind` we use here, `Stack` is the least wrong.
    let allocated =
        cx.allocate(layout, MemoryKind::Stack).expect("OOM: failed to allocate for uninit check");

    cx.write_scalar(scalar, &allocated).unwrap();

    cx.validate_operand(
        &allocated.into(),
        /*recursive*/ false,
        /*reset_provenance_and_padding*/ false,
    )
    .discard_err()
    .is_some()
}
