///|
/// Scheduling controls for deterministic execution planning.
pub(all) struct SchedulePolicy {
  datum_first : Bool
  group_by_device : Bool
  max_batch_size : Int
}

///|
/// A scheduled plan with operational cost and batches.
pub(all) struct ScheduledPlan {
  part_id : String
  steps : Array[MeasurementStep]
  phases : Array[ExecutionPhase]
  batches : Array[Array[MeasurementStep]]
  phase_count : Int
  device_switches : Int
  estimated_seconds : Double
}

///|
/// Return a conservative default scheduling policy.
pub fn default_schedule_policy() -> SchedulePolicy {
  { datum_first: true, group_by_device: true, max_batch_size: 16 }
}

///|
/// Construct an explicit scheduling policy.
pub fn schedule_policy(
  datum_first~ : Bool,
  group_by_device~ : Bool,
  max_batch_size~ : Int,
) -> SchedulePolicy {
  { datum_first, group_by_device, max_batch_size }
}

///|
fn schedule_steps(
  plan : MeasurementPlan,
  policy : SchedulePolicy,
) -> Array[MeasurementStep] {
  let ordered = if policy.datum_first {
    execution_order(plan)
  } else {
    plan.steps
  }
  if policy.group_by_device {
    let grouped_plan = MeasurementPlan::{
      part_id: plan.part_id,
      steps: ordered,
      summary: plan.summary,
    }
    grouped_execution_order(grouped_plan)
  } else {
    ordered
  }
}

///|
/// Split steps into non-empty fixed-size batches.
pub fn split_steps_into_batches(
  steps : Array[MeasurementStep],
  max_batch_size : Int,
) -> Array[Array[MeasurementStep]] {
  let batches = []
  if max_batch_size <= 0 {
    batches
  } else {
    let mut start = 0
    while start < steps.length() {
      let end = if start + max_batch_size < steps.length() {
        start + max_batch_size
      } else {
        steps.length()
      }
      let batch = []
      for i in start.. ScheduledPlan {
  let steps = schedule_steps(plan, policy)
  let scheduled_plan = MeasurementPlan::{
    part_id: plan.part_id,
    steps,
    summary: plan.summary,
  }
  let phases = execution_phases(scheduled_plan)
  let batches = split_steps_into_batches(steps, policy.max_batch_size)
  {
    part_id: plan.part_id,
    steps,
    phases,
    batches,
    phase_count: phases.length(),
    device_switches: device_switch_count(steps),
    estimated_seconds: estimated_execution_seconds(scheduled_plan),
  }
}

///|
/// Return whether two executions of the same plan yield identical IDs/order.
pub fn schedule_is_deterministic(plan : MeasurementPlan) -> Bool {
  let policy = default_schedule_policy()
  let first = schedule_plan(plan, policy)
  let second = schedule_plan(plan, policy)
  if first.steps.length() != second.steps.length() {
    false
  } else {
    let mut same = true
    for i in 0.. Bool {
  if max_batch_size <= 0 {
    batches.length() == 0
  } else {
    let mut result = true
    for batch in batches {
      if batch.length() == 0 || batch.length() > max_batch_size {
        result = false
      }
    }
    result
  }
}

///|
/// Return all scheduled feature IDs.
pub fn scheduled_feature_ids(schedule : ScheduledPlan) -> Array[String] {
  let result = []
  for step in schedule.steps {
    result.push(step.feature_id)
  }
  result
}

///|
/// Return whether the schedule contains a datum phase.
pub fn schedule_establishes_datum(schedule : ScheduledPlan) -> Bool {
  let mut result = false
  for phase in schedule.phases {
    if phase.phase == PlanPhase::EstablishDatum {
      result = true
    }
  }
  result
}

///|
/// Estimate total samples across schedule batches.
pub fn scheduled_sample_count(schedule : ScheduledPlan) -> Int {
  let mut result = 0
  for step in schedule.steps {
    match step.strategy {
      SinglePoint => result += 1
      RepeatedPoint(count~) => result += count
      Scan(count~) => result += count
    }
  }
  result
}

///|
/// Return the largest batch size in a schedule.
pub fn largest_batch_size(schedule : ScheduledPlan) -> Int {
  let mut result = 0
  for batch in schedule.batches {
    if batch.length() > result {
      result = batch.length()
    }
  }
  result
}