///|
/// Naming Conversion Utilities — P0-057.
/// Provides snake_case ↔ PascalCase/camelCase conversion with initialism support,
/// and case style application for JSON/db tags.
//
// Copyright 2026 Mairzzcllo
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

///|
/// Capitalize the first ASCII lowercase character.
pub fn capitalize_first(s : String) -> String {
  if s.length() == 0 {
    return s
  }
  let first = s[0]
  if first >= 'a' && first <= 'z' {
    let upper = (first.to_int() - 32).unsafe_to_char()
    [upper] + s[1:].to_owned()
  } else {
    s
  }
}

///|
/// Lowercase the first ASCII uppercase character.
pub fn lowercase_first(s : String) -> String {
  if s.length() == 0 {
    return s
  }
  let first = s[0]
  if first >= 'A' && first <= 'Z' {
    let lower = (first.to_int() + 32).unsafe_to_char()
    [lower] + s[1:].to_owned()
  } else {
    s
  }
}

///|
/// Parse a comma-separated list of initialisms into an array.
/// e.g., "ID,URL,HTTP" → ["ID", "URL", "HTTP"]
pub fn parse_initialisms(s : String) -> Array[String] {
  if s.length() == 0 {
    return []
  }
  let parts = s.split(",")
  let trimmed = parts.map(fn(p) { p.trim().to_owned() })
  let result : Array[String] = []
  for p in trimmed {
    if p.length() > 0 {
      result.push(p)
    }
  }
  result
}

///|
/// Check if a word matches any initialism (case-insensitive).
/// Returns the canonical initialism form if found, None otherwise.
fn match_initialism(word : String, initialisms : Array[String]) -> String? {
  let lower = word.to_lower()
  for init in initialisms {
    if init.to_lower() == lower {
      return Some(init)
    }
  }
  None
}

///|
/// Convert snake_case to PascalCase with initialism support.
/// e.g., "get_user_by_id" with initialisms=["ID","URL"] → "GetUserByID"
///
/// ## Edge Case Behaviors (ADR-004 / P1-041)
///
/// The conversion is a pure algorithmic mapping with the following known edge cases.
/// These behaviors are intentionally preserved; callers should be aware of:
///
/// | Input           | Output       | Reason                                         |
/// |-----------------|--------------|------------------------------------------------|
/// | `"user__name"`  | `"UserName"` | Double underscore → empty split segment, silently dropped |
/// | `"_user"`       | `"User"`     | Leading underscore → empty first segment, dropped |
/// | `"user_name_"`  | `"UserName"` | Trailing underscore → empty last segment, dropped |
/// | `"USER_NAME"`   | `"USERNAME"` | All-caps input stays uppercase (capitalize_first is no-op on uppercase) |
/// | `"userName"`    | `"UserName"` | No underscore → only first letter capitalized |
/// | `"user_name"`   | `"UserName"` | Same output as `"UserName"` — collision risk |
/// | `"_"`           | `""`         | Single underscore → two empty segments → empty string |
/// | `""`            | `"UnnamedType"` | Empty input returns sentinel `"UnnamedType"` |
///
/// **Collision warning**: Different snake_case inputs can produce identical
/// PascalCase output (e.g., `"user_name"` and `"UserName"` both → `"UserName"`).
/// The naming layer does not detect or prevent collisions — higher-level
/// codegen (e.g., `generate_types_from_catalog`) is responsible for avoiding
/// conflicts (e.g., enum name vs struct name generating the same type).
///
/// **Enum/Struct collision**: When a table and an enum share the same base name,
/// `generate_types_from_catalog` disambiguates enums via `StatusType` suffix (P1-060).
///
/// **Empty input**: Callers should ensure non-empty input; empty string returns
/// sentinel `"UnnamedType"` which avoids collision with tables named 'empty'.
pub fn snake_to_pascal(s : String, initialisms : Array[String]) -> String {
  if s.length() == 0 {
    return "UnnamedType"
  }
  let parts = s.split("_")
  let mut result = ""
  for part in parts {
    let word = part.to_owned()
    match match_initialism(word, initialisms) {
      Some(init) => result = result + init
      None => result = result + capitalize_first(word)
    }
  }
  result
}

///|
fn is_vowel_char(c : Char) -> Bool {
  c == 'a' ||
  c == 'e' ||
  c == 'i' ||
  c == 'o' ||
  c == 'u' ||
  c == 'A' ||
  c == 'E' ||
  c == 'I' ||
  c == 'O' ||
  c == 'U'
}

///|
/// Singularize a SQL table name for struct naming (default when emit_exact_table_names=false).
/// Handles common English plural patterns; leaves ambiguous names (e.g. status) unchanged.
pub fn singularize_table_name(name : String) -> String {
  if name.length() <= 1 {
    return name
  }
  if name.has_suffix("ies") && name.length() > 3 {
    return name[0:name.length() - 3].to_owned() + "y"
  }
  if name.has_suffix("s") && !name.has_suffix("ss") && name.length() >= 2 {
    let prev = name[name.length() - 2].unsafe_to_char()
    if !is_vowel_char(prev) {
      return name[0:name.length() - 1].to_owned()
    }
  }
  name
}

