use std::cell::Cell;

use rustc_data_structures::fingerprint::Fingerprint;
use rustc_hir::def_id::LOCAL_CRATE;
use rustc_session::utils::was_invoked_from_cargo;
use tracing::instrument;

use crate::dep_graph::{DepGraphData, SerializedDepNodeIndex};
use crate::ich::StableHashState;
use crate::ty::TyCtxt;

#[inline]
#[instrument(skip(tcx, dep_graph_data, result, hash_result, format_value), level = "debug")]
pub fn incremental_verify_ich<'tcx, V>(
    tcx: TyCtxt<'tcx>,
    dep_graph_data: &DepGraphData,
    result: &V,
    prev_index: SerializedDepNodeIndex,
    hash_result: Option<fn(&mut StableHashState<'_>, &V) -> Fingerprint>,
    format_value: fn(&V) -> String,
) {
    if !dep_graph_data.is_index_green(prev_index) {
        incremental_verify_ich_not_green(tcx, prev_index)
    }

    let new_hash = hash_result.map_or(Fingerprint::ZERO, |f| {
        tcx.with_stable_hashing_context(|mut hcx| f(&mut hcx, result))
    });

    let old_hash = dep_graph_data.prev_value_fingerprint_of(prev_index);

    if new_hash != old_hash {
        incremental_verify_ich_failed(tcx, prev_index, &|| format_value(result));
    }
}

#[cold]
#[inline(never)]
fn incremental_verify_ich_not_green<'tcx>(tcx: TyCtxt<'tcx>, prev_index: SerializedDepNodeIndex) {
    panic!(
        "fingerprint for green query instance not loaded from cache: {:?}",
        tcx.dep_graph.data().unwrap().prev_node_of(prev_index)
    )
}

// Note that this is marked #[cold] and intentionally takes `dyn Debug` for `result`,
// as we want to avoid generating a bunch of different implementations for LLVM to
// chew on (and filling up the final binary, too).
#[cold]
#[inline(never)]
fn incremental_verify_ich_failed<'tcx>(
    tcx: TyCtxt<'tcx>,
    prev_index: SerializedDepNodeIndex,
    result: &dyn Fn() -> String,
) {
    // When we emit an error message and panic, we try to debug-print the `DepNode`
    // and query result. Unfortunately, this can cause us to run additional queries,
    // which may result in another fingerprint mismatch while we're in the middle
    // of processing this one. To avoid a double-panic (which kills the process
    // before we can print out the query static), we print out a terse
    // but 'safe' message if we detect a reentrant call to this method.
    thread_local! {
        static INSIDE_VERIFY_PANIC: Cell<bool> = const { Cell::new(false) };
    };

    let old_in_panic = INSIDE_VERIFY_PANIC.replace(true);

    if old_in_panic {
        tcx.dcx().emit_err(crate::error::Reentrant);
    } else {
        let run_cmd = if was_invoked_from_cargo() {
            format!("run `cargo clean -p {}` or `cargo clean`", tcx.crate_name(LOCAL_CRATE))
        } else {
            "clean your build cache".to_owned()
        };

        let dep_node = tcx.dep_graph.data().unwrap().prev_node_of(prev_index);
        tcx.dcx().emit_err(crate::error::IncrementCompilation {
            run_cmd,
            dep_node: format!("{dep_node:?}"),
        });
        panic!("Found unstable fingerprints for {dep_node:?}: {}", result());
    }

    INSIDE_VERIFY_PANIC.set(old_in_panic);
}
