///|
pub fn build_command() -> @clap.Command {
  @clap.Command(
    "ffetch",
    about="Print system information that looks like fastfetch output",
    flags=[@clap.FlagArg("no-logo", about="Do not print a logo")],
    options=[
      @clap.OptionArg("model", about=model_about()),
      @clap.OptionArg("user", short='u', about="user name"),
      @clap.OptionArg("host", short='H', about="host name"),
      @clap.OptionArg("os", short='o', about="OS"),
      @clap.OptionArg("device", short='d', about="device model"),
      @clap.OptionArg("kernel", short='k', about="kernel"),
      @clap.OptionArg("uptime", short='t', about="uptime hours,mins, e.g. 3,12"),
      @clap.OptionArg("shell", short='s', about="shell"),
      @clap.OptionArg(
        "resolution",
        short='r',
        about="resolution width,height, e.g. 3456,2234",
      ),
      @clap.OptionArg("de", short='e', about="desktop environment"),
      @clap.OptionArg("wm", short='w', about="window manager"),
      @clap.OptionArg("wm-theme", about="WM theme"),
      @clap.OptionArg("terminal", short='m', about="terminal"),
      @clap.OptionArg("term-font", about="terminal font"),
      @clap.OptionArg(
        "cpu",
        short='c',
        about="CPU name,cores,clock GHz, e.g. \"Apple M3 Max,16,4.05\"",
      ),
      @clap.OptionArg(
        "gpu",
        short='g',
        about="GPU name[,vram GB], e.g. \"Apple M3 Max\" or \"RTX 4090,24\"",
      ),
      @clap.OptionArg(
        "memory",
        short='M',
        about="memory used,total in GiB, e.g. 46.3,128",
      ),
      @clap.OptionArg(
        "logo",
        short='l',
        about="logo name: macos, linux, arch, ubuntu, debian, fedora, nixos, windows, alpine, android, artix, deepin, endeavouros, gentoo, manjaro, mx, opensuse, pop, raspbian, rocky, slackware, suse, termux, none",
      ),
      @clap.OptionArg(
        "color",
        short='C',
        about="logo color: auto (use logo palette) or black, red, green, yellow, blue, magenta, cyan, white, none",
        default_values=["auto"],
      ),
    ],
  )
}

///|
/// Value provided by the user on the command line, if any.
/// Distinct from the model defaults because the argparse default_values
/// are unset for field options.
fn override_value(matches : @clap.Matches, name : String) -> String? {
  matches.values.get(name).bind(arr => arr.get(0))
}

///|
fn ansi_code(color : String) -> String {
  match color {
    "black" => "30"
    "red" => "31"
    "green" => "32"
    "yellow" => "33"
    "blue" => "34"
    "magenta" => "35"
    "cyan" => "36"
    "white" => "37"
    _ => ""
  }
}

///|
/// Render the fake system-info output for the parsed command line.
pub fn render(matches : @clap.Matches) -> String {
  let model = override_value(matches, "model")
    .bind(model_by_name)
    .unwrap_or(default_model())
  let info = render_info(matches, model)
  let no_logo = matches.flags.get("no-logo").unwrap_or(false)
  let logo = {
    guard !no_logo else { None }
    logo_by_name(override_value(matches, "logo").unwrap_or(model.logo))
  }
  match logo {
    None => render_info_only(info)
    Some(logo) =>
      render_with_logo(
        info,
        decode_logo(logo, override_value(matches, "color").unwrap_or("")),
      )
  }
}

