1use std::collections::HashMap;
7use std::sync::{Arc, RwLock};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use crate::{Element, ElementId, Scene, SceneDocument};
11
12pub const DEFAULT_SESSION: &str = "default";
14
15const DEFAULT_WIDTH: f32 = 800.0;
17
18const DEFAULT_HEIGHT: f32 = 600.0;
20
21#[derive(Debug, thiserror::Error)]
23pub enum StoreError {
24 #[error("Lock poisoned")]
26 LockPoisoned,
27 #[error("Session not found: {0}")]
29 SessionNotFound(String),
30 #[error("Element not found: {0}")]
32 ElementNotFound(String),
33 #[error("Scene error: {0}")]
35 SceneError(String),
36}
37
38#[derive(Debug, Clone, Default)]
58pub struct SceneStore {
59 scenes: Arc<RwLock<HashMap<String, Scene>>>,
60}
61
62impl SceneStore {
63 #[must_use]
67 pub fn new() -> Self {
68 let mut scenes = HashMap::new();
69 scenes.insert(
70 DEFAULT_SESSION.to_string(),
71 Scene::new(DEFAULT_WIDTH, DEFAULT_HEIGHT),
72 );
73 Self {
74 scenes: Arc::new(RwLock::new(scenes)),
75 }
76 }
77
78 #[must_use]
82 pub fn get_or_create(&self, session_id: &str) -> Scene {
83 let mut scenes = self
84 .scenes
85 .write()
86 .unwrap_or_else(std::sync::PoisonError::into_inner);
87 scenes
88 .entry(session_id.to_string())
89 .or_insert_with(|| Scene::new(DEFAULT_WIDTH, DEFAULT_HEIGHT))
90 .clone()
91 }
92
93 #[must_use]
95 pub fn get(&self, session_id: &str) -> Option<Scene> {
96 let scenes = self
97 .scenes
98 .read()
99 .unwrap_or_else(std::sync::PoisonError::into_inner);
100 scenes.get(session_id).cloned()
101 }
102
103 pub fn replace(&self, session_id: &str, scene: Scene) -> Result<(), StoreError> {
112 let mut scenes = self
113 .scenes
114 .write()
115 .unwrap_or_else(std::sync::PoisonError::into_inner);
116 scenes.insert(session_id.to_string(), scene);
117 Ok(())
118 }
119
120 pub fn update<F>(&self, session_id: &str, f: F) -> Result<(), StoreError>
128 where
129 F: FnOnce(&mut Scene),
130 {
131 let mut scenes = self
132 .scenes
133 .write()
134 .unwrap_or_else(std::sync::PoisonError::into_inner);
135 let scene = scenes
136 .get_mut(session_id)
137 .ok_or_else(|| StoreError::SessionNotFound(session_id.to_string()))?;
138 f(scene);
139 Ok(())
140 }
141
142 pub fn add_element(&self, session_id: &str, element: Element) -> Result<ElementId, StoreError> {
150 let mut scenes = self
151 .scenes
152 .write()
153 .unwrap_or_else(std::sync::PoisonError::into_inner);
154 let scene = scenes
155 .entry(session_id.to_string())
156 .or_insert_with(|| Scene::new(DEFAULT_WIDTH, DEFAULT_HEIGHT));
157 let id = scene.add_element(element);
158 Ok(id)
159 }
160
161 pub fn remove_element(&self, session_id: &str, id: ElementId) -> Result<(), StoreError> {
168 let mut scenes = self
169 .scenes
170 .write()
171 .unwrap_or_else(std::sync::PoisonError::into_inner);
172 let scene = scenes
173 .get_mut(session_id)
174 .ok_or_else(|| StoreError::SessionNotFound(session_id.to_string()))?;
175 scene
176 .remove_element(&id)
177 .map_err(|e| StoreError::ElementNotFound(e.to_string()))?;
178 Ok(())
179 }
180
181 pub fn update_element<F>(&self, session_id: &str, id: ElementId, f: F) -> Result<(), StoreError>
188 where
189 F: FnOnce(&mut Element),
190 {
191 let mut scenes = self
192 .scenes
193 .write()
194 .unwrap_or_else(std::sync::PoisonError::into_inner);
195 let scene = scenes
196 .get_mut(session_id)
197 .ok_or_else(|| StoreError::SessionNotFound(session_id.to_string()))?;
198 let element = scene
199 .get_element_mut(id)
200 .ok_or_else(|| StoreError::ElementNotFound(id.to_string()))?;
201 f(element);
202 Ok(())
203 }
204
205 #[must_use]
209 pub fn scene_document(&self, session_id: &str) -> SceneDocument {
210 let scenes = self
211 .scenes
212 .read()
213 .unwrap_or_else(std::sync::PoisonError::into_inner);
214 let timestamp = current_timestamp_ms();
215 if let Some(scene) = scenes.get(session_id) {
216 SceneDocument::from_scene(session_id, scene, timestamp)
217 } else {
218 let empty_scene = Scene::new(DEFAULT_WIDTH, DEFAULT_HEIGHT);
219 SceneDocument::from_scene(session_id, &empty_scene, timestamp)
220 }
221 }
222
223 #[must_use]
225 pub fn session_ids(&self) -> Vec<String> {
226 let scenes = self
227 .scenes
228 .read()
229 .unwrap_or_else(std::sync::PoisonError::into_inner);
230 scenes.keys().cloned().collect()
231 }
232
233 pub fn clear(&self, session_id: &str) -> Result<(), StoreError> {
239 let mut scenes = self
240 .scenes
241 .write()
242 .unwrap_or_else(std::sync::PoisonError::into_inner);
243 let scene = scenes
244 .get_mut(session_id)
245 .ok_or_else(|| StoreError::SessionNotFound(session_id.to_string()))?;
246 scene.clear();
247 Ok(())
248 }
249}
250
251fn current_timestamp_ms() -> u64 {
253 SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| {
254 #[allow(clippy::cast_possible_truncation)]
256 {
257 d.as_millis() as u64
258 }
259 })
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265 use crate::ElementKind;
266
267 #[test]
268 fn test_new_creates_default_session() {
269 let store = SceneStore::new();
270 let ids = store.session_ids();
271 assert!(ids.contains(&DEFAULT_SESSION.to_string()));
272 }
273
274 #[test]
275 fn test_get_or_create_existing() {
276 let store = SceneStore::new();
277 let scene = store.get_or_create(DEFAULT_SESSION);
278 assert!((scene.viewport_width - DEFAULT_WIDTH).abs() < f32::EPSILON);
279 assert!((scene.viewport_height - DEFAULT_HEIGHT).abs() < f32::EPSILON);
280 }
281
282 #[test]
283 fn test_get_or_create_new_session() {
284 let store = SceneStore::new();
285 let scene = store.get_or_create("new-session");
286 assert!(scene.is_empty());
287 assert!(store.session_ids().contains(&"new-session".to_string()));
288 }
289
290 #[test]
291 fn test_get_nonexistent_returns_none() {
292 let store = SceneStore::new();
293 assert!(store.get("nonexistent").is_none());
294 }
295
296 #[test]
297 fn test_add_and_get_element() {
298 let store = SceneStore::new();
299 let element = Element::new(ElementKind::Text {
300 content: "Hello".to_string(),
301 font_size: 16.0,
302 color: "#000000".to_string(),
303 });
304
305 let id = store
306 .add_element(DEFAULT_SESSION, element)
307 .expect("should add element");
308
309 let scene = store.get(DEFAULT_SESSION).expect("session should exist");
310 assert!(scene.get_element(id).is_some());
311 }
312
313 #[test]
314 fn test_remove_element() {
315 let store = SceneStore::new();
316 let element = Element::new(ElementKind::Text {
317 content: "Remove me".to_string(),
318 font_size: 14.0,
319 color: "#FF0000".to_string(),
320 });
321
322 let id = store
323 .add_element(DEFAULT_SESSION, element)
324 .expect("should add");
325
326 store
327 .remove_element(DEFAULT_SESSION, id)
328 .expect("should remove");
329
330 let scene = store.get(DEFAULT_SESSION).expect("session exists");
331 assert!(scene.get_element(id).is_none());
332 }
333
334 #[test]
335 fn test_remove_nonexistent_element_fails() {
336 let store = SceneStore::new();
337 let fake_id = ElementId::new();
338 let result = store.remove_element(DEFAULT_SESSION, fake_id);
339 assert!(matches!(result, Err(StoreError::ElementNotFound(_))));
340 }
341
342 #[test]
343 fn test_update_element() {
344 let store = SceneStore::new();
345 let element = Element::new(ElementKind::Text {
346 content: "Original".to_string(),
347 font_size: 12.0,
348 color: "#000000".to_string(),
349 });
350
351 let id = store
352 .add_element(DEFAULT_SESSION, element)
353 .expect("should add");
354
355 store
356 .update_element(DEFAULT_SESSION, id, |el| {
357 el.transform.x = 100.0;
358 el.transform.y = 200.0;
359 })
360 .expect("should update");
361
362 let scene = store.get(DEFAULT_SESSION).expect("session exists");
363 let updated = scene.get_element(id).expect("element exists");
364 assert!((updated.transform.x - 100.0).abs() < f32::EPSILON);
365 assert!((updated.transform.y - 200.0).abs() < f32::EPSILON);
366 }
367
368 #[test]
369 fn test_replace_scene() {
370 let store = SceneStore::new();
371 let mut new_scene = Scene::new(1920.0, 1080.0);
372 new_scene.add_element(Element::new(ElementKind::Text {
373 content: "New scene".to_string(),
374 font_size: 20.0,
375 color: "#00FF00".to_string(),
376 }));
377
378 store.replace(DEFAULT_SESSION, new_scene).expect("replace");
379
380 let scene = store.get(DEFAULT_SESSION).expect("session exists");
381 assert!((scene.viewport_width - 1920.0).abs() < f32::EPSILON);
382 assert_eq!(scene.element_count(), 1);
383 }
384
385 #[test]
386 fn test_clear_session() {
387 let store = SceneStore::new();
388 store
389 .add_element(
390 DEFAULT_SESSION,
391 Element::new(ElementKind::Text {
392 content: "Test".to_string(),
393 font_size: 16.0,
394 color: "#000".to_string(),
395 }),
396 )
397 .expect("add");
398
399 store.clear(DEFAULT_SESSION).expect("clear");
400
401 let scene = store.get(DEFAULT_SESSION).expect("session exists");
402 assert!(scene.is_empty());
403 }
404
405 #[test]
406 fn test_clear_nonexistent_session_fails() {
407 let store = SceneStore::new();
408 let result = store.clear("nonexistent");
409 assert!(matches!(result, Err(StoreError::SessionNotFound(_))));
410 }
411
412 #[test]
413 fn test_scene_document() {
414 let store = SceneStore::new();
415 store
416 .add_element(
417 DEFAULT_SESSION,
418 Element::new(ElementKind::Text {
419 content: "Doc test".to_string(),
420 font_size: 14.0,
421 color: "#123456".to_string(),
422 }),
423 )
424 .expect("add");
425
426 let doc = store.scene_document(DEFAULT_SESSION);
427 assert_eq!(doc.session_id, DEFAULT_SESSION);
428 assert_eq!(doc.elements.len(), 1);
429 }
430
431 #[test]
432 fn test_scene_document_nonexistent_returns_empty() {
433 let store = SceneStore::new();
434 let doc = store.scene_document("nonexistent");
435 assert_eq!(doc.session_id, "nonexistent");
436 assert!(doc.elements.is_empty());
437 }
438
439 #[test]
440 fn test_update_session() {
441 let store = SceneStore::new();
442 store
443 .update(DEFAULT_SESSION, |scene| {
444 scene.zoom = 2.0;
445 scene.pan_x = 50.0;
446 })
447 .expect("update");
448
449 let scene = store.get(DEFAULT_SESSION).expect("exists");
450 assert!((scene.zoom - 2.0).abs() < f32::EPSILON);
451 assert!((scene.pan_x - 50.0).abs() < f32::EPSILON);
452 }
453
454 #[test]
455 fn test_update_nonexistent_session_fails() {
456 let store = SceneStore::new();
457 let result = store.update("nonexistent", |_| {});
458 assert!(matches!(result, Err(StoreError::SessionNotFound(_))));
459 }
460}