aimcal-cli 0.12.1

AIM - Analyze. Interact. Manage Your Time, with calendar support
Documentation
// SPDX-FileCopyrightText: 2025-2026 Zexin Yuan <aim@yzx9.xyz>
//
// SPDX-License-Identifier: Apache-2.0

use std::{cell::RefCell, error::Error, ops::Deref, rc::Rc};

use aimcal_core::{Aim, Event, EventDraft, EventPatch, EventStatus};

use crate::tui::dispatcher::{Action, Dispatcher};
use crate::util::{format_datetime, parse_datetime};

pub trait EventStoreLike {
    type Output<'a>: Deref<Target = EventStore>
    where
        Self: 'a;

    fn event(&self) -> Self::Output<'_>;
}

#[derive(Debug)]
pub struct EventStore {
    pub data: EventData,
    pub dirty: EventMarker,

    /// Whether the user submit the changes
    pub submit: bool,
}

impl EventStore {
    pub fn from_draft(draft: EventDraft) -> Self {
        Self::new(EventData {
            description: draft.description.unwrap_or_default(),
            end: draft.end.map(format_datetime).unwrap_or_default(),
            start: draft.start.map(format_datetime).unwrap_or_default(),
            status: draft.status,
            summary: draft.summary,
        })
    }

    pub fn from_patch(event: &impl Event, patch: EventPatch) -> Self {
        Self::new(EventData {
            description: match patch.description {
                Some(v) => v.unwrap_or_default(),
                None => event.description().unwrap_or_default().into_owned(),
            },
            start: match patch.start {
                Some(v) => v.map(format_datetime).unwrap_or_default(),
                None => event.start().map(format_datetime).unwrap_or_default(),
            },
            end: match patch.end {
                Some(v) => v.map(format_datetime).unwrap_or_default(),
                None => event.end().map(format_datetime).unwrap_or_default(),
            },
            status: patch.status.or_else(|| event.status()).unwrap_or_default(),
            summary: patch.summary.unwrap_or_else(|| event.summary().to_string()),
        })
    }

    fn new(data: EventData) -> Self {
        Self {
            data,
            dirty: EventMarker::default(),
            submit: false,
        }
    }

    pub fn submit_draft(self, aim: &Aim) -> Result<EventDraft, Box<dyn Error>> {
        Ok(EventDraft {
            calendar_id: None,
            description: self.dirty.description.then_some(self.data.description),
            start: parse_datetime(&aim.now(), &self.data.start)?,
            end: parse_datetime(&aim.now(), &self.data.end)?,
            status: self.data.status,
            summary: if self.data.summary.is_empty() {
                "New event".to_string()
            } else {
                self.data.summary
            },
        })
    }

    pub fn submit_patch(self, aim: &Aim) -> Result<EventPatch, Box<dyn Error>> {
        Ok(EventPatch {
            description: if self.dirty.description {
                if self.data.description.is_empty() {
                    Some(None)
                } else {
                    Some(Some(self.data.description.clone()))
                }
            } else {
                None
            },
            start: if self.dirty.start {
                Some(parse_datetime(&aim.now(), &self.data.start)?)
            } else {
                None
            },
            end: if self.dirty.end {
                Some(parse_datetime(&aim.now(), &self.data.end)?)
            } else {
                None
            },
            status: self.dirty.status.then_some(self.data.status),
            summary: self.dirty.summary.then(|| self.data.summary.clone()),
        })
    }

    pub fn register_to(that: Rc<RefCell<Self>>, dispatcher: &mut Dispatcher) {
        let callback = Rc::new(RefCell::new(move |action: &Action| match action {
            Action::UpdateEventDescription(v) => {
                let mut that = that.borrow_mut();
                that.data.description.clone_from(v);
                that.dirty.description = true;
            }
            Action::UpdateEventStart(v) => {
                let mut that = that.borrow_mut();
                that.data.start.clone_from(v);
                that.dirty.start = true;
            }
            Action::UpdateEventEnd(v) => {
                let mut that = that.borrow_mut();
                that.data.end.clone_from(v);
                that.dirty.end = true;
            }
            Action::UpdateEventStatus(v) => {
                let mut that = that.borrow_mut();
                that.data.status = *v;
                that.dirty.status = true;
            }
            Action::UpdateEventSummary(v) => {
                let mut that = that.borrow_mut();
                that.data.summary.clone_from(v);
                that.dirty.summary = true;
            }
            Action::SubmitChanges => {
                let mut that = that.borrow_mut();
                that.submit = true;
            }
            _ => (),
        }));
        dispatcher.register(callback);
    }
}

impl EventStoreLike for EventStore {
    type Output<'a> = &'a EventStore;

    fn event(&self) -> &EventStore {
        self
    }
}

#[derive(Debug)]
pub struct EventData {
    pub description: String,
    pub start: String,
    pub end: String,
    pub status: EventStatus,
    pub summary: String,
}

impl Default for EventData {
    fn default() -> Self {
        Self {
            description: String::new(),
            start: String::new(),
            end: String::new(),
            status: EventStatus::Confirmed,
            summary: String::new(),
        }
    }
}

#[derive(Debug, Default)]
#[expect(clippy::struct_excessive_bools)]
pub struct EventMarker {
    description: bool,
    start: bool,
    end: bool,
    status: bool,
    summary: bool,
}