//! Parses a Kotlin source file with the Rust runtime's generated parser and
//! prints the parse tree in the same diff-friendly form as
//! `tests/kotlin-parity/dump_python.py`. The smoke workflow compares the two
//! outputs to enforce parser-tree parity with antlr4-python3-runtime.
//!
//! `Instant::now` is used purely for `--time` perf measurements at the request
//! of a developer running the dumper interactively; the parity comparison
//! itself remains deterministic.
#![allow(clippy::disallowed_methods)]

use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use std::time::Instant;

use antlr4_runtime::{Node, NodeKind};

mod generated {
    #![allow(dead_code, unused_imports, unreachable_pub, unused_qualifications)]
    pub mod kotlin_lexer;
    pub mod kotlin_parser;
}

use generated::kotlin_lexer::KotlinLexer;
use generated::kotlin_parser;

#[derive(Clone, Copy, Eq, PartialEq)]
enum EntryRule {
    KotlinFile,
    Script,
}

impl EntryRule {
    fn parse(value: &str) -> Option<Self> {
        match value {
            "kotlinFile" => Some(Self::KotlinFile),
            "script" => Some(Self::Script),
            _ => None,
        }
    }
}

fn dump<S: AsRef<str>>(
    out: &mut dyn Write,
    tree: Node<'_>,
    rule_names: &[S],
    depth: usize,
) -> io::Result<()> {
    let pad = "  ".repeat(depth);
    match tree.kind() {
        NodeKind::Rule => {
            let rule = tree.as_rule().expect("rule node kind checked");
            let name = rule_names
                .get(rule.rule_index())
                .map_or("<?>", |name| name.as_ref());
            writeln!(
                out,
                "{pad}Rule({name}, children={})",
                rule.child_count()
            )?;
            for child in rule.children() {
                dump(out, child, rule_names, depth + 1)?;
            }
        }
        NodeKind::Terminal => writeln!(
            out,
            "{pad}Term({:?})",
            tree.as_terminal().expect("terminal node kind checked").text()
        )?,
        NodeKind::Error => writeln!(
            out,
            "{pad}Err({:?})",
            tree.as_error().expect("error node kind checked").text()
        )?,
    }
    Ok(())
}

fn main() -> ExitCode {
    let mut args = env::args().skip(1);
    let mut input: Option<PathBuf> = None;
    let mut output: Option<PathBuf> = None;
    let mut iters: usize = 1;
    let mut report_time = false;
    let mut entry_rule = EntryRule::KotlinFile;
    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--input" => input = args.next().map(PathBuf::from),
            "--output" => output = args.next().map(PathBuf::from),
            "--rule" => {
                let Some(value) = args.next() else {
                    eprintln!("missing value for --rule <kotlinFile|script>");
                    return ExitCode::from(2);
                };
                let Some(parsed) = EntryRule::parse(&value) else {
                    eprintln!("invalid --rule value: {value} (expected kotlinFile or script)");
                    return ExitCode::from(2);
                };
                entry_rule = parsed;
            }
            "--iters" => {
                let Some(value) = args.next() else {
                    eprintln!("missing value for --iters <n>");
                    return ExitCode::from(2);
                };
                match value.parse::<usize>() {
                    Ok(parsed) if parsed >= 1 => iters = parsed,
                    _ => {
                        eprintln!("invalid --iters value: {value} (expected integer >= 1)");
                        return ExitCode::from(2);
                    }
                }
            }
            "--time" => report_time = true,
            other => {
                eprintln!("unknown argument: {other}");
                return ExitCode::from(2);
            }
        }
    }
    let Some(input) = input else {
        eprintln!("missing --input <path>");
        return ExitCode::from(2);
    };
    let src = match fs::read_to_string(&input) {
        Ok(text) => text,
        Err(err) => {
            eprintln!("failed to read {}: {err}", input.display());
            return ExitCode::from(1);
        }
    };

    let mut last_tree = None;
    let mut min_us: u128 = u128::MAX;
    let mut total_us: u128 = 0;
    for _ in 0..iters {
        let mut started = None;
        let tree = match kotlin_parser::parse(&src, KotlinLexer::new, |parser| {
            started = Some(Instant::now());
            match entry_rule {
                EntryRule::KotlinFile => parser.kotlin_file(),
                EntryRule::Script => parser.script(),
            }
        }) {
            Ok(tree) => tree,
            Err(err) => {
                eprintln!("parse failed: {err}");
                return ExitCode::from(1);
            }
        };
        let elapsed = started
            .expect("parse helper should enter the entry rule")
            .elapsed()
            .as_micros();
        min_us = min_us.min(elapsed);
        total_us += elapsed;
        last_tree = Some(tree);
    }
    if report_time {
        let avg_us = total_us / iters as u128;
        eprintln!(
            "parse: iters={iters} min={:.3}ms avg={:.3}ms",
            min_us as f64 / 1000.0,
            avg_us as f64 / 1000.0,
        );
    }
    let Some(parsed) = last_tree else {
        eprintln!("no parse iterations produced a tree");
        return ExitCode::from(1);
    };

    let rule_names = kotlin_parser::rule_names();

    let mut sink: Box<dyn Write> = match output {
        Some(path) => match fs::File::create(&path) {
            Ok(file) => Box::new(io::BufWriter::new(file)),
            Err(err) => {
                eprintln!("failed to create {}: {err}", path.display());
                return ExitCode::from(1);
            }
        },
        None => Box::new(io::stdout().lock()),
    };
    if let Err(err) = dump(
        sink.as_mut(),
        parsed.tree(),
        &rule_names,
        0,
    ) {
        eprintln!("write failed: {err}");
        return ExitCode::from(1);
    }
    if let Err(err) = sink.flush() {
        eprintln!("flush failed: {err}");
        return ExitCode::from(1);
    }

    ExitCode::SUCCESS
}
