///|
/// The command line front end.
///
/// The interface is deliberately boring: one subcommand per experiment, text in,
/// text out, and every number printed comes from the same library the tests
/// exercise. `run` and `compare` grow out of this skeleton as the TCP layer
/// lands; today `demo` already walks the whole stack, from a scenario to a link
/// report.
fn main {
  let args = @env.args()
  let command = if args.length() > 1 { args[1] } else { "help" }
  match command {
    "version" => println(version_line())
    "run" => run_scenario(args)
    "plot" => plot_trace(args)
    "compare" => compare_scenarios(args)
    "sweep" => sweep_scenarios(args)
    "list" => list_scenarios()
    "help" | "-h" | "--help" => usage()
    _ => {
      println("unknown command: " + command)
      println("")
      usage()
    }
  }
}

///|
fn version_line() -> String {
  "moonnet-lab 0.1.0 (deterministic packet-level network simulator)"
}

///|
fn usage() -> Unit {
  println(version_line())
  println("")
  println("usage: moonnet ")
  println("")
  println("commands:")
  println(
    "  run FILE  run a scenario file; --trace adds TCP state and event history",
  )
  println("  plot FILE render a single-flow trace JSON document as SVG")
  println(
    "  compare FILE --cc reno,cubic   run one scenario under several algorithms",
  )
  println(
    "  sweep FILE --field loss|delay_ms|bandwidth_mbps|queue_packets|seed --from A --to B --steps N --cc reno,cubic",
  )
  println("  list      list the scenario files in ./scenarios")
  println("  version   print the version")
  println("  help      print this message")
}

///|
/// Run one scenario under several algorithms.
fn compare_scenarios(args : Array[String]) -> Unit {
  let options = match
    parse_common(args, "compare  --cc reno,cubic") {
    Ok(value) => value
    Err(_) => return
  }
  let scenario = match options.scenario {
    Some(value) => value
    None => return
  }
  let results = @lab.compare(scenario, options.algorithms, seeds=options.seeds)
  if options.as_json {
    println(@lab.compare_json(scenario, results, seeds=options.seeds))
  } else {
    println(@lab.compare_lines(scenario, results, seeds=options.seeds))
  }
}

///|
/// Run a scenario once per value of one parameter.
///
///     moonnet sweep scenarios/long-fat.json --field loss --from 0 --to 0.02 --steps 5 --cc reno,cubic
fn sweep_scenarios(args : Array[String]) -> Unit {
  let options = match
    parse_common(
      args, "sweep  --field loss --from 0 --to 0.02 --steps 5 --cc reno,cubic",
    ) {
    Ok(value) => value
    Err(_) => return
  }
  let scenario = match options.scenario {
    Some(value) => value
    None => return
  }
  let field = match @lab.SweepField::parse(options.field) {
    Ok(value) => value
    Err(message) => {
      println(message)
      return
    }
  }
  match field.validate_range(options.from, options.to, options.steps) {
    Ok(_) => ()
    Err(message) => {
      println(message)
      return
    }
  }
  let rows = @lab.sweep(
    scenario,
    field,
    options.from,
    options.to,
    options.steps,
    options.algorithms,
    seeds=options.seeds,
  )
  if options.as_json {
    println(
      @lab.sweep_json(
        scenario,
        field,
        rows,
        options.algorithms,
        seeds=options.seeds,
      ),
    )
  } else {
    println(@lab.sweep_lines(scenario, field, rows, options.algorithms))
  }
}

///|
/// The options `compare` and `sweep` share.
struct CommonOptions {
  scenario : @lab.Scenario?
  algorithms : Array[String]
  field : String
  from : Double
  to : Double
  steps : Int
  seeds : Int
  as_json : Bool
}

