///|
/// 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 a number with two decimals, rounded to nearest hundredth,
/// e.g. `46.3` -> `"46.30"`.
fn fixed_two(x : Float) -> String {
  let hundredths = (x * 100.0).round().to_int()
  let whole = hundredths / 100
  let frac = hundredths % 100
  let frac_text = if frac < 10 { "0\{frac}" } else { "\{frac}" }
  "\{whole}.\{frac_text}"
}

///|
/// 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 => {
        s.parse_uptime().map(u => u.render())
      }),
    ),
    ..field_line(
      "Shell",
      override_value(matches, "shell").unwrap_or(model.shell),
    ),
    ..field_line(
      "Resolution",
      structured_field(
        matches,
        "resolution",
        model.resolution.map(r => r.render()).unwrap_or(""),
        s => s.parse_resolution().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 => {
        s.parse_cpu().map(c => c.render())
      }),
    ),
    ..field_line(
      "GPU",
      structured_field(matches, "gpu", model.gpu.render(), s => {
        s.parse_gpu().map(g => g.render())
      }),
    ),
    ..field_line(
      "Memory",
      structured_field(matches, "memory", model.memory.render(), s => {
        s.parse_memory().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 render_info_only(info : Array[String]) -> String {
  "\{info.join("\n")}\n"
}

///|
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.. {
        buf.write_string(text)
        buf.write_string(" ".repeat(width - line_width))
      }
      None => buf.write_string(" ".repeat(width))
    }
    buf.write_string("  ")
    if info.get(i) is Some(text) {
      buf.write_string(text)
    }
    buf.write_string("\n")
  }
  buf.to_string()
}

///|
fn field_line(label : String, value : String) -> 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 {
  guard !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 = match color_mode {
    "auto" | "none" => ""
    _ => 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
  if !carry.is_empty() {
    write_ansi(buf, carry)
  }
  let chars = line.to_array()
  let mut index = 0
  while index < chars.length() {
    match (chars[index], chars.get(index + 1)) {
      ('$', Some('$')) => {
        buf.write_char('$')
        width += 1
        index += 2
      }
      ('$', Some(n)) 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
      }
      ('$', _) => {
        buf.write_char('$')
        width += 1
        index += 1
      }
      (c, _) => {
        buf.write_char(c)
        width += 1
        index += 1
      }
    }
  }
  if !carry.is_empty() {
    buf.write_string("\u{1B}[0m")
  }
  { text: buf.to_string(), width, carry }
}