aboutsummaryrefslogtreecommitdiff
path: root/apps/cassiopeia/src/timeline.rs
use crate::{
    data::{Delta, Invoice, Session},
    error::{UserError, UserResult},
    Date, Time,
};

/// A timeline entry of sessions and invoices
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum Entry {
    Session(Session),
    Invoice(Invoice),
}

impl From<Session> for Entry {
    fn from(s: Session) -> Self {
        Self::Session(s)
    }
}

impl From<Invoice> for Entry {
    fn from(i: Invoice) -> Self {
        Self::Invoice(i)
    }
}

/// A timeline of sessions and invoices, ordered chronologically
#[derive(Debug, Default, Clone)]
pub(crate) struct Timeline {
    inner: Vec<Entry>,
}

impl Timeline {
    /// Take a set of sessions and invoices to sort into a timeline
    pub(crate) fn build(s: Vec<Session>, i: Vec<Invoice>) -> Self {
        let mut inner: Vec<_> = s.into_iter().map(|s| Entry::Session(s)).collect();
        inner.append(&mut i.into_iter().map(|i| Entry::Invoice(i)).collect());
        Self { inner }
    }

    /// Utility function to get the last session in the timeline
    fn last_session(&mut self) -> Option<&mut Session> {
        self.inner
            .iter_mut()
            .find(|e| match e {
                Entry::Session(_) => true,
                _ => false,
            })
            .map(|e| match e {
                Entry::Session(ref mut s) => s,
                _ => unreachable!(),
            })
    }

    /// Utility function to get the last invoice in the timeline
    fn last_invoice(&self) -> Option<&Invoice> {
        self.inner
            .iter()
            .find(|e| match e {
                Entry::Invoice(_) => true,
                _ => false,
            })
            .map(|e| match e {
                Entry::Invoice(ref s) => s,
                _ => unreachable!(),
            })
    }

    /// Get a list of sessions that happened up to a certain invoice date
    ///
    /// **WARNING** If there is no invoice with the given date, this
    /// function will return garbage data, so don't call it with
    /// invoice dates that don't exist.
    ///
    /// Because: if the date passes other invoices on the way, the accumulator
    /// will be discarded and a new count will be started.
    pub(crate) fn session_iter(&self, date: &Date) -> Vec<&Session> {
        self.inner
            .iter()
            .fold((false, vec![]), |(mut done, mut acc), entry| {
                match (done, entry) {
                    // Put sessions into the accumulator
                    (false, Entry::Session(ref s)) => acc.push(s),
                    // When we reach the target invoice, terminate the iterator
                    (false, Entry::Invoice(ref i)) if &i.date == date => done = true,
                    // When we hit another invoice, empty accumulator
                    (false, Entry::Invoice(_)) => acc.clear(),
                    // When we are ever "done", skip all other entries
                    (true, _) => {}
                }

                (done, acc)
            })
            .1
    }

    /// Start a new session, if no active session is already in progress
    pub(crate) fn start(&mut self, time: Time) -> UserResult<Delta> {
        match self.last_session() {
            Some(s) if !s.finished() => Err(UserError::ActiveSessionExists),
            _ => Ok(()),
        }?;

        self.inner.push(Session::start(time.clone()).into());
        Ok(Delta::Start(time))
    }

    /// Stop an ongoing session, if one exists
    pub(crate) fn stop(&mut self, time: Time) -> UserResult<Delta> {
        match self.last_session() {
            Some(s) if s.finished() => Err(UserError::NoActiveSession),
            _ => Ok(()),
        }?;

        self.last_session().unwrap().stop(time.clone());
        Ok(Delta::Stop(time))
    }

    /// Create a new invoice on the given day
    pub(crate) fn invoice(&mut self, date: Date) -> UserResult<Delta> {
        match self.last_invoice() {
            // If an invoice on the same day exists already
            Some(i) if i.date == date => Err(UserError::SameDayInvoice),
            // If there was no work since the last invoice
            Some(ref i) if self.session_iter(&i.date).len() == 0 => Err(UserError::NoWorkInvoice),
            // Otherwise everything is coolio
            _ => Ok(()),
        }?;

        self.inner.push(Invoice::new(date.clone()).into());
        Ok(Delta::Invoice(date))
    }
}