///|
/// List a device's network interfaces.
///
///   moon run examples/walk_table
///   moon run examples/walk_table -- 192.0.2.10 public 161
///
/// Walks the MIB-II interface table and prints one row per interface.
///
/// The interesting part is the shape of a table on the wire. `ifTable`
/// is indexed by `ifIndex`, and each cell has the OID
/// `ifEntry..`. A walk returns cells in lexicographic
/// order, which is column-major: every interface's name, then every
/// interface's type, and so on. So printing a row per interface means
/// regrouping the cells by their trailing arc, in two passes — collect,
/// then assemble. That shape is the same for every MIB table, so it is
/// worth reading once.

///|
/// One cell of the table, as the walk returned it.
struct Cell {
  index : UInt
  column : UInt
  value : @ber.BerValue
}

///|
/// One row, assembled from the cells that share an index.
struct Interface {
  index : UInt
  descr : String
  kind : String
  mtu : String
  speed : String
  status : String
  address : String
}

///|
fn main {
  let cli = @common.Cli::read()
  let target = cli.target()
  println(
    "listing the interfaces of \{target.host}:\{target.port} (community \"\{@utf8.decode_lossy(target.community)}\")\n",
  )
  let outcome = Some(list(target)) catch {
    @session.SnmpError::NoResponse => {
      println(
        "no reply. Check the address, the community, and that the agent is running.",
      )
      None
    }
    e => {
      println("could not list the interfaces: \{Repr(e)}")
      None
    }
  }
  // The socket is closed here rather than inside `list`, so a reply that
  // is still in flight when the work is done is not thrown away.
  match outcome {
    Some(socket) => socket.close()
    None => @common.fail(1)
  }
}

///|
/// Walk the table and print it.
fn list(target : @common.Target) -> @transport.Transport raise {
  let socket = @transport.Transport::open(0, 3000)
  let session = @session.Session::new(
    socket,
    target.host,
    target.port,
    @pdu.V2c,
    target.community,
    2000,
    1,
    1,
  )

  // How many interfaces the device says it has. Worth asking separately:
  // it tells "no interfaces" apart from "the walk went wrong", and it is
  // one cheap GET.
  let reported = session.get(@mib.if_number())
  match reported.value {
    @ber.BerValue::Integer(n) => println("ifNumber = \{n}\n")
    other => println("ifNumber = \{@mib.format_value(other)}\n")
  }

  // `walk_bulk` is the v2c way to read a table. GETBULK returns a run of
  // successors per round trip, so a table costs a handful of requests
  // rather than one per cell. 25 is the batch size; 500 is a ceiling on
  // the whole walk, so a device with an enormous table cannot make this
  // run forever.
  let entries = session.walk_bulk(@mib.if_entry(), 25, 500)

  let cells : Array[Cell] = []
  for entry in entries {
    // `ifEntry..`: the last arc is the row, the one
    // before it the column.
    let parts = entry.name.parts()
    if parts.length() < 2 {
      continue
    }
    cells.push({
      index: parts[parts.length() - 1],
      column: parts[parts.length() - 2],
      value: entry.value,
    })
  }

  // The distinct indexes, in the order the walk first mentioned them.
  // Small enough to scan rather than to keep a set of.
  let indexes : Array[UInt] = []
  for cell in cells {
    let mut seen = false
    for known in indexes {
      if known == cell.index {
        seen = true
      }
    }
    if !seen {
      indexes.push(cell.index)
    }
  }

  let rows : Array[Interface] = []
  for index in indexes {
    rows.push({
      index,
      descr: text_at(cells, index, 2),
      kind: type_at(cells, index, 3),
      mtu: raw_at(cells, index, 4),
      speed: speed_at(cells, index, 5),
      status: status_at(cells, index, 8),
      address: address_at(cells, index, 6),
    })
  }

  print_table(rows)
  socket
}

///|
/// The cell at `.`, if the walk brought it back.
fn cell_at(cells : Array[Cell], index : UInt, column : UInt) -> @ber.BerValue? {
  for cell in cells {
    if cell.index == index && cell.column == column {
      return Some(cell.value)
    }
  }
  None
}

///|
/// A column rendered the way the mib package renders its type, or "?" if
/// the device did not send it.
fn raw_at(cells : Array[Cell], index : UInt, column : UInt) -> String {
  match cell_at(cells, index, column) {
    Some(value) => @mib.format_value(value)
    None => "?"
  }
}

