///| A request whose mandatory resources may be satisfied by one of several

///| named groups.  Each group is an all-of group; the planner chooses the

///| earliest feasible group, making it suitable for compatible equipment or

///|
/// interchangeable rooms.
pub struct FlexibleRequest {
  request : BookingRequest
  alternatives : Array[Array[String]]
} derive(Debug)

///|
pub enum FlexibleRequestError {
  NoAlternatives
  EmptyAlternative(Int)
  EmptyResourceId(Int, Int)
  DuplicateResourceInAlternative(Int, String)
} derive(Eq, Debug)

///|
pub fn FlexibleRequest::new(
  request : BookingRequest,
  alternatives : Array[Array[String]],
) -> Result[FlexibleRequest, FlexibleRequestError] {
  if alternatives.length() == 0 {
    return Err(NoAlternatives)
  }
  for group_index in 0.. BookingRequest {
  self.request
}

///|
pub fn FlexibleRequest::alternatives(
  self : FlexibleRequest,
) -> Array[Array[String]] {
  self.alternatives.copy()
}

///|
pub fn FlexibleRequest::render(self : FlexibleRequest) -> String {
  self.request.id() + " alternatives=" + self.alternatives.length().to_string()
}

///|
fn append_unique(
  ids : Array[String],
  additions : Array[String],
) -> Array[String] {
  let output = ids.copy()
  for id in additions {
    let mut exists = false
    for current in output {
      if current == id {
        exists = true
      }
    }
    if !exists {
      output.push(id)
    }
  }
  output
}

///|
fn should_choose(candidate : Allocation, current : Allocation) -> Bool {
  candidate.event().start() < current.event().start() ||
  (
    candidate.event().start() == current.event().start() &&
    candidate.resource_ids().join(",") < current.resource_ids().join(",")
  )
}

///|
/// Find the earliest choice among all compatible resource groups.  Resources

///|
/// already mandatory in the underlying request are included in every group.
pub fn Planner::find_flexible_earliest(
  self : Planner,
  flexible : FlexibleRequest,
) -> Result[Allocation, PlanError] {
  let request = flexible.request()
  let mut best : Allocation? = None
  for alternative in flexible.alternatives() {
    let ids = append_unique(request.required_resources(), alternative)
    let calendars = match self.calendars_for_ids(ids) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let search = request_search_window(request)
    match allocation_from_free(common_free(calendars, search), request, ids) {
      None => ()
      Some(candidate) =>
        match best {
          None => best = Some(candidate)
          Some(current) =>
            if should_choose(candidate, current) {
              best = Some(candidate)
            }
        }
    }
  }
  match best {
    Some(value) => Ok(value)
    None => Err(NoFeasibleSlot(request.id()))
  }
}

///|
/// Confirm a flexible selection.  This uses the allocation returned by the

///| same search and validates every selected resource before constructing the

///|
/// successor planner.
pub fn Planner::reserve_flexible_earliest(
  self : Planner,
  flexible : FlexibleRequest,
) -> Result[Reservation, PlanError] {
  let allocation = match self.find_flexible_earliest(flexible) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let updated : Array[ResourceCalendar] = []
  for calendar in self.resources {
    let mut affected = false
    for id in allocation.resource_ids() {
      if calendar.id() == id {
        affected = true
      }
    }
    if affected {
      match calendar.reserve(allocation.reserved()) {
        Ok(next) => updated.push(next)
        Err(_) => return Err(CannotReserve(calendar.id()))
      }
    } else {
      updated.push(calendar)
    }
  }
  match Planner::new(updated) {
    Ok(planner) => Ok({ allocation, planner })
    Err(error) => Err(error)
  }
}