use chrono::NaiveDate;
use todo_lib::{conv, todo, todotxt};
use crate::conf;
const TIME_FIELD: &str = "time";
const ID_RESERVED: usize = 999_999_999;
const SLOT_START: usize = 0;
const SLOT_START_EARLY: usize = 3;
pub const SLOT_FINISH: usize = 2;
const SLOT_FINISH_LATE: usize = 4;
const SLOT_MIDDLE: usize = 1;
const SLOT_SINGLE: usize = 5;
const SLOT_UNLIMITED: usize = 6;
const SLOT_START_AFTER: usize = 7;
const SLOT_FINISH_BEFORE: usize = 8;
pub const SLOT_NONE: usize = 9;
pub(crate) const MIN_IN_HOUR: u32 = 60;
const DEFAULT_AGENDA_START: u32 = 8 * MIN_IN_HOUR;
const DEFAULT_AGENDA_END: u32 = 20 * MIN_IN_HOUR;
const DEFAULT_SLOT_SIZE: u32 = 30;
pub(crate) const MIN_SLOT_SIZE: u32 = 15;
pub(crate) const DAY_END: u32 = 24 * MIN_IN_HOUR;
const DEFAULT_MARKS: &str = "┌│└╎╎─[~~";
fn time_to_minutes(t: u32) -> u32 {
t / 100 * MIN_IN_HOUR + t % 100
}
#[derive(PartialEq, Debug)]
pub enum SlotKind {
None,
Start,
StartEarly,
Middle,
Finish,
FinishLate,
Single,
Unlimited,
StartAfter,
FinishBefore,
}
impl SlotKind {
pub fn to_char_index(&self) -> usize {
match self {
SlotKind::None => SLOT_NONE,
SlotKind::Start => SLOT_START,
SlotKind::StartEarly => SLOT_START_EARLY,
SlotKind::Finish => SLOT_FINISH,
SlotKind::FinishLate => SLOT_FINISH_LATE,
SlotKind::Middle => SLOT_MIDDLE,
SlotKind::Single => SLOT_SINGLE,
SlotKind::Unlimited => SLOT_UNLIMITED,
SlotKind::StartAfter => SLOT_START_AFTER,
SlotKind::FinishBefore => SLOT_FINISH_BEFORE,
}
}
pub fn to_char_before_index(&self) -> usize {
match self {
SlotKind::None => SLOT_NONE,
SlotKind::Start => SLOT_MIDDLE,
SlotKind::StartEarly => SLOT_START_EARLY,
SlotKind::Finish => SLOT_NONE,
SlotKind::FinishLate => SLOT_FINISH_LATE,
SlotKind::Middle => SLOT_MIDDLE,
SlotKind::Single => SLOT_NONE,
SlotKind::Unlimited => SLOT_NONE,
SlotKind::StartAfter => SLOT_MIDDLE,
SlotKind::FinishBefore => SLOT_MIDDLE,
}
}
pub fn to_char_after_index(&self) -> usize {
match self {
SlotKind::None => SLOT_NONE,
SlotKind::Start => SLOT_NONE,
SlotKind::StartEarly => SLOT_NONE,
SlotKind::Finish => SLOT_MIDDLE,
SlotKind::FinishLate => SLOT_MIDDLE,
SlotKind::Middle => SLOT_MIDDLE,
SlotKind::Single => SLOT_NONE,
SlotKind::Unlimited => SLOT_NONE,
SlotKind::StartAfter => SLOT_NONE,
SlotKind::FinishBefore => SLOT_MIDDLE,
}
}
pub fn is_start(&self) -> bool {
*self == SlotKind::Start
|| *self == SlotKind::StartEarly
|| *self == SlotKind::Single
|| *self == SlotKind::Unlimited
|| *self == SlotKind::StartAfter
}
}
pub struct TaskSlot {
pub id: usize,
pub kind: SlotKind,
}
impl TaskSlot {
fn empty_slot() -> Self {
TaskSlot { id: ID_RESERVED, kind: SlotKind::None }
}
pub fn is_empty(&self) -> bool {
self.id == ID_RESERVED || self.kind == SlotKind::None
}
}
pub struct Slot {
pub time: u32,
pub tasks: Vec<TaskSlot>,
}
impl Slot {
pub fn start_cnt(&self) -> usize {
let mut cnt = 0;
for ts in &self.tasks {
if ts.kind.is_start() {
cnt += 1;
}
}
cnt
}
pub fn nth_start(&self, n: usize) -> Option<usize> {
let mut found = 0;
for (idx, ts) in self.tasks.iter().enumerate() {
if ts.kind.is_start() {
if found == n {
return Some(idx);
} else {
found += 1;
}
}
}
None
}
}
pub struct Agenda {
pub time_start: u32,
pub time_end: u32,
pub date: NaiveDate,
pub fields: Vec<String>,
pub slot_size: u32,
pub slots: Vec<Slot>,
pub all_day: Vec<usize>,
marks: Vec<char>,
}
impl Agenda {
pub fn new(dt: NaiveDate, conf: &conf::Conf) -> Self {
let mut ag = Agenda {
time_start: DEFAULT_AGENDA_START,
time_end: DEFAULT_AGENDA_END,
date: dt,
fields: Vec::new(),
slot_size: DEFAULT_SLOT_SIZE,
slots: Vec::new(),
all_day: Vec::new(),
marks: DEFAULT_MARKS.chars().collect(),
};
if let Some(s) = &conf.marks {
ag.marks = s.chars().collect();
if ag.marks.len() < SLOT_NONE {
ag.marks.push('~');
ag.marks.push('~');
}
}
if let Some(d_str) = &conf.slot {
if d_str.find(|c: char| !c.is_ascii_digit()).is_none() {
match d_str.parse::<u32>() {
Err(_) => {
eprintln!("Slot must be a positive integer number: '{d_str}'");
}
Ok(n) => {
if !(MIN_SLOT_SIZE..DAY_END).contains(&n) {
eprintln!(
"Slot value must be between {MIN_SLOT_SIZE} minutes and 24 hours: '{d_str}'. Using default slot size"
);
} else {
ag.slot_size = n;
}
}
}
} else {
match conv::str_to_duration(d_str) {
None => {
eprintln!("Failed to parse duration '{d_str}'");
}
Some(dur) => {
let dur = (dur as u32) / MIN_IN_HOUR;
if !(MIN_SLOT_SIZE..DAY_END).contains(&dur) {
eprintln!(
"Slot value must be between {MIN_SLOT_SIZE} minutes and 24 hours: '{d_str}'. Using default slot size"
);
} else {
ag.slot_size = dur;
}
}
}
}
}
if let Some(s) = &conf.time_range {
match conv::str_to_time_interval(s) {
conv::TimeInterval::Single(val) => {
if let Some(t) = val {
ag.time_start = time_to_minutes(t);
}
}
conv::TimeInterval::Range(valb, vale) => {
if let Some(st) = valb {
ag.time_start = time_to_minutes(st);
} else {
ag.time_start = 0;
}
if let Some(en) = vale {
ag.time_end = time_to_minutes(en);
} else {
ag.time_end = DAY_END;
}
}
}
if ag.time_start >= ag.time_end {
eprintln!("Agenda start time is greater than the agenda end time: {s}. Reset values to the defaults");
ag.time_start = DEFAULT_AGENDA_START;
ag.time_end = DEFAULT_AGENDA_END;
} else if ag.time_end - ag.time_start < ag.slot_size {
eprintln!(
"Too small difference between start and end time: {s}. It should be at least a slot size '{0}'. Update the end time",
ag.slot_size
);
ag.time_end = ag.time_start + ag.slot_size;
}
}
match &conf.on_fields {
None => ag.fields = vec!["due".to_string()],
Some(s) => {
for f in s.split(',') {
ag.fields.push(f.to_string());
}
}
};
if let Some(on_date) = &conf.on {
if let Some((s_fields, s_date)) = on_date.split_once('=') {
if !s_date.is_empty() {
match conv::str_to_date(s_date, dt) {
Some(d) => ag.date = d,
None => eprintln!("Failed to parse date: {s_date}"),
}
}
if !s_fields.is_empty() {
ag.fields.clear();
for f in s_fields.split(',') {
ag.fields.push(f.to_string());
}
}
} else if let Some(dval) = conv::str_to_date(on_date, dt) {
ag.date = dval;
} else {
ag.fields.clear();
for f in on_date.split(',') {
ag.fields.push(f.to_string());
}
};
}
let mut c_time = ag.time_start;
while c_time <= ag.time_end {
let slot = Slot { time: c_time, tasks: Vec::new() };
ag.slots.push(slot);
c_time += ag.slot_size;
}
ag
}
fn task_date(&self, task: &todotxt::Task, today: NaiveDate) -> Option<NaiveDate> {
for field in &self.fields {
let val = match field.as_str() {
"due" => task.due_date,
"created" => task.create_date,
"threshold" | "thr" => task.threshold_date,
fname => task.tags.get(fname).and_then(|fval| conv::str_to_date(fval, today)),
};
if val.is_some() {
return val;
}
}
None
}
fn task_time(&self, task: &todotxt::Task) -> Option<(u32, u32)> {
let range = task.tags.get(TIME_FIELD).map(|sval| conv::str_to_time_interval(sval));
match range {
Some(conv::TimeInterval::Single(s)) => s.map(|sval| {
let st = time_to_minutes(sval);
let en = if st > 0 { st - 1 } else { 0 };
(st, en)
}),
Some(conv::TimeInterval::Range(sb, se)) => {
let st = if let Some(v) = sb { time_to_minutes(v) } else { 0 };
let en = if let Some(v) = se { time_to_minutes(v) } else { DAY_END };
Some((st, en))
}
None => None,
}
}
fn unoccupied_column(&self, slot_st: usize, slot_en: usize, max_col: usize) -> usize {
for i in 0..max_col {
let mut occupied = false;
for idx in slot_st..=slot_en {
if self.slots[idx].tasks.len() > i && !self.slots[idx].tasks[i].is_empty() {
occupied = true;
break;
}
}
if !occupied {
return i;
}
}
ID_RESERVED
}
fn kind_for_first_slot(
&self,
slot_id: usize,
slot_st: usize,
slot_en: usize,
time_st: u32,
time_en: u32,
) -> SlotKind {
let slot_time = self.slots[slot_id].time;
if self.slots[slot_id].time > time_st {
SlotKind::StartEarly
} else if slot_st == slot_en && time_en > time_st {
SlotKind::Single
} else if slot_st == slot_en {
SlotKind::Unlimited
} else if slot_id == slot_st && slot_time == time_st {
SlotKind::Start
} else {
SlotKind::StartAfter
}
}
fn kind_for_middle_slot(
&self,
slot_id: usize,
slot_st: usize,
slot_en: usize,
time_st: u32,
time_en: u32,
) -> SlotKind {
let slot_time = self.slots[slot_id].time;
if slot_st == slot_en && time_en > time_st {
SlotKind::Single
} else if slot_st == slot_en {
SlotKind::Unlimited
} else if slot_id == slot_st && slot_time == time_st {
SlotKind::Start
} else if slot_id == slot_st {
SlotKind::StartAfter
} else if slot_id == slot_en && slot_time == time_en {
SlotKind::Finish
} else if slot_id == slot_en {
SlotKind::FinishBefore
} else {
SlotKind::Middle
}
}
fn kind_for_last_slot(
&self,
slot_id: usize,
slot_st: usize,
slot_en: usize,
time_st: u32,
time_en: u32,
) -> SlotKind {
let slot_time = self.slots[slot_id].time;
if slot_id == slot_st && slot_time == time_st {
SlotKind::Start
} else if slot_id == slot_st {
SlotKind::StartAfter
} else if self.slots[slot_id].time < time_en {
SlotKind::FinishLate
} else if slot_st == slot_en && time_en > time_st {
SlotKind::Single
} else if slot_st == slot_en {
SlotKind::Unlimited
} else if slot_time == time_en {
SlotKind::Finish
} else {
SlotKind::FinishBefore
}
}
fn fill_column(&mut self, slot_st: usize, slot_en: usize, col: usize, task_id: usize, time_st: u32, time_en: u32) {
for slot_id in slot_st..=slot_en {
while self.slots[slot_id].tasks.len() < col {
self.slots[slot_id].tasks.push(TaskSlot::empty_slot());
}
let new_slot = TaskSlot {
kind: if slot_id == 0 {
self.kind_for_first_slot(slot_id, slot_st, slot_en, time_st, time_en)
} else if slot_id == self.slots.len() - 1 {
self.kind_for_last_slot(slot_id, slot_st, slot_en, time_st, time_en)
} else {
self.kind_for_middle_slot(slot_id, slot_st, slot_en, time_st, time_en)
},
id: task_id,
};
if self.slots[slot_id].tasks.len() == col {
self.slots[slot_id].tasks.push(new_slot);
} else {
self.slots[slot_id].tasks[col] = new_slot;
}
}
}
fn add_task_to_agenda(&mut self, time_start: u32, time_end: u32, id: usize) {
if self.slots.is_empty() {
self.all_day.push(id);
return;
}
let time_end = if time_end < time_start { time_start } else { time_end };
let start_diff = time_start.saturating_sub(self.time_start);
let end_diff = time_end.saturating_sub(self.time_start);
let slot_st = if time_start >= self.time_start { (start_diff / self.slot_size) as usize } else { 0 };
let slot_start_at = slot_st as u32 * self.slot_size + self.time_start;
let mut slot_en = {
let en = (end_diff / self.slot_size) as usize;
if en >= self.slots.len() { self.slots.len() - 1 } else { en }
};
if (time_start < slot_start_at || time_end >= (slot_start_at + self.slot_size))
&& slot_en < self.slots.len() - 1
&& !end_diff.is_multiple_of(self.slot_size)
{
slot_en += 1;
}
let mut max_cols = 0usize;
let mut min_cols = ID_RESERVED;
for slot in &self.slots {
let l = slot.tasks.len();
if l < min_cols {
min_cols = l;
}
if l > max_cols {
max_cols = l;
}
}
let mut col = self.unoccupied_column(slot_st, slot_en, max_cols);
if col == ID_RESERVED {
col = max_cols;
}
self.fill_column(slot_st, slot_en, col, id, time_start, time_end);
}
pub fn fill_agenda(&mut self, tasks: &todo::TaskSlice, ids: &todo::IDSlice, today: NaiveDate) {
for tid in ids {
let task_date = self.task_date(&tasks[*tid], today);
if let Some(dt) = task_date {
if dt != self.date {
continue;
}
} else {
continue;
}
if let Some((task_st, task_en)) = self.task_time(&tasks[*tid]) {
let unlimited = task_st > task_en;
let before = !unlimited && (task_en < self.time_start);
let after = task_st > self.time_end;
if before || after {
continue;
}
self.add_task_to_agenda(task_st, task_en, *tid);
} else {
self.all_day.push(*tid);
}
}
}
pub fn max_columns(&self) -> usize {
let mut max = 0;
for slot in &self.slots {
if slot.tasks.len() > max {
max = slot.tasks.len();
}
}
max
}
pub fn mark(&self, idx: usize) -> char {
if idx >= SLOT_NONE { ' ' } else { self.marks[idx] }
}
}