use rustc_index::IndexVec;
use rustc_middle::mir::visit::{MutatingUseContext, NonMutatingUseContext, PlaceContext};
use rustc_middle::mir::*;
use rustc_middle::ty::{Ty, TyCtxt};
use rustc_session::Session;

use crate::check_pointers::{BorrowedFieldProjectionMode, PointerCheck, check_pointers};

pub(super) struct CheckNull;

impl<'tcx> crate::MirPass<'tcx> for CheckNull {
    fn is_enabled(&self, sess: &Session) -> bool {
        sess.ub_checks()
    }

    fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
        check_pointers(
            tcx,
            body,
            &[],
            insert_null_check,
            BorrowedFieldProjectionMode::NoFollowProjections,
        );
    }

    fn is_required(&self) -> bool {
        true
    }
}

fn insert_null_check<'tcx>(
    tcx: TyCtxt<'tcx>,
    pointer: Place<'tcx>,
    pointee_ty: Ty<'tcx>,
    context: PlaceContext,
    local_decls: &mut IndexVec<Local, LocalDecl<'tcx>>,
    stmts: &mut Vec<Statement<'tcx>>,
    source_info: SourceInfo,
) -> PointerCheck<'tcx> {
    // Cast the pointer to a *const ().
    let const_raw_ptr = Ty::new_imm_ptr(tcx, tcx.types.unit);
    let rvalue = Rvalue::Cast(CastKind::PtrToPtr, Operand::Copy(pointer), const_raw_ptr);
    let thin_ptr = local_decls.push(LocalDecl::with_source_info(const_raw_ptr, source_info)).into();
    stmts
        .push(Statement { source_info, kind: StatementKind::Assign(Box::new((thin_ptr, rvalue))) });

    // Transmute the pointer to a usize (equivalent to `ptr.addr()`).
    let rvalue = Rvalue::Cast(CastKind::Transmute, Operand::Copy(thin_ptr), tcx.types.usize);
    let addr = local_decls.push(LocalDecl::with_source_info(tcx.types.usize, source_info)).into();
    stmts.push(Statement { source_info, kind: StatementKind::Assign(Box::new((addr, rvalue))) });

    let zero = Operand::Constant(Box::new(ConstOperand {
        span: source_info.span,
        user_ty: None,
        const_: Const::Val(ConstValue::from_target_usize(0, &tcx), tcx.types.usize),
    }));

    let pointee_should_be_checked = match context {
        // Borrows pointing to "null" are UB even if the pointee is a ZST.
        PlaceContext::NonMutatingUse(NonMutatingUseContext::SharedBorrow)
        | PlaceContext::MutatingUse(MutatingUseContext::Borrow) => {
            // Pointer should be checked unconditionally.
            Operand::Constant(Box::new(ConstOperand {
                span: source_info.span,
                user_ty: None,
                const_: Const::Val(ConstValue::from_bool(true), tcx.types.bool),
            }))
        }
        // Other usages of null pointers only are UB if the pointee is not a ZST.
        _ => {
            let rvalue = Rvalue::NullaryOp(NullOp::SizeOf, pointee_ty);
            let sizeof_pointee =
                local_decls.push(LocalDecl::with_source_info(tcx.types.usize, source_info)).into();
            stmts.push(Statement {
                source_info,
                kind: StatementKind::Assign(Box::new((sizeof_pointee, rvalue))),
            });

            // Check that the pointee is not a ZST.
            let is_pointee_not_zst =
                local_decls.push(LocalDecl::with_source_info(tcx.types.bool, source_info)).into();
            stmts.push(Statement {
                source_info,
                kind: StatementKind::Assign(Box::new((
                    is_pointee_not_zst,
                    Rvalue::BinaryOp(
                        BinOp::Ne,
                        Box::new((Operand::Copy(sizeof_pointee), zero.clone())),
                    ),
                ))),
            });

            // Pointer needs to be checked only if pointee is not a ZST.
            Operand::Copy(is_pointee_not_zst)
        }
    };

    // Check whether the pointer is null.
    let is_null = local_decls.push(LocalDecl::with_source_info(tcx.types.bool, source_info)).into();
    stmts.push(Statement {
        source_info,
        kind: StatementKind::Assign(Box::new((
            is_null,
            Rvalue::BinaryOp(BinOp::Eq, Box::new((Operand::Copy(addr), zero))),
        ))),
    });

    // We want to throw an exception if the pointer is null and the pointee is not unconditionally
    // allowed (which for all non-borrow place uses, is when the pointee is ZST).
    let should_throw_exception =
        local_decls.push(LocalDecl::with_source_info(tcx.types.bool, source_info)).into();
    stmts.push(Statement {
        source_info,
        kind: StatementKind::Assign(Box::new((
            should_throw_exception,
            Rvalue::BinaryOp(
                BinOp::BitAnd,
                Box::new((Operand::Copy(is_null), pointee_should_be_checked)),
            ),
        ))),
    });

    // The final condition whether this pointer usage is ok or not.
    let is_ok = local_decls.push(LocalDecl::with_source_info(tcx.types.bool, source_info)).into();
    stmts.push(Statement {
        source_info,
        kind: StatementKind::Assign(Box::new((
            is_ok,
            Rvalue::UnaryOp(UnOp::Not, Operand::Copy(should_throw_exception)),
        ))),
    });

    // Emit a PointerCheck that asserts on the condition and otherwise triggers
    // a AssertKind::NullPointerDereference.
    PointerCheck {
        cond: Operand::Copy(is_ok),
        assert_kind: Box::new(AssertKind::NullPointerDereference),
    }
}
