// `mctl config` — the settings mctl reads before it starts: where the template
// library lives (`home`, `remote`, `branch`), how generated names are spelled
// (`style`), and which templates a bare `gen` picks (`template`).
//
// The file is `key = value` per line, `#` starts a comment. That is deliberately
// dull: it diffs cleanly, a human can edit it without a schema, and a v1 file —
// uppercase environment-style keys — is migrated by `migrate_config`, which reports
// every key it changed.

///|
/// One configuration: the fields mctl understands. `version` is the file format,
/// not the mctl version, and it goes up when `migrate_config` learns a new shape.
pub(all) struct Config {
  version : Int32
  home : String
  remote : String
  branch : String
  style : String
  templates : Array[String]
}

///|
/// The file mctl writes when there is none yet: the bundled library, the default
/// branch, the `gozero` spelling.
pub fn default_config() -> Config {
  {
    version: 2,
    home: "",
    remote: "https://github.com/moonbitstack/templates",
    branch: "main",
    style: "gozero",
    templates: [],
  }
}

///|
/// `line` split at its first `=`, both halves trimmed, or `None` when it has no `=`
/// or opens with `#`.
fn config_pair(line : String) -> (String, String)? {
  let s = @text.trim(line)
  if s == "" || s[0] == '#' {
    return None
  }
  let eq = @text.index_of_str(s, "=")
  if eq <= 0 {
    return None
  }
  Some((@text.trim(s[0:eq].to_owned()), @text.trim(s[eq + 1:].to_owned())))
}

///|
/// Read a configuration. A key mctl does not know is kept out of the result rather
/// than refused, so a file written by a newer mctl still loads. An empty or
/// unreadable-text file is the default configuration.
pub fn parse_config(text : String) -> Config {
  let mut c = default_config()
  for line in @text.split_char(text, '\n') {
    match config_pair(line) {
      None => ()
      Some(kv) => {
        let key = kv.0.to_lower()
        let value = kv.1
        match key {
          "version" =>
            match parse_int(value) {
              Some(v) =>
                c = {
                  version: v,
                  home: c.home,
                  remote: c.remote,
                  branch: c.branch,
                  style: c.style,
                  templates: c.templates,
                }
              None => ()
            }
          "home" | "moontcl_home" | "moonctl_home" =>
            c = {
              version: c.version,
              home: value,
              remote: c.remote,
              branch: c.branch,
              style: c.style,
              templates: c.templates,
            }
          "remote" | "moonctl_remote" =>
            c = {
              version: c.version,
              home: c.home,
              remote: value,
              branch: c.branch,
              style: c.style,
              templates: c.templates,
            }
          "branch" | "moonctl_branch" =>
            c = {
              version: c.version,
              home: c.home,
              remote: c.remote,
              branch: value,
              style: c.style,
              templates: c.templates,
            }
          "style" | "moonctl_style" =>
            c = {
              version: c.version,
              home: c.home,
              remote: c.remote,
              branch: c.branch,
              style: value,
              templates: c.templates,
            }
          "template" | "moonctl_template" =>
            if value != "" && c.templates.contains(value) == false {
              c = {
                version: c.version,
                home: c.home,
                remote: c.remote,
                branch: c.branch,
                style: c.style,
                templates: c.templates + [value],
              }
            }
          _ => ()
        }
      }
    }
  }
  c
}

///|
/// A decimal integer read out of `s`, or `None` when `s` is not one.
fn parse_int(s : String) -> Int32? {
  if s.length() == 0 {
    return None
  }
  let mut n = 0
  for i = 0; i < s.length(); i = i + 1 {
    let c = s[i]
    if c < '0' || c > '9' {
      return None
    }
    n = n * 10 + (c - '0').to_int()
  }
  Some(n)
}

///|
/// The configuration as the file it is stored in, with a comment saying what the
/// file is for and where the keys come from.
pub fn config_text(c : Config) -> String {
  let out : Array[String] = []
  out.push(
    "# mctl configuration — written by `mctl config set`, safe to edit by hand.",
  )
  out.push(
    "# `home` is a template library on disk; `remote` and `branch` are read when",
  )
  out.push("# there is no `home`, or when --remote is passed.")
  out.push("")
  out.push("version = " + c.version.to_string())
  out.push("home = " + c.home)
  out.push("remote = " + c.remote)
  out.push("branch = " + c.branch)
  out.push("style = " + c.style)
  for t in c.templates {
    out.push("template = " + t)
  }
  @text.join_lines(out) + "\n"
}

///|
/// The value of one key, or `None` when mctl has no such key.
pub fn config_get(c : Config, key : String) -> String? {
  match key.to_lower() {
    "version" => Some(c.version.to_string())
    "home" => Some(c.home)
    "remote" => Some(c.remote)
    "branch" => Some(c.branch)
    "style" => Some(c.style)
    "template" => Some(c.templates.join(", "))
    _ => None
  }
}

///|
/// `c` with one key set. `template` appends — a config may name several — while a
/// scalar key replaces what was there. An unknown key is returned unchanged, so a
/// caller checks the key against `config_keys` first.
pub fn config_set(c : Config, key : String, value : String) -> Config {
  match key.to_lower() {
    "home" => { ..c, home: value, }
    "remote" => { ..c, remote: value, }
    "branch" => { ..c, branch: value, }
    "style" => { ..c, style: value, }
    "template" =>
      if c.templates.contains(value) {
        c
      } else {
        { ..c, templates: c.templates + [value], }
      }
    _ => c
  }
}

///|
/// The keys `config get` and `config set` accept.
pub fn config_keys() -> Array[String] {
  ["version", "home", "remote", "branch", "style", "template"]
}

///|
/// A v1 configuration — the uppercase, environment-style keys mctl read before it
/// had a file — rewritten as the current shape. Returns the new text and one line
/// per key that changed, so `mctl config migrate` can say what it did.
pub fn migrate_config(text : String) -> (String, Array[String]) {
  let changes : Array[String] = []
  let mut c = default_config()
  let mut seen_version = false
  for line in @text.split_char(text, '\n') {
    match config_pair(line) {
      None => ()
      Some(kv) => {
        let raw_key = kv.0
        let low = raw_key.to_lower()
        let value = kv.1
        let canonical = if low == "moontcl_home" ||
          low == "moonctl_home" ||
          low == "goctl_home" {
          "home"
        } else if low == "moonctl_remote" || low == "goctl_remote" {
          "remote"
        } else if low == "moonctl_branch" {
          "branch"
        } else if low == "moonctl_style" {
          "style"
        } else if low == "moonctl_template" || low == "goctl_template" {
          "template"
        } else {
          low
        }
        if canonical == "version" {
          seen_version = true
        }
        if canonical != raw_key && config_keys().contains(canonical) {
          changes.push("\"" + raw_key + "\" → \"" + canonical + "\"")
        }
        let before = config_get(c, canonical)
        c = config_set(c, canonical, value)
        if canonical == "version" {
          match parse_int(value) {
            Some(1) => c = { ..c, version: 2, }
            _ => ()
          }
        }
        if canonical == "template" {
          let after = config_get(c, canonical)
          if before != after {
            changes.push("kept template \"" + value + "\"")
          }
        }
      }
    }
  }
  if seen_version == false {
    changes.push("added \"version = 2\"")
  }
  (config_text(c), changes)
}