//! Lightweight counters for lexer and prediction performance investigations.

#![allow(clippy::missing_const_for_thread_local)]

use std::cell::RefCell;
use std::collections::BTreeMap;

#[derive(Debug, Default)]
struct Counters {
    adaptive_calls: u64,
    forced_full_context_calls: u64,
    full_context_retries: u64,
    full_context_memo_hits: u64,
    full_context_memo_declined: u64,
    sll_conflicts: u64,
    reach_sll_calls: u64,
    reach_full_context_calls: u64,
    reach_input_configs: u64,
    reach_output_configs: u64,
    reach_max_input_configs: u64,
    reach_max_output_configs: u64,
    closure_calls: u64,
    closure_visited_total: u64,
    closure_visited_max: u64,
    config_add_calls: u64,
    config_inserts: u64,
    config_merges: u64,
    config_max_size: u64,
    context_merge_calls: u64,
    context_merge_identical: u64,
    context_merge_cache_hits: u64,
    context_merge_cache_misses: u64,
    context_merge_uncached: u64,
    context_cache_calls: u64,
    context_cache_hits: u64,
    context_cache_misses: u64,
    context_cache_inserts: u64,
    dfa_edge_lookups: u64,
    dfa_edge_hits: u64,
    dfa_edge_misses: u64,
    dfa_atn_fallbacks: u64,
    dfa_states_created: u64,
    dfa_states_deduplicated: u64,
    dfa_fingerprint_candidates: u64,
    dfa_fingerprint_collisions: u64,
    dfa_cache_imports: u64,
    dfa_cache_import_nanos: u64,
    dfa_cache_import_states: u64,
    dfa_cache_publications: u64,
    dfa_cache_publication_nanos: u64,
    dfa_cache_publication_states: u64,
    lexer_direct_ascii_chars: u64,
    lexer_generic_chars: u64,
    lexer_scalar_replay_chars: u64,
    lexer_bulk_committed_chars: u64,
    lexer_compiled_scalar_ascii_chars: u64,
    lexer_run_scan_calls: u64,
    lexer_run_scan_bytes: u64,
    lexer_run_scan_exits: u64,
    lexer_run_scan_ends: u64,
    lexer_run_rejected_states: u64,
    lexer_run_descriptors_none: u64,
    lexer_run_descriptors_any: u64,
    lexer_run_descriptors_until: u64,
    lexer_range_descriptors: [u64; 4],
    lexer_range_descriptor_classes: [u64; 4],
    lexer_range_scalar_calls: u64,
    lexer_range_scalar_bytes: u64,
    lexer_range_identifier_bytes: u64,
    lexer_range_number_bytes: u64,
    lexer_range_whitespace_bytes: u64,
    lexer_range_other_bytes: u64,
    parser_token_set_inline_sets: u64,
    parser_token_set_dense_sets: u64,
    parser_token_set_interval_sets: u64,
    parser_token_set_inline_bytes: u64,
    parser_token_set_dense_bytes: u64,
    parser_token_set_inline_hits: u64,
    parser_token_set_inline_misses: u64,
    parser_token_set_dense_hits: u64,
    parser_token_set_dense_misses: u64,
    parser_token_set_interval_hits: u64,
    parser_token_set_interval_misses: u64,
    parser_token_set_interval_probes_eliminated: u64,
    parser_token_set_interval_binary_probes_retained: u64,
    decisions: BTreeMap<usize, DecisionCounters>,
}

#[derive(Debug, Default)]
struct DecisionCounters {
    adaptive_calls: u64,
    forced_full_context_calls: u64,
    full_context_retries: u64,
    full_context_memo_hits: u64,
    full_context_memo_declined: u64,
    sll_conflicts: u64,
}

thread_local! {
    static COUNTERS: RefCell<Counters> = RefCell::new(Counters::default());
}

fn with_counters(update: impl FnOnce(&mut Counters)) {
    COUNTERS.with(|counters| update(&mut counters.borrow_mut()));
}