///|
fn render_info(matches : @clap.Matches, model : DeviceModel) -> Array[String] {
  let user = override_value(matches, "user").unwrap_or("kokic")
  let host = override_value(matches, "host").unwrap_or(model.host)
  let header = "\{user}@\{host}"
  let dashline = "-".repeat(header.char_length())
  [
    header,
    dashline,
    ..field_line("OS", override_value(matches, "os").unwrap_or(model.os)),
    ..field_line(
      "Host",
      override_value(matches, "device").unwrap_or(model.device),
    ),
    ..field_line(
      "Kernel",
      override_value(matches, "kernel").unwrap_or(model.kernel),
    ),
    ..field_line(
      "Uptime",
      structured_field(matches, "uptime", model.uptime.render(), s => {
        parse_uptime(s).map(u => u.render())
      }),
    ),
    ..field_line(
      "Shell",
      override_value(matches, "shell").unwrap_or(model.shell),
    ),
    ..field_line(
      "Resolution",
      structured_field(matches, "resolution", model.resolution.render(), s => {
        parse_resolution(s).map(r => r.render())
      }),
    ),
    ..field_line("DE", override_value(matches, "de").unwrap_or(model.de)),
    ..field_line("WM", override_value(matches, "wm").unwrap_or(model.wm)),
    ..field_line(
      "WM Theme",
      override_value(matches, "wm-theme").unwrap_or(model.wm_theme),
    ),
    ..field_line(
      "Terminal",
      override_value(matches, "terminal").unwrap_or(model.terminal),
    ),
    ..field_line(
      "Terminal Font",
      override_value(matches, "term-font").unwrap_or(model.term_font),
    ),
    ..field_line(
      "CPU",
      structured_field(matches, "cpu", model.cpu.render(), s => {
        parse_cpu(s).map(c => c.render())
      }),
    ),
    ..field_line(
      "GPU",
      structured_field(matches, "gpu", model.gpu.render(), s => {
        parse_gpu(s).map(g => g.render())
      }),
    ),
    ..field_line(
      "Memory",
      structured_field(matches, "memory", model.memory.render(), s => {
        parse_memory(s).map(m => m.render())
      }),
    ),
  ]
}

///|
/// The rendered value of a structured field: an explicit override when it
/// parses, otherwise the model default. An empty override hides the line,
/// like other fields.
fn structured_field(
  matches : @clap.Matches,
  name : String,
  default : String,
  parse : (String) -> String?,
) -> String {
  match override_value(matches, name) {
    Some("") => ""
    Some(s) => parse(s).unwrap_or(default)
    None => default
  }
}

///|
fn parse_int_part(s : StringView) -> Int? {
  Some(@string.parse_int(s)) catch {
    _ => None
  }
}

///|
fn parse_float_part(s : StringView) -> Float? {
  Some(Float::from_double(@string.parse_double(s))) catch {
    _ => None
  }
}

///|
/// Parse a `used,total` pair in GiB, e.g. `"46.3,128"`.
fn parse_memory(s : String) -> Memory? {
  let parts = s.split(",").to_array()
  if parts.length() != 2 {
    return None
  }
  guard parse_float_part(parts[0]) is Some(used) else { return None }
  guard parse_float_part(parts[1]) is Some(total) else { return None }
  Some({ used, total })
}

///|
/// Parse a `width,height` pair, e.g. `"3456,2234"`.
fn parse_resolution(s : String) -> Resolution? {
  let parts = s.split(",").to_array()
  if parts.length() != 2 {
    return None
  }
  guard parse_int_part(parts[0]) is Some(width) else { return None }
  guard parse_int_part(parts[1]) is Some(height) else { return None }
  Some({ width, height })
}

///|
/// Parse an `hours,mins` pair, e.g. `"3,12"`.
fn parse_uptime(s : String) -> Uptime? {
  let parts = s.split(",").to_array()
  if parts.length() != 2 {
    return None
  }
  guard parse_int_part(parts[0]) is Some(hours) else { return None }
  guard parse_int_part(parts[1]) is Some(mins) else { return None }
  Some({ hours, mins })
}

///|
/// Parse `name,cores,clock` in GHz, e.g. `"Apple M3 Max,16,4.05"`.
fn parse_cpu(s : String) -> Cpu? {
  let parts = s.split(",").to_array()
  if parts.length() != 3 {
    return None
  }
  guard parse_int_part(parts[1]) is Some(cores) else { return None }
  guard parse_float_part(parts[2]) is Some(clock) else { return None }
  Some({ name: parts[0].to_owned(), cores, clock })
}

