use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
use std::time::Duration;

use shuck_benchmark::{benchmark_cases, configure_benchmark_allocator, parse_fixture};
use shuck_indexer::Indexer;
use shuck_linter::{
    AnalysisRequest, LinterFacts, LinterSemanticArtifacts, LinterSettings, RuleSet,
    ShellCheckCodeMap,
};
use shuck_parser::parser::ParseResult;

configure_benchmark_allocator!();

fn build_linter_facts(
    source: &str,
    output: &ParseResult,
    indexer: &Indexer,
    semantic: &LinterSemanticArtifacts<'_>,
) -> usize {
    let facts = LinterFacts::build(&output.file, source, semantic, indexer);

    black_box(
        facts.commands().count()
            + facts.words().word_facts().count()
            + facts.words().single_quoted_fragments().len()
            + facts.words().backtick_fragments().len()
            + facts.words().pattern_charclass_spans().len()
            + facts.words().substring_expansion_fragments().len()
            + facts.words().case_modification_fragments().len()
            + facts.words().replacement_expansion_fragments().len(),
    )
}

fn lint_source(
    source: &str,
    settings: &LinterSettings,
    shellcheck_map: &ShellCheckCodeMap,
) -> usize {
    let output = parse_fixture(source);
    let diagnostics = AnalysisRequest::from_parse_result(&output, source, settings)
        .with_shellcheck_map(shellcheck_map)
        .lint();

    black_box(diagnostics.len())
}

fn bench_linter_facts(c: &mut Criterion) {
    let mut group = c.benchmark_group("linter_facts");

    for case in benchmark_cases() {
        group.sample_size(case.speed.sample_size());
        group.throughput(Throughput::Bytes(case.total_bytes()));
        group.bench_with_input(BenchmarkId::from_parameter(case.name), &case, |b, case| {
            b.iter_custom(|iters| {
                let mut total = Duration::ZERO;
                for _ in 0..iters {
                    let outputs = case
                        .files
                        .iter()
                        .map(|file| parse_fixture(file.source))
                        .collect::<Vec<_>>();
                    let indexers = case
                        .files
                        .iter()
                        .zip(outputs.iter())
                        .map(|(file, output)| Indexer::new(file.source, output))
                        .collect::<Vec<_>>();
                    let semantics = case
                        .files
                        .iter()
                        .zip(outputs.iter())
                        .zip(indexers.iter())
                        .map(|((file, output), indexer)| {
                            LinterSemanticArtifacts::build(&output.file, file.source, indexer)
                        })
                        .collect::<Vec<_>>();

                    let start = std::time::Instant::now();
                    let facts_size = case
                        .files
                        .iter()
                        .zip(outputs.iter())
                        .zip(indexers.iter())
                        .zip(semantics.iter())
                        .map(|(((file, output), indexer), semantic)| {
                            build_linter_facts(file.source, output, indexer, semantic)
                        })
                        .sum::<usize>();
                    black_box(facts_size);
                    total += start.elapsed();
                }
                total
            });
        });
    }

    group.finish();
}

fn bench_linter(c: &mut Criterion) {
    let mut group = c.benchmark_group("linter");
    let settings = LinterSettings {
        rules: RuleSet::all(),
        ..LinterSettings::default()
    };
    let shellcheck_map = ShellCheckCodeMap::default();

    for case in benchmark_cases() {
        group.sample_size(case.speed.sample_size());
        group.throughput(Throughput::Bytes(case.total_bytes()));
        group.bench_with_input(BenchmarkId::from_parameter(case.name), &case, |b, case| {
            b.iter(|| {
                let diagnostic_count: usize = case
                    .files
                    .iter()
                    .map(|file| lint_source(file.source, &settings, &shellcheck_map))
                    .sum();
                black_box(diagnostic_count);
            });
        });
    }

    group.finish();
}

criterion_group!(benches, bench_linter_facts, bench_linter);
criterion_main!(benches);