///|
/// Resolve catalog table name to MoonBit struct base (before PascalCase / keyword escape).
pub fn table_struct_base_name(
  table_name : String,
  emit_exact_table_names : Bool,
) -> String {
  if emit_exact_table_names {
    table_name
  } else {
    singularize_table_name(table_name)
  }
}

///|
/// Resolve catalog table name to MoonBit struct type name.
pub fn table_struct_type_name(
  table_name : String,
  emit_exact_table_names : Bool,
  initialisms : Array[String],
) -> String {
  escape_keyword(
    snake_to_pascal(
      table_struct_base_name(table_name, emit_exact_table_names),
      initialisms,
    ),
  )
}

///|
/// Convert snake_case to camelCase with initialism support.
///
/// Callers should normalize input to lowercase before calling when case is
/// unknown, to avoid mixed-case output like `uSERNAME` from all-caps input.
pub fn snake_to_camel(s : String, initialisms : Array[String]) -> String {
  if s.length() == 0 {
    return ""
  }
  let pascal = snake_to_pascal(s, initialisms)
  lowercase_first(pascal)
}

///|
/// Apply a case style to a string.
/// Supported styles: "snake" (no change), "camel", "pascal".
/// For "snake", the input is returned as-is (assumed to already be snake_case).
pub fn apply_case_style(s : String, style : String) -> String {
  match style {
    "snake" => s
    "camel" => snake_to_camel(s, [])
    "pascal" => snake_to_pascal(s, [])
    _ => s
  }
}

// === Tests ===

///|
test "capitalize_first: lowercase letter" {
  inspect(capitalize_first("hello"), content="Hello")
}

///|
test "capitalize_first: already uppercase" {
  inspect(capitalize_first("Hello"), content="Hello")
}

///|
test "capitalize_first: empty string" {
  inspect(capitalize_first(""), content="")
}

///|
test "lowercase_first: uppercase letter" {
  inspect(lowercase_first("Hello"), content="hello")
}

///|
test "lowercase_first: already lowercase" {
  inspect(lowercase_first("hello"), content="hello")
}

///|
test "lowercase_first: empty string" {
  inspect(lowercase_first(""), content="")
}

///|
test "parse_initialisms: empty string returns empty array" {
  let inits = parse_initialisms("")
  inspect(inits.length(), content="0")
}

///|
test "parse_initialisms: single initialism" {
  let inits = parse_initialisms("ID")
  inspect(inits.length(), content="1")
  inspect(inits[0], content="ID")
}

///|
test "parse_initialisms: multiple initialisms" {
  let inits = parse_initialisms("ID,URL,HTTP")
  inspect(inits.length(), content="3")
  inspect(inits[0], content="ID")
  inspect(inits[1], content="URL")
  inspect(inits[2], content="HTTP")
}

///|
test "parse_initialisms: with whitespace" {
  let inits = parse_initialisms(" ID , URL , HTTP ")
  inspect(inits.length(), content="3")
  inspect(inits[0], content="ID")
  inspect(inits[1], content="URL")
}

///|
test "snake_to_pascal: without initialisms same as old behavior" {
  inspect(snake_to_pascal("user_profiles", []), content="UserProfiles")
}

///|
test "snake_to_pascal: with initialisms applies init to matching words" {
  let inits = parse_initialisms("ID,URL,HTTP")
  inspect(snake_to_pascal("get_user_by_id", inits), content="GetUserByID")
}

///|
test "snake_to_pascal: multiple initialisms in one name" {
  let inits = parse_initialisms("API,ID,URL,HTTP")
  inspect(snake_to_pascal("http_api_url", inits), content="HTTPAPIURL")
}

///|
test "snake_to_pascal: initialism in middle of name" {
  let inits = parse_initialisms("ID,URL")
  inspect(snake_to_pascal("user_id_url", inits), content="UserIDURL")
}

///|
test "snake_to_pascal: empty string returns UnnamedType" {
  let inits = parse_initialisms("ID")
  inspect(snake_to_pascal("", inits), content="UnnamedType")
}

///|
test "snake_to_camel: basic conversion" {
  inspect(snake_to_camel("user_profiles", []), content="userProfiles")
}

///|
test "snake_to_camel: with initialisms" {
  let inits = parse_initialisms("ID,URL")
  inspect(snake_to_camel("get_user_by_id", inits), content="getUserByID")
}

///|
test "apply_case_style: snake returns as-is" {
  inspect(apply_case_style("user_id", "snake"), content="user_id")
}

///|
test "apply_case_style: camel conversion" {
  inspect(apply_case_style("user_id", "camel"), content="userId")
}

///|
test "apply_case_style: pascal conversion" {
  inspect(apply_case_style("user_id", "pascal"), content="UserId")
}