///|
/// Parse `name` or `name,vram` in GB, e.g. `"Apple M3 Max"` or `"RTX 4090,24"`.
fn parse_gpu(s : String) -> Gpu? {
  let parts = s.split(",").to_array()
  match parts.length() {
    1 => Some({ name: parts[0].to_owned(), vram: 0 })
    2 => {
      guard parse_int_part(parts[1]) is Some(vram) else { return None }
      Some({ name: parts[0].to_owned(), vram })
    }
    _ => None
  }
}

///|
fn render_info_only(info : Array[String]) -> String {
  let buf = StringBuilder()
  for line in info {
    buf.write_string(line)
    buf.write_string("\n")
  }
  buf.to_string()
}

///|
fn render_with_logo(info : Array[String], rendered : DecodedLogo) -> String {
  let buf = StringBuilder()
  let width = rendered.max_width
  let rows = rendered.lines.length().max(info.length())
  for i in 0.. Array[String] {
  guard !value.is_empty() else { [] }
  ["\{label}: \{value}"]
}

///|
priv struct DecodedLogo {
  lines : Array[(String, Int)]
  max_width : Int
}

///|
/// The palette entry used to color a logo cell: the forced color wins,
/// otherwise the palette color at `index`, unless colors are disabled.
fn resolve_code(
  palette : Array[String],
  forced_code : String,
  no_color : Bool,
  index : Int,
) -> String {
  if no_color {
    ""
  } else if !forced_code.is_empty() {
    forced_code
  } else {
    palette.get(index).unwrap_or("")
  }
}

///|
fn decode_logo(logo : Logo, color_mode : String) -> DecodedLogo {
  let no_color = color_mode == "none"
  let forced_code = if color_mode == "auto" || color_mode == "none" {
    ""
  } else {
    ansi_code(color_mode)
  }
  let mut carry = resolve_code(logo.palette, forced_code, no_color, 0)
  let lines : Array[(String, Int)] = []
  let mut max_width = 0
  for line in logo.lines {
    let decoded = decode_logo_line(
      line,
      logo.palette,
      forced_code,
      carry,
      no_color,
    )
    carry = decoded.carry
    lines.push((decoded.text, decoded.width))
    max_width = max_width.max(decoded.width)
  }
  { lines, max_width }
}

///|
priv struct DecodedLogoLine {
  text : String
  width : Int
  carry : String
}

///|
fn write_ansi(buf : StringBuilder, code : String) -> Unit {
  buf.write_string("\u{1B}[")
  buf.write_string(code)
  buf.write_string("m")
}

///|
fn decode_logo_line(
  line : String,
  palette : Array[String],
  forced_code : String,
  start_carry : String,
  no_color : Bool,
) -> DecodedLogoLine {
  let buf = StringBuilder()
  let mut carry = start_carry
  let mut width = 0
  let mut index = 0
  if !carry.is_empty() {
    write_ansi(buf, carry)
  }
  let chars = line.to_array()
  while index < chars.length() {
    let c = chars[index]
    if c == '$' && index + 1 < chars.length() {
      let n = chars[index + 1]
      if n == '$' {
        buf.write_char('$')
        width += 1
        index += 2
        continue
      }
      if n >= '1' && n <= '9' {
        let code = resolve_code(
          palette,
          forced_code,
          no_color,
          n.to_int() - '1'.to_int(),
        )
        carry = code
        if !code.is_empty() {
          write_ansi(buf, code)
        }
        index += 2
        continue
      }
    }
    buf.write_char(c)
    width += 1
    index += 1
  }
  if !carry.is_empty() {
    buf.write_string("\u{1B}[0m")
  }
  { text: buf.to_string(), width, carry }
}