1use std::{borrow::Cow, fmt::Display, num::NonZeroU32, str::FromStr};
6
7use aimcal_ical as ical;
8use aimcal_ical::{Description, DtEnd, DtStamp, DtStart, EventStatusValue, Summary, Uid, VEvent};
9use jiff::{Span, ToSpan, Zoned};
10
11use crate::{DateTimeAnchor, LooseDateTime};
12
13pub trait Event {
15 fn short_id(&self) -> Option<NonZeroU32> {
19 None
20 }
21
22 fn uid(&self) -> Cow<'_, str>;
24
25 fn description(&self) -> Option<Cow<'_, str>>;
27
28 fn start(&self) -> Option<LooseDateTime>;
30
31 fn end(&self) -> Option<LooseDateTime>;
33
34 fn status(&self) -> Option<EventStatus>;
36
37 fn summary(&self) -> Cow<'_, str>;
39}
40
41impl Event for VEvent<String> {
42 fn uid(&self) -> Cow<'_, str> {
43 self.uid.content.to_string().into() }
45
46 fn description(&self) -> Option<Cow<'_, str>> {
47 self.description
48 .as_ref()
49 .map(|a| a.content.to_string().into()) }
51
52 fn start(&self) -> Option<LooseDateTime> {
53 Some(self.dt_start.inner.clone().into())
54 }
55
56 fn end(&self) -> Option<LooseDateTime> {
57 self.dt_end.as_ref().map(|dt| dt.inner.clone().into())
58 }
59
60 fn status(&self) -> Option<EventStatus> {
61 self.status.as_ref().map(|s| s.value.into())
62 }
63
64 fn summary(&self) -> Cow<'_, str> {
65 self.summary
66 .as_ref()
67 .map_or_else(|| "".into(), |s| s.content.to_string().into()) }
69}
70
71#[derive(Debug, Clone)]
73pub struct EventDraft {
74 pub description: Option<String>,
76
77 pub start: Option<LooseDateTime>,
79
80 pub end: Option<LooseDateTime>,
82
83 pub status: EventStatus,
85
86 pub summary: String,
88}
89
90impl EventDraft {
91 pub(crate) fn default(now: &Zoned) -> Self {
93 let start = if now.time().minute() < 30 {
95 now.with()
96 .minute(30)
97 .second(0)
98 .subsec_nanosecond(0)
99 .build()
100 .unwrap()
101 } else {
102 (now + Span::new().hours(1))
103 .with()
104 .minute(0)
105 .second(0)
106 .subsec_nanosecond(0)
107 .build()
108 .unwrap()
109 };
110
111 Self {
112 description: None,
113 start: Some(start.clone().into()),
114 end: Some((start.checked_add(1.hours()).unwrap()).into()),
115 status: EventStatus::default(),
116 summary: String::new(),
117 }
118 }
119
120 pub(crate) fn resolve<'a>(&'a self, now: &'a Zoned) -> ResolvedEventDraft<'a> {
121 let default_duration = 1.hours();
122 let (start, end) = match (self.start.as_ref(), self.end.as_ref()) {
123 (Some(start), Some(end)) => (start.clone(), end.clone()),
124 (None, Some(end)) => {
125 let neg_duration = Span::new().hours(-1);
127 let start = match end {
128 LooseDateTime::DateOnly(d) => (*d).into(),
129 LooseDateTime::Floating(dt) => {
130 LooseDateTime::Floating(dt.checked_add(neg_duration).unwrap())
131 }
132 LooseDateTime::Local(dt) => {
133 LooseDateTime::Local(dt.checked_add(neg_duration).unwrap())
134 }
135 };
136 (start, end.clone())
137 }
138 (Some(start), None) => {
139 let end = match start {
141 LooseDateTime::DateOnly(d) => (*d).into(),
142 LooseDateTime::Floating(dt) => {
143 LooseDateTime::Floating(dt.checked_add(default_duration).unwrap())
144 }
145 LooseDateTime::Local(dt) => {
146 LooseDateTime::Local(dt.checked_add(default_duration).unwrap())
147 }
148 };
149 (start.clone(), end)
150 }
151 (None, None) => {
152 let end = now.checked_add(default_duration).unwrap();
153 (LooseDateTime::Local(now.clone()), LooseDateTime::Local(end))
154 }
155 };
156
157 ResolvedEventDraft {
158 description: self.description.as_deref(),
159 start,
160 end,
161 status: self.status,
162 summary: &self.summary,
163
164 now,
165 }
166 }
167}
168
169#[derive(Debug, Clone)]
170pub struct ResolvedEventDraft<'a> {
171 pub description: Option<&'a str>,
172 pub start: LooseDateTime,
173 pub end: LooseDateTime,
174 pub status: EventStatus,
175 pub summary: &'a str,
176
177 pub now: &'a Zoned,
178}
179
180impl ResolvedEventDraft<'_> {
181 pub(crate) fn into_ics(self, uid: &str) -> VEvent<String> {
183 VEvent {
184 uid: Uid::new(uid.to_string()),
185 dt_stamp: DtStamp::new(ical::DateTime::from(LooseDateTime::Local(self.now.clone()))),
186 dt_start: DtStart::new(self.start.into()),
187 dt_end: Some(DtEnd::new(self.end.into())),
188 duration: None,
189 summary: Some(Summary::new(self.summary.to_string())),
190 description: self.description.map(|d| Description::new(d.to_string())),
191 status: Some(ical::EventStatus::new(self.status.into())),
192 location: None,
193 geo: None,
194 url: None,
195 organizer: None,
196 attendees: Vec::new(),
197 last_modified: None,
198 transparency: None,
199 sequence: None,
200 priority: None,
201 classification: None,
202 resources: None,
203 categories: None,
204 rrule: None,
205 rdates: Vec::new(),
206 ex_dates: Vec::new(),
207 x_properties: Vec::new(),
208 retained_properties: Vec::new(),
209 alarms: Vec::new(),
210 }
211 }
212}
213
214#[derive(Debug, Default, Clone)]
216pub struct EventPatch {
217 pub description: Option<Option<String>>,
219
220 pub start: Option<Option<LooseDateTime>>,
222
223 pub end: Option<Option<LooseDateTime>>,
225
226 pub status: Option<EventStatus>,
228
229 pub summary: Option<String>,
231}
232
233impl EventPatch {
234 #[must_use]
236 pub fn is_empty(&self) -> bool {
237 self.description.is_none()
238 && self.start.is_none()
239 && self.end.is_none()
240 && self.status.is_none()
241 && self.summary.is_none()
242 }
243
244 pub(crate) fn resolve(&self, now: Zoned) -> ResolvedEventPatch<'_> {
245 ResolvedEventPatch {
246 description: self.description.as_ref().map(|opt| opt.as_deref()),
247 start: self.start.clone(),
248 end: self.end.clone(),
249 status: self.status,
250 summary: self.summary.as_deref(),
251
252 now,
253 }
254 }
255}
256
257#[derive(Debug, Default, Clone)]
259pub struct ResolvedEventPatch<'a> {
260 pub description: Option<Option<&'a str>>,
261 pub start: Option<Option<LooseDateTime>>,
262 pub end: Option<Option<LooseDateTime>>,
263 pub status: Option<EventStatus>,
264 pub summary: Option<&'a str>,
265
266 pub now: Zoned,
267}
268
269impl ResolvedEventPatch<'_> {
270 pub fn apply_to<'a>(&self, e: &'a mut VEvent<String>) -> &'a mut VEvent<String> {
272 if let Some(Some(desc)) = &self.description {
273 e.description = Some(Description::new((*desc).to_string()));
274 } else if self.description.is_some() {
275 e.description = None;
276 }
277
278 if let Some(Some(ref start)) = self.start {
279 e.dt_start = DtStart::new(start.clone().into());
280 }
281
282 if let Some(Some(ref end)) = self.end {
283 e.dt_end = Some(DtEnd::new(end.clone().into()));
284 } else if self.end.is_some() {
285 e.dt_end = None;
286 }
287
288 if let Some(status) = self.status {
289 e.status = Some(ical::EventStatus::new(status.into()));
290 }
291
292 if let Some(summary) = &self.summary {
293 e.summary = Some(Summary::new((*summary).to_string()));
294 }
295
296 if e.dt_stamp.inner.date().year == 1970 {
298 e.dt_stamp = DtStamp::new(ical::DateTime::from(LooseDateTime::Local(self.now.clone())));
299 }
300
301 e
302 }
303}
304
305#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
307#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
308pub enum EventStatus {
309 Tentative,
311
312 #[default]
314 Confirmed,
315
316 Cancelled,
318}
319
320const STATUS_TENTATIVE: &str = "TENTATIVE";
322const STATUS_CONFIRMED: &str = "CONFIRMED";
323const STATUS_CANCELLED: &str = "CANCELLED";
324
325impl AsRef<str> for EventStatus {
326 fn as_ref(&self) -> &str {
327 match self {
328 EventStatus::Tentative => STATUS_TENTATIVE,
329 EventStatus::Confirmed => STATUS_CONFIRMED,
330 EventStatus::Cancelled => STATUS_CANCELLED,
331 }
332 }
333}
334
335impl Display for EventStatus {
336 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337 self.as_ref().fmt(f)
338 }
339}
340
341impl FromStr for EventStatus {
342 type Err = ();
343
344 fn from_str(value: &str) -> Result<Self, Self::Err> {
345 match value {
346 STATUS_TENTATIVE => Ok(EventStatus::Tentative),
347 STATUS_CONFIRMED => Ok(EventStatus::Confirmed),
348 STATUS_CANCELLED => Ok(EventStatus::Cancelled),
349 _ => Err(()),
350 }
351 }
352}
353
354impl From<EventStatusValue> for EventStatus {
355 fn from(value: EventStatusValue) -> Self {
356 match value {
357 EventStatusValue::Tentative => EventStatus::Tentative,
358 EventStatusValue::Confirmed => EventStatus::Confirmed,
359 EventStatusValue::Cancelled => EventStatus::Cancelled,
360 }
361 }
362}
363
364impl From<EventStatus> for EventStatusValue {
365 fn from(value: EventStatus) -> Self {
366 match value {
367 EventStatus::Tentative => EventStatusValue::Tentative,
368 EventStatus::Confirmed => EventStatusValue::Confirmed,
369 EventStatus::Cancelled => EventStatusValue::Cancelled,
370 }
371 }
372}
373
374#[derive(Debug, Default, Clone)]
376pub struct EventConditions {
377 pub startable: Option<DateTimeAnchor>,
379
380 pub cutoff: Option<DateTimeAnchor>,
382}
383
384impl EventConditions {
385 pub(crate) fn resolve(&self, now: &Zoned) -> Result<ResolvedEventConditions, String> {
386 Ok(ResolvedEventConditions {
387 start_before: self
388 .cutoff
389 .as_ref()
390 .map(|w| w.resolve_at_end_of_day(now))
391 .transpose()?,
392 end_after: self
393 .startable
394 .as_ref()
395 .map(|w| w.resolve_at_start_of_day(now))
396 .transpose()?,
397 })
398 }
399}
400
401#[derive(Debug, Clone)]
402pub struct ResolvedEventConditions {
403 pub start_before: Option<Zoned>,
405
406 pub end_after: Option<Zoned>,
408}