1use crate::{
2 dummy_storage::{DummyStorage, DynStorage},
3 item::{Fact, Item, ItemGuard},
4 storage::Storage,
5};
6use rand::{thread_rng, Rng};
7use std::{
8 collections::HashMap,
9 marker::PhantomData,
10 time::{Duration, SystemTime},
11};
12
13pub type SeeAgainGaps = HashMap<u32, Duration>;
15pub type AnkiDB = Vec<Item>;
17
18mod private_trait {
22 pub trait CannotExternallyImplement {}
24}
25
26pub trait AnkiCardReturnType: private_trait::CannotExternallyImplement {}
28pub struct GiveItemGuards;
30pub struct GiveFacts;
32impl private_trait::CannotExternallyImplement for GiveItemGuards {}
33impl private_trait::CannotExternallyImplement for GiveFacts {}
34impl AnkiCardReturnType for GiveItemGuards {}
35impl AnkiCardReturnType for GiveFacts {}
36
37#[must_use]
39pub fn default_sag() -> SeeAgainGaps {
40 let mut hm = HashMap::new();
41 for i in 1..11 {
42 hm.insert(i, Duration::from_secs(u64::from(i) * 30));
43 }
44 hm
45}
46
47pub struct AnkiGame<S: Storage, T: AnkiCardReturnType> {
49 v: AnkiDB,
51 pub(crate) storage: S,
53 sag: SeeAgainGaps,
55 current: Option<(usize, bool)>,
57
58 _pd: PhantomData<T>,
60}
61
62impl<S: Storage, T: AnkiCardReturnType> AnkiGame<S, T> {
63 pub fn new(storage: S, sat: SeeAgainGaps) -> Result<Self, S::ErrorType> {
67 Ok(Self {
68 v: storage.read_db()?,
69 storage,
70 sag: sat,
71 current: None,
72 _pd: PhantomData,
73 })
74 }
75
76 pub fn add_card(&mut self, f: Fact) {
78 self.v.push(f.into());
79 self.storage.write_db(&self.v).unwrap();
80 }
81
82 #[must_use]
84 pub fn get_elgible(&self) -> Vec<Fact> {
85 let indicies = get_eligible(&self.v, &self.sag);
86 indicies
87 .into_iter()
88 .map(|index| &self.v[index].fact)
89 .cloned()
90 .collect()
91 }
92
93 #[must_use]
95 pub fn get_eligible_no(&self) -> usize {
96 get_eligible(&self.v, &self.sag).len()
97 }
98
99 #[must_use]
101 pub fn get_all_facts(&self) -> Vec<Fact> {
102 self.v.clone().into_iter().map(|i| i.fact).collect()
103 }
104
105 pub fn write_to_db(&mut self) -> Result<(), S::ErrorType> {
107 self.storage.write_db(&self.v)
108 }
109
110 fn get_an_index(&self) -> Option<(usize, bool)> {
114 let eligible = get_eligible(&self.v, &self.sag);
115
116 if eligible.is_empty() {
117 if self.v.is_empty() {
118 None
119 } else {
120 Some((thread_rng().gen_range(0..self.v.len()), false))
121 }
122 } else {
123 Some((eligible[thread_rng().gen_range(0..eligible.len())], true))
124 }
125 }
126}
127
128impl<S: Storage> AnkiGame<S, GiveItemGuards> {
129 pub fn get_item_guard(&mut self) -> Option<(ItemGuard<S>, bool)> {
133 if let Some((index, was_e)) = self.get_an_index() {
134 Some((ItemGuard::new(&mut self.v, index, &mut self.storage), was_e))
135 } else {
136 None
137 }
138 }
139}
140
141impl<S: Storage> AnkiGame<S, GiveFacts> {
142 pub fn get_fact(&mut self) -> Option<(Fact, bool)> {
146 if let Some((cu, was_e)) = self.current {
147 Some((self.v[cu].fact.clone(), was_e))
148 } else if self.v.is_empty() {
149 None
150 } else {
151 self.set_new_fact();
152 self.get_fact()
153 }
154 }
155
156 pub fn get_new_fact(&mut self) -> Option<(Fact, bool)> {
158 self.set_new_fact();
159 self.get_fact()
160 }
161
162 pub fn set_new_fact(&mut self) {
164 if let Some((index, we)) = self.get_an_index() {
165 self.current = Some((index, we));
166 }
167 }
168
169 pub fn finish_current_fact(&mut self, correct: bool) {
171 if let Some((cu, _)) = self.current {
172 self.v[cu].history.push(correct);
173 self.v[cu].last_tested = Some(SystemTime::now());
174 self.storage
175 .write_db(&self.v)
176 .expect("unable to write to db");
177 }
178
179 self.current = None;
180 }
181}
182
183impl<E: std::fmt::Debug, T: AnkiCardReturnType> DynStorage<E> for AnkiGame<DummyStorage, T> {
184 fn read_custom(&mut self, s: &dyn Storage<ErrorType = E>) -> Result<(), E> {
185 self.v = s.read_db()?;
186 Ok(())
187 }
188
189 fn write_custom(&mut self, s: &mut dyn Storage<ErrorType = E>) -> Result<(), E> {
190 s.write_db(&self.v)
191 }
192
193 fn exit_custom(&mut self, s: &mut dyn Storage<ErrorType = E>) {
194 s.exit_application();
195 }
196}
197
198#[must_use]
200pub fn get_eligible(items: &[Item], sag: &SeeAgainGaps) -> Vec<usize> {
201 items
202 .iter()
203 .enumerate()
204 .filter_map(|(index, item)| {
205 item.time_since_last_test()
206 .map_or(Some(index), |last_seen| {
207 sag.get(&item.get_streak()).map_or(Some(index), |distance| {
208 if &last_seen > distance {
209 Some(index)
210 } else {
211 None
212 }
213 })
214 })
215 })
216 .collect()
217}