1use anyhow::Context;
2use axum::{Json, extract::Multipart};
3use axum_session::{Session, SessionNullPool};
4use once_cell::sync::Lazy;
5use serde::Deserialize;
6use serde_json::Value;
7
8use std::{fs::File, io::Write, path::PathBuf, sync::Arc};
9
10use anyhow::anyhow;
11
12use crate::util::{self, get_solver_global, set_solver_global};
13
14use demystify::problem::{self, planner::PuzzlePlanner, solver::PuzzleSolver};
15
16macro_rules! include_model_file {
17 ($path:expr) => {
18 include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $path))
19 };
20}
21
22static EXAMPLES: Lazy<[(&str, &str, &str); 4]> = Lazy::new(|| {
23 [
24 (
25 "Sudoku",
26 include_model_file!("examples/eprime/sudoku.eprime"),
27 include_model_file!("examples/eprime/sudoku/puzzlingexample.param"),
28 ),
29 (
30 "MiracleSudoku",
31 include_model_file!("examples/eprime/miracle.eprime"),
32 include_model_file!("examples/eprime/miracle/original.param"),
33 ),
34 (
35 "StarBattle",
36 include_model_file!("examples/eprime/star-battle.eprime"),
37 include_model_file!("examples/eprime/star-battle/FATAtalkexample.param"),
38 ),
39 (
40 "Binairo",
41 include_model_file!("examples/eprime/binairo.essence"),
42 include_model_file!("examples/eprime/binairo/diiscu.param"),
43 ),
44 ]
45});
46
47pub async fn dump_full_solve(
48 session: Session<SessionNullPool>,
49) -> Result<Json<Value>, util::AppError> {
50 let solver = get_solver_global(&session)?;
51
52 let mut solver = solver.lock().unwrap();
53
54 let solve = solver.quick_solve();
55
56 Ok(Json(serde_json::value::to_value(solve).unwrap()))
57}
58
59pub async fn best_next_step(session: Session<SessionNullPool>) -> Result<String, util::AppError> {
60 let solver = get_solver_global(&session)?;
61
62 let mut solver = solver.lock().unwrap();
63
64 let (solve, lits) = solver.quick_solve_html_step();
65
66 solver.mark_lits_as_deduced(&lits);
67
68 if solve.is_empty() {
69 Ok("Please upload a puzzle or select an example to begin.".to_string())
70 } else {
71 Ok(solve)
72 }
73}
74
75pub async fn get_difficulties(session: Session<SessionNullPool>) -> Result<String, util::AppError> {
76 let solver = get_solver_global(&session)?;
77
78 let mut solver = solver.lock().unwrap();
79
80 let solve = solver.quick_generate_html_difficulties();
81
82 Ok(solve)
83}
84
85pub async fn refresh(session: Session<SessionNullPool>) -> Result<String, util::AppError> {
86 let solver = get_solver_global(&session)?;
87
88 let mut solver = solver.lock().unwrap();
89
90 let (solve, _) = solver.quick_display_html_step(None);
91
92 Ok(solve)
93}
94
95pub async fn click_literal(
96 headers: axum::http::header::HeaderMap,
97 session: Session<SessionNullPool>,
98) -> Result<String, util::AppError> {
99 let solver = get_solver_global(&session)?;
100
101 let mut solver = solver.lock().unwrap();
102
103 let cell = headers
104 .get("hx-trigger")
105 .context("Missing header: 'hx-trigger'")?;
106 let cell = cell.to_str()?;
107 let cell: Result<Vec<_>, _> = cell.split('_').skip(1).map(str::parse).collect();
108 let cell = cell?;
109
110 session.set("click_cell", &cell);
111
112 let (html, lits) = solver.quick_solve_html_step_for_literal(cell);
113
114 let lidx_lits: Vec<_> = lits.iter().map(|x| x.lidx()).collect();
115 session.set("lidx_lits", &lidx_lits);
116
117 Ok(html)
118}
119
120pub async fn upload_files(
121 session: Session<SessionNullPool>,
122 mut multipart: Multipart,
123) -> Result<String, util::AppError> {
124 let temp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
125
126 let mut model: Option<PathBuf> = None;
127 let mut param: Option<PathBuf> = None;
128
129 while let Some(field) = multipart
130 .next_field()
131 .await
132 .context("Failed to parse multipart upload")?
133 {
134 if field.name().unwrap() != "model" && field.name().unwrap() != "parameter" {
135 return Err(anyhow!(
136 "Form malformed -- should contain 'model' and 'parameter', but it contains '{}'",
137 field.name().unwrap()
138 )
139 .into());
140 }
141
142 let form_file_name = field.file_name().context("No filename")?;
144
145 eprintln!("Got file '{form_file_name}'!");
146
147 let file_name = if form_file_name.ends_with(".param") || form_file_name.ends_with(".json") {
148 if param.is_some() {
149 return Err(anyhow!("Cannot upload two param files (.param or .json)").into());
150 }
151
152 if form_file_name.ends_with(".param") {
153 param = Some("upload.param".into());
154 "upload.param"
155 } else {
156 param = Some("upload.json".into());
157 "upload.json"
158 }
159 } else if form_file_name.ends_with(".eprime") || form_file_name.ends_with(".essence") {
160 if model.is_some() {
161 return Err(anyhow!("Can only upload one .eprime or .essence file").into());
162 }
163 if form_file_name.ends_with(".eprime") {
164 model = Some("upload.eprime".into());
165 "upload.eprime"
166 } else {
167 model = Some("upload.essence".into());
168 "upload.essence"
169 }
170 } else {
171 return Err(anyhow!(
172 "Only expecting .param, .json, .eprime or .essence uploads, not '{}'",
173 form_file_name
174 )
175 .into());
176 };
177
178 let file_path = temp_dir.path().join(file_name);
180
181 let data = field.bytes().await.context("Failed to read file bytes")?;
183
184 let mut file_handle = File::create(file_path).context("Failed to open file for writing")?;
186
187 file_handle
189 .write_all(&data)
190 .context("Failed to write data!")?;
191 }
192
193 if model.is_none() {
194 return Ok(r###"
195 <div class="alert alert-danger">
196 <h4>Upload Error</h4>
197 <p>Please upload a model file (.eprime or .essence)</p>
198 </div>
199 "###
200 .to_string());
201 }
202
203 if param.is_none() {
204 return Ok(r###"
205 <div class="alert alert-danger">
206 <h4>Upload Error</h4>
207 <p>Please upload a parameter file (.param or .json)</p>
208 </div>
209 "###
210 .to_string());
211 }
212
213 match load_model(&session, temp_dir, model, param) {
214 Ok(_) => refresh(session).await,
215 Err(e) => Ok(format!(
216 r###"
217 <div class="alert alert-danger">
218 <h4>Failed to upload puzzle</h4>
219 <pre class="text-danger">{e:#}</pre>
220 <p>Please check your files and try again.</p>
221 </div>
222 "###
223 )),
224 }
225}
226
227#[derive(Deserialize)]
228pub struct ExampleParams {
229 example_name: String,
230}
231
232#[derive(Deserialize)]
233pub struct SubmitExampleParams {
234 param_content: String,
235 example_name: String,
236}
237
238pub async fn load_example(
239 _session: Session<SessionNullPool>,
240 form: axum::extract::Form<ExampleParams>,
241) -> Result<String, util::AppError> {
242 let example_name = form.example_name.clone();
243
244 let param_content = EXAMPLES
245 .iter()
246 .find(|(name, _, _)| *name == example_name)
247 .map(|(_, _, content)| *content)
248 .context(format!("Example '{example_name}' not found"))?;
249
250 Ok(format!(
251 r###"
252 <h5>Edit Parameters for {example_name}</h5>
253 <form id="paramForm" hx-post="/submitExample" hx-target="#mainSpace">
254 <input type="hidden" name="example_name" value="{example_name}">
255 <textarea name="param_content" class="form-control" rows="15" style="font-family: monospace;">{param_content}</textarea>
256 <button type="submit" class="btn btn-primary mt-2" hx-indicator="#indicator">
257 Submit Parameters
258 </button>
259 </form>
260 "###
261 ))
262}
263
264pub async fn get_example_names() -> String {
265 let options = EXAMPLES
266 .iter()
267 .map(|(name, _, _)| format!("<option value=\"{name}\">{name}</option>"))
268 .collect::<Vec<_>>()
269 .join("");
270
271 r###"<form id="exampleForm" hx-post="/loadExample" hx-target="#exampleParams">
272 <select name="example_name" class="form-select" required>
273 "###
274 .to_owned()
275 + &options
276 + r###"
277 </select>
278 <button type="submit" class="btn btn-primary mt-3" hx-indicator="#indicator">
279 Load Example
280 </button>
281 </form>
282 "###
283}
284
285pub async fn submit_example(
286 session: Session<SessionNullPool>,
287 form: axum::extract::Form<SubmitExampleParams>,
288) -> Result<String, util::AppError> {
289 let example_name = form.example_name.clone();
290 let param_content = form.param_content.clone();
291
292 let model_content = EXAMPLES
293 .iter()
294 .find(|(name, _, _)| *name == example_name)
295 .map(|(_, content, _)| *content)
296 .context(format!("Example '{example_name}' not found"))?;
297
298 let temp_dir = tempfile::tempdir().context("Failed to create temporary directory")?;
299
300 let model_dest = temp_dir.path().join("upload.eprime");
301 std::fs::write(&model_dest, model_content).context("Failed to write model file")?;
302
303 let param_dest = temp_dir.path().join("upload.param");
304 std::fs::write(¶m_dest, param_content).context("Failed to write param file")?;
305
306 match load_model(
307 &session,
308 temp_dir,
309 Some("upload.eprime".into()),
310 Some("upload.param".into()),
311 ) {
312 Ok(_) => refresh(session).await,
313 Err(e) => Ok(format!(
314 r###"
315 <div class="alert alert-danger">
316 <h4>Failed to load puzzle</h4>
317 <pre class="text-danger">{e:#}</pre>
318 <p>Please check your parameter file and try again.</p>
319 </div>
320 "###
321 )),
322 }
323}
324
325fn load_model(
326 session: &Session<SessionNullPool>,
327 temp_dir: tempfile::TempDir,
328 model: Option<PathBuf>,
329 param: Option<PathBuf>,
330) -> anyhow::Result<()> {
331 let puzzle = problem::parse::parse_essence(
332 &temp_dir.path().join(model.unwrap()),
333 &temp_dir.path().join(param.unwrap()),
334 )?;
335 let puzzle = Arc::new(puzzle);
336 let puz = PuzzleSolver::new(puzzle)?;
337 let plan = PuzzlePlanner::new(puz);
338 set_solver_global(session, plan);
339 Ok(())
340}