use crate::models::activity::ActivityStatus::{BestEffort, Postponed, Scheduled, Unprocessed};
use crate::models::activity::ActivityType;
use crate::models::activity::ActivityType::{
GetToMinDayBudget, GetToMinWeekBudget, TopUpWeekBudget,
};
use crate::models::budget::TimeBudgetType::Week;
use crate::models::calendar_interval::CalIntStatus;
use crate::models::calendar_interval::CalIntStatus::Claimable;
use crate::models::interval::Interval;
use crate::models::time_grid;
use crate::models::{activity::Activity, calendar::Calendar};
use std::cmp::{max, min};
use std::collections::BTreeSet;
use std::fmt::{Debug, Formatter};
struct LeastConflict {
start: usize,
end: usize,
claims: usize,
}
impl Debug for LeastConflict {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let _ = writeln!(
f,
"{:?} claims on {:?}-{:?} ({}-{}) day {:?}",
self.claims,
self.start,
self.end,
time_grid::format_slot_time_of_day(self.start),
time_grid::format_slot_time_of_day(self.end),
self.start / time_grid::SLOTS_PER_DAY
);
Ok(())
}
}
pub(crate) fn place(calendar: &mut Calendar, activities: &mut [Activity]) {
crate::log_debug!("Starting placing...");
calendar.register_activities(activities);
crate::log_dbg!(&calendar);
postpone(calendar, activities);
while let Some(act_index) = find_next_act_index(calendar, activities) {
crate::log_debug!(
"Found activity {} to schedule, act_index {}",
activities[act_index].title,
act_index,
);
crate::log_debug!(" with flex {}.", activities[act_index].flex());
let least_conflict: Option<LeastConflict> =
get_best_index_for(calendar, &activities[act_index]);
match least_conflict {
None => {
crate::log_debug!(
"No suitable position found for activity {}...",
activities[act_index].title
);
activities[act_index].mark_impossible();
continue;
}
Some(least_conflict_position) => {
crate::log_dbg!(&least_conflict_position);
let interval_to_use = Interval {
start: least_conflict_position.start,
end: least_conflict_position.end,
};
calendar.register(&interval_to_use, act_index);
if calendar.is_participating_in_a_budget(&activities[act_index].goal_id) {
calendar.reduce_budgets_for(
&activities[act_index].goal_id,
interval_to_use.start,
interval_to_use.end,
);
}
match activities[act_index].activity_type {
ActivityType::SimpleGoal => {
activities[act_index].duration_left -=
interval_to_use.end - interval_to_use.start;
if activities[act_index].duration_left == 0 {
activities[act_index].status = Scheduled; activities[act_index].reset_compatible_intervals();
}
}
GetToMinDayBudget => {
activities[act_index].duration_left -=
interval_to_use.end - interval_to_use.start;
if activities[act_index].duration_left == 0 {
activities[act_index].status = Scheduled;
activities[act_index].reset_compatible_intervals();
}
}
GetToMinWeekBudget => {
activities[act_index].duration_left -=
interval_to_use.end - interval_to_use.start;
if activities[act_index].duration_left == 0 {
activities[act_index].status = Scheduled;
activities[act_index].reset_compatible_intervals();
}
}
TopUpWeekBudget => {
activities[act_index].duration_left -=
interval_to_use.end - interval_to_use.start;
for budget in &calendar.budgets {
if budget
.participating_goals
.contains(&activities[act_index].goal_id)
{
for time_budget in &budget.time_budgets {
if time_budget.time_budget_type == Week
&& time_budget.max_scheduled == time_budget.scheduled
{
activities[act_index].status = Scheduled;
activities[act_index].reset_compatible_intervals();
}
}
}
}
if activities[act_index].duration_left == 0 {
activities[act_index].status = Scheduled;
activities[act_index].reset_compatible_intervals();
}
}
}
calendar.occupy(&interval_to_use, act_index, activities);
}
}
crate::log_debug!("Finding next activity to schedule...");
crate::log_dbg!(&calendar);
}
crate::log_debug!("No more activities to schedule.");
}
fn postpone(calendar: &mut Calendar, activities: &mut [Activity]) {
for activity in activities.iter_mut() {
if activity.deadline.is_none()
&& activity.status != BestEffort
&& activity.activity_type != GetToMinDayBudget
&& activity.activity_type != GetToMinWeekBudget
&& activity.activity_type != TopUpWeekBudget
&& !calendar.is_participating_in_a_budget(&activity.goal_id)
{
crate::log_debug!(
"Skipping activity {} and setting to Postponed since there is no deadline.",
activity.title
);
activity.status = Postponed;
continue;
}
}
}
fn find_next_act_index(calendar: &mut Calendar, activities: &mut [Activity]) -> Option<usize> {
for activity in activities.iter_mut() {
calendar.update_compatible_intervals(activity);
}
let mut highest_flex = 0;
let mut act_index_next_to_schedule: Option<usize> = None;
for (act_index, activity) in activities.iter_mut().enumerate() {
if !(activity.status == Unprocessed || activity.status == BestEffort)
{
continue;
};
crate::log_debug!(
"Getting flex for {}, act_index {}.",
activity.title,
act_index
);
let flex = activity.flex();
match flex {
0 => {
activity.mark_impossible();
}
1 => {
crate::log_debug!("Flex of 1 found for activity {}", activity.title);
act_index_next_to_schedule = Some(act_index);
break;
}
_ => {
if flex > highest_flex {
highest_flex = flex;
act_index_next_to_schedule = Some(act_index);
}
}
}
}
act_index_next_to_schedule
}
fn get_conflicts_for(calendar: &Calendar, start_index: usize, end_index: usize) -> usize {
let mut conflicts: Option<usize> = None;
for cal_int in &calendar.intervals {
if end_index <= cal_int.interval.start {
return conflicts.expect("When calling get conflicts a result is expected");
}
if start_index >= cal_int.interval.end {
continue;
}
let overlap_start = max(start_index, cal_int.interval.start);
let overlap_end = min(end_index, cal_int.interval.end);
match &cal_int.status {
Claimable(claims) => match conflicts {
None => {
conflicts = Some((overlap_end - overlap_start) * claims.len());
}
Some(number_of_conflicts) => {
conflicts =
Some(number_of_conflicts + (overlap_end - overlap_start) * claims.len());
}
},
CalIntStatus::Occupied(_act_index, _goal_id) => {}
}
}
conflicts.expect("When calling get conflicts a result is expected")
}
fn candidate_offsets(
calendar: &Calendar,
interval_start: usize,
interval_end: usize,
block_size: usize,
) -> Vec<usize> {
let max_offset = interval_end
.saturating_sub(interval_start)
.saturating_sub(block_size);
let mut offsets = BTreeSet::new();
offsets.insert(0);
if max_offset > 0 {
offsets.insert(max_offset);
}
let search_end = interval_start + max_offset + block_size;
for cal_int in &calendar.intervals {
if cal_int.interval.end <= interval_start {
continue;
}
if cal_int.interval.start >= search_end {
break;
}
if !matches!(cal_int.status, Claimable(_)) {
continue;
}
for boundary in [cal_int.interval.start, cal_int.interval.end] {
if boundary > interval_start {
let o_start = boundary - interval_start;
if o_start <= max_offset {
offsets.insert(o_start);
}
}
if boundary >= interval_start + block_size {
let o_end = boundary
.saturating_sub(interval_start)
.saturating_sub(block_size);
if o_end <= max_offset {
offsets.insert(o_end);
}
}
}
}
offsets.into_iter().collect()
}
fn get_best_index_for(calendar: &Calendar, activity: &Activity) -> Option<LeastConflict> {
let mut least_conflict: Option<LeastConflict> = None;
for interval in &activity.compatible_intervals {
let interval_len = interval.end - interval.start;
#[cfg(debug_assertions)]
assert!(
interval_len >= activity.min_block_size,
"Length of compatible activity interval should be >= min_block_size"
);
for inner_offset in candidate_offsets(
calendar,
interval.start,
interval.end,
activity.min_block_size,
) {
let new_conflicts = get_conflicts_for(
calendar,
interval.start + inner_offset,
interval.start + inner_offset + activity.min_block_size,
);
match least_conflict {
None => {
least_conflict = Some(LeastConflict {
start: interval.start + inner_offset,
end: interval.start + inner_offset + activity.min_block_size,
claims: new_conflicts,
});
}
Some(ref mut least_conflict) => {
if new_conflicts < least_conflict.claims {
least_conflict.start = interval.start + inner_offset;
least_conflict.end =
interval.start + inner_offset + activity.min_block_size;
least_conflict.claims = new_conflicts;
}
if new_conflicts == 1 {
break;
}
}
}
}
}
least_conflict
}
pub(crate) fn place_postponed_as_best_effort(calendar: &mut Calendar, activities: &mut [Activity]) {
crate::log_debug!("Placing postponed activities best effort...");
for activity in activities.iter_mut() {
if activity.status == Postponed {
crate::log_debug!(
"Setting postponed activity {} to BestEffort.",
activity.title
);
activity.status = BestEffort;
}
}
place(calendar, activities);
}