///|
test "apply_case_style: unknown style returns as-is" {
  inspect(apply_case_style("user_id", "unknown"), content="user_id")
}

// === P1-041: 命名转换边缘情况测试 (ADR-004) ===

///|
/// P1-041: Double underscore is folded — split produces an empty segment
/// between the two underscores, which is silently dropped.
test "P1-041: double underscore folds to single word boundary" {
  inspect(snake_to_pascal("user__name", []), content="UserName")
}

///|
/// P1-041: Leading underscore produces empty first segment, silently dropped.
/// The result is as if the leading underscore were not present.
test "P1-041: leading underscore is stripped" {
  inspect(snake_to_pascal("_user", []), content="User")
}

///|
/// P1-041: Trailing underscore produces empty last segment, silently dropped.
test "P1-041: trailing underscore is stripped" {
  inspect(snake_to_pascal("user_name_", []), content="UserName")
}

///|
/// P1-041: Leading + trailing underscores both stripped.
test "P1-041: leading and trailing underscores both stripped" {
  inspect(snake_to_pascal("__user__", []), content="User")
}

///|
/// P1-041: ALL CAPS input stays uppercase because capitalize_first is a no-op
/// when the first character is already uppercase. This is intentional —
/// callers should pre-normalize to lowercase before calling if needed.
test "P1-041: all-caps input stays uppercase (no case normalization)" {
  inspect(snake_to_pascal("USER_NAME", []), content="USERNAME")
}

///|
/// P1-041: camelCase input with no underscore only gets the first letter
/// capitalized. No word-boundary detection for case transitions.
test "P1-041: camelCase input only capitalizes first letter" {
  inspect(snake_to_pascal("userName", []), content="UserName")
}

///|
/// P1-041: Collision — two different snake_case inputs produce the same
/// PascalCase output. Callers MUST handle this at the codegen level.
test "P1-041: snake_case and PascalCase input collide to same output" {
  inspect(snake_to_pascal("user_name", []), content="UserName")
  inspect(snake_to_pascal("UserName", []), content="UserName")
}

///|
/// P1-041: Mixed case with underscores — only the first letter of each
/// underscore-separated segment is uppercased.
test "P1-041: mixed case with underscore boundaries" {
  inspect(snake_to_pascal("user_Name", []), content="UserName")
}

///|
/// P1-041: Single underscore input produces empty string (both segments empty).
test "P1-041: single underscore produces empty string" {
  inspect(snake_to_pascal("_", []), content="")
}

///|
/// P1-041: Only underscores produces empty string (all segments empty).
test "P1-041: only underscores produces empty string" {
  inspect(snake_to_pascal("___", []), content="")
}

///|
/// P1-041: Numbers within segments are preserved as-is.
test "P1-041: numbers are preserved in conversion" {
  inspect(snake_to_pascal("user_2_id", []), content="User2Id")
}

///|
/// P1-041: snake_to_camel with same edge cases — camelCase output
test "P1-041: snake_to_camel handles double underscore" {
  inspect(snake_to_camel("user__name", []), content="userName")
}

///|
/// P1-041: snake_to_camel with leading underscore
test "P1-041: snake_to_camel handles leading underscore" {
  inspect(snake_to_camel("_user", []), content="user")
}

///|
/// P1-041: snake_to_camel with all-caps
test "P1-041: snake_to_camel handles all-caps" {
  inspect(snake_to_camel("USER_NAME", []), content="uSERNAME")
}

///|
/// P1-041: snake_to_camel with camelCase input
test "P1-041: snake_to_camel handles camelCase input" {
  inspect(snake_to_camel("userName", []), content="userName")
}

///|
test "singularize_table_name: users to user" {
  inspect(singularize_table_name("users"), content="user")
}

///|
test "singularize_table_name: status unchanged" {
  inspect(singularize_table_name("status"), content="status")
}

///|
test "singularize_table_name: categories to category" {
  inspect(singularize_table_name("categories"), content="category")
}

///|
test "table_struct_type_name: default singularizes users to User" {
  inspect(table_struct_type_name("users", false, []), content="User")
}

///|
test "table_struct_type_name: exact mode keeps Users" {
  inspect(table_struct_type_name("users", true, []), content="Users")
}

///|
/// P1-041: Collision potential between table name and enum name.
/// If a table is named "status" and an enum is also named "status",
/// both snake_to_pascal to "Status", causing a compile error.
/// This test documents the collision; disambiguation is tracked in P1-041.
test "P1-041: table name and enum name produce same PascalCase (collision)" {
  // Simulating: table "user_status" and enum "user_status" both → "UserStatus"
  let table_pascal = snake_to_pascal("user_status", [])
  let enum_pascal = snake_to_pascal("user_status", [])
  inspect(table_pascal, content="UserStatus")
  inspect(enum_pascal, content="UserStatus")
  inspect(table_pascal == enum_pascal, content="true")
}