///|
fn collect_option_words(options : Array[OptionDef]) -> String {
  let words : Array[String] = []
  for opt in options {
    words.push("--" + opt.name)
    if has_short(opt) {
      words.push("-" + opt.short.to_string())
    }
  }
  words.join(" ")
}

///|
fn collect_command_names(commands : Array[CommandDef]) -> String {
  commands.map(fn(c) { c.name }).join(" ")
}

///|
fn escape_single_quote(s : String) -> String {
  s.replace(old="'", new="'\\''")
}

///|
/// dollar sign as variable to avoid JS template literal collision
let dollar : String = "$"

// === Bash ===

///|
fn render_bash_case(sb : StringBuilder, cmd : CommandDef, depth : Int) -> Unit {
  let indent = "  ".repeat(depth + 1)
  if cmd.subcommands.length() > 0 {
    let word_index = (depth + 2).to_string()
    sb.write_string(
      indent + "case \"" + dollar + "{words[" + word_index + "]}\" in\n",
    )
    for sub in cmd.subcommands {
      sb.write_string(indent + "  " + sub.name + ")\n")
      if sub.subcommands.length() > 0 {
        render_bash_case(sb, sub, depth + 1)
      } else {
        let w = collect_option_words(sub.options)
        let line = indent +
          "    COMPREPLY=($(compgen -W \"" +
          w +
          "\" -- \"" +
          dollar +
          "cur\"))\n"
        sb.write_string(line)
      }
      sb.write_string(indent + "    ;;\n")
    }
    let sub_names = collect_command_names(cmd.subcommands)
    let own_opts = collect_option_words(cmd.options)
    let all = if own_opts.length() > 0 {
      sub_names + " " + own_opts
    } else {
      sub_names
    }
    sb.write_string(indent + "  *)\n")
    let fallback = indent +
      "    COMPREPLY=($(compgen -W \"" +
      all +
      "\" -- \"" +
      dollar +
      "cur\"))\n"
    sb.write_string(fallback)
    sb.write_string(indent + "    ;;\n")
    sb.write_string(indent + "esac\n")
  } else {
    let w = collect_option_words(cmd.options)
    let line = indent +
      "COMPREPLY=($(compgen -W \"" +
      w +
      "\" -- \"" +
      dollar +
      "cur\"))\n"
    sb.write_string(line)
  }
}

///|
pub fn CliApp::render_bash_completion(self : CliApp) -> String {
  let sb = StringBuilder::new()
  let fn_name = "_" + self.name.replace(old="-", new="_")
  sb.write_string(fn_name + "() {\n")
  sb.write_string("  local cur prev words cword\n")
  sb.write_string("  _init_completion || return\n\n")
  sb.write_string("  case \"" + dollar + "{words[1]}\" in\n")
  for cmd in self.commands {
    sb.write_string("    " + cmd.name + ")\n")
    if cmd.subcommands.length() > 0 {
      render_bash_case(sb, cmd, 1)
    } else {
      let w = collect_option_words(cmd.options)
      let line = "      COMPREPLY=($(compgen -W \"" +
        w +
        "\" -- \"" +
        dollar +
        "cur\"))\n"
      sb.write_string(line)
    }
    sb.write_string("      ;;\n")
  }
  let names = collect_command_names(self.commands)
  sb.write_string("    *)\n")
  let fallback = "      COMPREPLY=($(compgen -W \"" +
    names +
    "\" -- \"" +
    dollar +
    "cur\"))\n"
  sb.write_string(fallback)
  sb.write_string("      ;;\n")
  sb.write_string("  esac\n")
  sb.write_string("}\n")
  sb.write_string("complete -F " + fn_name + " " + self.name + "\n")
  sb.to_string()
}

// === Zsh ===

