///|
fn available_days(days : Int, absent_day : Int, shift_kind : Int) -> Array[Int] {
  let slots : Array[Int] = []
  for day = 0; day < days; day = day + 1 {
    if day != absent_day {
      slots.push(day * 2 + shift_kind)
    }
  }
  slots
}

///|
fn benchmark_request(days : Int) -> @moonplan.CoverageRequest {
  let support_workers = (days * 2 + 3) / 4 + 1
  let front_workers = (days + 3) / 4 + 1
  let workers : Array[@moonplan.Worker] = []
  for index = 0; index < support_workers; index = index + 1 {
    workers.push({
      id: index + 1,
      name: "Support \{index + 1}",
      skills: ["support"],
      available_slots: available_days(days, index % days, 0),
    })
  }
  for index = 0; index < front_workers; index = index + 1 {
    workers.push({
      id: support_workers + index + 1,
      name: "Front \{index + 1}",
      skills: ["front"],
      available_slots: available_days(days, index % days, 1),
    })
  }
  let requirements : Array[@moonplan.CoverageRequirement] = []
  for day = 0; day < days; day = day + 1 {
    requirements.push({
      id: day * 2 + 1,
      name: "Support day \{day + 1}",
      slot: day * 2,
      required_skill: "support",
      workers_needed: 2,
    })
    requirements.push({
      id: day * 2 + 2,
      name: "Front day \{day + 1}",
      slot: day * 2 + 1,
      required_skill: "front",
      workers_needed: 1,
    })
  }
  {
    workers,
    requirements,
    policy: { max_shifts_per_worker: Some(4), minimum_slot_gap: 0, },
  }
}

///|
fn outcome_stats(
  outcome : @moonplan.CoverageSolveOutcome,
) -> (String, Int, Int) {
  match outcome {
    @moonplan.CoverageFound(roster) =>
      ("found", roster.stats.nodes, roster.stats.backtracks)
    @moonplan.CoverageProvedUnsatisfiable(stats) =>
      ("unsatisfiable", stats.nodes, stats.backtracks)
    @moonplan.CoverageBudgetExhausted(stats) =>
      ("budget_exhausted", stats.nodes, stats.backtracks)
  }
}

///|
fn run_case(
  days : Int,
  strategy : @moonplan.SearchStrategy,
  label : String,
  repetitions : Int,
) -> Unit raise {
  let request = benchmark_request(days)
  let start = @env.now()
  let mut status = ""
  let mut nodes = 0
  let mut backtracks = 0
  for iteration = 0; iteration < repetitions; iteration = iteration + 1 {
    let outcome = match request.solve_with_strategy(2000, strategy) {
      Err(error) => fail(error.message())
      Ok(outcome) => outcome
    }
    let (current_status, current_nodes, current_backtracks) = outcome_stats(
      outcome,
    )
    if iteration > 0 &&
      (
        status != current_status ||
        nodes != current_nodes ||
        backtracks != current_backtracks
      ) {
      fail("the same benchmark input produced different search evidence")
    }
    status = current_status
    nodes = current_nodes
    backtracks = current_backtracks
  }
  let elapsed = @env.now() - start
  println(
    "coverage_\{days}d,\{label},\{days * 3},\{repetitions},\{status},\{nodes},\{backtracks},\{elapsed}",
  )
}

///|
fn queens_problem(size : Int) -> @moonplan.Problem {
  let problem = @moonplan.Problem::new()
  let queens : Array[@moonplan.Var] = []
  for column = 0; column < size; column = column + 1 {
    queens.push(
      problem
      .add_variable("q\{column}", @moonplan.int_domain(0, size - 1))
      .unwrap(),
    )
  }
  problem.add_constraint(@moonplan.AllDifferent(queens.copy())).unwrap()
  for left = 0; left < size; left = left + 1 {
    for right = left + 1; right < size; right = right + 1 {
      let delta = right - left
      problem
      .add_constraint(
        @moonplan.OffsetDifferent(queens[right], queens[left], delta),
      )
      .unwrap()
      problem
      .add_constraint(
        @moonplan.OffsetDifferent(queens[right], queens[left], -delta),
      )
      .unwrap()
    }
  }
  problem
}

///|
fn run_queens(
  size : Int,
  strategy : @moonplan.SearchStrategy,
  label : String,
  repetitions : Int,
) -> Unit raise {
  let problem = queens_problem(size)
  let start = @env.now()
  let mut status = ""
  let mut nodes = 0
  let mut backtracks = 0
  for iteration = 0; iteration < repetitions; iteration = iteration + 1 {
    let outcome = problem.solve_with_node_limit(20000, strategy).unwrap()
    let (current_status, current_nodes, current_backtracks) = match outcome {
      @moonplan.Found(_, stats) => ("found", stats.nodes, stats.backtracks)
      @moonplan.ProvedUnsatisfiable(stats) =>
        ("unsatisfiable", stats.nodes, stats.backtracks)
      @moonplan.BudgetExhausted(stats) =>
        ("budget_exhausted", stats.nodes, stats.backtracks)
    }
    if iteration > 0 &&
      (
        status != current_status ||
        nodes != current_nodes ||
        backtracks != current_backtracks
      ) {
      fail("the same queens model produced different search evidence")
    }
    status = current_status
    nodes = current_nodes
    backtracks = current_backtracks
  }
  let elapsed = @env.now() - start
  println(
    "queens_\{size},\{label},\{size},\{repetitions},\{status},\{nodes},\{backtracks},\{elapsed}",
  )
}

///|
fn main raise {
  println(
    "case,strategy,variables,repetitions,status,nodes,backtracks,total_ms",
  )
  for days in [4, 7, 10] {
    let repetitions = if days == 10 { 2 } else { 5 }
    run_case(days, @moonplan.Mrv, "mrv", repetitions)
    run_case(days, @moonplan.DegreeLcv, "degree_lcv", repetitions)
  }
  for size in [8, 10] {
    let repetitions = if size == 10 { 1 } else { 3 }
    run_queens(size, @moonplan.Mrv, "mrv", repetitions)
    run_queens(size, @moonplan.DegreeLcv, "degree_lcv", repetitions)
  }
}