fn add_len(total: &mut u64, max: &mut u64, len: usize) {
    let len = u64::try_from(len).unwrap_or(u64::MAX);
    *total = total.saturating_add(len);
    *max = (*max).max(len);
}

const fn record_hit(hit: bool, hits: &mut u64, misses: &mut u64) {
    let counter = if hit { hits } else { misses };
    *counter = counter.saturating_add(1);
}

pub(crate) fn record_adaptive_call(decision: usize, forced_full_context: bool) {
    with_counters(|counters| {
        counters.adaptive_calls = counters.adaptive_calls.saturating_add(1);
        let decision_counters = counters.decisions.entry(decision).or_default();
        decision_counters.adaptive_calls = decision_counters.adaptive_calls.saturating_add(1);
        if forced_full_context {
            counters.forced_full_context_calls =
                counters.forced_full_context_calls.saturating_add(1);
            decision_counters.forced_full_context_calls = decision_counters
                .forced_full_context_calls
                .saturating_add(1);
        }
    });
}

pub(crate) fn record_full_context_retry(decision: usize) {
    with_counters(|counters| {
        counters.full_context_retries = counters.full_context_retries.saturating_add(1);
        let decision_counters = counters.decisions.entry(decision).or_default();
        decision_counters.full_context_retries =
            decision_counters.full_context_retries.saturating_add(1);
    });
}

pub(crate) fn record_full_context_memo_hit(decision: usize) {
    with_counters(|counters| {
        counters.full_context_memo_hits = counters.full_context_memo_hits.saturating_add(1);
        let decision_counters = counters.decisions.entry(decision).or_default();
        decision_counters.full_context_memo_hits =
            decision_counters.full_context_memo_hits.saturating_add(1);
    });
}

pub(crate) fn record_full_context_memo_declined(decision: usize) {
    with_counters(|counters| {
        counters.full_context_memo_declined = counters.full_context_memo_declined.saturating_add(1);
        let decision_counters = counters.decisions.entry(decision).or_default();
        decision_counters.full_context_memo_declined = decision_counters
            .full_context_memo_declined
            .saturating_add(1);
    });
}

pub(crate) fn record_sll_conflict(decision: usize) {
    with_counters(|counters| {
        counters.sll_conflicts = counters.sll_conflicts.saturating_add(1);
        let decision_counters = counters.decisions.entry(decision).or_default();
        decision_counters.sll_conflicts = decision_counters.sll_conflicts.saturating_add(1);
    });
}

pub(crate) fn record_reach_set(full_context: bool, input_configs: usize, output_configs: usize) {
    with_counters(|counters| {
        if full_context {
            counters.reach_full_context_calls = counters.reach_full_context_calls.saturating_add(1);
        } else {
            counters.reach_sll_calls = counters.reach_sll_calls.saturating_add(1);
        }
        add_len(
            &mut counters.reach_input_configs,
            &mut counters.reach_max_input_configs,
            input_configs,
        );
        add_len(
            &mut counters.reach_output_configs,
            &mut counters.reach_max_output_configs,
            output_configs,
        );
    });
}

pub(crate) fn record_closure(visited_configs: usize) {
    with_counters(|counters| {
        counters.closure_calls = counters.closure_calls.saturating_add(1);
        add_len(
            &mut counters.closure_visited_total,
            &mut counters.closure_visited_max,
            visited_configs,
        );
    });
}

pub(crate) fn record_config_add_call() {
    with_counters(|counters| {
        counters.config_add_calls = counters.config_add_calls.saturating_add(1);
    });
}

pub(crate) fn record_config_insert(size_after: usize) {
    with_counters(|counters| {
        counters.config_inserts = counters.config_inserts.saturating_add(1);
        counters.config_max_size = counters
            .config_max_size
            .max(u64::try_from(size_after).unwrap_or(u64::MAX));
    });
}

pub(crate) fn record_config_merge() {
    with_counters(|counters| {
        counters.config_merges = counters.config_merges.saturating_add(1);
    });
}