///|
fn render_zsh_command(
  sb : StringBuilder,
  cmd : CommandDef,
  app_name : String,
  full : String,
) -> Unit {
  let safe = full.replace(old="-", new="_")
  let fn_name = "_" + app_name.replace(old="-", new="_") + "_" + safe
  sb.write_string(fn_name + "() {\n")
  if cmd.subcommands.length() > 0 {
    sb.write_string("  local -a commands\n")
    sb.write_string("  commands=(\n")
    for sub in cmd.subcommands {
      let desc = escape_single_quote(sub.description)
      sb.write_string("    '" + sub.name + ":" + desc + "'\n")
    }
    sb.write_string("  )\n")
    sb.write_string("  _describe 'command' commands\n")
  }
  if cmd.options.length() > 0 {
    sb.write_string("  _arguments \\\n")
    for i, opt in cmd.options {
      let desc = escape_single_quote(opt.description)
      let flag = if has_short(opt) {
        "'-" +
        opt.short.to_string() +
        "[" +
        desc +
        "]' '--" +
        opt.name +
        "[" +
        desc +
        "]'"
      } else {
        "'--" + opt.name + "[" + desc + "]'"
      }
      let end = if i < cmd.options.length() - 1 { " \\" } else { "" }
      sb.write_string("    " + flag + end + "\n")
    }
  }
  sb.write_string("}\n\n")
  for sub in cmd.subcommands {
    let sub_full = full + "_" + sub.name
    render_zsh_command(sb, sub, app_name, sub_full)
  }
}

///|
pub fn CliApp::render_zsh_completion(self : CliApp) -> String {
  let sb = StringBuilder::new()
  sb.write_string("#compdef " + self.name + "\n\n")
  let fn_name = "_" + self.name.replace(old="-", new="_")
  sb.write_string(fn_name + "() {\n")
  sb.write_string("  local -a commands\n")
  sb.write_string("  commands=(\n")
  for cmd in self.commands {
    let desc = escape_single_quote(cmd.description)
    sb.write_string("    '" + cmd.name + ":" + desc + "'\n")
  }
  sb.write_string("  )\n")
  sb.write_string("  _describe 'command' commands\n")
  sb.write_string("}\n\n")
  for cmd in self.commands {
    render_zsh_command(sb, cmd, self.name, cmd.name)
  }
  sb.write_string("compdef " + fn_name + " " + self.name + "\n")
  sb.to_string()
}

// === Fish ===

///|
fn render_fish_options(
  sb : StringBuilder,
  app_name : String,
  condition : String,
  options : Array[OptionDef],
) -> Unit {
  for opt in options {
    let mut line = "complete -c " +
      app_name +
      " " +
      condition +
      " -l " +
      opt.name
    if has_short(opt) {
      line = line + " -s " + opt.short.to_string()
    }
    if opt.description.length() > 0 {
      line = line + " -d '" + escape_single_quote(opt.description) + "'"
    }
    if opt.type_ != BoolOpt {
      line = line + " -r"
    }
    sb.write_string(line + "\n")
  }
}

///|
fn render_fish_subcommands(
  sb : StringBuilder,
  app_name : String,
  condition : String,
  commands : Array[CommandDef],
) -> Unit {
  for cmd in commands {
    let desc = escape_single_quote(cmd.description)
    let line = "complete -c " +
      app_name +
      " " +
      condition +
      " -a " +
      cmd.name +
      " -d '" +
      desc +
      "'"
    sb.write_string(line + "\n")
    let sub_cond = "-n '__fish_seen_subcommand_from " + cmd.name + "'"
    render_fish_options(sb, app_name, sub_cond, cmd.options)
    render_fish_subcommands(sb, app_name, sub_cond, cmd.subcommands)
  }
}

///|
pub fn CliApp::render_fish_completion(self : CliApp) -> String {
  let sb = StringBuilder::new()
  sb.write_string("# Fish completions for " + self.name + "\n")
  let root_cond = "-n '__fish_use_subcommand'"
  for cmd in self.commands {
    let desc = escape_single_quote(cmd.description)
    let line = "complete -c " +
      self.name +
      " " +
      root_cond +
      " -a " +
      cmd.name +
      " -d '" +
      desc +
      "'"
    sb.write_string(line + "\n")
  }
  for cmd in self.commands {
    let cond = "-n '__fish_seen_subcommand_from " + cmd.name + "'"
    render_fish_options(sb, self.name, cond, cmd.options)
    render_fish_subcommands(sb, self.name, cond, cmd.subcommands)
  }
  sb.to_string()
}