// Shared benchmark dataset generator, ported from scripts/generate_file_list.rb.
// Produces identical dataset structure using a seeded RNG (xorshift64) instead
// of reading from pre-generated text files.

use std::path::PathBuf;

// ── RNG ───────────────────────────────────────────────────────────────────────

struct Rng(u64);

impl Rng {
    fn new(seed: u64) -> Self {
        // Splitmix64 initialisation so low seeds don't start in a bad state.
        let mut x = seed.wrapping_add(0x9e3779b97f4a7c15);
        x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
        x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb);
        x ^= x >> 31;
        Self(if x == 0 { 1 } else { x })
    }

    fn next(&mut self) -> u64 {
        let mut x = self.0;
        x ^= x << 13;
        x ^= x >> 7;
        x ^= x << 17;
        self.0 = x;
        x
    }

    // [0.0, 1.0)
    fn float(&mut self) -> f64 {
        (self.next() >> 11) as f64 * (1.0 / (1u64 << 53) as f64)
    }

    // [lo, hi] inclusive
    fn range(&mut self, lo: usize, hi: usize) -> usize {
        lo + (self.next() as usize % (hi - lo + 1))
    }

    fn pick<T>(&mut self, slice: &[T]) -> usize {
        self.next() as usize % slice.len()
    }
}

// ── Constants ─────────────────────────────────────────────────────────────────

const SEED: u64 = 12_345;

const EXT: &[&str] = &[
    "ts", "tsx", "js", "jsx", "json", "yaml", "yml", "css", "scss", "html", "md",
];
const COMMON: &[&str] = &["src", "lib", "components", "utils", "hooks", "test"];
const NODE: &[&str] = &[
    "react",
    "lodash",
    "axios",
    "chalk",
    "debug",
    "typescript",
    "eslint",
    "webpack",
    "vite",
];
const CONFIG: &[&str] = &[".github/workflows", ".vscode", "scripts", "config"];
const BUILD: &[&str] = &["dist", "build", "coverage"];

const SEARCH_WORDS: &[&str] = &[
    "component",
    "controller",
    "service",
    "manager",
    "provider",
    "handler",
    "adapter",
    "repository",
    "factory",
    "builder",
    "resolver",
    "loader",
    "validator",
    "serializer",
];

const PREFIX: &[&str] = &[
    "component",
    "component_test",
    "component_utils",
    "component_util",
    "component_util_test",
    "component_controller",
    "component_service",
];

// ── Helpers ───────────────────────────────────────────────────────────────────

// rng.rand < 0.95 ? rng.rand(1..10) : rng.rand(11..100)
fn files_in_dir(rng: &mut Rng) -> usize {
    if rng.float() < 0.95 {
        rng.range(1, 10)
    } else {
        rng.range(11, 100)
    }
}

// "file_{i}_{rand(10000)}.{ext[rand]}"
fn filename(i: usize, rng: &mut Rng) -> String {
    format!("file_{}_{}.{}", i, rng.range(0, 9_999), EXT[rng.pick(EXT)])
}

// depth 1..6 levels of COMMON joined by "/"
fn src_tree(rng: &mut Rng) -> String {
    let depth = rng.range(1, 6);
    (0..depth)
        .map(|_| COMMON[rng.pick(COMMON)])
        .collect::<Vec<_>>()
        .join("/")
}

// depth 8..15 levels of "dir_N" joined by "/"
fn deep_path(rng: &mut Rng) -> String {
    let depth = rng.range(8, 15);
    (0..depth)
        .map(|_| format!("dir_{}", rng.range(0, 19)))
        .collect::<Vec<_>>()
        .join("/")
}

// "{PREFIX[rand]}_{i}_{rand(100)}.ts"
fn similar_name(i: usize, rng: &mut Rng) -> String {
    format!("{}_{}_{}.ts", PREFIX[rng.pick(PREFIX)], i, rng.range(0, 99))
}

fn capitalize(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(c) => c.to_ascii_uppercase().to_string() + chars.as_str(),
    }
}

// Two SEARCH_WORDS combined as camelCase / snake_case / kebab-case + suffix
fn search_filename(rng: &mut Rng) -> String {
    let a = SEARCH_WORDS[rng.pick(SEARCH_WORDS)];
    let b = SEARCH_WORDS[rng.pick(SEARCH_WORDS)];
    let style = rng.range(0, 2);
    let base = match style {
        0 => format!("{}{}", a, capitalize(b)),
        1 => format!("{}_{}", a, b),
        _ => format!("{}-{}", a, b),
    };
    format!("{}_{}.ts", base, rng.range(0, 49))
}