///|
/// Read the scenario file and the options both commands accept.
fn parse_common(
  args : Array[String],
  usage : String,
) -> Result[CommonOptions, String] {
  if args.length() < 3 {
    println("usage: moonnet " + usage)
    return Err("missing arguments")
  }
  let scenario = match load_scenario(args[2]) {
    Ok(value) => value
    Err(message) => {
      println(message)
      return Err(message)
    }
  }
  let mut algorithms : Array[String] = ["reno"]
  let mut field = "loss"
  let mut from = 0.0
  let mut to = 0.01
  let mut steps = 5
  let mut seeds = 1
  let mut as_json = false
  let mut index = 3
  while index < args.length() {
    let option = args[index]
    let has_value = index + 1 < args.length()
    if option == "--cc" && has_value {
      match @lab.algorithms_of(args[index + 1]) {
        Ok(value) => algorithms = value
        Err(message) => {
          println(message)
          return Err(message)
        }
      }
      index = index + 2
    } else if option == "--field" && has_value {
      field = args[index + 1]
      index = index + 2
    } else if option == "--from" && has_value {
      from = match parse_number(args[index + 1]) {
        Ok(value) => value
        Err(message) => {
          println(message)
          return Err(message)
        }
      }
      index = index + 2
    } else if option == "--to" && has_value {
      to = match parse_number(args[index + 1]) {
        Ok(value) => value
        Err(message) => {
          println(message)
          return Err(message)
        }
      }
      index = index + 2
    } else if option == "--steps" && has_value {
      steps = match parse_integer(args[index + 1], "steps") {
        Ok(value) => value
        Err(message) => {
          println(message)
          return Err(message)
        }
      }
      index = index + 2
    } else if option == "--json" {
      as_json = true
      index = index + 1
    } else if option == "--seeds" && has_value {
      seeds = match parse_integer(args[index + 1], "seeds") {
        Ok(value) => value
        Err(message) => {
          println(message)
          return Err(message)
        }
      }
      index = index + 2
    } else {
      println("unknown option: " + option)
      return Err(option)
    }
  }
  Ok({
    scenario: Some(scenario),
    algorithms,
    field,
    from,
    to,
    steps,
    seeds,
    as_json,
  })
}

///|
/// Read and check a scenario file, reporting what was wrong with it.
fn load_scenario(path : String) -> Result[@lab.Scenario, String] {
  let text = @fs.read_file_to_string(path) catch {
    _ => return Err("cannot read " + path)
  }
  let document = match @json.parse(text) {
    Ok(value) => value
    Err(message) => return Err(path + " is not valid JSON, " + message)
  }
  match @lab.Scenario::from_json(document) {
    Ok(value) => Ok(value)
    Err(message) => Err(path + " is not a usable scenario: " + message)
  }
}

///|
/// Render an archived trace JSON document as a standalone SVG on stdout.
///
///     moonnet plot trace.json > trace.svg
fn plot_trace(args : Array[String]) -> Unit {
  if args.length() != 3 {
    println("usage: moonnet plot  > trace.svg")
    return
  }
  let path = args[2]
  let source = match read_plot_input(path) {
    Ok(value) => value
    Err(message) => {
      println(message)
      return
    }
  }
  match @lab.trace_svg_from_json(source) {
    Ok(svg) => println(svg)
    Err(message) => println("cannot render trace: " + message)
  }
}

///|
/// Read UTF-8 files and PowerShell's common UTF-16 redirected output.
fn read_plot_input(path : String) -> Result[String, String] {
  let bytes = @fs.read_file_to_bytes(path) catch {
    _ => return Err("cannot read " + path)
  }
  match decode_plot_bytes(bytes) {
    Ok(value) => Ok(value)
    Err(message) => Err("cannot decode " + path + ": " + message)
  }
}

///|
/// Decode UTF-8 and BOM-marked UTF-16 text used by redirected CLI output.
fn decode_plot_bytes(bytes : Bytes) -> Result[String, String] {
  let first = match bytes.get(0) {
    Some(value) => value.to_int()
    None => 0
  }
  let second = match bytes.get(1) {
    Some(value) => value.to_int()
    None => 0
  }
  if first == 255 && second == 254 {
    let text = @utf16.decode(
      bytes,
      ignore_bom=true,
      endianness=@utf16.Endian::Little,
    ) catch {
      _ => return Err("invalid UTF-16LE")
    }
    Ok(text)
  } else if first == 254 && second == 255 {
    let text = @utf16.decode(
      bytes,
      ignore_bom=true,
      endianness=@utf16.Endian::Big,
    ) catch {
      _ => return Err("invalid UTF-16BE")
    }
    Ok(text)
  } else {
    Ok(@utf8.decode_lossy(bytes, ignore_bom=true))
  }
}

///|
fn parse_number(text : String) -> Result[Double, String] {
  let value = @string.parse_double(text) catch {
    _ => return Err("\"" + text + "\" is not a number")
  }
  Ok(value)
}