pub(crate) fn record_context_merge_call() {
    with_counters(|counters| {
        counters.context_merge_calls = counters.context_merge_calls.saturating_add(1);
    });
}

pub(crate) fn record_context_merge_identical() {
    with_counters(|counters| {
        counters.context_merge_identical = counters.context_merge_identical.saturating_add(1);
    });
}

pub(crate) fn record_context_merge_cache_hit() {
    with_counters(|counters| {
        counters.context_merge_cache_hits = counters.context_merge_cache_hits.saturating_add(1);
    });
}

pub(crate) fn record_context_merge_cache_miss() {
    with_counters(|counters| {
        counters.context_merge_cache_misses = counters.context_merge_cache_misses.saturating_add(1);
    });
}

pub(crate) fn record_context_merge_uncached() {
    with_counters(|counters| {
        counters.context_merge_uncached = counters.context_merge_uncached.saturating_add(1);
    });
}

pub(crate) fn record_context_cache_call() {
    with_counters(|counters| {
        counters.context_cache_calls = counters.context_cache_calls.saturating_add(1);
    });
}

pub(crate) fn record_context_cache_hit() {
    with_counters(|counters| {
        counters.context_cache_hits = counters.context_cache_hits.saturating_add(1);
    });
}

pub(crate) fn record_context_cache_miss() {
    with_counters(|counters| {
        counters.context_cache_misses = counters.context_cache_misses.saturating_add(1);
    });
}

pub(crate) fn record_context_cache_insert() {
    with_counters(|counters| {
        counters.context_cache_inserts = counters.context_cache_inserts.saturating_add(1);
    });
}

pub(crate) fn record_dfa_edge_lookup(hit: bool) {
    with_counters(|counters| {
        counters.dfa_edge_lookups = counters.dfa_edge_lookups.saturating_add(1);
        if hit {
            counters.dfa_edge_hits = counters.dfa_edge_hits.saturating_add(1);
        } else {
            counters.dfa_edge_misses = counters.dfa_edge_misses.saturating_add(1);
            counters.dfa_atn_fallbacks = counters.dfa_atn_fallbacks.saturating_add(1);
        }
    });
}

pub(crate) fn record_dfa_state_created() {
    with_counters(|counters| {
        counters.dfa_states_created = counters.dfa_states_created.saturating_add(1);
    });
}

pub(crate) fn record_dfa_state_deduplicated() {
    with_counters(|counters| {
        counters.dfa_states_deduplicated = counters.dfa_states_deduplicated.saturating_add(1);
    });
}

pub(crate) fn record_dfa_fingerprint_candidate() {
    with_counters(|counters| {
        counters.dfa_fingerprint_candidates = counters.dfa_fingerprint_candidates.saturating_add(1);
    });
}

pub(crate) fn record_dfa_fingerprint_collision() {
    with_counters(|counters| {
        counters.dfa_fingerprint_collisions = counters.dfa_fingerprint_collisions.saturating_add(1);
    });
}

pub(crate) fn record_dfa_cache_import(nanos: u128, states: usize) {
    with_counters(|counters| {
        counters.dfa_cache_imports = counters.dfa_cache_imports.saturating_add(1);
        counters.dfa_cache_import_nanos = counters
            .dfa_cache_import_nanos
            .saturating_add(u64::try_from(nanos).unwrap_or(u64::MAX));
        counters.dfa_cache_import_states = counters
            .dfa_cache_import_states
            .saturating_add(u64::try_from(states).unwrap_or(u64::MAX));
    });
}

pub(crate) fn record_dfa_cache_publication(nanos: u128, states: usize) {
    with_counters(|counters| {
        counters.dfa_cache_publications = counters.dfa_cache_publications.saturating_add(1);
        counters.dfa_cache_publication_nanos = counters
            .dfa_cache_publication_nanos
            .saturating_add(u64::try_from(nanos).unwrap_or(u64::MAX));
        counters.dfa_cache_publication_states = counters
            .dfa_cache_publication_states
            .saturating_add(u64::try_from(states).unwrap_or(u64::MAX));
    });
}

