// What a spec's own names become in SQL. `--style` names what mctl invents — the
// files of a tree and the functions inside them — and deliberately leaves the
// names the spec chose alone, because a MoonBit type has to keep its capital. A
// table and a column are neither: they are names the schema implies, and a team
// whose tables are `Users` and whose columns are `createdAt` should say so once, in
// the spec, rather than rename them in every generated file.
//
// The spec says it in its `info` block, next to the title and the version:
//
//   info (
//     tableNaming:  "plural_snake"
//     columnNaming: "camel"
//   )

///|
/// The casing a generated name takes.
pub(all) enum Naming {
  /// `created_at`: every word lower-cased, joined by `_`.
  Snake
  /// `createdAt`: the first word lower-cased, the rest capitalized, run together.
  Camel
  /// `CreatedAt`: every word capitalized, run together.
  Pascal
  /// `created-at`: every word lower-cased, joined by `-`.
  Kebab
  /// `CREATED_AT`: every word upper-cased, joined by `_`.
  ScreamingSnake
}

///|
/// How a spec's own names are spelled in SQL: the table a `type` block becomes,
/// its columns, and whether a table is named for one row (`user`) or for the set
/// (`users`).
pub(all) struct NamingPolicy {
  table : Naming
  column : Naming
  plural : Bool
}

///|
/// The policy a spec that says nothing gets: lower snake for both, one row per
/// name — the spelling every SQL style guide starts from.
pub let default_naming : NamingPolicy = {
  table: Snake,
  column: Snake,
  plural: false,
}

///|
/// A `Naming` as its option spells it: `snake`, `camel`, `pascal`, `kebab` or
/// `screaming`. `plural_snake` is `snake` with plural tables, and `pluralSnake`
/// spells the same.
fn naming_of_option(text : String) -> (Naming, Bool)? {
  match @text.trim(text).to_lower() {
    "snake" => Some((Snake, false))
    "camel" => Some((Camel, false))
    "pascal" => Some((Pascal, false))
    "kebab" => Some((Kebab, false))
    "screaming" | "screaming_snake" | "screamingsnake" =>
      Some((ScreamingSnake, false))
    "plural_snake" | "pluralsnake" | "plural" => Some((Snake, true))
    _ => None
  }
}

///|
/// The policy a spec's `info` block asks for, or `None` when it asks for none —
/// which leaves each generator spelling names the way it always has. A value that
/// is not a casing is left at the default and named by `naming_problems`, which
/// `validate` reports, so generation never guesses a spelling the spec did not ask
/// for.
pub fn NamingPolicy::of_info(info : Array[(String, String)]) -> NamingPolicy? {
  let mut policy = default_naming
  let mut asked = false
  for kv in info {
    match @text.trim(kv.0).to_lower() {
      "tablenaming" | "table_naming" | "naming.table" =>
        match naming_of_option(kv.1) {
          Some((naming, plural)) => {
            policy = { ..policy, table: naming, }
            if plural {
              policy = { ..policy, plural: true, }
            }
            asked = true
          }
          None => ()
        }
      "columnnaming" | "column_naming" | "naming.column" =>
        match naming_of_option(kv.1) {
          Some((naming, _)) => {
            policy = { ..policy, column: naming, }
            asked = true
          }
          None => ()
        }
      "tableplural" | "table_plural" | "naming.plural" => {
        let value = @text.trim(kv.1).to_lower()
        if value == "true" || value == "false" {
          policy = { ..policy, plural: value == "true", }
          asked = true
        }
      }
      _ => ()
    }
  }
  if asked {
    Some(policy)
  } else {
    None
  }
}

///|
/// What the spec's `info` block says that `NamingPolicy` could not read, one line
/// per entry, for `validate` to report: a naming key with a value that is not a
/// casing, or with no value at all.
pub fn naming_problems(info : Array[(String, String)]) -> Array[String] {
  let out : Array[String] = []
  for kv in info {
    let key = @text.trim(kv.0).to_lower()
    let known = key == "tablenaming" ||
      key == "table_naming" ||
      key == "naming.table" ||
      key == "columnnaming" ||
      key == "column_naming" ||
      key == "naming.column" ||
      key == "tableplural" ||
      key == "table_plural" ||
      key == "naming.plural"
    if known == false {
      continue
    }
    if key == "tableplural" || key == "table_plural" || key == "naming.plural" {
      let value = @text.trim(kv.1).to_lower()
      if value != "true" && value != "false" {
        out.push("info " + kv.0 + ": want true or false, got \"" + kv.1 + "\"")
      }
    } else if naming_of_option(kv.1) is None {
      out.push(
        "info " +
        kv.0 +
        ": want snake, camel, pascal, kebab, screaming or plural_snake, got \"" +
        kv.1 +
        "\"",
      )
    }
  }
  out
}

///|
/// `name` in this casing.
pub fn Naming::spell(self : Naming, name : String) -> String {
  let ws = words(name)
  let parts : Array[String] = []
  for i = 0; i < ws.length(); i = i + 1 {
    let w = ws[i]
    parts.push(
      match self {
        Snake | Kebab => w.to_lower()
        ScreamingSnake => upper_all(w.to_lower())
        Camel =>
          if i == 0 {
            w.to_lower()
          } else {
            @text.upper_first(w.to_lower())
          }
        Pascal => @text.upper_first(w.to_lower())
      },
    )
  }
  let gap = match self {
    Kebab => "-"
    Camel | Pascal => ""
    _ => "_"
  }
  let mut out = ""
  for i = 0; i < parts.length(); i = i + 1 {
    if i > 0 {
      out = out + gap
    }
    out = out + parts[i]
  }
  out
}

///|
/// `word` in the plural, by the two rules that cover most English nouns: `y` after
/// a consonant becomes `ies`, a sibilant takes `es`, and everything else takes `s`.
/// It is a rule, not a dictionary — a name it gets wrong is spelled by asking for
/// `snake` tables instead of `plural_snake`.
fn plural(word : String) -> String {
  let w = word.to_lower()
  let n = w.length()
  if n == 0 {
    return word
  }
  if n >= 2 && w[n - 1] == 'y' {
    let before = w[n - 2]
    let vowel = before == 'a' ||
      before == 'e' ||
      before == 'i' ||
      before == 'o' ||
      before == 'u'
    if vowel == false {
      return w[0:n - 1].to_owned() + "ies"
    }
  }
  if @text.ends_with(w, "s") ||
    @text.ends_with(w, "x") ||
    @text.ends_with(w, "z") ||
    @text.ends_with(w, "ch") ||
    @text.ends_with(w, "sh") {
    return w + "es"
  }
  w + "s"
}

///|
/// The table a `type` block called `name` becomes, under this policy.
pub fn NamingPolicy::table_name(self : NamingPolicy, name : String) -> String {
  if self.plural == false {
    return self.table.spell(name)
  }
  // The plural is the last word's: `BlogPost` is `blog_posts`, not `blogs_posts`.
  let ws = words(name)
  let out : Array[String] = []
  for i = 0; i < ws.length(); i = i + 1 {
    out.push(if i + 1 == ws.length() { plural(ws[i]) } else { ws[i] })
  }
  self.table.spell(out.join("_"))
}

///|
/// The column a field called `name` becomes, under this policy.
pub fn NamingPolicy::column_name(self : NamingPolicy, name : String) -> String {
  self.column.spell(name)
}