use rustc_abi::ExternAbi;
use rustc_errors::{DiagCtxtHandle, E0781, struct_span_code_err};
use rustc_hir::{self as hir, HirId};
use rustc_middle::bug;
use rustc_middle::ty::layout::{LayoutCx, LayoutError, TyAndLayout};
use rustc_middle::ty::{self, TyCtxt, TypeVisitableExt};
use rustc_span::Span;

use crate::diagnostics;

/// Check conditions on inputs and outputs that the cmse ABIs impose: arguments and results MUST be
/// returned via registers (i.e. MUST NOT spill to the stack). LLVM will also validate these
/// conditions, but by checking them here rustc can emit nicer error messages.
pub(crate) fn validate_cmse_abi<'tcx>(
    tcx: TyCtxt<'tcx>,
    dcx: DiagCtxtHandle<'_>,
    hir_id: HirId,
    abi: ExternAbi,
    fn_sig: ty::PolyFnSig<'tcx>,
) {
    let fn_decl = match abi {
        ExternAbi::CmseNonSecureCall => match tcx.hir_node(hir_id) {
            hir::Node::Ty(hir::Ty { kind: hir::TyKind::FnPtr(fn_ptr_ty), .. }) => fn_ptr_ty.decl,
            _ => {
                let span = match tcx.parent_hir_node(hir_id) {
                    hir::Node::Item(hir::Item {
                        kind: hir::ItemKind::ForeignMod { .. },
                        span,
                        ..
                    }) => *span,
                    _ => tcx.hir_span(hir_id),
                };
                struct_span_code_err!(
                    dcx,
                    span,
                    E0781,
                    "the `\"cmse-nonsecure-call\"` ABI is only allowed on function pointers"
                )
                .emit();
                return;
            }
        },
        ExternAbi::CmseNonSecureEntry => {
            let Some(hir::FnSig { decl, .. }) = tcx.hir_node(hir_id).fn_sig() else {
                // might happen when this ABI is used incorrectly. That will be handled elsewhere
                return;
            };

            // An `extern "cmse-nonsecure-entry"` function cannot be c-variadic. We run
            // into https://github.com/rust-lang/rust/issues/132142 if we don't explicitly bail.
            if decl.c_variadic() {
                return;
            }

            decl
        }
        _ => return,
    };

    if let Err((span, layout_err)) = is_valid_cmse_inputs(tcx, dcx, fn_sig, fn_decl, abi) {
        if should_emit_layout_error(abi, layout_err) {
            dcx.emit_err(diagnostics::CmseGeneric { span, abi });
        }
    }

    if let Err(layout_err) = is_valid_cmse_output(tcx, dcx, fn_sig, fn_decl, abi) {
        if should_emit_layout_error(abi, layout_err) {
            dcx.emit_err(diagnostics::CmseGeneric { span: fn_decl.output.span(), abi });
        }
    }
}

/// Returns whether the inputs will fit into the available registers
fn is_valid_cmse_inputs<'tcx>(
    tcx: TyCtxt<'tcx>,
    dcx: DiagCtxtHandle<'_>,
    fn_sig: ty::PolyFnSig<'tcx>,
    fn_decl: &hir::FnDecl<'tcx>,
    abi: ExternAbi,
) -> Result<(), (Span, &'tcx LayoutError<'tcx>)> {
    let mut accum = 0u64;
    let mut excess_argument_spans = Vec::new();

    // this type is only used for layout computation, which does not rely on regions
    let fn_sig = tcx.instantiate_bound_regions_with_erased(fn_sig);
    let fn_sig = tcx.erase_and_anonymize_regions(fn_sig);

    for (ty, hir_ty) in fn_sig.inputs().iter().zip(fn_decl.inputs) {
        if ty.has_infer_types() {
            let err = LayoutError::Unknown(*ty);
            return Err((hir_ty.span, tcx.arena.alloc(err)));
        }

        let layout = tcx
            .layout_of(ty::TypingEnv::fully_monomorphized().as_query_input(*ty))
            .map_err(|e| (hir_ty.span, e))?;

        let align = layout.layout.align().bytes();
        let size = layout.layout.size().bytes();

        accum += size;
        accum = accum.next_multiple_of(Ord::max(4, align));

        // i.e. exceeds 4 32-bit registers
        if accum > 16 {
            excess_argument_spans.push(hir_ty.span);
        }
    }

    if !excess_argument_spans.is_empty() {
        // fn f(x: u32, y: u32, z: u32, w: u16, q: u16) -> u32,
        //                                      ^^^^^^
        dcx.emit_err(diagnostics::CmseInputsStackSpill { spans: excess_argument_spans, abi });
    }

    Ok(())
}