///|
/// A column whose value is a string, without the quotes a report does
/// not need.
fn text_at(cells : Array[Cell], index : UInt, column : UInt) -> String {
  match cell_at(cells, index, column) {
    Some(@ber.BerValue::OctetString(b)) => @utf8.decode_lossy(b)
    Some(other) => @mib.format_value(other)
    None => "?"
  }
}

///|
/// ifType is an IANAifType integer, so the bare number tells a reader
/// nothing; the mib formatter names the common ones.
fn type_at(cells : Array[Cell], index : UInt, column : UInt) -> String {
  match cell_at(cells, index, column) {
    Some(@ber.BerValue::Integer(n)) => @mib.format_if_type(n)
    Some(other) => @mib.format_value(other)
    None => "?"
  }
}

///|
/// ifOperStatus is 1 up, 2 down, and so on.
fn status_at(cells : Array[Cell], index : UInt, column : UInt) -> String {
  match cell_at(cells, index, column) {
    Some(@ber.BerValue::Integer(n)) => @mib.format_if_oper_status(n)
    Some(other) => @mib.format_value(other)
    None => "?"
  }
}

///|
/// ifSpeed is a Gauge32 counted in bits per second, which is unreadable
/// as a bare number; a link is described in Mb/s or Gb/s.
fn speed_at(cells : Array[Cell], index : UInt, column : UInt) -> String {
  match cell_at(cells, index, column) {
    Some(@ber.BerValue::Gauge32(bits)) =>
      if bits == 0 {
        // A device is allowed to report zero when it cannot tell.
        "unknown"
      } else if bits == 0xFFFFFFFF {
        // RFC 3635 §3.1.4: the maximum means the bandwidth is either
        // greater than 4.29 Gb/s or not known. Printing "4 Gb/s" would
        // state a number the device did not give.
        ">4 Gb/s"
      } else if bits >= 1000000000 {
        scaled(bits, 1000000000, "Gb/s")
      } else if bits >= 1000000 {
        scaled(bits, 1000000, "Mb/s")
      } else if bits >= 1000 {
        scaled(bits, 1000, "kb/s")
      } else {
        "\{bits} b/s"
      }
    Some(other) => @mib.format_value(other)
    None => "?"
  }
}

///|
/// A bit rate in the given unit, with two decimals when there is a
/// remainder, so that 10750000 is "10.75 Mb/s" rather than a rounded 10.
fn scaled(bits : UInt, unit : UInt, label : String) -> String {
  let whole = bits / unit
  let remainder = bits % unit
  if remainder == 0 {
    "\{whole} \{label}"
  } else {
    // Dividing the unit first keeps the intermediate value inside 32
    // bits: the remainder times 100 would not fit.
    let hundredths = remainder / (unit / 100)
    "\{whole}.\{@common.pad_left(hundredths.to_string(), 2)} \{label}"
  }
}

///|
/// ifPhysAddress is a byte string, usually six octets of MAC address.
fn address_at(cells : Array[Cell], index : UInt, column : UInt) -> String {
  match cell_at(cells, index, column) {
    Some(@ber.BerValue::OctetString(b)) => @mib.format_mac_address(b)
    Some(other) => @mib.format_value(other)
    None => ""
  }
}

///|
/// The columns this program prints, in the order it prints them.
fn column_of(row : Interface) -> Array[String] {
  [
    row.index.to_string(),
    row.descr,
    row.kind,
    row.mtu,
    row.speed,
    row.status,
    row.address,
  ]
}

///|
/// Print the rows as a table, with the widest cell in each column
/// setting the width so nothing wraps.
fn print_table(rows : Array[Interface]) -> Unit {
  let headers : Array[String] = [
    "index", "name", "type", "mtu", "speed", "status", "address",
  ]
  // The first pass measures and the second writes: a value wider than
  // its header has to be known before the header is printed.
  let widths : Array[Int] = []
  for header in headers {
    widths.push(header.length())
  }
  for row in rows {
    let cells = column_of(row)
    for i in 0.. widths[i] {
        widths[i] = cells[i].length()
      }
    }
  }

  println(join_row(headers, widths))
  for row in rows {
    println(join_row(column_of(row), widths))
  }
  println("\n\{rows.length()} interface(s)")
}

///|
/// One line of the table, each cell padded to its column width.
fn join_row(cells : Array[String], widths : Array[Int]) -> String {
  let mut line = ""
  for i in 0..