pub(crate) fn record_lexer_direct_ascii(count: usize) {
    with_counters(|counters| {
        counters.lexer_direct_ascii_chars = counters
            .lexer_direct_ascii_chars
            .saturating_add(u64::try_from(count).unwrap_or(u64::MAX));
    });
}

pub(crate) fn record_lexer_generic_char() {
    with_counters(|counters| {
        counters.lexer_generic_chars = counters.lexer_generic_chars.saturating_add(1);
    });
}

pub(crate) fn record_lexer_scalar_replay(count: usize) {
    with_counters(|counters| {
        counters.lexer_scalar_replay_chars = counters
            .lexer_scalar_replay_chars
            .saturating_add(u64::try_from(count).unwrap_or(u64::MAX));
    });
}

pub(crate) fn record_lexer_bulk_commit(count: usize) {
    with_counters(|counters| {
        counters.lexer_bulk_committed_chars = counters
            .lexer_bulk_committed_chars
            .saturating_add(u64::try_from(count).unwrap_or(u64::MAX));
    });
}

pub(crate) fn record_lexer_compiled_scalar_ascii(count: usize) {
    with_counters(|counters| {
        counters.lexer_compiled_scalar_ascii_chars = counters
            .lexer_compiled_scalar_ascii_chars
            .saturating_add(u64::try_from(count).unwrap_or(u64::MAX));
    });
}

pub(crate) fn record_lexer_run_scan(bytes: usize, found_exit: bool) {
    with_counters(|counters| {
        counters.lexer_run_scan_calls = counters.lexer_run_scan_calls.saturating_add(1);
        counters.lexer_run_scan_bytes = counters
            .lexer_run_scan_bytes
            .saturating_add(u64::try_from(bytes).unwrap_or(u64::MAX));
        if found_exit {
            counters.lexer_run_scan_exits = counters.lexer_run_scan_exits.saturating_add(1);
        } else {
            counters.lexer_run_scan_ends = counters.lexer_run_scan_ends.saturating_add(1);
        }
    });
}

pub(crate) fn record_lexer_run_rejected_state() {
    with_counters(|counters| {
        counters.lexer_run_rejected_states = counters.lexer_run_rejected_states.saturating_add(1);
    });
}

pub(crate) fn record_lexer_run_descriptor(kind: crate::atn::lexer_dfa::AsciiRunDescriptorKind) {
    use crate::atn::lexer_dfa::AsciiRunDescriptorKind;

    with_counters(|counters| match kind {
        AsciiRunDescriptorKind::None => {
            counters.lexer_run_descriptors_none =
                counters.lexer_run_descriptors_none.saturating_add(1);
        }
        AsciiRunDescriptorKind::Any => {
            counters.lexer_run_descriptors_any =
                counters.lexer_run_descriptors_any.saturating_add(1);
        }
        AsciiRunDescriptorKind::Until => {
            counters.lexer_run_descriptors_until =
                counters.lexer_run_descriptors_until.saturating_add(1);
        }
        AsciiRunDescriptorKind::Ranges { count, class } => {
            if let Some(counter) = count
                .checked_sub(1)
                .and_then(|index| counters.lexer_range_descriptors.get_mut(usize::from(index)))
            {
                *counter = counter.saturating_add(1);
            }
            let class_index = match class {
                crate::atn::ascii_range::AsciiRangeClass::Identifier => 0,
                crate::atn::ascii_range::AsciiRangeClass::Number => 1,
                crate::atn::ascii_range::AsciiRangeClass::Whitespace => 2,
                crate::atn::ascii_range::AsciiRangeClass::Other => 3,
            };
            counters.lexer_range_descriptor_classes[class_index] =
                counters.lexer_range_descriptor_classes[class_index].saturating_add(1);
        }
    });
}

