///|
fn field_text(map : Map[String, Json], key : String) -> String {
  match map.get(key) {
    Some(String(text)) => text
    Some(Number(n, ..)) => n.to_int64().to_string()
    _ => ""
  }
}

///|
fn field_list(map : Map[String, Json], key : String) -> Array[String] {
  let result : Array[String] = []
  match map.get(key) {
    Some(Array(items)) =>
      for item in items {
        match item {
          String(text) => result.push(text)
          _ => ()
        }
      }
    _ => ()
  }
  result
}

///|
/// `ls -l` style permission string, for example `-rwxr-xr-x`.
fn mode_string(kind : String, mode_text : String) -> String {
  let mut mode = 0
  for c in mode_text {
    mode = mode * 8 + (c.to_int() - '0'.to_int())
  }
  let type_char = match kind {
    "dir" => 'd'
    "symlink" => 'l'
    "hardlink" => 'h'
    "special" => '?'
    _ => '-'
  }
  let out = StringBuilder()
  out.write_char(type_char)
  let bits = [
    (0o400, 'r'),
    (0o200, 'w'),
    (0o100, 'x'),
    (0o040, 'r'),
    (0o020, 'w'),
    (0o010, 'x'),
    (0o004, 'r'),
    (0o002, 'w'),
    (0o001, 'x'),
  ]
  for i, bit in bits {
    let set = (mode & bit.0) != 0
    let special = match i {
      2 => (mode & 0o4000) != 0
      5 => (mode & 0o2000) != 0
      8 => (mode & 0o1000) != 0
      _ => false
    }
    let c = if special {
      if i == 8 {
        if set {
          't'
        } else {
          'T'
        }
      } else if set {
        's'
      } else {
        'S'
      }
    } else if set {
      bit.1
    } else {
      '-'
    }
    out.write_char(c)
  }
  out.to_string()
}

///|
fn pad_left(text : String, width : Int32) -> String {
  if text.length() >= width {
    text
  } else {
    " ".repeat(width - text.length()) + text
  }
}

///|
/// Human readable rendering of a package report (see `DebPackage::to_json`).
fn render_report(report : Json) -> String raise CliError {
  guard report is Object(map) else {
    raise Failed("internal error: bad report")
  }
  let out = StringBuilder()
  let line = (label : String, value : String) => {
    if value != "" {
      out.write_string(
        "\{label}\{" ".repeat(15 - label.length().min(14))}\{value}\n",
      )
    }
  }
  line("Package:", field_text(map, "name"))
  line("Version:", field_text(map, "version"))
  line("Release:", field_text(map, "release"))
  line("Architecture:", field_text(map, "architecture"))
  line("Maintainer:", field_text(map, "maintainer"))
  line("Homepage:", field_text(map, "homepage"))
  line("License:", field_text(map, "license"))
  line("Vendor:", field_text(map, "vendor"))
  line("Section:", field_text(map, "section"))
  let size = field_text(map, "installed_size_kib")
  if size != "" {
    line("Installed size:", "\{size} KiB")
  }
  for
    relation in [
      ("Depends:", "depends"),
      ("Recommends:", "recommends"),
      ("Suggests:", "suggests"),
      ("Conflicts:", "conflicts"),
      ("Replaces:", "replaces"),
      ("Provides:", "provides"),
    ] {
    line(relation.0, field_list(map, relation.1).join(", "))
  }
  let description = field_text(map, "description")
  let lines = description.split("\n").to_array()
  for i, text in lines {
    let text = text.to_owned()
    if i == 0 {
      line("Description:", text)
    } else if text.length() == 0 {
      out.write_string("\n")
    } else {
      out.write_string("\{" ".repeat(15)}\{text}\n")
    }
  }
  let scripts = field_list(map, "scripts")
  if scripts.length() > 0 {
    line("Scripts:", scripts.join(", "))
  }
  let conffiles = field_list(map, "conffiles")
  if conffiles.length() > 0 {
    line("Config files:", conffiles.join(", "))
  }
  match map.get("compression") {
    Some(Object(compression)) =>
      if compression.contains("payload") {
        line("Compression:", "payload \{field_text(compression, "payload")}")
      } else {
        line(
          "Compression:",
          "control \{field_text(compression, "control")}, data \{field_text(compression, "data")}",
        )
      }
    _ => ()
  }
  match map.get("files") {
    Some(Array(files)) => {
      out.write_string("\nFiles (\{files.length()}):\n")
      for file in files {
        guard file is Object(entry) else { continue }
        let kind = field_text(entry, "type")
        let mode = mode_string(kind, field_text(entry, "mode"))
        let owner = "\{field_text(entry, "owner")}/\{field_text(entry, "group")}"
        let size = pad_left(field_text(entry, "size"), 9)
        let link = match field_text(entry, "link") {
          "" => ""
          target => " -> \{target}"
        }
        out.write_string(
          "  \{mode}  \{owner}  \{size}  \{field_text(entry, "path")}\{link}\n",
        )
        match entry.get("elf") {
          Some(Object(elf)) => out.write_string("    \{elf_line(elf)}\n")
          _ => ()
        }
      }
    }
    _ => ()
  }
  out.to_string()
}

///|
/// One line about an ELF file, from the `elf` object of `inspect --json`.
fn elf_line(elf : Map[String, Json]) -> String {
  match elf.get("error") {
    Some(String(message)) => "ELF, but \{message}"
    _ => {
      let order = if field_text(elf, "byte_order") == "little" {
        "LSB"
      } else {
        "MSB"
      }
      let arch = match field_text(elf, "arch") {
        "" => ""
        name => " (\{name})"
      }
      let out = StringBuilder()
      out.write_string(
        "ELF \{field_text(elf, "bits")}-bit \{order} \{field_text(elf, "machine_name")}\{arch}, \{field_text(elf, "linkage")}",
      )
      match field_text(elf, "interpreter") {
        "" => ()
        path => out.write_string(", interpreter \{path}")
      }
      let needed = field_list(elf, "needed")
      if needed.length() > 0 {
        let libraries = needed.join(", ")
        out.write_string(", needs \{libraries}")
      }
      out.to_string()
    }
  }
}

///|
fn cmd_inspect(args : Array[String]) -> Int32 raise CliError {
  let options = parse_options(args, Map([]), { "--json": "json" })
  let path = match options.positional {
    [path] => path
    [] => raise Usage("inspect needs the path of a package file")
    _ => raise Usage("inspect takes exactly one package file")
  }
  let report = load_package(path).report(path)
  if options.flag("json") {
    println(report.stringify(indent=2))
  } else {
    println(render_report(report).trim_end().to_owned())
  }
  0
}