///|
fn parse_integer(text : String, option : String) -> Result[Int, String] {
  let value = match parse_number(text) {
    Ok(value) => value
    Err(message) => return Err(message)
  }
  let integer = value.to_int()
  if value != integer.to_double() {
    Err("--" + option + " must be a whole number")
  } else {
    Ok(integer)
  }
}

///|
/// Run an experiment described by a scenario file.
///
/// This is the command the whole project has been building towards: the numbers
/// come from the same library the tests exercise, and the experiment itself is a
/// document anyone can edit and re-run.
///
///     moonnet run scenarios/long-fat.json --cc cubic
///     moonnet run scenarios/long-fat.json --json > report.json
fn run_scenario(args : Array[String]) -> Unit {
  if args.length() < 3 {
    println(
      "usage: moonnet run  [--cc none|reno|cubic] [--discipline drop-tail|red|codel] [--json] [--trace]",
    )
    return
  }
  let path = args[2]
  let mut algorithm = ""
  let mut discipline = ""
  let mut as_json = false
  let mut trace = false
  let mut index = 3
  while index < args.length() {
    if args[index] == "--cc" && index + 1 < args.length() {
      algorithm = args[index + 1]
      index = index + 2
    } else if args[index] == "--discipline" && index + 1 < args.length() {
      discipline = args[index + 1]
      index = index + 2
    } else if args[index] == "--json" {
      as_json = true
      index = index + 1
    } else if args[index] == "--trace" {
      trace = true
      index = index + 1
    } else {
      println("unknown option: " + args[index])
      return
    }
  }
  let text = @fs.read_file_to_string(path) catch {
    _ => {
      println("cannot read " + path)
      return
    }
  }
  let document = match @json.parse(text) {
    Ok(value) => value
    Err(message) => {
      println(path + " is not valid JSON, " + message)
      return
    }
  }
  let scenario = match @lab.Scenario::from_json(document) {
    Ok(value) => value
    Err(message) => {
      println(path + " is not a usable scenario: " + message)
      return
    }
  }
  if algorithm != "" {
    match @lab.known_algorithm(algorithm) {
      Ok(_) => ()
      Err(message) => {
        println(message)
        return
      }
    }
  }
  if discipline != "" {
    match @lab.known_discipline(discipline) {
      Ok(_) => ()
      Err(message) => {
        println(message)
        return
      }
    }
  }
  // The override rewrites the uplink, which is the link that carries the data in
  // every scenario in this repository. Rewriting both ends would silently change
  // the return path as well, and a run is supposed to vary one thing.
  let with_cc = if algorithm == "" {
    scenario
  } else {
    { ..scenario, algorithm, }
  }
  let experiment = if discipline == "" {
    with_cc
  } else {
    { ..with_cc, uplink: { ..with_cc.uplink, discipline, }, }
  }
  // A scenario that names several flows is a fairness experiment: the flows
  // share one path, so the interesting output is per-flow rather than per-run.
  if experiment.flow_algorithms().length() > 1 {
    if trace {
      println("--trace currently supports single-flow scenarios")
      return
    }
    let fairness = @lab.run_fairness(experiment)
    if as_json {
      println(fairness.to_json_text())
    } else {
      println(fairness.to_lines())
    }
  } else if trace {
    let traced = @lab.run_with_trace(experiment)
    if as_json {
      println(traced.to_json_text())
    } else {
      println(traced.to_lines())
    }
  } else {
    let report = @lab.run(experiment)
    if as_json {
      println(report.to_json_text())
    } else {
      println(report.to_lines())
    }
  }
}

///|
/// List the scenario files in the directory the tool expects them in.
fn list_scenarios() -> Unit {
  let names = @fs.read_dir("scenarios") catch {
    _ => {
      println("no scenarios directory here; run this from the project root")
      return
    }
  }
  println("scenarios:")
  for name in names {
    if name.has_suffix(".json") {
      println("  " + name)
    }
  }
}

///|
/// The congestion control comparison: the same lossy path, the same seed, one
/// difference — whether the sender is allowed to ignore congestion.
///
/// The interesting column is not the transfer time. Uncontrolled, the sender
/// pushes into a queue that has already dropped a packet, and the retransmission
/// races the packets behind it. Controlled, it halves its window on the first
/// evidence of loss, which costs throughput here and is the entire reason the
/// network does not collapse when everyone does it at once.