pub(crate) fn record_lexer_range_scan(
    class: crate::atn::ascii_range::AsciiRangeClass,
    bytes: usize,
) {
    use crate::atn::ascii_range::AsciiRangeClass;

    let bytes = u64::try_from(bytes).unwrap_or(u64::MAX);
    with_counters(|counters| {
        counters.lexer_range_scalar_calls = counters.lexer_range_scalar_calls.saturating_add(1);
        counters.lexer_range_scalar_bytes = counters.lexer_range_scalar_bytes.saturating_add(bytes);
        let class_bytes = match class {
            AsciiRangeClass::Identifier => &mut counters.lexer_range_identifier_bytes,
            AsciiRangeClass::Number => &mut counters.lexer_range_number_bytes,
            AsciiRangeClass::Whitespace => &mut counters.lexer_range_whitespace_bytes,
            AsciiRangeClass::Other => &mut counters.lexer_range_other_bytes,
        };
        *class_bytes = class_bytes.saturating_add(bytes);
    });
}

pub(crate) fn record_parser_token_set_selection(
    kind: crate::atn::parser_atn::ParserTokenSetKind,
    bytes: usize,
) {
    use crate::atn::parser_atn::ParserTokenSetKind;

    let bytes = u64::try_from(bytes).unwrap_or(u64::MAX);
    with_counters(|counters| match kind {
        ParserTokenSetKind::Inline128 => {
            counters.parser_token_set_inline_sets =
                counters.parser_token_set_inline_sets.saturating_add(1);
            counters.parser_token_set_inline_bytes =
                counters.parser_token_set_inline_bytes.saturating_add(bytes);
        }
        ParserTokenSetKind::Dense => {
            counters.parser_token_set_dense_sets =
                counters.parser_token_set_dense_sets.saturating_add(1);
            counters.parser_token_set_dense_bytes =
                counters.parser_token_set_dense_bytes.saturating_add(bytes);
        }
        ParserTokenSetKind::Intervals => {
            counters.parser_token_set_interval_sets =
                counters.parser_token_set_interval_sets.saturating_add(1);
        }
    });
}

pub(crate) fn record_parser_token_set_probe(
    kind: crate::atn::parser_atn::ParserTokenSetKind,
    hit: bool,
) {
    use crate::atn::parser_atn::ParserTokenSetKind;

    with_counters(|counters| {
        match kind {
            ParserTokenSetKind::Inline128 => record_hit(
                hit,
                &mut counters.parser_token_set_inline_hits,
                &mut counters.parser_token_set_inline_misses,
            ),
            ParserTokenSetKind::Dense => record_hit(
                hit,
                &mut counters.parser_token_set_dense_hits,
                &mut counters.parser_token_set_dense_misses,
            ),
            ParserTokenSetKind::Intervals => record_hit(
                hit,
                &mut counters.parser_token_set_interval_hits,
                &mut counters.parser_token_set_interval_misses,
            ),
        }
        let probe_counter = if kind == ParserTokenSetKind::Intervals {
            &mut counters.parser_token_set_interval_binary_probes_retained
        } else {
            &mut counters.parser_token_set_interval_probes_eliminated
        };
        *probe_counter = probe_counter.saturating_add(1);
    });
}

pub fn reset() {
    COUNTERS.with(|counters| {
        let mut counters = counters.borrow_mut();
        // Compiled DFAs are usually initialized during warmup and then reused.
        // Keep their static descriptor inventory while clearing timed counters.
        let descriptors = (
            counters.lexer_run_descriptors_none,
            counters.lexer_run_descriptors_any,
            counters.lexer_run_descriptors_until,
            counters.lexer_range_descriptors,
            counters.lexer_range_descriptor_classes,
            counters.parser_token_set_inline_sets,
            counters.parser_token_set_dense_sets,
            counters.parser_token_set_interval_sets,
            counters.parser_token_set_inline_bytes,
            counters.parser_token_set_dense_bytes,
        );
        *counters = Counters::default();
        counters.lexer_run_descriptors_none = descriptors.0;
        counters.lexer_run_descriptors_any = descriptors.1;
        counters.lexer_run_descriptors_until = descriptors.2;
        counters.lexer_range_descriptors = descriptors.3;
        counters.lexer_range_descriptor_classes = descriptors.4;
        counters.parser_token_set_inline_sets = descriptors.5;
        counters.parser_token_set_dense_sets = descriptors.6;
        counters.parser_token_set_interval_sets = descriptors.7;
        counters.parser_token_set_inline_bytes = descriptors.8;
        counters.parser_token_set_dense_bytes = descriptors.9;
    });
}

