1use std::{
2 collections::HashMap,
3 sync::{Arc, Mutex, OnceLock},
4};
5
6use anyhow::bail;
7use axum::{
8 http::StatusCode,
9 response::{IntoResponse, Response},
10};
11
12use axum_session::{Session, SessionNullPool};
13use demystify::{
14 problem::{musdict::MusContext, planner::PuzzlePlanner},
15 snapshot::{SessionSnapshot, export_snapshot, import_snapshot},
16};
17use uuid::Uuid;
18
19#[derive(Clone)]
20pub struct AppState {
21 pub tera: Arc<tera::Tera>,
22 pub strategy_db: Arc<demystify::named_strategy::Database>,
23}
24
25pub struct ExploreState {
26 pub all_muses: Vec<MusContext>,
27 pub current_index: usize,
28}
29
30pub struct SolverSession {
31 pub planner: PuzzlePlanner,
32 pub explore: Option<ExploreState>,
33 pub explore_enabled: bool,
34 pub history: Vec<PuzzlePlanner>,
35 pub game: Option<GameState>,
36}
37
38#[derive(Debug, Clone)]
39pub struct GameState {
40 pub level_id: String,
41 pub failures: u32,
42 pub hints_used: u32,
43 pub completed: bool,
44}
45
46impl GameState {
47 pub fn new(level_id: String) -> Self {
48 Self {
49 level_id,
50 failures: 0,
51 hints_used: 0,
52 completed: false,
53 }
54 }
55}
56
57impl SolverSession {
58 pub fn new(planner: PuzzlePlanner) -> Self {
59 let snapshot = planner
60 .fork()
61 .expect("Failed to fork initial planner state");
62 Self {
63 planner,
64 explore: None,
65 explore_enabled: false,
66 history: vec![snapshot],
67 game: None,
68 }
69 }
70
71 pub fn snapshot(&mut self) {
72 let snapshot = self.planner.fork().expect("Failed to fork planner state");
73 self.history.push(snapshot);
74 }
75
76 pub fn goto_step(&mut self, step: usize) -> anyhow::Result<()> {
77 anyhow::ensure!(step < self.history.len(), "Step {step} out of range");
78 self.planner = self.history[step].fork()?;
79 self.history.truncate(step + 1);
80 self.explore = None;
81 self.explore_enabled = false;
82 Ok(())
83 }
84}
85
86pub struct AppError(anyhow::Error);
87
88impl IntoResponse for AppError {
89 fn into_response(self) -> Response {
90 (
91 StatusCode::INTERNAL_SERVER_ERROR,
92 format!("Something went wrong: {}", self.0),
93 )
94 .into_response()
95 }
96}
97
98impl<E> From<E> for AppError
99where
100 E: Into<anyhow::Error>,
101{
102 fn from(err: E) -> Self {
103 Self(err.into())
104 }
105}
106
107fn solver_global(
108 uuid: Uuid,
109 set_solver: Option<Arc<Mutex<SolverSession>>>,
110) -> Option<Arc<Mutex<SolverSession>>> {
111 type GlobalPuzzleStorage = Mutex<HashMap<Uuid, Arc<Mutex<SolverSession>>>>;
112 static SOLVER: OnceLock<GlobalPuzzleStorage> = OnceLock::new();
113 let m = SOLVER.get_or_init(|| Mutex::new(HashMap::new()));
114
115 if let Some(solver) = set_solver {
116 m.lock().unwrap().insert(uuid, solver);
117 None
118 } else {
119 m.lock().unwrap().get(&uuid).cloned()
120 }
121}
122
123pub fn get_solver_global(
124 session: &Session<SessionNullPool>,
125) -> anyhow::Result<Arc<Mutex<SolverSession>>> {
126 let uuid = session.get_session_id().uuid();
127 let solver = solver_global(uuid, None);
128 if let Some(solver) = solver {
129 Ok(solver)
130 } else {
131 bail!("No solver -- have you uploaded files?");
132 }
133}
134
135pub fn set_solver_global(session: &Session<SessionNullPool>, set_solver: PuzzlePlanner) {
136 let uuid = session.get_session_id().uuid();
137 solver_global(
138 uuid,
139 Some(Arc::new(Mutex::new(SolverSession::new(set_solver)))),
140 );
141}
142
143pub fn set_solver_global_session(
144 session: &Session<SessionNullPool>,
145 solver_session: SolverSession,
146) {
147 let uuid = session.get_session_id().uuid();
148 solver_global(uuid, Some(Arc::new(Mutex::new(solver_session))));
149}
150
151impl SolverSession {
160 pub fn export_snapshot(&self) -> anyhow::Result<SessionSnapshot> {
161 export_snapshot(&self.history)
162 }
163}
164
165pub fn import_snapshot_into_session(
166 snapshot: SessionSnapshot,
167 strategy_db: Arc<demystify::named_strategy::Database>,
168) -> anyhow::Result<SolverSession> {
169 let history = import_snapshot(snapshot, strategy_db)?;
170 let live = history
171 .last()
172 .expect("import_snapshot returns at least one entry")
173 .fork()?;
174 Ok(SolverSession {
175 planner: live,
176 explore: None,
177 explore_enabled: false,
178 history,
179 game: None,
180 })
181}