//! Minimal web GUI for entering a cube state and solving it.
use anyhow::Result;
use axum::{
extract::Json,
response::Html,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
const HTML: &str = r#"<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Rubik's Cube Solver</title>
<style>
body { font-family: sans-serif; margin: 20px auto; max-width: 760px; }
.scene { width: 360px; height: 360px; perspective: 900px; margin: 20px auto; overflow: visible; }
.cube { width: 100%; height: 100%; position: relative; transform-style: preserve-3d; transform: rotateX(-20deg) rotateY(30deg); cursor: grab; }
.face { position: absolute; width: 240px; height: 240px; display: grid; grid-template-columns: repeat(3, 80px); grid-template-rows: repeat(3, 80px); border: 1px solid #222; box-sizing: border-box; }
.face.front { transform: translateZ(120px); }
.face.back { transform: rotateY(180deg) translateZ(120px); }
.face.right { transform: rotateY(90deg) translateZ(120px); }
.face.left { transform: rotateY(-90deg) translateZ(120px); }
.face.top { transform: rotateX(90deg) translateZ(120px); }
.face.bottom { transform: rotateX(-90deg) translateZ(120px); }
.sticker { border: 1px solid #111; cursor: pointer; box-sizing: border-box; }
.U { background: #fff; }
.R { background: #f00; }
.F { background: #0f0; }
.D { background: #ff0; }
.L { background: #fa0; }
.B { background: #00f; }
button { margin: 6px 4px; padding: 6px 12px; }
#out { font-family: monospace; margin-top: 10px; white-space: pre-wrap; }
#str { font-family: monospace; word-break: break-all; }
.hint { max-width: 640px; }
</style>
</head>
<body>
<h1>Rubik's Cube Solver</h1>
<p class="hint"><b>How to enter:</b> Drag to rotate the cube. Click any sticker to cycle its color (U R F D L B). Start from a solved cube and set colors to match your cube, holding U on top and F in front as you rotate. Click Solve.</p>
<div class="scene"><div class="cube" id="cube"></div></div>
<button onclick="solve()">Solve</button>
<button onclick="reset()">Reset</button>
<p>State: <code id="str"></code></p>
<p>Paste state: <input id="manual" size="60"><button onclick="applyStr()">Apply</button></p>
<div id="out"></div>
<script>
const colors = ['U','R','F','D','L','B'];
let state = Array(54).fill('U');
const faceDefs = [
{cls:'front', start:18},
{cls:'back', start:45},
{cls:'right', start:9},
{cls:'left', start:36},
{cls:'top', start:0},
{cls:'bottom', start:27},
];
const cube = document.getElementById('cube');
faceDefs.forEach(fd => {
const face = document.createElement('div');
face.className = 'face '+fd.cls;
for (let i=0;i<9;i++){
const idx = fd.start+i;
const s = document.createElement('div');
s.className = 'sticker '+state[idx];
s.dataset.idx = idx;
s.onclick = (e) => { e.stopPropagation();
const cur = colors.indexOf(state[idx]);
state[idx] = colors[(cur+1)%colors.length];
s.className = 'sticker '+state[idx];
updateStr();
};
face.appendChild(s);
}
cube.appendChild(face);
});
function updateStr(){ document.getElementById('str').textContent = state.join(''); }
updateStr();
// rotation
let rotX=-20, rotY=30, dragging=false, lastX=0,lastY=0;
cube.addEventListener('mousedown', e=>{ dragging=true; lastX=e.clientX; lastY=e.clientY; cube.style.cursor='grabbing'; });
window.addEventListener('mousemove', e=>{
if(!dragging) return;
const dx=e.clientX-lastX, dy=e.clientY-lastY;
rotY += dx*0.5; rotX -= dy*0.5;
cube.style.transform = `rotateX(${rotX}deg) rotateY(${rotY}deg)`;
lastX=e.clientX; lastY=e.clientY;
});
window.addEventListener('mouseup', ()=>{ dragging=false; cube.style.cursor='grab'; });
function reset(){ state.fill('U'); document.querySelectorAll('.sticker').forEach((s,i)=>{ /* need map */}); location.reload(); }
function applyStr(){
const v = document.getElementById('manual').value.trim();
if (v.length !== 54) { alert('need 54 chars'); return; }
for (let i=0;i<54;i++){ state[i]=v[i]; }
document.querySelectorAll('.sticker').forEach(s=>{ const idx=+s.dataset.idx; s.className='sticker '+state[idx]; });
updateStr();
}
async function solve(){
const str = state.join('');
const res = await fetch('/solve', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({state: str})});
const j = await res.json();
document.getElementById('out').textContent = j.solution || j.error || 'no solution';
}
</script>
</body>
</html>"#;
#[derive(Deserialize)]
struct SolveReq {
state: String,
}
#[derive(Serialize)]
struct SolveRes {
solution: String,
}
#[derive(Serialize)]
struct ErrRes {
error: String,
}
pub async fn run(port: u16) -> Result<()> {
let app = Router::new()
.route("/", get(|| async { Html(HTML) }))
.route("/solve", post(solve_handler));
let addr = SocketAddr::from(([127, 0, 0, 1], port));
println!("GUI running at http://{}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn solve_handler(Json(req): Json<SolveReq>) -> Result<Json<SolveRes>, Json<ErrRes>> {
let state = req.state;
let sol = tokio::task::spawn_blocking(move || {
crate::solver::solve_facelet(&state, 20, 5.0)
})
.await
.map_err(|e| Json(ErrRes { error: e.to_string() }))?
.map_err(|e| Json(ErrRes { error: e.to_string() }))?;
Ok(Json(SolveRes {
solution: crate::notation::format_moves(&sol),
}))
}