///|
fn die(message : String) -> Unit {
  let j : Json = { "error": message }
  println(j.stringify())
  @sys.exit(2)
}

///|
fn[T] must(value : Result[T, String]) -> T {
  match value {
    Ok(v) => v
    Err(e) => {
      die(e)
      abort(e)
    }
  }
}

///|
fn read(path : String) -> String {
  @fs.read_file_to_string(path) catch {
    _ => {
      die("cannot read " + path)
      abort("read failed")
    }
  }
}

///|
fn nonnegative(value : String) -> Int32 {
  let mut n = 0
  if value == "" {
    die("empty integer option")
    return 0
  }
  for c in value {
    if c < '0' || c > '9' || n > 10000000 {
      die("invalid integer option: " + value)
      return 0
    }
    n = n * 10 + c.to_int() - '0'.to_int()
  }
  n
}

///|
fn main {
  let raw = @env.args()
  let args : Array[String] = []
  let offset = if raw.length() > 1 && raw[1].has_suffix(".js") { 1 } else { 0 }
  for i in offset.. ["--budget", "--limit"]
      "closest" => ["--budget", "--baseline"]
      _ => ["--assignment"]
    }
    if !allowed.contains(key) || flags.contains(key) || i + 1 >= args.length() {
      die("unknown, duplicate or incomplete option: " + key)
      return
    }
    flags[key] = args[i + 1]
    i += 2
  }
  let budget = match flags.get("--budget") {
    None => 100000
    Some(s) => nonnegative(s)
  }
  let limit = match flags.get("--limit") {
    None => 1000
    Some(s) => nonnegative(s)
  }
  if budget > 100000000 || limit < 1 || limit > 1000000 {
    die("budget/limit out of range")
    return
  }
  let before = must(@model.parse_document(read(args[2]))).model
  let after = must(@model.parse_document(read(args[3]))).model
  match op {
    "diff" => {
      let r = must(
        @delta.compare(
          before,
          after,
          config=@model.options(budget~, max_solutions=limit),
        ),
      )
      println(@delta.comparison_to_json(r).stringify(indent=2))
      match r.relation {
        Changed => @sys.exit(1)
        Unknown => @sys.exit(3)
        Equivalent => ()
      }
    }
    "closest" => {
      let path = match flags.get("--baseline") {
        Some(p) => p
        None => {
          die("--baseline is required")
          return
        }
      }
      let baseline = must(@model.parse_assignment(read(path)))
      let r = must(@delta.closest_change(before, after, baseline, budget~))
      println(@delta.closest_to_json(r).stringify(indent=2))
      if r.change is Some(_) {
        @sys.exit(1)
      }
      if r.stop != @model.Exhausted {
        @sys.exit(3)
      }
    }
    "replay" => {
      let path = match flags.get("--assignment") {
        Some(p) => p
        None => {
          die("--assignment is required")
          return
        }
      }
      let assignment = must(@model.parse_assignment(read(path)))
      match @delta.replay(before, after, assignment) {
        Ok(c) => println(@delta.change_to_json(c).stringify(indent=2))
        Err(e) => {
          let j : Json = { "witness": false, "reason": e }
          println(j.stringify())
          @sys.exit(1)
        }
      }
    }
    _ => ()
  }
}