use rustc_abi::Size;
use rustc_data_structures::fx::FxIndexSet;
use rustc_hir::def_id::DefId;
use rustc_hir::limit::Limit;
use rustc_middle::mir::visit::Visitor as MirVisitor;
use rustc_middle::mir::{self, Location, traversal};
use rustc_middle::ty::{self, AssocTag, Instance, Ty, TyCtxt, TypeFoldable};
use rustc_session::lint::builtin::LARGE_ASSIGNMENTS;
use rustc_span::{Ident, Span, Spanned, sym};
use tracing::{debug, trace};

use crate::errors::LargeAssignmentsLint;

struct MoveCheckVisitor<'tcx> {
    tcx: TyCtxt<'tcx>,
    instance: Instance<'tcx>,
    body: &'tcx mir::Body<'tcx>,
    /// Spans for move size lints already emitted. Helps avoid duplicate lints.
    move_size_spans: Vec<Span>,
}

pub(crate) fn check_moves<'tcx>(
    tcx: TyCtxt<'tcx>,
    instance: Instance<'tcx>,
    body: &'tcx mir::Body<'tcx>,
) {
    let mut visitor = MoveCheckVisitor { tcx, instance, body, move_size_spans: vec![] };
    for (bb, data) in traversal::mono_reachable(body, tcx, instance) {
        visitor.visit_basic_block_data(bb, data)
    }
}

impl<'tcx> MirVisitor<'tcx> for MoveCheckVisitor<'tcx> {
    fn visit_terminator(&mut self, terminator: &mir::Terminator<'tcx>, location: Location) {
        match terminator.kind {
            mir::TerminatorKind::Call { ref func, ref args, ref fn_span, .. }
            | mir::TerminatorKind::TailCall { ref func, ref args, ref fn_span } => {
                let callee_ty = func.ty(self.body, self.tcx);
                let callee_ty = self.monomorphize(callee_ty);
                self.check_fn_args_move_size(callee_ty, args, *fn_span, location);
            }
            _ => {}
        }

        // We deliberately do *not* visit the nested operands here, to avoid
        // hitting `visit_operand` for function arguments.
    }

    fn visit_operand(&mut self, operand: &mir::Operand<'tcx>, location: Location) {
        self.check_operand_move_size(operand, location);
    }
}

impl<'tcx> MoveCheckVisitor<'tcx> {
    fn monomorphize<T>(&self, value: T) -> T
    where
        T: TypeFoldable<TyCtxt<'tcx>>,
    {
        trace!("monomorphize: self.instance={:?}", self.instance);
        self.instance.instantiate_mir_and_normalize_erasing_regions(
            self.tcx,
            ty::TypingEnv::fully_monomorphized(),
            ty::EarlyBinder::bind(value),
        )
    }

    fn check_operand_move_size(&mut self, operand: &mir::Operand<'tcx>, location: Location) {
        let limit = self.tcx.move_size_limit();
        if limit.0 == 0 {
            return;
        }

        let source_info = self.body.source_info(location);
        debug!(?source_info);

        if let Some(too_large_size) = self.operand_size_if_too_large(limit, operand) {
            self.lint_large_assignment(limit.0, too_large_size, location, source_info.span);
        };
    }

    pub(super) fn check_fn_args_move_size(
        &mut self,
        callee_ty: Ty<'tcx>,
        args: &[Spanned<mir::Operand<'tcx>>],
        fn_span: Span,
        location: Location,
    ) {
        let limit = self.tcx.move_size_limit();
        if limit.0 == 0 {
            return;
        }

        if args.is_empty() {
            return;
        }

        // Allow large moves into container types that themselves are cheap to move
        let ty::FnDef(def_id, _) = *callee_ty.kind() else {
            return;
        };
        if self.tcx.skip_move_check_fns(()).contains(&def_id) {
            return;
        }

        debug!(?def_id, ?fn_span);

        for arg in args {
            // Moving args into functions is typically implemented with pointer
            // passing at the llvm-ir level and not by memcpy's. So always allow
            // moving args into functions.
            let operand: &mir::Operand<'tcx> = &arg.node;
            if let mir::Operand::Move(_) = operand {
                continue;
            }

            if let Some(too_large_size) = self.operand_size_if_too_large(limit, operand) {
                self.lint_large_assignment(limit.0, too_large_size, location, arg.span);
            };
        }
    }

    fn operand_size_if_too_large(
        &mut self,
        limit: Limit,
        operand: &mir::Operand<'tcx>,
    ) -> Option<Size> {
        let ty = operand.ty(self.body, self.tcx);
        let ty = self.monomorphize(ty);
        let Ok(layout) =
            self.tcx.layout_of(ty::TypingEnv::fully_monomorphized().as_query_input(ty))
        else {
            return None;
        };
        if layout.size.bytes_usize() > limit.0 {
            debug!(?layout);
            Some(layout.size)
        } else {
            None
        }
    }

    fn lint_large_assignment(
        &mut self,
        limit: usize,
        too_large_size: Size,
        location: Location,
        span: Span,
    ) {
        let source_info = self.body.source_info(location);

        let lint_root = source_info.scope.lint_root(&self.body.source_scopes);
        let Some(lint_root) = lint_root else {
            // This happens when the issue is in a function from a foreign crate that
            // we monomorphized in the current crate. We can't get a `HirId` for things
            // in other crates.
            // FIXME: Find out where to report the lint on. Maybe simply crate-level lint root
            // but correct span? This would make the lint at least accept crate-level lint attributes.
            return;
        };

        // If the source scope is inlined by the MIR inliner, report the lint on the call site.
        let reported_span = self
            .body
            .source_scopes
            .get(source_info.scope)
            .and_then(|source_scope_data| source_scope_data.inlined)
            .map(|(_, call_site)| call_site)
            .unwrap_or(span);

        for previously_reported_span in &self.move_size_spans {
            if previously_reported_span.overlaps(reported_span) {
                return;
            }
        }

        self.tcx.emit_node_span_lint(
            LARGE_ASSIGNMENTS,
            lint_root,
            reported_span,
            LargeAssignmentsLint {
                span: reported_span,
                size: too_large_size.bytes(),
                limit: limit as u64,
            },
        );

        self.move_size_spans.push(reported_span);
    }
}

fn assoc_fn_of_type<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId, fn_ident: Ident) -> Option<DefId> {
    for &impl_def_id in tcx.inherent_impls(def_id) {
        if let Some(new) = tcx.associated_items(impl_def_id).find_by_ident_and_kind(
            tcx,
            fn_ident,
            AssocTag::Fn,
            def_id,
        ) {
            return Some(new.def_id);
        }
    }
    None
}

pub(crate) fn skip_move_check_fns(tcx: TyCtxt<'_>, _: ()) -> FxIndexSet<DefId> {
    let fns = [
        (tcx.lang_items().owned_box(), "new"),
        (tcx.get_diagnostic_item(sym::Rc), "new"),
        (tcx.get_diagnostic_item(sym::Arc), "new"),
    ];
    fns.into_iter()
        .filter_map(|(def_id, fn_name)| {
            def_id.and_then(|def_id| assoc_fn_of_type(tcx, def_id, Ident::from_str(fn_name)))
        })
        .collect()
}
