import { render } from "tradjs/client";
import * as THREE from "three";

const COLS = 24, ROWS = 18, START_STEP = 120;
type Point = { x: number; y: number };
type PowerKind = "shield" | "slow" | "multi";
type Power = Point & { kind: PowerKind };

export default function mount() {
  const root = document.getElementById("game-root");
  if (!root) throw new Error("Missing #game-root");

  render(
    <div className="game">
      <style>{`
        .game{min-height:100vh;box-sizing:border-box;display:grid;place-items:center;padding:16px;color:#edffe8;background:radial-gradient(circle at 50% 0,#173d35,#050d0c 68%);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;user-select:none}.panel{width:min(96vw,880px)}
        .head,.foot,.stats,.actions,.powers{display:flex;align-items:center}.head{justify-content:space-between;gap:12px;margin-bottom:9px}.title{margin:0;color:#caff72;font:900 clamp(25px,5vw,42px)/1 sans-serif;letter-spacing:-.055em;text-shadow:0 0 25px #72ff6655}.sub,.help{color:#89a79b;font-size:10px}.sub{margin:6px 0 0}.stats{gap:6px}.stat{min-width:57px;padding:6px 8px;border:1px solid #31564a;border-radius:8px;background:#0a1c18cc;text-align:center}.label{display:block;color:#78998b;font-size:8px;letter-spacing:.12em}.value{font-size:17px;font-weight:900}
        .stage{position:relative;overflow:hidden;border:2px solid #4d8754;border-radius:13px;background:#071612;box-shadow:0 20px 55px #0009}.viewport{width:100%;aspect-ratio:4/3;touch-action:none}.viewport canvas{display:block;width:100%!important;height:100%!important}.powers{position:absolute;top:10px;left:10px;gap:6px;pointer-events:none}.chip{display:none;padding:5px 8px;border:1px solid currentColor;border-radius:99px;background:#07140ddd;font-size:10px;font-weight:900}.chip.on{display:block}.shield{color:#65ddff}.slow{color:#ad8cff}.multi{color:#ffd65a}
        .overlay{position:absolute;inset:0;display:none;place-items:center;background:#03100cc9;text-align:center;backdrop-filter:blur(4px)}.overlay.show{display:grid}.card h2{margin:0 0 7px;color:#ff856c;font-size:clamp(30px,7vw,52px)}.card p{color:#c1d7cd;line-height:1.55;white-space:pre-line}.button{border:0;border-radius:8px;cursor:pointer;font:800 12px inherit}.retry{padding:11px 18px;color:#092014;background:#bdff75;box-shadow:0 5px 0 #558a56}.foot{justify-content:space-between;gap:10px;margin-top:10px}.help{line-height:1.6}.actions{gap:9px}.pause{padding:9px 11px;border:1px solid #41675a;color:#dfffd5;background:#10271f}.pad{display:grid;grid-template:repeat(2,35px)/repeat(3,35px);gap:4px}.pad button{border:1px solid #41675a;border-radius:7px;color:#dfffd5;background:#10271f;font-size:15px;touch-action:manipulation}.up{grid-column:2}.left{grid-column:1;grid-row:2}.down{grid-column:2;grid-row:2}.right{grid-column:3;grid-row:2}@media(max-width:560px){.sub{display:none}.stat{min-width:42px;padding:5px}.value{font-size:14px}.help{font-size:8px}}
      `}</style>
      <main className="panel">
        <header className="head"><div><h1 className="title">NEON SERPENT 3D</h1><p className="sub">WebGL Hazard Run · survive the shifting arena.</p></div><div className="stats"><div className="stat"><span className="label">SCORE</span><span id="score" className="value">0</span></div><div className="stat"><span className="label">BEST</span><span id="best" className="value">0</span></div><div className="stat"><span className="label">LEVEL</span><span id="level" className="value">1</span></div></div></header>
        <section className="stage">
          <div id="viewport" className="viewport" aria-label="Three-dimensional snake hazard arena" />
          <div className="powers"><span id="shield" className="chip shield">◆ SHIELD</span><span id="slow" className="chip slow">◷ SLOW</span><span id="multi" className="chip multi">×2 SCORE</span></div>
          <div id="overlay" className="overlay"><div className="card"><h2 id="end-title">CRASHED</h2><p id="result">Score: 0</p><button id="retry" className="button retry">Retry — Enter</button></div></div>
        </section>
        <footer className="foot"><div className="help">Move: Arrows / WASD / swipe · Pause: Space / P<br />◆ shield · ◷ slow time · ×2 score · R restart</div><div className="actions"><button id="pause" className="button pause">Pause</button><div className="pad"><button className="up" data-dir="up">▲</button><button className="left" data-dir="left">◀</button><button className="down" data-dir="down">▼</button><button className="right" data-dir="right">▶</button></div></div></footer>
      </main>
    </div>, root
  );

  const viewport = root.querySelector("#viewport") as HTMLElement;
  const scoreEl = root.querySelector("#score")!, bestEl = root.querySelector("#best")!, levelEl = root.querySelector("#level")!;
  const overlay = root.querySelector("#overlay")!, endTitle = root.querySelector("#end-title")!, result = root.querySelector("#result")!;
  const retry = root.querySelector("#retry") as HTMLButtonElement, pauseBtn = root.querySelector("#pause") as HTMLButtonElement;
  const chips: Record<PowerKind, Element> = { shield: root.querySelector("#shield")!, slow: root.querySelector("#slow")!, multi: root.querySelector("#multi")! };

  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
  renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
  renderer.setSize(960, 720, false);
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  viewport.appendChild(renderer.domElement);

  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x06130f);
  scene.fog = new THREE.Fog(0x06130f, 18, 33);
  const camera = new THREE.PerspectiveCamera(39, 4 / 3, .1, 60);
  camera.position.set(0, 17.5, 15.5);
  camera.lookAt(0, 0, 0);
  scene.add(new THREE.HemisphereLight(0xb8ffe0, 0x07120e, 1.45));
  const keyLight = new THREE.DirectionalLight(0xd8ffb0, 2.2);
  keyLight.position.set(-7, 14, 8); keyLight.castShadow = true; scene.add(keyLight);
  const arena = new THREE.Group(); scene.add(arena);
  const floor = new THREE.Mesh(new THREE.PlaneGeometry(COLS, ROWS), new THREE.MeshStandardMaterial({ color: 0x0b211a, roughness: .82, metalness: .1 }));
  floor.rotation.x = -Math.PI / 2; floor.position.y = -.48; floor.receiveShadow = true; arena.add(floor);
  const grid = new THREE.GridHelper(COLS, COLS, 0x315c49, 0x17382c);
  grid.scale.z = ROWS / COLS; grid.position.y = -.46; arena.add(grid);
  const borderMat = new THREE.MeshStandardMaterial({ color: 0x285d45, emissive: 0x143522 });
  [[0,-ROWS/2-.3,COLS+.8,.65],[0,ROWS/2+.3,COLS+.8,.65],[-COLS/2-.3,0,.65,ROWS+.8],[COLS/2+.3,0,.65,ROWS+.8]].forEach(([x,z,w,d])=>{const m=new THREE.Mesh(new THREE.BoxGeometry(w,.8,d),borderMat);m.position.set(x,0,z);m.castShadow=true;arena.add(m)});

  const snakeGroup = new THREE.Group(), obstacleGroup = new THREE.Group(), itemGroup = new THREE.Group();
  arena.add(snakeGroup, obstacleGroup, itemGroup);
  const bodyGeo = new THREE.BoxGeometry(.78,.78,.78), rockGeo = new THREE.DodecahedronGeometry(.47,0), foodGeo = new THREE.IcosahedronGeometry(.34,1), ringGeo = new THREE.TorusGeometry(.42,.09,10,24);
  const bodyMat = new THREE.MeshStandardMaterial({color:0x5ceb71,emissive:0x164f20,roughness:.28});
  const headMat = new THREE.MeshStandardMaterial({color:0xd4ff79,emissive:0x5a7d21,roughness:.2});
  const rockMat = new THREE.MeshStandardMaterial({color:0x344b46,roughness:.72,metalness:.18});
  const foodMat = new THREE.MeshStandardMaterial({color:0xff765c,emissive:0xb52616,emissiveIntensity:1.4});
  const powerMats:Record<PowerKind,THREE.MeshStandardMaterial>={shield:new THREE.MeshStandardMaterial({color:0x65ddff,emissive:0x16768c}),slow:new THREE.MeshStandardMaterial({color:0xad8cff,emissive:0x49258c}),multi:new THREE.MeshStandardMaterial({color:0xffd65a,emissive:0x8b6110})};
  const foodMesh = new THREE.Mesh(foodGeo,foodMat); foodMesh.castShadow=true; itemGroup.add(foodMesh);
  let powerMesh:THREE.Mesh|null=null;

  let snake:Point[]=[], oldSnake:Point[]=[], food:Point={x:0,y:0}, obstacles:Point[]=[], power:Power|null=null;
  let dir:Point={x:1,y:0}, queue:Point[]=[], score=0, best=0, foods=0, dead=false, paused=false, shieldHits=0, powerTimer=0, powerKind:PowerKind|null=null;
  let acc=0, previous=performance.now(), raf=0, shake=0, pointer:Point|null=null, audio:AudioContext|null=null;
  const same=(a:Point,b:Point)=>a.x===b.x&&a.y===b.y;
  const world=(p:Point)=>({x:p.x-COLS/2+.5,z:p.y-ROWS/2+.5});
  const occupied=(p:Point)=>snake.some(s=>same(s,p))||obstacles.some(o=>same(o,p));
  function tone(f:number,d=.09){try{audio||=new AudioContext();if(audio.state==="suspended")audio.resume();const o=audio.createOscillator(),g=audio.createGain();o.frequency.value=f;g.gain.setValueAtTime(.035,audio.currentTime);g.gain.exponentialRampToValueAtTime(.0001,audio.currentTime+d);o.connect(g);g.connect(audio.destination);o.start();o.stop(audio.currentTime+d)}catch{}}
  function emptyPoint(){let p:Point;do p={x:Math.floor(Math.random()*COLS),y:Math.floor(Math.random()*ROWS)};while(occupied(p)||same(p,food)||(!!power&&same(p,power)));return p}
  function placeFood(){food=emptyPoint();const w=world(food);foodMesh.position.set(w.x,.05,w.z)}
  function rebuildObstacles(){obstacleGroup.clear();obstacles.forEach((o,i)=>{const m=new THREE.Mesh(rockGeo,rockMat);const w=world(o);m.position.set(w.x,.03,w.z);m.rotation.set(i*.7,i*1.1,0);m.castShadow=true;m.receiveShadow=true;obstacleGroup.add(m)})}
  function buildArena(){obstacles=[];const count=7+Math.min(14,Math.floor(foods/3));for(let i=0;i<count;i++){let p:Point;do p={x:2+Math.floor(Math.random()*(COLS-4)),y:1+Math.floor(Math.random()*(ROWS-2))};while(obstacles.some(o=>same(o,p))||snake.some(s=>Math.abs(s.x-p.x)+Math.abs(s.y-p.y)<3));obstacles.push(p)}rebuildObstacles()}
  function syncPower(){if(powerMesh){itemGroup.remove(powerMesh);powerMesh=null}if(power){powerMesh=new THREE.Mesh(ringGeo,powerMats[power.kind]);const w=world(power);powerMesh.position.set(w.x,.12,w.z);powerMesh.rotation.x=Math.PI/2;powerMesh.castShadow=true;itemGroup.add(powerMesh)}}
  function syncSnake(alpha:number){while(snakeGroup.children.length<snake.length){const m=new THREE.Mesh(bodyGeo,bodyMat);m.castShadow=true;m.receiveShadow=true;snakeGroup.add(m)}while(snakeGroup.children.length>snake.length)snakeGroup.remove(snakeGroup.children.at(-1)!);snake.forEach((p,i)=>{const old=oldSnake[Math.min(i,oldSnake.length-1)]||p,w=world({x:old.x+(p.x-old.x)*alpha,y:old.y+(p.y-old.y)*alpha});const m=snakeGroup.children[i] as THREE.Mesh;m.position.set(w.x,i?0:.08,w.z);m.material=i?bodyMat:headMat;const taper=Math.max(.58,1-i*.018);m.scale.setScalar(taper)})}
  function reset(){snake=[{x:6,y:9},{x:5,y:9},{x:4,y:9},{x:3,y:9}];oldSnake=snake.map(p=>({...p}));dir={x:1,y:0};queue=[];score=foods=shieldHits=0;power=null;powerKind=null;powerTimer=0;dead=paused=false;shake=acc=0;previous=performance.now();buildArena();placeFood();syncPower();scoreEl.textContent="0";levelEl.textContent="1";pauseBtn.textContent="Pause";retry.textContent="Retry — Enter";overlay.classList.remove("show");Object.values(chips).forEach(c=>c.classList.remove("on"));syncSnake(1)}
  function turn(next:Point){const last=queue.length?queue[queue.length-1]:dir;if(dead||same(next,last)||(next.x===-last.x&&next.y===-last.y))return;if(queue.length<2)queue.push(next)}
  function activate(kind:PowerKind){powerKind=kind;powerTimer=kind==="shield"?9000:7000;shieldHits=kind==="shield"?1:0;power=null;syncPower();Object.values(chips).forEach(c=>c.classList.remove("on"));chips[kind].classList.add("on");tone(kind==="multi"?820:650,.16)}
  function lose(){dead=true;shake=.7;tone(105,.45);best=Math.max(best,score);bestEl.textContent=String(best);endTitle.textContent="CRASHED";result.textContent=`Score: ${score} · Length: ${snake.length}\nPower-ups found: ${Math.floor(foods/3)}`;retry.textContent="Retry — Enter";overlay.classList.add("show")}
  function update(){oldSnake=snake.map(p=>({...p}));if(queue.length)dir=queue.shift()!;const head={x:snake[0].x+dir.x,y:snake[0].y+dir.y},wall=head.x<0||head.x>=COLS||head.y<0||head.y>=ROWS,hitBody=snake.slice(0,-1).some(p=>same(p,head)),hitRock=obstacles.some(p=>same(p,head));if(wall||hitBody||hitRock){if(shieldHits&&!wall){shieldHits=0;powerTimer=0;powerKind=null;chips.shield.classList.remove("on");obstacles=obstacles.filter(p=>!same(p,head));rebuildObstacles();shake=.35;tone(240,.18)}else return lose()}snake.unshift(head);if(same(head,food)){foods++;score+=powerKind==="multi"?20:10;scoreEl.textContent=String(score);best=Math.max(best,score);bestEl.textContent=String(best);levelEl.textContent=String(1+Math.floor(foods/4));tone(430+foods*15);if(foods%4===0){buildArena();obstacles=obstacles.filter(o=>!snake.some(s=>same(s,o)));rebuildObstacles()}placeFood();if(foods%3===0&&!power){const kinds:PowerKind[]=["shield","slow","multi"];power={...emptyPoint(),kind:kinds[Math.floor(Math.random()*3)]};syncPower()}}else snake.pop();if(power&&same(head,power))activate(power.kind)}
  function togglePause(){if(dead)return;paused=!paused;pauseBtn.textContent=paused?"Resume":"Pause";endTitle.textContent="PAUSED";result.textContent="Press Space, P, or Resume";retry.textContent="Restart Run";overlay.classList.toggle("show",paused);acc=0;previous=performance.now()}
  function frame(now:number){const elapsed=Math.min(70,now-previous);previous=now;let step=Math.max(58,START_STEP-score*.32);if(!dead&&!paused){if(powerTimer>0){powerTimer-=elapsed;if(powerTimer<=0){powerKind=null;shieldHits=0;Object.values(chips).forEach(c=>c.classList.remove("on"))}}if(powerKind==="slow")step*=1.55;acc+=elapsed;while(acc>=step){update();acc-=step;if(dead)break}}syncSnake(dead||paused?1:Math.min(1,acc/step));const t=now*.001;foodMesh.rotation.set(t*.8,t*1.5,t*.4);foodMesh.scale.setScalar(1+Math.sin(t*5)*.12);if(powerMesh){powerMesh.rotation.z=t*1.8;powerMesh.position.y=.15+Math.sin(t*4)*.14}snakeGroup.children.forEach((m,i)=>{(m as THREE.Mesh).position.y+=(Math.sin(t*5-i*.55)+1)*.018});if(shake>.005){camera.position.x=(Math.random()-.5)*shake;camera.position.y=17.5+(Math.random()-.5)*shake;shake*=.88}else camera.position.set(0,17.5,15.5);camera.lookAt(0,0,0);renderer.render(scene,camera);raf=requestAnimationFrame(frame)}

  const dirs:Record<string,Point>={arrowup:{x:0,y:-1},w:{x:0,y:-1},arrowdown:{x:0,y:1},s:{x:0,y:1},arrowleft:{x:-1,y:0},a:{x:-1,y:0},arrowright:{x:1,y:0},d:{x:1,y:0}};
  const keydown=(e:KeyboardEvent)=>{const k=e.key.toLowerCase();if(dirs[k]){e.preventDefault();turn(dirs[k])}if(k===" "||k==="p"){e.preventDefault();togglePause()}if(k==="r"||(dead&&k==="enter")){e.preventDefault();reset()}};
  const pointerDown=(e:PointerEvent)=>{pointer={x:e.clientX,y:e.clientY};renderer.domElement.setPointerCapture(e.pointerId)};
  const pointerUp=(e:PointerEvent)=>{if(!pointer)return;const dx=e.clientX-pointer.x,dy=e.clientY-pointer.y;pointer=null;if(Math.max(Math.abs(dx),Math.abs(dy))>17)turn(Math.abs(dx)>Math.abs(dy)?{x:Math.sign(dx),y:0}:{x:0,y:Math.sign(dy)})};
  const padClick=(e:Event)=>{const n=(e.currentTarget as HTMLElement).dataset.dir,m:Record<string,Point>={up:{x:0,y:-1},down:{x:0,y:1},left:{x:-1,y:0},right:{x:1,y:0}};if(n)turn(m[n])};
  const resize=()=>{const r=viewport.getBoundingClientRect();renderer.setSize(Math.max(1,r.width),Math.max(1,r.height),false);camera.aspect=r.width/Math.max(1,r.height);camera.updateProjectionMatrix()};
  const retryClick=()=>reset();window.addEventListener("keydown",keydown);window.addEventListener("resize",resize);renderer.domElement.addEventListener("pointerdown",pointerDown);renderer.domElement.addEventListener("pointerup",pointerUp);retry.addEventListener("click",retryClick);pauseBtn.addEventListener("click",togglePause);const pads=Array.from(root.querySelectorAll("[data-dir]"));pads.forEach(b=>b.addEventListener("click",padClick));resize();reset();raf=requestAnimationFrame(frame);
  return()=>{cancelAnimationFrame(raf);window.removeEventListener("keydown",keydown);window.removeEventListener("resize",resize);renderer.domElement.removeEventListener("pointerdown",pointerDown);renderer.domElement.removeEventListener("pointerup",pointerUp);retry.removeEventListener("click",retryClick);pauseBtn.removeEventListener("click",togglePause);pads.forEach(b=>b.removeEventListener("click",padClick));if(audio)audio.close();scene.traverse(o=>{const m=o as THREE.Mesh;if(m.geometry)m.geometry.dispose();if(m.material){const materials=Array.isArray(m.material)?m.material:[m.material];materials.forEach(x=>x.dispose())}});renderer.dispose();root.replaceChildren()};
}