/* Copyright 2024, Savanni D'Gerinel This file is part of FitnessTrax. FitnessTrax is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. FitnessTrax is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with FitnessTrax. If not, see . */ use crate::app::App; use dimensioned::si; use emseries::{Record, RecordId, Recordable}; use ft_core::{TimeDistance, TraxRecord}; use std::{ collections::HashMap, ops::Deref, sync::{Arc, RwLock}, }; #[derive(Clone, Debug)] enum RecordState { Original(Record), New(T), Updated(Record), #[allow(unused)] Deleted(Record), } impl RecordState { #[allow(unused)] fn id(&self) -> Option<&RecordId> { match self { RecordState::Original(ref r) => Some(&r.id), RecordState::New(ref r) => None, RecordState::Updated(ref r) => Some(&r.id), RecordState::Deleted(ref r) => Some(&r.id), } } fn data(&self) -> Option<&Record> { match self { RecordState::Original(ref r) => Some(&r), RecordState::New(ref r) => None, RecordState::Updated(ref r) => Some(&r), RecordState::Deleted(ref r) => Some(&r), } } fn with_value(self, value: T) -> RecordState { match self { RecordState::Original(r) => RecordState::Updated(Record { data: value, ..r }), RecordState::New(_) => RecordState::New(value), RecordState::Updated(r) => RecordState::Updated(Record { data: value, ..r }), RecordState::Deleted(r) => RecordState::Updated(Record { data: value, ..r }), } } #[allow(unused)] fn with_delete(self) -> Option> { match self { RecordState::Original(r) => Some(RecordState::Deleted(r)), RecordState::New(r) => None, RecordState::Updated(r) => Some(RecordState::Deleted(r)), RecordState::Deleted(r) => Some(RecordState::Deleted(r)), } } } impl Deref for RecordState { type Target = T; fn deref(&self) -> &Self::Target { match self { RecordState::Original(ref r) => &r.data, RecordState::New(ref r) => r, RecordState::Updated(ref r) => &r.data, RecordState::Deleted(ref r) => &r.data, } } } #[derive(Default)] struct DayDetailViewModelInner {} #[derive(Clone, Default)] pub struct DayDetailViewModel { app: Option, pub date: chrono::NaiveDate, weight: Arc>>>, steps: Arc>>>, records: Arc>>>, original_records: Vec>, } impl DayDetailViewModel { pub fn new(date: chrono::NaiveDate, records: Vec>, app: App) -> Self { let s = Self { app: Some(app), date, weight: Arc::new(RwLock::new(None)), steps: Arc::new(RwLock::new(None)), records: Arc::new(RwLock::new(HashMap::new())), original_records: records, }; s.populate_records(); s } pub fn weight(&self) -> Option> { (*self.weight.read().unwrap()).as_ref().map(|w| w.weight) } pub fn set_weight(&self, new_weight: si::Kilogram) { let mut record = self.weight.write().unwrap(); let new_record = match *record { Some(ref rstate) => rstate.clone().with_value(ft_core::Weight { date: self.date, weight: new_weight, }), None => RecordState::New(ft_core::Weight { date: self.date, weight: new_weight, }), }; *record = Some(new_record); } pub fn steps(&self) -> Option { (*self.steps.read().unwrap()).as_ref().map(|w| w.count) } pub fn set_steps(&self, new_count: u32) { let mut record = self.steps.write().unwrap(); let new_record = match *record { Some(ref rstate) => rstate.clone().with_value(ft_core::Steps { date: self.date, count: new_count, }), None => RecordState::New(ft_core::Steps { date: self.date, count: new_count, }), }; *record = Some(new_record); } pub fn biking_summary(&self) -> (si::Meter, si::Second) { self.records.read().unwrap().iter().fold( (0. * si::M, 0. * si::S), |(acc_distance, acc_duration), (_, record)| match record.data() { Some(Record { data: TraxRecord::BikeRide(TimeDistance { distance, duration, .. }), .. }) => ( distance .map(|distance| acc_distance + distance) .unwrap_or(acc_distance), (duration .map(|duration| acc_duration + duration) .unwrap_or(acc_duration)), ), _ => (acc_distance, acc_duration), }, ) } pub fn records(&self) -> Vec> { let read_lock = self.records.read().unwrap(); read_lock .iter() .map(|(_, record_state)| record_state.data()) .filter_map(|r| r) .cloned() .collect::>>() } pub fn save(&self) { glib::spawn_future({ let s = self.clone(); async move { if let Some(app) = s.app { let weight_record = s.weight.read().unwrap().clone(); match weight_record { Some(RecordState::New(weight)) => { let _ = app.put_record(TraxRecord::Weight(weight)).await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(weight)) => { let _ = app .update_record(Record { id: weight.id, data: TraxRecord::Weight(weight.data), }) .await; } Some(RecordState::Deleted(_)) => {} None => {} } let steps_record = s.steps.read().unwrap().clone(); match steps_record { Some(RecordState::New(steps)) => { let _ = app.put_record(TraxRecord::Steps(steps)).await; } Some(RecordState::Original(_)) => {} Some(RecordState::Updated(steps)) => { let _ = app .update_record(Record { id: steps.id, data: TraxRecord::Steps(steps.data), }) .await; } Some(RecordState::Deleted(_)) => {} None => {} } let records = s .records .write() .unwrap() .drain() .map(|(_, record)| record) .collect::>>(); for record in records { match record { RecordState::New(data) => { let _ = app.put_record(data).await; } RecordState::Original(_) => {} RecordState::Updated(r) => { let _ = app.update_record(r.clone()).await; } RecordState::Deleted(_) => unimplemented!(), } } } } }); } pub fn revert(&self) { self.populate_records(); } fn populate_records(&self) { let records = self.original_records.clone(); let (weight_records, records): (Vec>, Vec>) = records.into_iter().partition(|r| r.data.is_weight()); let (step_records, records): (Vec>, Vec>) = records.into_iter().partition(|r| r.data.is_steps()); *self.weight.write().unwrap() = weight_records .first() .and_then(|r| match r.data { TraxRecord::Weight(ref w) => Some((r.id.clone(), w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })); *self.steps.write().unwrap() = step_records .first() .and_then(|r| match r.data { TraxRecord::Steps(ref w) => Some((r.id.clone(), w.clone())), _ => None, }) .map(|(id, w)| RecordState::Original(Record { id, data: w })); *self.records.write().unwrap() = records .into_iter() .map(|r| (r.id.clone(), RecordState::Original(r))) .collect::>>(); } } /* struct SavedRecordIterator<'a> { read_lock: RwLockReadGuard<'a, HashMap>>, iter: Box> + 'a>, } impl<'a> SavedRecordIterator<'a> { fn new(records: Arc>>>) -> Self { let read_lock = records.read().unwrap(); let iter = read_lock .iter() .map(|(_, record_state)| record_state.data()) .filter_map(|r| r); Self { read_lock, iter: Box::new(iter), } } } impl<'a> Iterator for SavedRecordIterator<'a> { type Item = &'a Record; fn next(&mut self) -> Option { None } } */