// ── Generators ────────────────────────────────────────────────────────────────

pub const TOTAL: usize = 200_000;

/// Monorepo: packages / apps / services / config / build / node_modules layout.
pub fn generate_monorepo() -> Vec<PathBuf> {
    let mut rng = Rng::new(SEED);
    let mut paths = Vec::with_capacity(TOTAL);
    let mut i = 0;

    while i < TOTAL {
        let r = rng.float();
        let base = if r < 0.45 {
            format!("packages/pkg_{}/{}", rng.range(0, 199), src_tree(&mut rng))
        } else if r < 0.65 {
            format!("apps/app_{}/{}", rng.range(0, 49), src_tree(&mut rng))
        } else if r < 0.80 {
            format!(
                "services/service_{}/{}",
                rng.range(0, 79),
                src_tree(&mut rng)
            )
        } else if r < 0.90 {
            CONFIG[rng.pick(CONFIG)].to_owned()
        } else if r < 0.97 {
            BUILD[rng.pick(BUILD)].to_owned()
        } else {
            format!(
                "node_modules/{}_{}/{}",
                NODE[rng.pick(NODE)],
                rng.range(0, 99),
                src_tree(&mut rng)
            )
        };

        let count = files_in_dir(&mut rng);
        for _ in 0..count {
            if i >= TOTAL {
                break;
            }
            paths.push(PathBuf::from(format!("{}/{}", base, filename(i, &mut rng))));
            i += 1;
        }
    }

    paths
}

/// Pathological: flat megadir / deeply nested tree / many similar names.
pub fn generate_pathological() -> Vec<PathBuf> {
    let mut rng = Rng::new(SEED + 1);
    let mut paths = Vec::with_capacity(TOTAL);
    let mut i = 0;

    while i < TOTAL {
        let r = rng.float();
        let (base, similar) = if r < 0.50 {
            ("flat_big_dir".to_owned(), false)
        } else if r < 0.80 {
            (format!("deep_tree/{}", deep_path(&mut rng)), false)
        } else {
            ("similar_names".to_owned(), true)
        };

        let count = files_in_dir(&mut rng);
        for _ in 0..count {
            if i >= TOTAL {
                break;
            }
            let name = if similar {
                similar_name(i, &mut rng)
            } else {
                filename(i, &mut rng)
            };
            paths.push(PathBuf::from(format!("{}/{}", base, name)));
            i += 1;
        }
    }

    paths
}

/// Search-stress: all files live under search/<word>/ with fuzzy-unfriendly names.
pub fn generate_search_stress() -> Vec<PathBuf> {
    let mut rng = Rng::new(SEED + 2);
    let mut paths = Vec::with_capacity(TOTAL);
    let mut i = 0;

    while i < TOTAL {
        let base = format!("search/{}", SEARCH_WORDS[rng.pick(SEARCH_WORDS)]);

        let count = files_in_dir(&mut rng);
        for _ in 0..count {
            if i >= TOTAL {
                break;
            }
            paths.push(PathBuf::from(format!(
                "{}/{}",
                base,
                search_filename(&mut rng)
            )));
            i += 1;
        }
    }

    paths
}

// ── Dataset enum ──────────────────────────────────────────────────────────────

#[derive(Clone, Copy)]
#[allow(dead_code)]
pub enum Dataset {
    Monorepo,
    Pathological,
    SearchStress,
}

impl Dataset {
    #[allow(dead_code)]
    pub fn generate(self) -> Vec<PathBuf> {
        match self {
            Dataset::Monorepo => generate_monorepo(),
            Dataset::Pathological => generate_pathological(),
            Dataset::SearchStress => generate_search_stress(),
        }
    }

    #[allow(dead_code)]
    pub fn to_file_index(self) -> oo_ide::file_index::FileIndex {
        oo_ide::file_index::FileIndex::from_paths(self.generate())
    }
}

impl std::fmt::Display for Dataset {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Dataset::Monorepo => f.write_str("monorepo"),
            Dataset::Pathological => f.write_str("pathological"),
            Dataset::SearchStress => f.write_str("search_stress"),
        }
    }
}