use std::{
    env,
    fs::{self, DirEntry, File},
    io::Write,
    path::{Path, PathBuf},
    process::Command,
};

const RENAMES: &[(&str, &str)] = &[
    ("CUsizeResult", "size_t"),
    ("CIsizeResult", "ssize_t"),
    ("CEofResult", "int"),
    ("CIntCountResult", "int"),
    ("CIntZeroResult", "int"),
    ("CFdResult", "int"),
    ("COffsetResult", "off_t"),
    ("CPidResult", "pid_t"),
    ("AtomicU32", "uint32_t"),
    ("AtomicBool", "bool"),
];

fn include_dir(d: &DirEntry) -> bool {
    d.metadata().map(|m| m.is_dir()).unwrap_or(false)
        && d.path()
            .iter()
            .nth(2)
            .is_some_and(|c| c.to_str().is_some_and(|x| !x.starts_with("_")))
}

fn content_matches(path: impl AsRef<Path>, data: &[u8]) -> bool {
    let content = fs::read(path).unwrap();
    content == data
}

fn generate_header(config_path: impl AsRef<Path>, header_output: impl AsRef<Path>) {
    let config_path = config_path.as_ref();
    let header_output = header_output.as_ref();
    let relative_path = config_path
        .strip_prefix("src/headers")
        .ok()
        .and_then(|p| p.parent())
        .and_then(|p| p.to_str())
        .unwrap();

    // NOTE: a hack for nl_types.h
    let relative_path = if relative_path.contains("nl_types") {
        relative_path.to_owned()
    } else {
        relative_path.replace("_", "/")
    };
    // TODO use outer workspace's target directory?
    let header_path = header_output.join(&relative_path).with_extension("h");
    let mod_path = config_path.with_file_name("mod.rs");

    let mut config = cbindgen::Config::from_file(config_path).unwrap();

    config
        .export
        .rename
        .extend(RENAMES.iter().map(|&(x, y)| (x.into(), y.into())));
    config.cpp_compat = true;
    config.function.no_return = Some("__attribute__((noreturn))".into());

    let mut output = vec![];

    cbindgen::Builder::new()
        .with_config(config)
        .with_src(mod_path)
        .generate()
        .unwrap()
        .write(&mut output);

    if !header_path.exists() || !content_matches(&header_path, &output) {
        if let Some(parent) = header_path.parent() {
            fs::create_dir_all(parent).expect("Could not create header directory");
        }
        fs::write(&header_path, &output[..]).expect("Could not write header");
    }
}

fn compile_crt0(arch: &str, output_dir: impl AsRef<Path>) {
    let output_dir = output_dir.as_ref();
    let mut command = Command::new("clang");
    let input_dir = PathBuf::from("crt").join(arch);
    let crt0_c = input_dir.join("crt0.c");
    let crt0_s = input_dir.join("crt0.S");

    let input_file = if crt0_c.exists() {
        crt0_c
    } else if crt0_s.exists() {
        crt0_s
    } else {
        panic!("No crt0.* file for {arch}");
    };

    command
        .arg(format!("--target={arch}-unknown-none"))
        .arg("-nostdlib")
        .arg("-fPIC")
        .arg("-c");

    #[allow(clippy::single_match)]
    match arch {
        "riscv64" => {
            command.arg("-march=rv64gc");
        }
        _ => (),
    }

    command
        .arg("-o")
        .arg(output_dir.join("crt0.o"))
        .arg(input_file);

    if !command.status().unwrap().success() {
        panic!("Couldn't compile crt0.o");
    }
}

fn ctype_item(ch: u8) -> String {
    let mut traits = vec![];
    // TODO include vertical tab here
    if ch.is_ascii_whitespace() {
        traits.push("_ISspace");
    }
    if ch.is_ascii_graphic() || ch == b' ' {
        traits.push("_ISprint");
    }
    if ch.is_ascii_control() {
        traits.push("_IScntrl");
    }
    if ch.is_ascii_uppercase() {
        traits.push("_ISupper");
    }
    if ch.is_ascii_lowercase() {
        traits.push("_ISlower");
    }
    if ch.is_ascii_alphabetic() {
        traits.push("_ISalpha");
    }
    if ch.is_ascii_digit() {
        traits.push("_ISdigit");
    }
    if ch.is_ascii_punctuation() {
        traits.push("_ISpunct");
    }
    if ch.is_ascii_hexdigit() {
        traits.push("_ISxdigit");
    }
    if ch == b' ' || ch == b'\t' {
        traits.push("_ISblank");
    }

    if traits.is_empty() {
        "0,".to_owned()
    } else {
        let mut str = String::new();
        for (i, t) in traits.iter().enumerate() {
            if i != 0 {
                str.push('|');
            }
            str.push_str(t);
        }
        str.push(',');
        str
    }
}

fn generate_ctype_table(output_dir: impl AsRef<Path>) {
    // Table size: 384, __ctype_b_loc points to 128th element to allow the table to be indexed
    // by a signed char or EOF (-1) as well as any unsigned char.

    let mut output = File::create(output_dir.as_ref().join("ctype_b.rs")).unwrap();

    output
        .write_all(b"pub(crate) static __ctype_b_table: [c_ushort; 384] = [")
        .unwrap();

    for ch in 128..=255u8 {
        let traits_string = ctype_item(ch);
        output.write_all(traits_string.as_bytes()).unwrap();
    }

    for ch in 0..=255u8 {
        let traits_string = ctype_item(ch);
        output.write_all(traits_string.as_bytes()).unwrap();
    }

    output.write_all(b"];").unwrap();
}

fn main() {
    let target = env::var("TARGET").expect("$TARGET is not set");
    let profile = env::var("PROFILE").expect("$PROFILE is not set");
    let arch = env::var("CARGO_CFG_TARGET_ARCH").expect("$CARGO_CFG_TARGET_ARCH is not set");
    let compile_output_dir = env::var("OUT_DIR").expect("$OUT_DIR is not set");

    let output_dir = PathBuf::from(format!("target/{target}/{profile}"));
    let header_output = output_dir.join("include");

    generate_ctype_table(compile_output_dir);
    compile_crt0(&arch, output_dir);

    fs::read_dir("src/headers")
        .unwrap()
        .filter_map(Result::ok)
        .filter(include_dir)
        .map(|d| d.path().as_path().join("cbindgen.toml"))
        .filter(|p| p.exists())
        .for_each(|p| {
            generate_header(&p, &header_output);
        });
}
