use std::{fs, ops::Range, path::Path};

pub(crate) fn in_place(api: &str, path: &Path) {
    let path = {
        let dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
        Path::new(&dir).join(path)
    };

    let mut text = fs::read_to_string(&path).unwrap_or_else(|_| panic!("failed to read {path:?}"));

    let (insert_to, indent) = locate(&text);

    let api: String =
        with_preamble(api)
            .lines()
            .map(|it| {
                if it.trim().is_empty() {
                    "\n".to_string()
                } else {
                    format!("{}{}\n", indent, it)
                }
            })
            .collect();
    text.replace_range(insert_to, &api);

    fs::write(&path, text.as_bytes()).unwrap();
}

pub(crate) fn stdout(api: &str) {
    print!("{}", with_preamble(api))
}

fn with_preamble(api: &str) -> String {
    format!(
        "\
// generated start
// The following code is generated by `xflags` macro.
// Run `env UPDATE_XFLAGS=1 cargo build` to regenerate.
{}
// generated end
",
        api.trim()
    )
}

fn locate(text: &str) -> (Range<usize>, String) {
    if let Some(it) = locate_existing(text) {
        return it;
    }
    if let Some(it) = locate_new(text) {
        return it;
    }
    panic!("failed to update xflags in place")
}

fn locate_existing(text: &str) -> Option<(Range<usize>, String)> {
    let start_idx = text.find("// generated start")?;
    let start_idx = newline_before(text, start_idx);

    let end_idx = text.find("// generated end")?;
    let end_idx = newline_after(text, end_idx);

    let indent = indent_at(text, start_idx);

    Some((start_idx..end_idx, indent))
}

fn newline_before(text: &str, start_idx: usize) -> usize {
    text[..start_idx].rfind('\n').map_or(start_idx, |it| it + 1)
}

fn newline_after(text: &str, start_idx: usize) -> usize {
    start_idx + text[start_idx..].find('\n').map_or(text[start_idx..].len(), |it| it + 1)
}

fn indent_at(text: &str, start_idx: usize) -> String {
    text[start_idx..].chars().take_while(|&it| it == ' ').collect()
}

fn locate_new(text: &str) -> Option<(Range<usize>, String)> {
    let mut idx = text.find("xflags!")?;
    let mut lvl = 0i32;
    for c in text[idx..].chars() {
        idx += c.len_utf8();
        match c {
            '{' => lvl += 1,
            '}' if lvl == 1 => break,
            '}' => lvl -= 1,
            _ => (),
        }
    }
    let indent = indent_at(text, newline_before(text, idx));
    if text[idx..].starts_with('\n') {
        idx += 1;
    }
    Some((idx..idx, indent))
}
