1use std::{fmt::Display, num::NonZeroU32, str::FromStr};
6
7use chrono::{DateTime, Duration, Local, Timelike};
8use icalendar::{Component, EventLike};
9
10use crate::{DateTimeAnchor, LooseDateTime};
11
12pub trait Event {
14 fn short_id(&self) -> Option<NonZeroU32> {
18 None
19 }
20
21 fn uid(&self) -> &str;
23
24 fn description(&self) -> Option<&str>;
26
27 fn start(&self) -> Option<LooseDateTime>;
29
30 fn end(&self) -> Option<LooseDateTime>;
32
33 fn status(&self) -> Option<EventStatus>;
35
36 fn summary(&self) -> &str;
38}
39
40impl Event for icalendar::Event {
41 fn uid(&self) -> &str {
42 self.get_uid().unwrap_or_default()
43 }
44
45 fn description(&self) -> Option<&str> {
46 self.get_description()
47 }
48
49 fn start(&self) -> Option<LooseDateTime> {
50 self.get_start().map(Into::into)
51 }
52
53 fn end(&self) -> Option<LooseDateTime> {
54 self.get_end().map(Into::into)
55 }
56
57 fn status(&self) -> Option<EventStatus> {
58 self.get_status().map(EventStatus::from)
59 }
60
61 fn summary(&self) -> &str {
62 self.get_summary().unwrap_or_default()
63 }
64}
65
66#[derive(Debug)]
68pub struct EventDraft {
69 pub description: Option<String>,
71
72 pub start: Option<LooseDateTime>,
74
75 pub end: Option<LooseDateTime>,
77
78 pub status: EventStatus,
80
81 pub summary: String,
83}
84
85impl EventDraft {
86 pub(crate) fn default(now: DateTime<Local>) -> Self {
88 let start = if now.minute() < 30 {
90 now.with_minute(30).unwrap().with_second(0).unwrap()
91 } else {
92 (now + Duration::hours(1))
93 .with_minute(0)
94 .unwrap()
95 .with_second(0)
96 .unwrap()
97 };
98
99 Self {
100 description: None,
101 start: Some(start.into()),
102 end: Some((start + Duration::hours(1)).into()),
103 status: EventStatus::default(),
104 summary: String::new(),
105 }
106 }
107
108 pub(crate) fn into_ics(self, now: &DateTime<Local>, uid: &str) -> icalendar::Event {
110 let mut event = icalendar::Event::with_uid(uid);
111
112 if let Some(description) = self.description {
113 Component::description(&mut event, &description);
114 }
115
116 let default_duration = Duration::hours(1);
117 let (start, end) = match (self.start, self.end) {
118 (Some(start), Some(end)) => (start, end),
119 (None, Some(end)) => {
120 let start = match end {
122 LooseDateTime::DateOnly(d) => d.into(),
123 LooseDateTime::Floating(dt) => (dt - default_duration).into(),
124 LooseDateTime::Local(dt) => (dt - default_duration).into(),
125 };
126 (start, end)
127 }
128 (Some(start), None) => {
129 let end = match start {
131 LooseDateTime::DateOnly(d) => d.into(),
132 LooseDateTime::Floating(dt) => (dt + default_duration).into(),
133 LooseDateTime::Local(dt) => (dt + default_duration).into(),
134 };
135 (start, end)
136 }
137 (None, None) => {
138 let start = *now;
139 let end = (start + default_duration).into();
140 (start.into(), end)
141 }
142 };
143 EventLike::starts(&mut event, start);
144 EventLike::ends(&mut event, end);
145
146 icalendar::Event::status(&mut event, self.status.into());
147
148 Component::summary(&mut event, &self.summary);
149
150 event
151 }
152}
153
154#[derive(Debug, Default, Clone)]
156pub struct EventPatch {
157 pub description: Option<Option<String>>,
159
160 pub start: Option<Option<LooseDateTime>>,
162
163 pub end: Option<Option<LooseDateTime>>,
165
166 pub status: Option<EventStatus>,
168
169 pub summary: Option<String>,
171}
172
173impl EventPatch {
174 pub fn is_empty(&self) -> bool {
176 self.description.is_none()
177 && self.start.is_none()
178 && self.end.is_none()
179 && self.status.is_none()
180 && self.summary.is_none()
181 }
182
183 pub(crate) fn apply_to<'a>(&self, e: &'a mut icalendar::Event) -> &'a mut icalendar::Event {
185 if let Some(description) = &self.description {
186 match description {
187 Some(desc) => e.description(desc),
188 None => e.remove_description(),
189 };
190 }
191
192 if let Some(start) = &self.start {
193 match start {
194 Some(s) => e.starts(*s),
195 None => e.remove_starts(),
196 };
197 }
198
199 if let Some(end) = &self.end {
200 match end {
201 Some(ed) => e.ends(*ed),
202 None => e.remove_ends(),
203 };
204 }
205
206 if let Some(status) = self.status {
207 e.status(status.into());
208 }
209
210 if let Some(summary) = &self.summary {
211 e.summary(summary);
212 }
213
214 e
215 }
216}
217
218#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
220#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
221pub enum EventStatus {
222 Tentative,
224
225 #[default]
227 Confirmed,
228
229 Cancelled,
231}
232
233const STATUS_TENTATIVE: &str = "TENTATIVE";
234const STATUS_CONFIRMED: &str = "CONFIRMED";
235const STATUS_CANCELLED: &str = "CANCELLED";
236
237impl AsRef<str> for EventStatus {
238 fn as_ref(&self) -> &str {
239 match self {
240 EventStatus::Tentative => STATUS_TENTATIVE,
241 EventStatus::Confirmed => STATUS_CONFIRMED,
242 EventStatus::Cancelled => STATUS_CANCELLED,
243 }
244 }
245}
246
247impl Display for EventStatus {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 write!(f, "{}", self.as_ref())
250 }
251}
252
253impl FromStr for EventStatus {
254 type Err = ();
255
256 fn from_str(value: &str) -> Result<Self, Self::Err> {
257 match value {
258 STATUS_TENTATIVE => Ok(EventStatus::Tentative),
259 STATUS_CONFIRMED => Ok(EventStatus::Confirmed),
260 STATUS_CANCELLED => Ok(EventStatus::Cancelled),
261 _ => Err(()),
262 }
263 }
264}
265
266impl From<EventStatus> for icalendar::EventStatus {
267 fn from(status: EventStatus) -> Self {
268 match status {
269 EventStatus::Tentative => icalendar::EventStatus::Tentative,
270 EventStatus::Confirmed => icalendar::EventStatus::Confirmed,
271 EventStatus::Cancelled => icalendar::EventStatus::Cancelled,
272 }
273 }
274}
275
276impl From<icalendar::EventStatus> for EventStatus {
277 fn from(status: icalendar::EventStatus) -> Self {
278 match status {
279 icalendar::EventStatus::Tentative => EventStatus::Tentative,
280 icalendar::EventStatus::Confirmed => EventStatus::Confirmed,
281 icalendar::EventStatus::Cancelled => EventStatus::Cancelled,
282 }
283 }
284}
285
286#[derive(Debug, Default, Clone, Copy)]
288pub struct EventConditions {
289 pub startable: Option<DateTimeAnchor>,
291
292 pub cutoff: Option<DateTimeAnchor>,
294}
295
296#[derive(Debug)]
297pub(crate) struct ParsedEventConditions {
298 pub start_before: Option<DateTime<Local>>,
300
301 pub end_after: Option<DateTime<Local>>,
303}
304
305impl ParsedEventConditions {
306 pub fn parse(now: &DateTime<Local>, conds: &EventConditions) -> Self {
307 Self {
308 start_before: conds.cutoff.map(|w| w.resolve_at_end_of_day(now)),
309 end_after: conds.startable.map(|w| w.resolve_at_start_of_day(now)),
310 }
311 }
312}