use chrono::NaiveDateTime;
use serde::Deserialize;
use std::cmp::{max, min};
use std::fmt;
use std::ops::Add;
use std::vec;
use crate::models::activity::ActivityStatus::Impossible;
use crate::models::budget::{CalendarBudget, TimeBudget};
use crate::models::calendar_interval::CalIntStatus;
use crate::models::interval::Interval;
use crate::services::interval_helper;
use super::goal::Goal;
use super::{calendar::Calendar, time_grid};
const MAX_CONTIGUOUS_SIMPLE_BLOCK_SLOTS: usize = 8 * time_grid::SLOTS_PER_HOUR;
#[derive(Clone)]
pub struct Activity {
pub goal_id: String,
pub activity_type: ActivityType,
pub title: String,
pub min_block_size: usize,
pub max_block_size: usize,
pub total_duration: usize,
pub duration_left: usize,
pub status: ActivityStatus,
pub start: NaiveDateTime,
pub deadline: Option<NaiveDateTime>,
pub compatible_intervals: Vec<Interval>,
pub incompatible_intervals: Vec<Interval>,
pub flex: Option<usize>,
}
impl Activity {
pub(crate) fn reset_compatible_intervals(&mut self) {
self.compatible_intervals = vec![];
self.flex = None;
}
}
impl Activity {
pub(crate) fn flex_read_only(&self) -> Option<usize> {
self.flex
}
}
impl Activity {
pub(crate) fn remove_interval(&mut self, interval_to_remove: &Interval) {
let mut result: Vec<Interval> = Vec::with_capacity(self.compatible_intervals.len());
for own_interval in &self.compatible_intervals {
if interval_to_remove.end <= own_interval.start {
result.push(own_interval.clone());
continue;
}
if interval_to_remove.start >= own_interval.end {
result.push(own_interval.clone());
continue;
}
let overlap_start = max(interval_to_remove.start, own_interval.start);
let overlap_end = min(interval_to_remove.end, own_interval.end);
self.flex = None;
self.incompatible_intervals.push(Interval {
start: overlap_start,
end: overlap_end,
});
if overlap_start == own_interval.start && overlap_end == own_interval.end {
continue;
}
if overlap_start == own_interval.start {
if own_interval.end - overlap_end >= self.min_block_size {
result.push(Interval {
start: overlap_end,
end: own_interval.end,
});
} else {
self.incompatible_intervals.push(Interval {
start: overlap_end,
end: own_interval.end,
});
}
continue;
}
if overlap_end == own_interval.end {
if overlap_start - own_interval.start >= self.min_block_size {
result.push(Interval {
start: own_interval.start,
end: overlap_start,
});
} else {
self.incompatible_intervals.push(Interval {
start: own_interval.start,
end: overlap_start,
});
}
continue;
}
if overlap_start - own_interval.start >= self.min_block_size {
result.push(Interval {
start: own_interval.start,
end: overlap_start,
});
} else {
self.incompatible_intervals.push(Interval {
start: own_interval.start,
end: overlap_start,
});
}
if own_interval.end - overlap_end >= self.min_block_size {
result.push(Interval {
start: overlap_end,
end: own_interval.end,
});
} else {
self.incompatible_intervals.push(Interval {
start: overlap_end,
end: own_interval.end,
});
}
}
self.compatible_intervals = result;
if !self.incompatible_intervals.is_empty() {
self.flex = None;
}
}
}
impl Activity {
pub(crate) fn flex_reset(&mut self) {
self.flex = None;
}
}
impl Activity {
pub fn mark_impossible(&mut self) {
self.status = Impossible;
}
pub fn flex(&mut self) -> usize {
if let Some(flex) = self.flex {
crate::log_debug!("Flex {} from cache.", self.flex.unwrap());
return flex;
}
let mut flex: usize = 0;
for interval in &self.compatible_intervals {
let interval_size = interval.end - interval.start;
#[cfg(debug_assertions)]
assert!(
self.min_block_size <= interval_size,
"Placer should remove+unregister INcompatible intervals before calling flex."
);
let interval_flex = interval_size - self.min_block_size + 1;
flex += interval_flex;
}
self.flex = Some(flex);
crate::log_debug!("Flex {} calculated.", flex);
flex
}
pub(crate) fn get_simple_activities(goal: &Goal, calendar: &mut Calendar) -> Vec<Activity> {
let (adjusted_goal_start, adjusted_goal_deadline) = goal.get_adj_start_deadline(calendar);
let mut activities: Vec<Activity> = Vec::with_capacity(1);
if let Some(mut activity_total_duration) = goal.min_duration {
let mut min_block_size = activity_total_duration;
if activity_total_duration > MAX_CONTIGUOUS_SIMPLE_BLOCK_SLOTS {
min_block_size = 1;
}
let max_block_size = min_block_size;
let periods = calendar.get_periods_for(&goal.id);
let mut adjusted_activity_deadline =
adjusted_goal_deadline.unwrap_or(calendar.end_date_time); if goal.deadline.is_none() && !calendar.is_participating_in_a_budget(&goal.id) {
adjusted_activity_deadline = adjusted_goal_deadline.unwrap_or(
calendar
.end_date_time
.add(time_grid::slot_span(time_grid::SLOTS_PER_DAY as i64)),
);
};
let compatible_intervals: Vec<Interval> = interval_helper::get_compatible_intervals(
calendar,
periods,
adjusted_goal_start,
adjusted_activity_deadline,
&goal.not_on.clone(),
);
let mut already_placed_for_goal_id: usize = 0;
for cal_interval in &calendar.intervals {
match &cal_interval.status {
CalIntStatus::Claimable(_) => {}
CalIntStatus::Occupied(_, goal_id) => {
if goal.id.eq(goal_id) {
already_placed_for_goal_id +=
cal_interval.interval.end - cal_interval.interval.start;
}
}
}
}
if already_placed_for_goal_id >= activity_total_duration {
return vec![];
}
activity_total_duration -= already_placed_for_goal_id;
crate::log_dbg!(&compatible_intervals);
let activity = Activity {
goal_id: goal.id.clone(),
activity_type: ActivityType::SimpleGoal,
title: goal.title.clone(),
min_block_size,
max_block_size,
total_duration: activity_total_duration,
duration_left: activity_total_duration,
status: ActivityStatus::Unprocessed,
start: adjusted_goal_start,
deadline: goal.deadline,
compatible_intervals,
incompatible_intervals: vec![],
flex: None,
};
crate::log_dbg!(&activity);
activities.push(activity);
}
activities
}
pub(crate) fn get_activities_to_get_to_min_day_budget(
budget: &CalendarBudget,
calendar: &Calendar,
time_budget: &TimeBudget,
) -> Vec<Activity> {
let mut activities: Vec<Activity> = Vec::with_capacity(1);
let adjusted_goal_start = calendar.get_datetime_of(time_budget.calendar_start_index);
let adjusted_goal_deadline = calendar.get_datetime_of(time_budget.calendar_end_index);
let hours_to_schedule = time_budget.min_scheduled - time_budget.scheduled;
let compatible_intervals: Vec<Interval> = interval_helper::get_compatible_intervals(
calendar,
Some(&budget.periods),
adjusted_goal_start,
adjusted_goal_deadline,
&None,
);
activities.push(Activity {
goal_id: budget.budget_id.clone(),
activity_type: ActivityType::GetToMinDayBudget,
title: budget.title.clone(),
min_block_size: 1,
max_block_size: hours_to_schedule,
total_duration: hours_to_schedule,
duration_left: hours_to_schedule,
status: ActivityStatus::Unprocessed,
start: adjusted_goal_start,
deadline: Some(adjusted_goal_deadline),
compatible_intervals,
incompatible_intervals: vec![],
flex: None,
});
activities
}
pub fn get_activities_to_get_min_week_budget(
budget: &CalendarBudget,
calendar: &Calendar,
time_budget: &TimeBudget,
) -> Vec<Activity> {
let mut activities: Vec<Activity> = vec![];
let adjusted_goal_start = calendar.get_datetime_of(time_budget.calendar_start_index);
let adjusted_goal_deadline = calendar.get_datetime_of(time_budget.calendar_end_index);
let max_hours = time_budget.max_scheduled - time_budget.scheduled;
let compatible_intervals: Vec<Interval> = interval_helper::get_compatible_intervals(
calendar,
Some(&budget.periods),
adjusted_goal_start,
adjusted_goal_deadline,
&None,
);
activities.push(Activity {
goal_id: budget.budget_id.clone(),
activity_type: ActivityType::GetToMinWeekBudget,
title: budget.title.clone(),
min_block_size: 1,
max_block_size: max_hours,
total_duration: max_hours,
duration_left: max_hours,
status: ActivityStatus::Unprocessed,
start: adjusted_goal_start,
deadline: Some(adjusted_goal_deadline),
compatible_intervals,
incompatible_intervals: vec![],
flex: None,
});
activities
}
pub fn get_activities_to_top_up_week_budget(
budget: &CalendarBudget,
calendar: &Calendar,
time_budget: &TimeBudget,
max_per_week: usize,
) -> Vec<Activity> {
let mut activities: Vec<Activity> = vec![];
let max_hours = min(
time_budget.max_scheduled - time_budget.scheduled,
max_per_week,
);
if max_hours == 0 {
return activities;
}
let adjusted_start =
time_grid::datetime_at(calendar.start_date_time, time_budget.calendar_start_index);
let adjusted_end = calendar.get_datetime_of(time_budget.calendar_end_index);
let compatible_intervals: Vec<Interval> = interval_helper::get_compatible_intervals(
calendar,
Some(&budget.periods),
adjusted_start,
adjusted_end,
&None,
);
activities.push(Activity {
goal_id: budget.budget_id.clone(),
activity_type: ActivityType::TopUpWeekBudget,
title: budget.title.clone(),
min_block_size: 1,
max_block_size: max_hours,
total_duration: max_hours,
duration_left: max_hours,
status: ActivityStatus::Unprocessed,
start: adjusted_start,
deadline: Some(adjusted_end),
compatible_intervals,
incompatible_intervals: vec![],
flex: None,
});
activities
}
}
#[derive(Debug, PartialEq, Clone, Deserialize, Hash)]
pub enum ActivityStatus {
Unprocessed,
Processed,
Scheduled,
Impossible,
Postponed,
BestEffort,
}
#[derive(Clone, Debug, PartialEq, Hash)]
pub enum ActivityType {
SimpleGoal,
GetToMinDayBudget,
GetToMinWeekBudget,
TopUpWeekBudget,
}
impl fmt::Debug for Activity {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f)?;
writeln!(f, "title: {:?}", self.title)?;
writeln!(f, "status:{:?}", self.status)?;
writeln!(f, "total duration: {:?}", self.total_duration)?;
writeln!(f, "duration left: {:?}", self.duration_left)?;
write!(f, "flex:{:?}", self.flex_read_only())?;
for interval in &self.compatible_intervals {
write!(f, "\nInterval :{:?}", interval)?;
}
Ok(())
}
}