///|
pub fn build_schedule(matrix : ScenarioMatrix) -> Array[ScheduleBatch] {
  let batches : Array[ScheduleBatch] = []
  let targets = all_scenario_targets()
  let mut target_index = 0
  let mut batch_index = 0
  while target_index < targets.length() {
    let target = targets[target_index]
    let steps : Array[ScheduleStep] = []
    let mut step_index = 0
    let mut spec_index = 0
    while spec_index < matrix.specs.length() {
      let spec = matrix.specs[spec_index]
      if spec.has_target(target) {
        steps.push(ScheduleStep::{
          index: step_index,
          case_id: scenario_case_key(spec.id, target),
          target,
          lane: schedule_lane(target),
          command: spec.command,
          estimated_ms: schedule_estimate(matrix, spec),
        })
        step_index += 1
      }
      spec_index += 1
    }
    if steps.length() > 0 {
      batches.push(ScheduleBatch::{
        index: batch_index,
        target,
        lane: schedule_lane(target),
        steps,
        parallel_safe: schedule_lane(target) == Restricted,
      })
      batch_index += 1
    }
    target_index += 1
  }
  batches
}

///|
pub fn schedule_lane(target : ScenarioTarget) -> ScheduleLane {
  match target {
    Native => HostBound
    Js => HostBound
    Wasm => Restricted
    WasmGc => Restricted
  }
}

///|
pub fn schedule_total_estimate(batches : Array[ScheduleBatch]) -> Int {
  let mut total = 0
  let mut batch_index = 0
  while batch_index < batches.length() {
    let batch = batches[batch_index]
    let mut batch_total = 0
    let mut step_index = 0
    while step_index < batch.steps.length() {
      batch_total += batch.steps[step_index].estimated_ms
      step_index += 1
    }
    if batch.parallel_safe && batch.steps.length() > 0 {
      let mut largest = 0
      step_index = 0
      while step_index < batch.steps.length() {
        if batch.steps[step_index].estimated_ms > largest {
          largest = batch.steps[step_index].estimated_ms
        }
        step_index += 1
      }
      total += largest
    } else {
      total += batch_total
    }
    batch_index += 1
  }
  total
}

///|
pub fn schedule_is_deterministic(batches : Array[ScheduleBatch]) -> Bool {
  let mut expected_batch = 0
  let mut batch_index = 0
  while batch_index < batches.length() {
    let batch = batches[batch_index]
    if batch.index != expected_batch || batch.steps.length() == 0 {
      return false
    }
    let mut expected_step = 0
    let mut step_index = 0
    while step_index < batch.steps.length() {
      if batch.steps[step_index].index != expected_step ||
        batch.steps[step_index].target != batch.target {
        return false
      }
      expected_step += 1
      step_index += 1
    }
    expected_batch += 1
    batch_index += 1
  }
  true
}

///|
pub fn schedule_markdown(batches : Array[ScheduleBatch]) -> String {
  let mut out = "# Scenario Execution Schedule\n\n"
  out = out + "- Batches: " + batches.length().to_string() + "\n"
  out = out +
    "- Estimated elapsed time: " +
    schedule_total_estimate(batches).to_string()
  out = out + " ms\n\n"
  let mut batch_index = 0
  while batch_index < batches.length() {
    let batch = batches[batch_index]
    out = out + "## Batch " + batch.index.to_string() + " - `"
    out = out + batch.target.name() + "`\n\n"
    out = out + "- Lane: " + batch.lane.name() + "\n"
    out = out + "- Parallel safe: " + bool_text(batch.parallel_safe) + "\n\n"
    out = out +
      "| Step | Case | Estimate | Command |\n| ---: | --- | ---: | --- |\n"
    let mut step_index = 0
    while step_index < batch.steps.length() {
      let step = batch.steps[step_index]
      out = out + "| " + step.index.to_string() + " | `" + step.case_id + "` | "
      out = out +
        step.estimated_ms.to_string() +
        " ms | `" +
        step.command +
        "` |\n"
      step_index += 1
    }
    out = out + "\n"
    batch_index += 1
  }
  out
}

///|
pub fn schedule_json(batches : Array[ScheduleBatch]) -> String {
  let mut out = "{\"deterministic\":" +
    bool_text(schedule_is_deterministic(batches)) +
    ",\"estimate_ms\":" +
    schedule_total_estimate(batches).to_string() +
    ",\"batches\":["
  let mut batch_index = 0
  while batch_index < batches.length() {
    if batch_index > 0 {
      out = out + ","
    }
    let batch = batches[batch_index]
    out = out + "{\"index\":" + batch.index.to_string() + ","
    out = out + "\"target\":\"" + batch.target.name() + "\","
    out = out + "\"lane\":\"" + batch.lane.name() + "\","
    out = out +
      "\"parallel_safe\":" +
      bool_text(batch.parallel_safe) +
      ",\"steps\":["
    let mut step_index = 0
    while step_index < batch.steps.length() {
      if step_index > 0 {
        out = out + ","
      }
      let step = batch.steps[step_index]
      out = out + "{\"index\":" + step.index.to_string() + ","
      out = out + "\"case\":\"" + json_escape(step.case_id) + "\","
      out = out + "\"estimate_ms\":" + step.estimated_ms.to_string() + ","
      out = out + "\"command\":\"" + json_escape(step.command) + "\"}"
      step_index += 1
    }
    out = out + "]}"
    batch_index += 1
  }
  out + "]}"
}

///|
fn schedule_estimate(matrix : ScenarioMatrix, spec : ScenarioSpec) -> Int {
  match observation_for(matrix.observations, spec.id, spec.targets[0]) {
    None => if spec.timeout_ms > 60000 { 60000 } else { spec.timeout_ms }
    Some(observation) => observation.duration_ms
  }
}