//! Casing transforms — port of `protocol/mizan-generate/generator/lib/casing.mjs`.
//!
//! The Mizan IR uses snake_case names (`user_id`, `update_profile`). Per-target
//! identifier conventions vary: TypeScript wants `pascalCase`/`camelCase`,
//! Rust wants `snake_case` (with `r#`-escaping for keywords). These helpers
//! pin the conversion so emit-targets share one vocabulary.


fn split_parts(s: &str) -> Vec<&str> {
    s.split(|c: char| c == '.' || c == '-' || c == '_')
        .filter(|p| !p.is_empty())
        .collect()
}


fn uppercase_first(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        Some(first) => first.to_uppercase().chain(chars).collect(),
        None => String::new(),
    }
}


fn lowercase_first(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        Some(first) => first.to_lowercase().chain(chars).collect(),
        None => String::new(),
    }
}


pub fn pascal_case(s: &str) -> String {
    split_parts(s).into_iter().map(uppercase_first).collect()
}


pub fn camel_case(s: &str) -> String {
    let pascal = pascal_case(s);
    lowercase_first(&pascal)
}


/// Insert underscores at lowercase/digit-to-uppercase boundaries, unify with
/// the existing `.`/`-`/`_` separators, then lowercase + join.
pub fn snake_case(s: &str) -> String {
    let mut with_boundaries = String::with_capacity(s.len() + 4);
    let mut prev: Option<char> = None;
    for c in s.chars() {
        if let Some(p) = prev {
            if (p.is_ascii_lowercase() || p.is_ascii_digit()) && c.is_ascii_uppercase() {
                with_boundaries.push('_');
            }
        }
        with_boundaries.push(c);
        prev = Some(c);
    }
    split_parts(&with_boundaries)
        .into_iter()
        .map(|p| p.to_ascii_lowercase())
        .collect::<Vec<_>>()
        .join("_")
}


/// Rust reserved words that can be escaped via `r#` (excludes `crate`, `self`,
/// `Self`, `super`, `extern`, which can't be raw-escaped on stable).
const RUST_RAW_KEYWORDS: &[&str] = &[
    "as", "break", "const", "continue", "else", "enum", "false", "fn", "for",
    "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
    "ref", "return", "static", "struct", "trait", "true", "type", "unsafe",
    "use", "where", "while", "async", "await", "dyn", "abstract", "become",
    "box", "do", "final", "macro", "override", "priv", "typeof", "unsized",
    "virtual", "yield", "try", "union",
];

const RUST_HARD_RESERVED: &[&str] = &["crate", "self", "Self", "super", "extern"];


pub fn rust_ident(name: &str) -> String {
    let snake = snake_case(name);
    if RUST_HARD_RESERVED.contains(&snake.as_str()) {
        format!("{snake}_")
    } else if RUST_RAW_KEYWORDS.contains(&snake.as_str()) {
        format!("r#{snake}")
    } else {
        snake
    }
}


pub fn rust_type_ident(name: &str) -> String {
    let pascal = pascal_case(name);
    if RUST_HARD_RESERVED.contains(&pascal.as_str()) {
        format!("{pascal}_")
    } else if RUST_RAW_KEYWORDS.contains(&pascal.as_str()) {
        format!("r#{pascal}")
    } else {
        pascal
    }
}


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn pascal_case_matches_js_codegen() {
        assert_eq!(pascal_case("user_profile"), "UserProfile");
        assert_eq!(pascal_case("find-user"), "FindUser");
        assert_eq!(pascal_case("api.v1.users"), "ApiV1Users");
        assert_eq!(pascal_case(""), "");
    }

    #[test]
    fn camel_case_matches_js_codegen() {
        assert_eq!(camel_case("user_profile"), "userProfile");
        assert_eq!(camel_case("UpdateProfile"), "updateProfile");
    }

    #[test]
    fn snake_case_inserts_pascal_boundaries() {
        assert_eq!(snake_case("UserProfile"), "user_profile");
        assert_eq!(snake_case("camelCase"), "camel_case");
        assert_eq!(snake_case("already_snake"), "already_snake");
        assert_eq!(snake_case("HTTPResponse"), "httpresponse"); // matches JS behavior
    }

    #[test]
    fn rust_ident_escapes_keywords() {
        assert_eq!(rust_ident("type"), "r#type");
        assert_eq!(rust_ident("normal"), "normal");
        assert_eq!(rust_ident("self"), "self_");
    }
}