/// Returns whether the output will fit into the available registers
fn is_valid_cmse_output<'tcx>(
    tcx: TyCtxt<'tcx>,
    dcx: DiagCtxtHandle<'_>,
    fn_sig: ty::PolyFnSig<'tcx>,
    fn_decl: &hir::FnDecl<'tcx>,
    abi: ExternAbi,
) -> Result<(), &'tcx LayoutError<'tcx>> {
    // this type is only used for layout computation, which does not rely on regions
    let fn_sig = tcx.instantiate_bound_regions_with_erased(fn_sig);
    let fn_sig = tcx.erase_and_anonymize_regions(fn_sig);
    let return_type = fn_sig.output();

    // `impl Trait` is already disallowed with `cmse-nonsecure-call`, because that ABI is only
    // allowed on function pointers, and function pointers cannot contain `impl Trait` in their
    // signature.
    //
    // Here we explicitly disallow `impl Trait` in the `cmse-nonsecure-entry` return type too, to
    // prevent query cycles when calculating the layout. This ABI is meant to be used with
    // `#[no_mangle]` or similar, so generics in the type really don't make sense.
    //
    // see also https://github.com/rust-lang/rust/issues/147242.
    if abi == ExternAbi::CmseNonSecureEntry && return_type.has_opaque_types() {
        dcx.emit_err(diagnostics::CmseImplTrait { span: fn_decl.output.span(), abi });
        return Ok(());
    }

    if return_type.has_infer_types() {
        let err = LayoutError::Unknown(return_type);
        return Err(tcx.arena.alloc(err));
    }

    let typing_env = ty::TypingEnv::fully_monomorphized();
    let layout = tcx.layout_of(typing_env.as_query_input(return_type))?;
    let layout_cx = LayoutCx::new(tcx, typing_env);

    if !is_valid_cmse_output_layout(layout_cx, layout) {
        dcx.emit_err(diagnostics::CmseOutputStackSpill { span: fn_decl.output.span(), abi });
    }

    Ok(())
}

/// Returns whether the output will fit into the available registers
fn is_valid_cmse_output_layout<'tcx>(cx: LayoutCx<'tcx>, layout: TyAndLayout<'tcx>) -> bool {
    let size = layout.layout.size().bytes();

    if size <= 4 {
        return true;
    } else if size != 8 {
        return false;
    }

    // Accept (transparently wrapped) scalar 64-bit primitives.
    matches!(
        layout.peel_transparent_wrappers(&cx).ty.kind(),
        ty::Int(ty::IntTy::I64) | ty::Uint(ty::UintTy::U64) | ty::Float(ty::FloatTy::F64)
    )
}

fn should_emit_layout_error<'tcx>(abi: ExternAbi, layout_err: &'tcx LayoutError<'tcx>) -> bool {
    use LayoutError::*;

    match layout_err {
        TooGeneric(ty) => {
            match abi {
                ExternAbi::CmseNonSecureCall => {
                    // prevent double reporting of this error
                    !ty.has_opaque_types()
                }
                ExternAbi::CmseNonSecureEntry => true,
                _ => bug!("invalid ABI: {abi}"),
            }
        }
        Unknown(..)
        | SizeOverflow(..)
        | InvalidSimd { .. }
        | NormalizationFailure(..)
        | ReferencesError(..) => {
            false // not our job to report these
        }
    }
}
