#!/usr/bin/env python3
"""Minimal URDF viewer server (stdlib only) — stand-in for the lab's cad viewer while phe108 is down.
Same URL interface: /?dir=<abs dir>&file=<urdf rel>  +  /__cad/catalog?dir=&file= (200 if loadable).
Serves files under ALLOW_ROOT via /files/<abs-path>; the page uses three.js + urdf-loader (CDN) with
orbit controls + joint sliders. Runs on 127.0.0.1:4178 behind the cloudflared ingress (cad.omidlab.net)."""
import json
import os
import posixpath
import urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

ALLOW_ROOT = "/data/cameron"
PORT = 4178

PAGE = """<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>cad viewer (recovery)</title>
<style>
 body{margin:0;background:#16181d;color:#dde;font-family:system-ui,sans-serif;overflow:hidden}
 #hud{position:fixed;top:0;left:0;padding:10px 14px;background:#16181dcc;z-index:9;max-width:340px}
 #hud h3{margin:0 0 6px;font-size:14px;font-weight:600}
 #hud .path{font-size:11px;color:#8a93a6;word-break:break-all}
 #joints{margin-top:8px}
 .jrow{display:flex;align-items:center;gap:8px;font-size:12px;margin:3px 0}
 .jrow label{width:110px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
 .jrow input{flex:1}
 .jrow span{width:44px;text-align:right;color:#9fb0d0}
 #err{color:#ff7a7a;font-size:12px;white-space:pre-wrap}
 .note{font-size:10px;color:#667;margin-top:6px}
</style>
<script type="importmap">
{"imports":{
  "three":"https://unpkg.com/three@0.158.0/build/three.module.js",
  "three/examples/jsm/":"https://unpkg.com/three@0.158.0/examples/jsm/"
}}
</script></head>
<body>
<div id="hud"><h3>cad viewer <span style="color:#e0a63f">(recovery host)</span></h3>
<div class="path" id="path"></div><div id="joints"></div><div id="err"></div>
<div class="note">temporary viewer on omid-fleet while phe108 is down — drag to orbit, scroll to zoom</div></div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import URDFLoader from 'https://unpkg.com/urdf-loader@0.12.6/src/URDFLoader.js';

const q = new URLSearchParams(location.search);
const dir = q.get('dir') || '/data/cameron/cad_recovery';
const file = q.get('file') || '';
document.getElementById('path').textContent = dir + '/' + file;

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x16181d);
const cam = new THREE.PerspectiveCamera(50, innerWidth/innerHeight, 0.001, 100);
cam.position.set(0.5, 0.4, 0.5);
const ren = new THREE.WebGLRenderer({antialias:true});
ren.setSize(innerWidth, innerHeight); ren.setPixelRatio(devicePixelRatio);
document.body.appendChild(ren.domElement);
const ctl = new OrbitControls(cam, ren.domElement);
scene.add(new THREE.HemisphereLight(0xffffff, 0x445566, 1.6));
const dl = new THREE.DirectionalLight(0xffffff, 1.4); dl.position.set(1,2,1); scene.add(dl);
const grid = new THREE.GridHelper(2, 20, 0x334, 0x223); scene.add(grid);

if (file) {
  const loader = new URDFLoader();
  loader.load('/files' + dir + '/' + file, robot => {
    robot.rotation.x = -Math.PI/2;              // URDF Z-up -> three Y-up
    scene.add(robot);
    const box = new THREE.Box3().setFromObject(robot);
    const c = box.getCenter(new THREE.Vector3()), s = box.getSize(new THREE.Vector3()).length();
    ctl.target.copy(c); cam.position.set(c.x + s*0.7, c.y + s*0.5, c.z + s*0.7); ctl.update();
    const jd = document.getElementById('joints');
    for (const [name, j] of Object.entries(robot.joints)) {
      if (j.jointType === 'fixed') continue;
      const lo = isFinite(j.limit.lower) ? j.limit.lower : -3.14;
      const hi = isFinite(j.limit.upper) ? j.limit.upper : 3.14;
      const row = document.createElement('div'); row.className = 'jrow';
      row.innerHTML = `<label title="${name}">${name}</label><input type="range" min="${lo}" max="${hi}" step="0.01" value="${j.angle||0}"><span>${(+j.angle||0).toFixed(2)}</span>`;
      const [inp, val] = [row.querySelector('input'), row.querySelector('span')];
      inp.addEventListener('input', () => { robot.setJointValue(name, +inp.value); val.textContent = (+inp.value).toFixed(2); });
      jd.appendChild(row);
    }
  }, undefined, e => { document.getElementById('err').textContent = 'load error: ' + (e.message || e); });
} else document.getElementById('err').textContent = 'no ?file= given';

addEventListener('resize', () => { cam.aspect = innerWidth/innerHeight; cam.updateProjectionMatrix(); ren.setSize(innerWidth, innerHeight); });
(function loop(){ requestAnimationFrame(loop); ctl.update(); ren.render(scene, cam); })();
</script></body></html>"""


def safe(path):
    p = posixpath.normpath(path)
    return p.startswith(ALLOW_ROOT + "/") or p == ALLOW_ROOT


class H(BaseHTTPRequestHandler):
    def _send(self, code, body, ctype="text/html; charset=utf-8"):
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        u = urllib.parse.urlparse(self.path)
        if u.path == "/" or u.path == "":
            return self._send(200, PAGE.encode())
        if u.path == "/__cad/catalog":
            qs = urllib.parse.parse_qs(u.query)
            f = posixpath.join(qs.get("dir", [""])[0], qs.get("file", [""])[0])
            ok = safe(f) and os.path.isfile(f)
            return self._send(200 if ok else 404, json.dumps({"ok": ok, "path": f}).encode(), "application/json")
        if u.path.startswith("/files/"):
            f = posixpath.normpath(urllib.parse.unquote(u.path[len("/files"):]))
            if not (safe(f) and os.path.isfile(f)):
                return self._send(404, b"not found", "text/plain")
            ext = f.rsplit(".", 1)[-1].lower()
            ctype = {"urdf": "application/xml", "stl": "application/octet-stream",
                     "glb": "model/gltf-binary", "png": "image/png"}.get(ext, "application/octet-stream")
            with open(f, "rb") as fh:
                return self._send(200, fh.read(), ctype)
        return self._send(404, b"not found", "text/plain")

    def log_message(self, *a):
        pass


if __name__ == "__main__":
    print(f"cad recovery viewer on 127.0.0.1:{PORT}, root {ALLOW_ROOT}")
    ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever()