pub fn dump() {
    COUNTERS.with(|counters| {
        let counters = counters.borrow();
        dump_totals(&counters);
        dump_decisions(&counters);
    });
}

fn dump_totals(counters: &Counters) {
    for (name, value) in totals(counters) {
        print_counter(name, value);
    }
}

fn dump_decisions(counters: &Counters) {
    for (decision, counters) in &counters.decisions {
        print_decision_counter(*decision, "adaptive_calls", counters.adaptive_calls);
        print_decision_counter(
            *decision,
            "forced_full_context_calls",
            counters.forced_full_context_calls,
        );
        print_decision_counter(
            *decision,
            "full_context_retries",
            counters.full_context_retries,
        );
        print_decision_counter(
            *decision,
            "full_context_memo_hits",
            counters.full_context_memo_hits,
        );
        print_decision_counter(
            *decision,
            "full_context_memo_declined",
            counters.full_context_memo_declined,
        );
        print_decision_counter(*decision, "sll_conflicts", counters.sll_conflicts);
    }
}

const fn totals(counters: &Counters) -> [(&'static str, u64); 82] {
    [
        ("prediction.adaptive_calls", counters.adaptive_calls),
        (
            "prediction.forced_full_context_calls",
            counters.forced_full_context_calls,
        ),
        (
            "prediction.full_context_retries",
            counters.full_context_retries,
        ),
        (
            "prediction.full_context_memo_hits",
            counters.full_context_memo_hits,
        ),
        (
            "prediction.full_context_memo_declined",
            counters.full_context_memo_declined,
        ),
        ("prediction.sll_conflicts", counters.sll_conflicts),
        ("reach.sll_calls", counters.reach_sll_calls),
        (
            "reach.full_context_calls",
            counters.reach_full_context_calls,
        ),
        ("reach.input_configs", counters.reach_input_configs),
        ("reach.output_configs", counters.reach_output_configs),
        ("reach.max_input_configs", counters.reach_max_input_configs),
        (
            "reach.max_output_configs",
            counters.reach_max_output_configs,
        ),
        ("closure.calls", counters.closure_calls),
        ("closure.visited_total", counters.closure_visited_total),
        ("closure.visited_max", counters.closure_visited_max),
        ("config.add_calls", counters.config_add_calls),
        ("config.inserts", counters.config_inserts),
        ("config.merges", counters.config_merges),
        ("config.max_size", counters.config_max_size),
        ("context_merge.calls", counters.context_merge_calls),
        ("context_merge.identical", counters.context_merge_identical),
        (
            "context_merge.cache_hits",
            counters.context_merge_cache_hits,
        ),
        (
            "context_merge.cache_misses",
            counters.context_merge_cache_misses,
        ),
        ("context_merge.uncached", counters.context_merge_uncached),
        ("context_cache.calls", counters.context_cache_calls),
        ("context_cache.hits", counters.context_cache_hits),
        ("context_cache.misses", counters.context_cache_misses),
        ("context_cache.inserts", counters.context_cache_inserts),
        ("dfa.edge_lookups", counters.dfa_edge_lookups),
        ("dfa.warm_hits", counters.dfa_edge_hits),
        ("dfa.warm_misses", counters.dfa_edge_misses),
        ("dfa.atn_fallbacks", counters.dfa_atn_fallbacks),
        ("dfa.states_created", counters.dfa_states_created),
        ("dfa.states_deduplicated", counters.dfa_states_deduplicated),
        (
            "dfa.fingerprint_candidates",
            counters.dfa_fingerprint_candidates,
        ),
        (
            "dfa.fingerprint_collisions",
            counters.dfa_fingerprint_collisions,
        ),
        ("dfa_cache.imports", counters.dfa_cache_imports),
        ("dfa_cache.import_nanos", counters.dfa_cache_import_nanos),
        ("dfa_cache.import_states", counters.dfa_cache_import_states),
        ("dfa_cache.publications", counters.dfa_cache_publications),
        (
            "dfa_cache.publication_nanos",
            counters.dfa_cache_publication_nanos,
        ),
        (
            "dfa_cache.publication_states",
            counters.dfa_cache_publication_states,
        ),
        (
            "lexer.direct_ascii_chars",
            counters.lexer_direct_ascii_chars,
        ),
        ("lexer.generic_chars", counters.lexer_generic_chars),
        (
            "lexer.scalar_replay_chars",
            counters.lexer_scalar_replay_chars,
        ),
        (
            "lexer.bulk_committed_chars",
            counters.lexer_bulk_committed_chars,
        ),
        (
            "lexer.compiled_scalar_ascii_chars",
            counters.lexer_compiled_scalar_ascii_chars,
        ),
        ("lexer.run_scan_calls", counters.lexer_run_scan_calls),
        ("lexer.run_scan_bytes", counters.lexer_run_scan_bytes),
        ("lexer.run_scan_exits", counters.lexer_run_scan_exits),
        ("lexer.run_scan_ends", counters.lexer_run_scan_ends),
        (
            "lexer.run_rejected_states",
            counters.lexer_run_rejected_states,
        ),
        (
            "lexer.run_descriptors.none",
            counters.lexer_run_descriptors_none,
        ),
        (
            "lexer.run_descriptors.any",
            counters.lexer_run_descriptors_any,
        ),
        (
            "lexer.run_descriptors.until",
            counters.lexer_run_descriptors_until,
        ),
        (
            "lexer.range_descriptors.1",
            counters.lexer_range_descriptors[0],
        ),
        (
            "lexer.range_descriptors.2",
            counters.lexer_range_descriptors[1],
        ),
        (
            "lexer.range_descriptors.3",
            counters.lexer_range_descriptors[2],
        ),
        (
            "lexer.range_descriptors.4",
            counters.lexer_range_descriptors[3],
        ),
        (
            "lexer.range_descriptor_classes.identifier",
            counters.lexer_range_descriptor_classes[0],
        ),
        (
            "lexer.range_descriptor_classes.number",
            counters.lexer_range_descriptor_classes[1],
        ),
        (
            "lexer.range_descriptor_classes.whitespace",
            counters.lexer_range_descriptor_classes[2],
        ),
        (
            "lexer.range_descriptor_classes.other",
            counters.lexer_range_descriptor_classes[3],
        ),
        (
            "lexer.range_scalar_calls",
            counters.lexer_range_scalar_calls,
        ),
        (
            "lexer.range_scalar_bytes",
            counters.lexer_range_scalar_bytes,
        ),
        (
            "lexer.range_identifier_bytes",
            counters.lexer_range_identifier_bytes,
        ),
        (
            "lexer.range_number_bytes",
            counters.lexer_range_number_bytes,
        ),
        (
            "lexer.range_whitespace_bytes",
            counters.lexer_range_whitespace_bytes,
        ),
        ("lexer.range_other_bytes", counters.lexer_range_other_bytes),
        (
            "parser.token_set.selection.inline128",
            counters.parser_token_set_inline_sets,
        ),
        (
            "parser.token_set.selection.dense",
            counters.parser_token_set_dense_sets,
        ),
        (
            "parser.token_set.selection.intervals",
            counters.parser_token_set_interval_sets,
        ),
        (
            "parser.token_set.inline_bytes",
            counters.parser_token_set_inline_bytes,
        ),
        (
            "parser.token_set.dense_bytes",
            counters.parser_token_set_dense_bytes,
        ),
        (
            "parser.token_set.inline_hits",
            counters.parser_token_set_inline_hits,
        ),
        (
            "parser.token_set.inline_misses",
            counters.parser_token_set_inline_misses,
        ),
        (
            "parser.token_set.dense_hits",
            counters.parser_token_set_dense_hits,
        ),
        (
            "parser.token_set.dense_misses",
            counters.parser_token_set_dense_misses,
        ),
        (
            "parser.token_set.interval_hits",
            counters.parser_token_set_interval_hits,
        ),
        (
            "parser.token_set.interval_misses",
            counters.parser_token_set_interval_misses,
        ),
        (
            "parser.token_set.interval_probes_eliminated",
            counters.parser_token_set_interval_probes_eliminated,
        ),
        (
            "parser.token_set.interval_binary_probes_retained",
            counters.parser_token_set_interval_binary_probes_retained,
        ),
    ]
}

#[cfg(test)]
pub(crate) fn lexer_snapshot() -> [u64; 4] {
    COUNTERS.with(|counters| {
        let counters = counters.borrow();
        [
            counters.lexer_direct_ascii_chars,
            counters.lexer_generic_chars,
            counters.lexer_scalar_replay_chars,
            counters.lexer_bulk_committed_chars,
        ]
    })
}

#[cfg(test)]
pub(crate) fn lexer_run_snapshot() -> [u64; 6] {
    COUNTERS.with(|counters| {
        let counters = counters.borrow();
        [
            counters.lexer_compiled_scalar_ascii_chars,
            counters.lexer_run_scan_calls,
            counters.lexer_run_scan_bytes,
            counters.lexer_run_scan_exits,
            counters.lexer_run_scan_ends,
            counters.lexer_run_rejected_states,
        ]
    })
}

#[cfg(test)]
pub(crate) fn lexer_range_descriptor_snapshot() -> [u64; 11] {
    COUNTERS.with(|counters| {
        let counters = counters.borrow();
        [
            counters.lexer_run_descriptors_none,
            counters.lexer_run_descriptors_any,
            counters.lexer_run_descriptors_until,
            counters.lexer_range_descriptors[0],
            counters.lexer_range_descriptors[1],
            counters.lexer_range_descriptors[2],
            counters.lexer_range_descriptors[3],
            counters.lexer_range_descriptor_classes[0],
            counters.lexer_range_descriptor_classes[1],
            counters.lexer_range_descriptor_classes[2],
            counters.lexer_range_descriptor_classes[3],
        ]
    })
}

#[cfg(test)]
pub(crate) fn lexer_range_scan_snapshot() -> [u64; 6] {
    COUNTERS.with(|counters| {
        let counters = counters.borrow();
        [
            counters.lexer_range_scalar_calls,
            counters.lexer_range_scalar_bytes,
            counters.lexer_range_identifier_bytes,
            counters.lexer_range_number_bytes,
            counters.lexer_range_whitespace_bytes,
            counters.lexer_range_other_bytes,
        ]
    })
}

#[cfg(test)]
pub(crate) fn parser_token_set_snapshot() -> [u64; 13] {
    COUNTERS.with(|counters| {
        let counters = counters.borrow();
        [
            counters.parser_token_set_inline_sets,
            counters.parser_token_set_dense_sets,
            counters.parser_token_set_interval_sets,
            counters.parser_token_set_inline_bytes,
            counters.parser_token_set_dense_bytes,
            counters.parser_token_set_inline_hits,
            counters.parser_token_set_inline_misses,
            counters.parser_token_set_dense_hits,
            counters.parser_token_set_dense_misses,
            counters.parser_token_set_interval_hits,
            counters.parser_token_set_interval_misses,
            counters.parser_token_set_interval_probes_eliminated,
            counters.parser_token_set_interval_binary_probes_retained,
        ]
    })
}

fn print_counter(name: &str, value: u64) {
    #[allow(clippy::print_stderr)]
    {
        eprintln!("perf {name}={value}");
    }
}

fn print_decision_counter(decision: usize, name: &str, value: u64) {
    #[allow(clippy::print_stderr)]
    {
        eprintln!("perf decision.{decision}.{name}={value}");
    }
}
