tallyweb_frontend/countable/
countable.rs

1use chrono::TimeDelta;
2use leptos::{SignalGetUntracked, SignalUpdateUntracked};
3use serde::{Deserialize, Serialize};
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::sync::{Arc, Mutex};
7
8use super::*;
9
10#[derive(Debug, Default, Clone, Serialize, Deserialize)]
11pub struct CountableStore {
12    owner: uuid::Uuid,
13    store: HashMap<CountableId, Countable>,
14    selection: Vec<CountableId>,
15    is_changed: RefCell<bool>,
16}
17
18impl CountableStore {
19    pub fn new(owner: uuid::Uuid, store: HashMap<CountableId, Countable>) -> Self {
20        Self {
21            owner,
22            store,
23            ..Default::default()
24        }
25    }
26
27    pub fn owner(&self) -> uuid::Uuid {
28        self.owner
29    }
30
31    pub fn merge_checked(&mut self, other: Self) -> Result<(), AppError> {
32        for (id, other_c) in other.store {
33            if other_c.is_archived() {
34                self.store.insert(id, other_c);
35            } else if let Some(c) = self.get(&id)
36                && (c.last_edit_checked()? > other_c.last_edit_checked()? || c.is_archived())
37            {
38                continue;
39            } else {
40                self.store.insert(id, other_c);
41            }
42        }
43
44        self.is_changed.replace(true);
45
46        Ok(())
47    }
48
49    pub fn merge(&mut self, other: Self) {
50        self.merge_checked(other).unwrap()
51    }
52
53    pub fn contains(&self, countable: &CountableId) -> bool {
54        self.store.contains_key(countable)
55    }
56
57    pub fn get(&self, countable: &CountableId) -> Option<Countable> {
58        self.store.get(countable).cloned()
59    }
60
61    pub fn len(&self) -> usize {
62        self.store.len()
63    }
64
65    pub fn new_countable_checked(
66        &mut self,
67        name: &str,
68        kind: CountableKind,
69        parent: Option<CountableId>,
70    ) -> Result<CountableId, AppError> {
71        let countable = Countable::new(name, kind, self.owner, parent);
72        let key = countable.clone().into();
73        self.store.insert(key, countable);
74        if let Some(parent) = parent {
75            self.get(&parent)
76                .ok_or(AppError::CountableNotFound)?
77                .add_child_checked(key)?
78        }
79
80        self.is_changed.replace(true);
81
82        Ok(key)
83    }
84
85    pub fn new_countable(
86        &mut self,
87        name: &str,
88        kind: CountableKind,
89        parent: Option<CountableId>,
90    ) -> CountableId {
91        self.new_countable_checked(name, kind, parent).unwrap()
92    }
93
94    pub fn filter(&self, filter: impl Fn(&Countable) -> bool) -> Self {
95        let mut store = self.raw_filter(filter);
96        // add back any missing parents
97        for i in store.store.clone().into_keys() {
98            let mut id = i;
99            while let Some(p) = self.parent(&id) {
100                id = p.uuid().into();
101                store.store.insert(id, p);
102            }
103        }
104
105        store
106    }
107
108    pub fn raw_filter(&self, filter: impl Fn(&Countable) -> bool) -> Self {
109        let store: HashMap<CountableId, Countable> = self
110            .store
111            .iter()
112            .filter(|(_, b)| filter(b))
113            .map(|(a, b)| (*a, b.clone()))
114            .collect();
115
116        Self {
117            owner: self.owner,
118            store,
119            selection: self.selection.clone(),
120            ..Default::default()
121        }
122    }
123
124    pub fn archive(&mut self, countable: &CountableId) -> Option<Countable> {
125        for child in self.children_checked(countable).ok()? {
126            self.archive(&child.uuid_checked().ok()?.into())?;
127        }
128
129        match self.get(countable)? {
130            Countable::Counter(c) => c.lock().ok()?.is_deleted = true,
131            Countable::Phase(p) => p.lock().ok()?.is_deleted = true,
132            Countable::Chain(_) => todo!(),
133        }
134
135        self.get(countable)
136    }
137
138    pub fn root_nodes(&self) -> Vec<Countable> {
139        self.store
140            .values()
141            .filter(|v| self.parent(&v.uuid().into()).is_none())
142            .cloned()
143            .collect()
144    }
145
146    pub fn nodes(&self) -> Vec<Countable> {
147        self.store.values().cloned().collect()
148    }
149
150    pub fn has_child(&self, countable: &CountableId, child: &CountableId) -> bool {
151        self.children(countable)
152            .into_iter()
153            .map(CountableId::from)
154            .collect::<Vec<_>>()
155            .contains(child)
156    }
157
158    pub fn children_checked(&self, countable: &CountableId) -> Result<Vec<Countable>, AppError> {
159        Ok(
160            match self
161                .store
162                .get(countable)
163                .ok_or(AppError::CountableNotFound)?
164            {
165                Countable::Counter(c) => {
166                    let children = c.lock()?.children.clone();
167                    children
168                        .into_iter()
169                        .filter_map(|id| self.store.get(&id).cloned())
170                        .collect()
171                }
172                _ => Vec::new(),
173            },
174        )
175    }
176
177    pub fn children(&self, countable: &CountableId) -> Vec<Countable> {
178        self.children_checked(countable).unwrap()
179    }
180
181    pub fn parent_checked(&self, countable: &CountableId) -> Result<Option<Countable>, AppError> {
182        Ok(match self.store.get(countable) {
183            Some(Countable::Counter(c)) => {
184                c.lock()?.parent.and_then(|id| self.store.get(&id)).cloned()
185            }
186            Some(Countable::Phase(p)) => self.store.get(&p.lock()?.parent).cloned(),
187            Some(Countable::Chain(_)) => todo!(),
188            None => None,
189        })
190    }
191
192    pub fn parent(&self, countable: &CountableId) -> Option<Countable> {
193        self.parent_checked(countable).unwrap()
194    }
195
196    pub fn last_child_checked(&self, countable: &CountableId) -> Result<Countable, AppError> {
197        let children = self.children_checked(countable)?;
198        if children.is_empty() {
199            return self
200                .store
201                .get(countable)
202                .ok_or(AppError::CountableNotFound)
203                .cloned();
204        } else {
205            self.last_child_checked(&children.last().unwrap().uuid_checked()?.into())
206        }
207    }
208
209    pub fn last_child(&self, countable: &CountableId) -> Countable {
210        self.last_child_checked(countable).unwrap()
211    }
212
213    pub fn kind_checked(&self, countable: &CountableId) -> Result<CountableKind, AppError> {
214        Ok(
215            match self
216                .store
217                .get(countable)
218                .ok_or(AppError::CountableNotFound)?
219            {
220                Countable::Counter(_) => CountableKind::Counter,
221                Countable::Phase(_) => CountableKind::Phase,
222                Countable::Chain(_) => CountableKind::Chain,
223            },
224        )
225    }
226
227    pub fn kind(&self, countable: &CountableId) -> CountableKind {
228        self.kind_checked(countable).unwrap()
229    }
230
231    pub fn name_checked(&self, countable: &CountableId) -> Result<String, AppError> {
232        self.store
233            .get(countable)
234            .ok_or(AppError::CountableNotFound)?
235            .name_checked()
236    }
237
238    pub fn name(&self, countable: &CountableId) -> String {
239        self.name_checked(countable).unwrap()
240    }
241
242    pub fn set_name_checked(&self, countable: &CountableId, name: &str) -> Result<(), AppError> {
243        match self
244            .store
245            .get(countable)
246            .ok_or(AppError::CountableNotFound)?
247        {
248            Countable::Counter(c) => c.lock()?.name = name.into(),
249            Countable::Phase(p) => p.lock()?.name = name.into(),
250            Countable::Chain(_) => todo!(),
251        };
252
253        self.is_changed.replace(true);
254
255        Ok(())
256    }
257
258    pub fn set_name(&self, countable: &CountableId, name: &str) {
259        self.set_name_checked(countable, name).unwrap()
260    }
261
262    pub fn count_checked(&self, countable: &CountableId) -> Result<i32, AppError> {
263        Ok(
264            match self
265                .store
266                .get(countable)
267                .ok_or(AppError::CountableNotFound)?
268            {
269                Countable::Counter(c) => {
270                    let mut sum = 0;
271                    for child in c.lock()?.children.iter() {
272                        sum += self.count_checked(child)?;
273                    }
274                    sum
275                }
276                Countable::Phase(p) => p.lock()?.count,
277                Countable::Chain(_) => todo!(),
278            },
279        )
280    }
281
282    pub fn count(&self, countable: &CountableId) -> i32 {
283        self.count_checked(countable).unwrap()
284    }
285
286    pub fn set_count_checked(&self, countable: &CountableId, count: i32) -> Result<(), AppError> {
287        let diff = count - self.count_checked(countable)?;
288        self.add_count_checked(countable, diff)?;
289        self.is_changed.replace(true);
290        Ok(())
291    }
292
293    pub fn set_count(&self, countable: &CountableId, count: i32) {
294        self.set_count_checked(countable, count).unwrap()
295    }
296
297    pub fn add_count_checked(&self, countable: &CountableId, mut add: i32) -> Result<(), AppError> {
298        match self
299            .store
300            .get(countable)
301            .ok_or(AppError::CountableNotFound)?
302        {
303            Countable::Counter(c) => {
304                for child in c.lock()?.children.iter().rev() {
305                    let child_count = self.count_checked(child)?;
306                    if child_count + add <= 0 {
307                        self.set_count_checked(child, 0)?;
308                        add += self.count_checked(child)?;
309                    } else {
310                        self.set_count_checked(child, child_count + add)?;
311                        return Ok(());
312                    }
313                }
314            }
315            Countable::Phase(p) => {
316                p.lock()?.count += add;
317            }
318            Countable::Chain(_) => todo!(),
319        }
320
321        self.is_changed.replace(true);
322
323        Ok(())
324    }
325
326    pub fn add_count(&self, countable: &CountableId, add: i32) {
327        self.add_count_checked(countable, add).unwrap();
328    }
329
330    pub fn time_checked(&self, countable: &CountableId) -> Result<TimeDelta, AppError> {
331        match self
332            .store
333            .get(countable)
334            .ok_or(AppError::CountableNotFound)?
335        {
336            Countable::Counter(c) => {
337                let mut time = TimeDelta::zero();
338                for child in c.lock()?.children.iter() {
339                    time += self.time_checked(child)?;
340                }
341                Ok(time)
342            }
343            Countable::Phase(p) => Ok(p.lock()?.time),
344            Countable::Chain(_) => todo!(),
345        }
346    }
347
348    pub fn time(&self, countable: &CountableId) -> TimeDelta {
349        self.time_checked(countable).unwrap()
350    }
351
352    pub fn set_time_checked(
353        &self,
354        countable: &CountableId,
355        time: TimeDelta,
356    ) -> Result<(), AppError> {
357        let diff = time - self.time_checked(countable)?;
358        self.add_time_checked(countable, diff)?;
359        self.is_changed.replace(true);
360        Ok(())
361    }
362
363    pub fn set_time(&self, countable: &CountableId, time: TimeDelta) {
364        self.set_time_checked(countable, time).unwrap()
365    }
366
367    pub fn add_time_checked(
368        &self,
369        countable: &CountableId,
370        mut add: TimeDelta,
371    ) -> Result<(), AppError> {
372        match self
373            .store
374            .get(countable)
375            .ok_or(AppError::CountableNotFound)?
376        {
377            Countable::Counter(c) => {
378                for child in c.lock()?.children.iter().rev() {
379                    let child_time = self.time_checked(child)?;
380                    if child_time + add <= TimeDelta::zero() {
381                        self.set_time_checked(child, TimeDelta::zero())?;
382                        add += self.time_checked(child)?;
383                    } else {
384                        self.set_time_checked(child, child_time + add)?;
385                    }
386                }
387            }
388            Countable::Phase(p) => {
389                p.lock()?.time += add;
390            }
391            Countable::Chain(_) => todo!(),
392        }
393
394        self.is_changed.replace(true);
395
396        Ok(())
397    }
398
399    pub fn add_time(&self, countable: &CountableId, add: TimeDelta) {
400        self.add_time_checked(countable, add).unwrap();
401    }
402
403    pub fn hunttype_checked(&self, countable: &CountableId) -> Result<Hunttype, AppError> {
404        match self
405            .store
406            .get(countable)
407            .ok_or(AppError::CountableNotFound)?
408        {
409            Countable::Counter(c) => {
410                let children = c.lock()?.children.clone();
411
412                let ht = children
413                    .first()
414                    .and_then(|child| self.hunttype_checked(child).ok())
415                    .unwrap_or_default();
416
417                for child in children.iter() {
418                    if self.hunttype_checked(child)? != ht {
419                        return Ok(Hunttype::Mixed);
420                    }
421                }
422                Ok(ht)
423            }
424            Countable::Phase(p) => Ok(p.lock()?.hunt_type),
425            Countable::Chain(_) => todo!(),
426        }
427    }
428
429    pub fn hunttype(&self, countable: &CountableId) -> Hunttype {
430        self.hunttype_checked(countable).unwrap()
431    }
432
433    pub fn rolls_checked(&self, countable: &CountableId) -> Result<usize, AppError> {
434        Ok(
435            match self
436                .store
437                .get(countable)
438                .ok_or(AppError::CountableNotFound)?
439            {
440                Countable::Counter(c) => c
441                    .lock()?
442                    .children
443                    .iter()
444                    .map(|child| self.rolls_checked(child))
445                    .collect::<Result<Vec<_>, AppError>>()?
446                    .into_iter()
447                    .sum(),
448                Countable::Phase(_) => self.hunttype_checked(countable)?.rolls()(
449                    self.count_checked(countable)?,
450                    self.has_charm_checked(countable)?,
451                ),
452                Countable::Chain(_) => todo!(),
453            },
454        )
455    }
456
457    pub fn rolls(&self, countable: &CountableId) -> usize {
458        self.rolls_checked(countable).unwrap()
459    }
460
461    pub fn odds_checked(&self, countable: &CountableId) -> Result<f64, AppError> {
462        Ok(
463            match self
464                .store
465                .get(countable)
466                .ok_or(AppError::CountableNotFound)?
467            {
468                Countable::Counter(c) => {
469                    let sum = c
470                        .lock()?
471                        .children
472                        .iter()
473                        .map(|child| {
474                            let odds = self.odds_checked(child)?;
475                            Ok(odds * self.count_checked(child)? as f64)
476                        })
477                        .collect::<Result<Vec<_>, AppError>>()?
478                        .into_iter()
479                        .sum::<f64>();
480                    sum / (self.count_checked(countable)? as f64).max(1.0)
481                }
482                Countable::Phase(p) => p.lock()?.hunt_type.odds(),
483                Countable::Chain(_) => todo!(),
484            },
485        )
486    }
487
488    pub fn odds(&self, countable: &CountableId) -> f64 {
489        self.odds_checked(countable).unwrap()
490    }
491
492    pub fn progress_checked(&self, countable: &CountableId) -> Result<f64, AppError> {
493        let prob = 1.0 / self.odds_checked(countable)?;
494        let rolls = self.rolls_checked(countable)?;
495        Ok(
496            match self
497                .store
498                .get(countable)
499                .ok_or(AppError::CountableNotFound)?
500            {
501                Countable::Counter(c) => {
502                    let children_len = c.lock()?.children.len();
503                    let mut chance = 0.0;
504                    for k in 0..((self.completed_checked(countable)? + 1).min(children_len)) {
505                        let combs = n_choose_k(rolls, k);
506                        chance +=
507                            combs * prob.powi(k as i32) * (1.0 - prob).powi((rolls - k) as i32)
508                    }
509
510                    1.0 - chance
511                }
512                Countable::Phase(_) => 1.0 - (1.0 - prob).powi(rolls as i32),
513                Countable::Chain(_) => todo!(),
514            },
515        )
516    }
517
518    pub fn progress(&self, countable: &CountableId) -> f64 {
519        self.progress_checked(countable).unwrap()
520    }
521
522    pub fn completed_checked(&self, countable: &CountableId) -> Result<usize, AppError> {
523        Ok(
524            match self
525                .store
526                .get(countable)
527                .ok_or(AppError::CountableNotFound)?
528            {
529                Countable::Counter(c) => c
530                    .lock()?
531                    .children
532                    .iter()
533                    .map(|child| self.completed_checked(child))
534                    .collect::<Result<Vec<_>, AppError>>()?
535                    .into_iter()
536                    .sum(),
537                Countable::Phase(p) => p.lock()?.success.into(),
538                Countable::Chain(_) => todo!(),
539            },
540        )
541    }
542
543    pub fn has_charm_checked(&self, countable: &CountableId) -> Result<bool, AppError> {
544        Ok(
545            match self
546                .store
547                .get(countable)
548                .ok_or(AppError::CountableNotFound)?
549            {
550                Countable::Counter(c) => {
551                    let mut has = true;
552                    for child in c.lock()?.children.iter() {
553                        has &= self.has_charm_checked(child)?;
554                    }
555                    has
556                }
557                Countable::Phase(p) => p.lock()?.has_charm,
558                Countable::Chain(_) => todo!(),
559            },
560        )
561    }
562
563    pub fn has_charm(&self, countable: &CountableId) -> bool {
564        self.has_charm_checked(countable).unwrap()
565    }
566
567    pub fn is_success_checked(&self, countable: &CountableId) -> Result<bool, AppError> {
568        Ok(
569            match self
570                .store
571                .get(countable)
572                .ok_or(AppError::CountableNotFound)?
573            {
574                Countable::Counter(c) => c
575                    .lock()?
576                    .children
577                    .last()
578                    .and_then(|child| self.is_success_checked(child).ok())
579                    .unwrap_or_default(),
580                Countable::Phase(p) => p.lock()?.success,
581                Countable::Chain(_) => todo!(),
582            },
583        )
584    }
585
586    pub fn is_success(&self, countable: &CountableId) -> bool {
587        self.is_success_checked(countable).unwrap()
588    }
589
590    pub fn toggle_success_checked(&self, countable: &CountableId) -> Result<(), AppError> {
591        match self
592            .store
593            .get(countable)
594            .ok_or(AppError::CountableNotFound)?
595        {
596            Countable::Counter(_) => {}
597            Countable::Phase(p) => {
598                let success = p.lock()?.success;
599                p.lock()?.success = !success;
600            }
601            Countable::Chain(_) => todo!(),
602        };
603
604        self.is_changed.replace(true);
605
606        Ok(())
607    }
608
609    pub fn toggle_success(&self, countable: &CountableId) {
610        self.toggle_success_checked(countable).unwrap()
611    }
612
613    pub fn created_at_checked(
614        &self,
615        countable: &CountableId,
616    ) -> Result<chrono::NaiveDateTime, AppError> {
617        self.store
618            .get(countable)
619            .ok_or(AppError::CountableNotFound)?
620            .created_at_checked()
621    }
622
623    pub fn created_at(&self, countable: &CountableId) -> chrono::NaiveDateTime {
624        self.created_at_checked(countable).unwrap()
625    }
626}
627
628#[typetag::serde]
629impl Savable for leptos::RwSignal<CountableStore> {
630    fn indexed_db_name(&self) -> String {
631        "Countable".into()
632    }
633
634    fn save_indexed<'a>(
635        &'a self,
636        obj: indexed_db::ObjectStore<AppError>,
637    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), AppError>> + 'a>> {
638        use wasm_bindgen::JsValue;
639
640        self.update_untracked(|s| {
641            s.is_changed.replace(false);
642        });
643
644        Box::pin(async move {
645            obj.clear().await?;
646            for c in self.get_untracked().store.values() {
647                let key = JsValue::from_str(&c.uuid().to_string());
648                let value = c.as_js();
649
650                obj.put_kv(&key, &value?).await?;
651            }
652            Ok(())
653        })
654    }
655
656    fn save_endpoint(
657        &self,
658    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), leptos::ServerFnError>>>>
659    {
660        self.update_untracked(|s| {
661            s.is_changed.replace(false);
662        });
663
664        Box::pin(api::update_countable_many(
665            self.get_untracked().store.into_values().collect(),
666        ))
667    }
668
669    fn message(&self) -> Option<leptos::View> {
670        None
671    }
672
673    fn clone_box(&self) -> Box<dyn Savable> {
674        Box::new(*self)
675    }
676
677    fn has_change(&self) -> bool {
678        *self.get_untracked().is_changed.borrow()
679    }
680}
681
682#[typetag::serde]
683impl Savable for CountableStore {
684    fn indexed_db_name(&self) -> String {
685        "Countable".into()
686    }
687
688    fn save_indexed<'a>(
689        &'a self,
690        obj: indexed_db::ObjectStore<AppError>,
691    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), AppError>> + 'a>> {
692        use wasm_bindgen::JsValue;
693
694        self.is_changed.replace(false);
695
696        Box::pin(async move {
697            obj.clear().await?;
698            for c in self.store.values() {
699                let key = JsValue::from_str(&c.uuid().to_string());
700                let value = c.as_js();
701
702                obj.put_kv(&key, &value?).await?;
703            }
704            Ok(())
705        })
706    }
707
708    fn save_endpoint(
709        &self,
710    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), leptos::ServerFnError>>>>
711    {
712        self.is_changed.replace(false);
713        let cloned = self.clone();
714        Box::pin(api::update_countable_many(
715            cloned.store.into_values().collect(),
716        ))
717    }
718
719    fn message(&self) -> Option<leptos::View> {
720        None
721    }
722
723    fn clone_box(&self) -> Box<dyn Savable> {
724        Box::new(self.clone())
725    }
726
727    fn has_change(&self) -> bool {
728        *self.is_changed.borrow()
729    }
730}
731
732#[typetag::serde]
733impl Savable for Vec<Countable> {
734    fn indexed_db_name(&self) -> String {
735        "Countable".into()
736    }
737
738    fn save_indexed<'a>(
739        &'a self,
740        obj: indexed_db::ObjectStore<AppError>,
741    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), AppError>> + 'a>> {
742        use wasm_bindgen::JsValue;
743
744        Box::pin(async move {
745            for c in self {
746                let key = JsValue::from_str(&c.uuid().to_string());
747                let value = c.as_js();
748
749                obj.put_kv(&key, &value?).await?;
750            }
751            Ok(())
752        })
753    }
754
755    fn save_endpoint(
756        &self,
757    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), leptos::ServerFnError>>>>
758    {
759        Box::pin(api::update_countable_many(self.clone()))
760    }
761
762    fn message(&self) -> Option<leptos::View> {
763        None
764    }
765
766    fn clone_box(&self) -> Box<dyn Savable> {
767        Box::new(self.clone())
768    }
769    fn has_change(&self) -> bool {
770        true
771    }
772}
773
774#[typetag::serde]
775impl Savable for Countable {
776    fn indexed_db_name(&self) -> String {
777        "Countable".into()
778    }
779
780    fn save_indexed<'a>(
781        &'a self,
782        obj: indexed_db::ObjectStore<AppError>,
783    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), AppError>> + 'a>> {
784        use wasm_bindgen::JsValue;
785        let key = JsValue::from_str(&self.uuid().to_string());
786        let value = self.as_js();
787        Box::pin(async move {
788            obj.put_kv(&key, &value?).await?;
789            Ok(())
790        })
791    }
792
793    fn save_endpoint(
794        &self,
795    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), leptos::ServerFnError>>>>
796    {
797        Box::pin(api::update_countable_many(vec![self.clone()]))
798    }
799
800    fn message(&self) -> Option<leptos::View> {
801        None
802    }
803
804    fn clone_box(&self) -> Box<dyn Savable> {
805        Box::new(self.clone())
806    }
807
808    fn has_change(&self) -> bool {
809        true
810    }
811}
812
813#[derive(
814    Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord,
815)]
816pub struct CountableId(uuid::Uuid);
817
818impl From<uuid::Uuid> for CountableId {
819    fn from(value: uuid::Uuid) -> Self {
820        Self(value)
821    }
822}
823
824#[derive(Debug, Clone, Serialize, Deserialize)]
825pub enum Countable {
826    Counter(Arc<Mutex<Counter>>),
827    Phase(Arc<Mutex<Phase>>),
828    Chain(Arc<Mutex<Chain>>),
829}
830
831impl From<Countable> for CountableId {
832    fn from(value: Countable) -> Self {
833        value.uuid().into()
834    }
835}
836
837impl From<&Countable> for CountableId {
838    fn from(value: &Countable) -> Self {
839        value.uuid().into()
840    }
841}
842
843impl Countable {
844    pub fn new(
845        name: &str,
846        kind: CountableKind,
847        owner_uuid: uuid::Uuid,
848        parent: Option<CountableId>,
849    ) -> Self {
850        match kind {
851            CountableKind::Counter => Self::Counter(Arc::new(Mutex::new(Counter::new(
852                name.into(),
853                owner_uuid,
854                parent,
855            )))),
856            CountableKind::Phase => Self::Phase(Arc::new(Mutex::new(Phase::new(
857                name.into(),
858                owner_uuid,
859                parent.expect("Phase has to have a parent"),
860            )))),
861            CountableKind::Chain => todo!(),
862        }
863    }
864
865    pub fn add_child_checked(&self, child: CountableId) -> Result<(), AppError> {
866        match self {
867            Countable::Counter(c) => {
868                c.lock()?.children.push(child);
869                Ok(())
870            }
871            Countable::Phase(_) => Err(AppError::CannotContainChildren("Phase".into())),
872            Countable::Chain(_) => Err(AppError::CannotContainChildren("Chain".into())),
873        }
874    }
875
876    pub fn add_child(&self, child: CountableId) {
877        self.add_child_checked(child).unwrap()
878    }
879
880    pub fn uuid_checked(&self) -> Result<uuid::Uuid, AppError> {
881        Ok(match self {
882            Countable::Counter(c) => c.lock()?.uuid,
883            Countable::Phase(p) => p.lock()?.uuid,
884            Countable::Chain(_) => todo!(),
885        })
886    }
887
888    pub fn uuid(&self) -> uuid::Uuid {
889        self.uuid_checked().unwrap()
890    }
891
892    pub fn name_checked(&self) -> Result<String, AppError> {
893        Ok(match self {
894            Countable::Counter(c) => c.lock()?.name.clone(),
895            Countable::Phase(p) => p.lock()?.name.clone(),
896            Countable::Chain(_) => todo!(),
897        })
898    }
899
900    pub fn name(&self) -> String {
901        self.name_checked().unwrap()
902    }
903
904    pub fn set_name_checked(&self, name: &str) -> Result<(), AppError> {
905        match self {
906            Countable::Counter(c) => c.lock()?.name = name.into(),
907            Countable::Phase(p) => p.lock()?.name = name.into(),
908            Countable::Chain(_) => todo!(),
909        }
910
911        Ok(())
912    }
913
914    pub fn set_name(&self, name: &str) {
915        self.set_name_checked(name).unwrap()
916    }
917
918    pub fn created_at_checked(&self) -> Result<chrono::NaiveDateTime, AppError> {
919        Ok(match self {
920            Countable::Counter(c) => c.lock()?.created_at,
921            Countable::Phase(p) => p.lock()?.created_at,
922            Countable::Chain(_) => todo!(),
923        })
924    }
925
926    pub fn created_at(&self) -> chrono::NaiveDateTime {
927        self.created_at_checked().unwrap()
928    }
929
930    pub fn last_edit_checked(&self) -> Result<chrono::NaiveDateTime, AppError> {
931        Ok(match self {
932            Countable::Counter(c) => c.lock()?.last_edit,
933            Countable::Phase(p) => p.lock()?.last_edit,
934            Countable::Chain(_) => todo!(),
935        })
936    }
937
938    pub fn last_edit(&self) -> chrono::NaiveDateTime {
939        self.last_edit_checked().unwrap()
940    }
941
942    pub fn is_archived_checked(&self) -> Result<bool, AppError> {
943        Ok(match self {
944            Countable::Counter(c) => c.lock()?.is_deleted,
945            Countable::Phase(p) => p.lock()?.is_deleted,
946            Countable::Chain(_) => todo!(),
947        })
948    }
949
950    pub fn is_archived(&self) -> bool {
951        self.is_archived_checked().unwrap()
952    }
953
954    pub fn as_js(&self) -> Result<wasm_bindgen::JsValue, AppError> {
955        Ok(js_sys::JSON::parse(&serde_json::to_string(&self)?)?)
956    }
957
958    pub fn from_js(val: wasm_bindgen::JsValue) -> Result<Self, AppError> {
959        let this = serde_json::from_str(
960            &js_sys::JSON::stringify(&val)?
961                .as_string()
962                .unwrap_or_default(),
963        )?;
964        Ok(this)
965    }
966}
967
968impl PartialEq for Countable {
969    fn eq(&self, other: &Self) -> bool {
970        match (self, other) {
971            (Countable::Counter(a), Countable::Counter(b)) => match (a.try_lock(), b.try_lock()) {
972                (Ok(a), Ok(b)) => a.eq(&*b),
973                _ => false,
974            },
975            (Countable::Phase(a), Countable::Phase(b)) => match (a.try_lock(), b.try_lock()) {
976                (Ok(a), Ok(b)) => a.eq(&*b),
977                _ => false,
978            },
979            (Countable::Chain(a), Countable::Chain(b)) => match (a.try_lock(), b.try_lock()) {
980                (Ok(a), Ok(b)) => a.eq(&*b),
981                _ => false,
982            },
983            _ => false,
984        }
985    }
986}
987
988impl Eq for Countable {}
989
990#[cfg(feature = "ssr")]
991impl From<backend::DbCounter> for Countable {
992    fn from(value: backend::DbCounter) -> Self {
993        Self::Counter(Arc::new(Mutex::new(Counter {
994            uuid: value.uuid,
995            owner_uuid: value.owner_uuid,
996            parent: None,
997            children: Vec::new(),
998            name: value.name,
999            last_edit: value.last_edit,
1000            created_at: value.created_at,
1001            is_deleted: value.is_deleted,
1002        })))
1003    }
1004}
1005
1006#[cfg(feature = "ssr")]
1007impl From<backend::DbPhase> for Countable {
1008    fn from(value: backend::DbPhase) -> Self {
1009        Self::Phase(Arc::new(Mutex::new(Phase {
1010            uuid: value.uuid,
1011            owner_uuid: value.owner_uuid,
1012            parent: value.parent_uuid.into(),
1013            name: value.name,
1014            count: value.count,
1015            time: chrono::Duration::milliseconds(value.time),
1016            hunt_type: value.hunt_type.into(),
1017            has_charm: value.has_charm,
1018            success: value.success,
1019            last_edit: value.last_edit,
1020            created_at: value.created_at,
1021            is_deleted: value.is_deleted,
1022        })))
1023    }
1024}
1025
1026#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
1027pub enum CountableKind {
1028    #[default]
1029    Counter,
1030    Phase,
1031    Chain,
1032}
1033
1034impl std::fmt::Display for CountableKind {
1035    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1036        match self {
1037            Self::Counter => write!(f, "Counter"),
1038            Self::Phase => write!(f, "Phase"),
1039            Self::Chain => write!(f, "Chain"),
1040        }
1041    }
1042}
1043
1044#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1045pub struct Counter {
1046    pub uuid: uuid::Uuid,
1047    pub owner_uuid: uuid::Uuid,
1048    pub parent: Option<CountableId>,
1049    #[serde(default)]
1050    pub children: Vec<CountableId>,
1051    pub name: String,
1052    pub last_edit: chrono::NaiveDateTime,
1053    pub created_at: chrono::NaiveDateTime,
1054    pub is_deleted: bool,
1055}
1056
1057impl Counter {
1058    fn new(name: String, owner_uuid: uuid::Uuid, parent: Option<CountableId>) -> Self {
1059        Self {
1060            uuid: uuid::Uuid::new_v4(),
1061            owner_uuid,
1062            parent,
1063            children: Vec::new(),
1064            name,
1065            last_edit: chrono::Utc::now().naive_utc(),
1066            created_at: chrono::Utc::now().naive_utc(),
1067            is_deleted: false,
1068        }
1069    }
1070}
1071
1072#[cfg(feature = "ssr")]
1073impl Into<backend::DbCounter> for Counter {
1074    fn into(self) -> backend::DbCounter {
1075        backend::DbCounter {
1076            uuid: self.uuid,
1077            owner_uuid: self.owner_uuid,
1078            name: self.name,
1079            last_edit: self.last_edit,
1080            created_at: self.created_at,
1081            is_deleted: self.is_deleted,
1082        }
1083    }
1084}
1085
1086#[serde_with::serde_as]
1087#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
1088pub struct Phase {
1089    pub uuid: uuid::Uuid,
1090    pub owner_uuid: uuid::Uuid,
1091    pub parent: CountableId,
1092    pub name: String,
1093    pub count: i32,
1094    #[serde_as(as = "serde_with::DurationMilliSeconds<i64>")]
1095    pub time: chrono::Duration,
1096    pub hunt_type: Hunttype,
1097    pub has_charm: bool,
1098    pub success: bool,
1099    pub last_edit: chrono::NaiveDateTime,
1100    pub created_at: chrono::NaiveDateTime,
1101    pub is_deleted: bool,
1102}
1103
1104impl Phase {
1105    fn new(name: String, owner_uuid: uuid::Uuid, parent: CountableId) -> Self {
1106        Self {
1107            uuid: uuid::Uuid::new_v4(),
1108            owner_uuid,
1109            parent,
1110            name,
1111            last_edit: chrono::Utc::now().naive_utc(),
1112            created_at: chrono::Utc::now().naive_utc(),
1113            ..Default::default()
1114        }
1115    }
1116}
1117
1118#[cfg(feature = "ssr")]
1119impl Into<backend::DbPhase> for Phase {
1120    fn into(self) -> backend::DbPhase {
1121        backend::DbPhase {
1122            uuid: self.uuid,
1123            owner_uuid: self.owner_uuid,
1124            parent_uuid: self.parent.0,
1125            name: self.name,
1126            count: self.count,
1127            time: self.time.num_milliseconds(),
1128            hunt_type: self.hunt_type.into(),
1129            has_charm: self.has_charm,
1130            dexnav_encounters: None,
1131            success: self.success,
1132            last_edit: self.last_edit,
1133            created_at: self.created_at,
1134            is_deleted: self.is_deleted,
1135        }
1136    }
1137}
1138
1139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1140pub struct Chain {}
1141
1142#[allow(clippy::upper_case_acronyms)]
1143#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1144pub enum Hunttype {
1145    #[default]
1146    OldOdds,
1147    NewOdds,
1148    SOS,
1149    // DexNav(DexNav),
1150    Masuda(Masuda),
1151    Mixed,
1152}
1153
1154#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
1155pub enum Masuda {
1156    GenIV,
1157    GenV,
1158    #[default]
1159    GenVI,
1160    // TODO: make this a generation enum
1161    // TODO: gen VIII+ has 6 rerolls
1162}
1163
1164impl Hunttype {
1165    fn rolls(&self) -> impl Fn(i32, bool) -> usize {
1166        match self {
1167            Hunttype::OldOdds => {
1168                |count, has_charm: bool| (count * if has_charm { 3 } else { 1 }) as usize
1169            }
1170            Hunttype::NewOdds => {
1171                |count, has_charm: bool| (count * if has_charm { 3 } else { 1 }) as usize
1172            }
1173            Hunttype::SOS => |count, has_charm: bool| match count {
1174                c if c < 10 => (count * if has_charm { 3 } else { 1 }) as usize,
1175                c if c < 20 => (10 + (count - 10) * if has_charm { 3 + 4 } else { 1 + 4 }) as usize,
1176                c if c < 30 => (60 + (count - 20) * if has_charm { 3 + 8 } else { 1 + 8 }) as usize,
1177                _ => (50 + (count - 30) * if has_charm { 3 + 13 } else { 1 + 12 }) as usize,
1178            },
1179            Hunttype::Masuda(Masuda::GenIV) => {
1180                |count, has_charm: bool| (count * if has_charm { 3 + 4 } else { 1 + 4 }) as usize
1181            }
1182            Hunttype::Masuda(_) => {
1183                |count, has_charm: bool| (count * if has_charm { 3 + 5 } else { 1 + 5 }) as usize
1184            }
1185            Hunttype::Mixed => unreachable!(),
1186        }
1187    }
1188
1189    fn odds(&self) -> f64 {
1190        match self {
1191            Hunttype::OldOdds | Hunttype::Masuda(Masuda::GenIV) => 8192.0,
1192            _ => 4096.0,
1193        }
1194    }
1195
1196    pub fn repr(&self) -> &'static str {
1197        match self {
1198            Self::OldOdds => "Old Odds",
1199            Self::NewOdds => "New Odds",
1200            Self::SOS => "SOS",
1201            Self::Masuda(Masuda::GenIV) => "Masuda (gen IV)",
1202            Self::Masuda(Masuda::GenV) => "Masuda (gen V)",
1203            Self::Masuda(Masuda::GenVI) => "Masuda (gen VI+)",
1204            Self::Mixed => todo!(),
1205        }
1206    }
1207}
1208
1209impl From<Hunttype> for &'static str {
1210    fn from(val: Hunttype) -> Self {
1211        match val {
1212            Hunttype::OldOdds => "OldOdds",
1213            Hunttype::NewOdds => "NewOdds",
1214            Hunttype::SOS => "SOS",
1215            Hunttype::Masuda(Masuda::GenIV) => "MasudaGenIV",
1216            Hunttype::Masuda(Masuda::GenV) => "MasudaGenV",
1217            Hunttype::Masuda(Masuda::GenVI) => "MasudaGenVI",
1218            Hunttype::Mixed => todo!(),
1219        }
1220    }
1221}
1222
1223impl TryFrom<String> for Hunttype {
1224    type Error = String;
1225
1226    fn try_from(value: String) -> Result<Self, Self::Error> {
1227        return match value.as_str() {
1228            "OldOdds" => Ok(Self::OldOdds),
1229            "NewOdds" => Ok(Self::NewOdds),
1230            "SOS" => Ok(Self::SOS),
1231            "MasudaGenIV" => Ok(Self::Masuda(Masuda::GenIV)),
1232            "MasudaGenV" => Ok(Self::Masuda(Masuda::GenV)),
1233            "MasudaGenVI" => Ok(Self::Masuda(Masuda::GenVI)),
1234            _ => Err(String::from(
1235                "Hunttype should be one of the following: OldOdds, NewOdds, SOS, Masuda",
1236            )),
1237        };
1238    }
1239}
1240
1241impl From<Hunttype> for components::SelectOption {
1242    fn from(val: Hunttype) -> Self {
1243        (val.repr(), val.into()).into()
1244    }
1245}
1246
1247impl From<Hunttype> for leptos::Attribute {
1248    fn from(val: Hunttype) -> Self {
1249        let str: &'static str = val.into();
1250        leptos::Attribute::String(str.into())
1251    }
1252}
1253
1254impl leptos::IntoAttribute for Hunttype {
1255    fn into_attribute(self) -> leptos::Attribute {
1256        self.into()
1257    }
1258
1259    fn into_attribute_boxed(self: Box<Self>) -> leptos::Attribute {
1260        (*self).into()
1261    }
1262}
1263
1264#[cfg(feature = "ssr")]
1265impl From<backend::Hunttype> for Hunttype {
1266    fn from(value: backend::Hunttype) -> Self {
1267        match value {
1268            backend::Hunttype::OldOdds => Self::OldOdds,
1269            backend::Hunttype::NewOdds => Self::NewOdds,
1270            backend::Hunttype::SOS => Self::SOS,
1271            backend::Hunttype::DexNav => todo!(),
1272            backend::Hunttype::MasudaGenIV => Self::Masuda(Masuda::GenIV),
1273            backend::Hunttype::MasudaGenV => Self::Masuda(Masuda::GenV),
1274            backend::Hunttype::MasudaGenVI => Self::Masuda(Masuda::GenVI),
1275        }
1276    }
1277}
1278
1279#[cfg(feature = "ssr")]
1280impl Into<backend::Hunttype> for Hunttype {
1281    fn into(self) -> backend::Hunttype {
1282        match self {
1283            Self::OldOdds => backend::Hunttype::OldOdds,
1284            Self::NewOdds => backend::Hunttype::NewOdds,
1285            Self::SOS => backend::Hunttype::SOS,
1286            Self::Masuda(Masuda::GenIV) => backend::Hunttype::MasudaGenIV,
1287            Self::Masuda(Masuda::GenV) => backend::Hunttype::MasudaGenV,
1288            Self::Masuda(Masuda::GenVI) => backend::Hunttype::MasudaGenVI,
1289            Self::Mixed => unreachable!(),
1290        }
1291    }
1292}
1293
1294fn n_choose_k(n: usize, k: usize) -> f64 {
1295    match (n, k) {
1296        (n, k) if k > n => 0.0,
1297        (_, 0) => 1.0,
1298        (n, k) if k > n / 2 => n_choose_k(n, n - k),
1299        (n, k) => n as f64 / k as f64 * n_choose_k(n - 1, k - 1),
1300    }
1301}