///|
fn package_text(shape : String, size : Int, index : Int) -> String {
  let text = StringBuilder()
  text.write_string(
    "Name: p\{index}\nDescription: generated benchmark\nVersion: 1\n",
  )
  if index == 0 && (shape == "wide" || shape == "shared" || shape == "virtual") {
    text.write_string("Requires: ")
    let end = if shape == "shared" { size - 1 } else { size }
    for i = 1; i < end; i = i + 1 {
      if i > 1 {
        text.write_string(", ")
      }
      text.write_string(if shape == "virtual" { "v\{i} >= 1" } else { "p\{i}" })
    }
    text.write_string("\n")
  } else if (shape == "chain" || shape == "broken") && index + 1 < size {
    text.write_string("Requires: p\{index + 1}\n")
  } else if shape == "broken" && index + 1 == size {
    text.write_string("Requires: missing\n")
  } else if shape == "shared" && index > 0 && index + 1 < size {
    text.write_string("Requires: p\{size - 1}\n")
  }
  if shape == "virtual" && index > 0 {
    text.write_string("Provides: v\{index} = 1\n")
  }
  text.write_string("Cflags: -I/fixture/p\{index}\nLibs: -lp\{index}\n")
  text.to_string()
}

///|
fn main {
  let args = @env.args()
  if args.length() != 4 {
    println(
      "Usage: benchmark   ",
    )
    println(
      "       benchmark expansion  ",
    )
    @sys.exit(2)
  }
  let shape = args[1]
  let size = @string.parse_int(args[2]) catch { _ => 0 }
  let operation = args[3]
  if shape == "expansion" &&
    size >= 4 &&
    size <= 2097152 &&
    (operation == "expand-variable" || operation == "expand-literal") {
    let chunk = "x".repeat(size)
    let variables : Map[String, String] = { "chunk": chunk }
    let value = if operation == "expand-literal" { chunk } else { "${chunk}" }
    let start = @bench.monotonic_clock_start()
    let result = @pc.expand(value, variables, location={
      source: "boundary.pc",
      line: 1,
      column: 1,
    })
    let elapsed_us = @bench.monotonic_clock_end(start)
    let checksum = match result {
      Ok(value) => {
        if size > 1048576 || value.length() != size {
          abort("Incorrect expansion boundary")
        }
        size
      }
      Err(problem) => {
        if size <= 1048576 || problem.code != "PC009" {
          abort("Incorrect expansion rejection")
        }
        1
      }
    }
    println(
      Json::object({
        "shape": Json(shape),
        "size": Json(size),
        "operation": Json(operation),
        "load_us": Json(0),
        "elapsed_us": Json(elapsed_us),
        "checksum": Json(checksum),
      }).stringify(),
    )
    return
  }
  if size < 4 ||
    size > 100000 ||
    !(shape == "chain" ||
    shape == "broken" ||
    shape == "wide" ||
    shape == "shared" ||
    shape == "virtual") {
    println("Invalid benchmark shape or size (4..100000)")
    @sys.exit(2)
  }
  let start = @bench.monotonic_clock_start()
  let packages = @pc.PackageSet::new()
  for i = 0; i < size; i = i + 1 {
    let id = "p\{i}"
    let document = @pc.parse(package_text(shape, size, i), source=id + ".pc")
    if !packages.add(id, document).is_empty() {
      abort("Invalid generated package")
    }
  }
  let load_us = @bench.monotonic_clock_end(start)
  let start = @bench.monotonic_clock_start()
  let checksum : Int64 = match operation {
    "load" => size.to_int64()
    "resolve" => {
      let result = packages.resolve("p0")
      if !expected_diagnostics(result.diagnostics, shape) ||
        result.order.length() != size ||
        result.order[size - 1] != "p0" {
        abort("Incorrect resolution")
      }
      result.order.length().to_int64()
    }
    "cflags" => {
      let result = packages.collect_cflags("p0")
      if !expected_diagnostics(result.diagnostics, shape) ||
        result.flags.length() != size ||
        result.flags[0].package_id != "p0" {
        abort("Incorrect compiler flags")
      }
      result.flags.length().to_int64()
    }
    "path-last" => {
      let result = packages.dependency_path("p0", "p\{size - 1}")
      let expected = if shape == "chain" || shape == "broken" {
        size
      } else if shape == "shared" {
        3
      } else {
        2
      }
      if result.length() != expected ||
        result[0] != "p0" ||
        result[result.length() - 1] != "p\{size - 1}" {
        abort("Incorrect last dependency path")
      }
      result.length().to_int64()
    }
    "paths" => {
      let paths = packages.dependency_paths("p0")
      let mut length = 0L
      for i = 0; i < size; i = i + 1 {
        let path = paths.path_to("p\{i}")
        length = length + path.length().to_int64()
      }
      let expected = if shape == "chain" || shape == "broken" {
        size.to_int64() * (size.to_int64() + 1L) / 2L
      } else if shape == "shared" {
        size.to_int64() * 2L
      } else {
        size.to_int64() * 2L - 1L
      }
      if length != expected {
        abort("Incorrect dependency path lengths")
      }
      length
    }
    "check" => {
      if !expected_diagnostics(packages.check_all(), shape) {
        abort("Incorrect graph diagnostics")
      }
      size.to_int64()
    }
    _ => {
      println("Invalid benchmark operation")
      @sys.exit(2)
      0L
    }
  }
  let elapsed_us = if operation == "load" {
    load_us
  } else {
    @bench.monotonic_clock_end(start)
  }
  println(
    Json::object({
      "shape": Json(shape),
      "size": Json(size),
      "operation": Json(operation),
      "load_us": Json(load_us),
      "elapsed_us": Json(elapsed_us),
      "checksum": Json(checksum),
    }).stringify(),
  )
}

///|
fn expected_diagnostics(
  diagnostics : Array[@pc.Diagnostic],
  shape : String,
) -> Bool {
  if shape == "broken" {
    diagnostics.length() == 1 &&
    diagnostics[0].code == "PC015" &&
    diagnostics[0].message == "Required package not found: missing"
  } else {
    diagnostics.is_empty()
  }
}