// Importe gemäß Import-Map
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

(function safeMain(){
  // Robust gegen Fehlstarts
  window.addEventListener('error', (e) => console.error('[APP] Window error:', e.message, e.error));
  window.addEventListener('unhandledrejection', (e) => console.error('[APP] Unhandled promise rejection:', e.reason));

  (async function(){
    const statusEl = document.getElementById('status');
    const fpsEl = document.getElementById('fps');
    const frameInfoEl = document.getElementById('frameInfo');
    const frames10sEl = document.getElementById('frames10s');
    //const cfgEl = document.getElementById('cfg');
    const canvas = document.getElementById('three');

    function setStatus(txt) { statusEl.textContent = txt; console.log('[APP]', txt); }

    // --- Config laden ---
    setStatus('Lade /config …');
    let sensor, pose;
    try {
      const res = await fetch('/config', { cache: 'no-store' });
      if (!res.ok) throw new Error(`/config HTTP ${res.status}`);
      const json = await res.json();
      sensor = json.sensor; pose = json.pose;
      //cfgEl.textContent = JSON.stringify({ sensor, pose }, null, 2);
      setStatus('Config geladen');
    } catch (err) {
      console.error('[APP] /config Fehler:', err);
      setStatus('Fehler: /config nicht erreichbar');
      return;
    }

    // --- Three.js Setup ---
    const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0xffffff); // weißer Hintergrund

    const camera = new THREE.PerspectiveCamera(60, 1, 0.01, 200);
    camera.position.set(0.5, 0.5, 1.2);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 0, 0.4);

    function resize() {
      const w = window.innerWidth ; //- 320; // Seitenpanel
      const h = window.innerHeight - 48; // Header
      renderer.setSize(w, h, false);
      camera.aspect = Math.max(0.1, w / h);
      camera.updateProjectionMatrix();
    }

    // Licht & Helfer
    scene.add(new THREE.AmbientLight(0xffffff, 0.8));
    const dir = new THREE.DirectionalLight(0xffffff, 0.5); dir.position.set(1,2,3); scene.add(dir);
    const grid = new THREE.GridHelper(8, 80, 0xbbc4ce, 0xe0e6eb); grid.position.y = -0.001; scene.add(grid);
    const axesWorld = new THREE.AxesHelper(1.0); scene.add(axesWorld); // R/G/B
    const axesSensor = new THREE.AxesHelper(0.5); scene.add(axesSensor); // Sensorpose

    // Instanced spheres
    const W = sensor.gridW, H = sensor.gridH;
    const instances = W * H;
    const radius = (sensor.sphereDiameterM || 0.05) / 2;
    
    const geom = new THREE.SphereGeometry(radius, 16, 12);

    // 1) Basis-Vertexfarben = Weiß (1,1,1) an die Geometrie hängen
    const vcount = geom.attributes.position.count;
    const white = new Float32Array(vcount * 3);
    white.fill(1.0);
    geom.setAttribute('color', new THREE.BufferAttribute(white, 3));

    // 2) Material: Vertexfarben aktivieren, neutraler Materialfarbton
    const mat = new THREE.MeshBasicMaterial({ color: 0xffffff, vertexColors: true });

    // 3) InstancedMesh erzeugen
    const spheres = new THREE.InstancedMesh(geom, mat, instances);
    spheres.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
    
    
    if (!spheres.instanceColor) {
      const colors = new Float32Array(instances * 3);
      for (let i = 0; i < instances; i++) {
        colors[i * 3 + 0] = 1.0; // R
        colors[i * 3 + 1] = 1.0; // G
        colors[i * 3 + 2] = 1.0; // B
      }
      spheres.instanceColor = new THREE.InstancedBufferAttribute(colors, 3);
    }

    
    spheres.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
    scene.add(spheres);
    const dummy = new THREE.Object3D();

    // Pose
    let sensorPose = {
      position: new THREE.Vector3(...pose.position),
      rotationEulerDeg: pose.rotationEulerDeg,
      eulerOrder: pose.eulerOrder || 'ZYX'
    };
    function applyPose(vec) {
      const euler = new THREE.Euler(
        THREE.MathUtils.degToRad(sensorPose.rotationEulerDeg?.[0] || 0),
        THREE.MathUtils.degToRad(sensorPose.rotationEulerDeg?.[1] || 0),
        THREE.MathUtils.degToRad(sensorPose.rotationEulerDeg?.[2] || 0),
        sensorPose.eulerOrder || 'ZYX'
      );
      const q = new THREE.Quaternion().setFromEuler(euler);
      vec.applyQuaternion(q).add(sensorPose.position);
      return vec;
    }
    function updateSensorAxes() {
      axesSensor.position.copy(sensorPose.position);
      const euler = new THREE.Euler(
        THREE.MathUtils.degToRad(sensorPose.rotationEulerDeg?.[0] || 0),
        THREE.MathUtils.degToRad(sensorPose.rotationEulerDeg?.[1] || 0),
        THREE.MathUtils.degToRad(sensorPose.rotationEulerDeg?.[2] || 0),
        sensorPose.eulerOrder || 'ZYX'
      );
      axesSensor.setRotationFromEuler(euler);
    }
    updateSensorAxes();


    // Distanz-Farbskala: 0 m (nah) = rot, 4 m (weit) = blau 
    function colorForDist(m) {
      // Normierung auf 0..4 m
      let t = THREE.MathUtils.clamp(m/4 , 0, 1);   // << const -> let
      t = Math.min(Math.max(t, 0.01), 0.99);          // sanftes Clamping, vermeidet reinste Endfarben

      const col = new THREE.Color();
      col.setHSL( (1 - t) * 0.0 + t * (240/360), 1.0, 0.5 );
      return col;
    }



    // Richtungsvektor je Zelle (FoV Konfiguration aus sensor.*)
    const hTan = Math.tan(THREE.MathUtils.degToRad(sensor.hFovDeg) / 2);
    const vTan = Math.tan(THREE.MathUtils.degToRad(sensor.vFovDeg) / 2);
    function cellDir(xIdx, yIdx) {
      const u = ((xIdx + 0.5) / W) * 2 - 1; // -1..+1
      const v = ((yIdx + 0.5) / H) * 2 - 1; // -1..+1
      const dx = u * hTan;
      const dy = -v * vTan; // screen-y -> world up
      return new THREE.Vector3(dx, dy, 1).normalize();
    }

    // --- WebSockets ---
    function explainCloseCode(code) {
      switch (code) {
        case 1000: return 'Normal Closure';
        case 1001: return 'Going Away';
        case 1002: return 'Protocol Error';
        case 1003: return 'Unsupported Data';
        case 1005: return 'No Status (internal)';
        case 1006: return 'Abnormal Closure – häufig: Handshake blockiert (Pfad/Port), Reverse-Proxy, CORS/Mixed-Content, Firewall';
        case 1007: return 'Invalid Payload';
        case 1008: return 'Policy Violation';
        case 1009: return 'Message Too Big';
        case 1011: return 'Server Error';
        default: return 'Unbekannt';
      }
    }

    function makeWS(path, label) {
      const proto = location.protocol === 'https:' ? 'wss' : 'ws';
      const url = `${proto}://${location.host}${path}`;
      const ws = new WebSocket(url);
      ws.binaryType = 'arraybuffer';

      ws.addEventListener('open', () => {
        setStatus(`${label}: Verbunden (${url})`);
      });

      ws.addEventListener('close', (ev) => {
        console.warn(`[WS ${label}] close`, { code: ev.code, reason: ev.reason, wasClean: ev.wasClean });
        const note = explainCloseCode(ev.code);
        setStatus(`${label}: Getrennt (code ${ev.code}${note ? ` · ${note}` : ''})`);
        console.info(`[WS ${label}] Diagnose: Prüfe Pfad ${path}, Server-Port, Mixed-Content (http+ws vs https+wss), evtl. Proxy/Firewall.`);
      });

      ws.addEventListener('error', (err) => {
        console.error(`[WS ${label}] error`, err);
        setStatus(`${label}: Fehler (siehe Konsole)`);
      });

      ws.addEventListener('message', (ev) => {
        // nur Sizing loggen, um Konsole nicht zu fluten
        // console.debug(`[WS ${label}] message`, typeof ev.data, ev.data?.byteLength || ev.data?.length);
      });

      return ws;
    }

    const wsFrames = makeWS('/ws/frames', 'Frames');   // Datenstrom
    const wsState  = makeWS('/ws/state',  'State');    // Pose/Status

    // Client-Statistik: empfangene Frames pro 10s
    let framesReceived10s = 0;
    setInterval(() => {
      frames10sEl.textContent = String(framesReceived10s);
      console.info(`[STAT 10s Client] framesReceived:${framesReceived10s}`);
      framesReceived10s = 0;
    }, 10_000);

    wsState.onmessage = (ev) => {
      try {
        const msg = JSON.parse(typeof ev.data === 'string' ? ev.data : new TextDecoder().decode(ev.data));
        if (msg.type === 'pose' && msg.pose) {
          sensorPose.position = new THREE.Vector3(...msg.pose.position);
          sensorPose.rotationEulerDeg = msg.pose.rotationEulerDeg;
          sensorPose.eulerOrder = msg.pose.eulerOrder || 'ZYX';
          updateSensorAxes();
        }
      } catch (e) {
        console.error('[WS State] parse error', e);
      }
    };

    // CSV: ts,w,h,d0..dN
    function handleCSV(text) {
      try {
        const s = text.trim();
        const p = s.split(',');
        if (p.length < 3) return;

        
        const expected = 3 + W * H;
        if (p.length < expected) {
          console.warn('[CSV] zu kurze Zeile', { got: p.length, expected });
          return; // oder fehlende Werte als NaN behandeln
        }


        const w = parseInt(p[1], 10), h = parseInt(p[2], 10);
        if (w !== W || h !== H) {
          console.warn('[CSV] Unerwartete Dimension:', w, 'x', h, 'erwartet:', W, 'x', H);
          return;
        }
        
        const vals = new Array(W * H);
        for (let i = 0; i < W * H; i++) {
          const raw = p[3 + i];
          // Leere Felder oder fehlende Indizes abfangen
          if (raw === undefined || raw === null || raw === '') {
            vals[i] = NaN;
            continue;
          }
          const v = Number(raw); // robust gegen floats und "nan"
          vals[i] = Number.isFinite(v) ? v : NaN;
        }


        const col = new THREE.Color();
        for (let y=0; y<H; y++) {
          for (let x=0; x<W; x++) {
            const idx = y*W + x;
            const mm = vals[idx];
            const m = mm * 0.001; // mm -> m
            const dir = cellDir(x,y);
            const posLocal = dir.clone().multiplyScalar(m);
            const posWorld = applyPose(posLocal);
            dummy.position.copy(posWorld);
            dummy.rotation.set(0,0,0);
            dummy.updateMatrix();
            spheres.setMatrixAt(idx, dummy.matrix);
            col.copy(colorForDist(m));
            spheres.setColorAt(idx, col);
            //const testColor = new THREE.Color(0x00ff00);
            //spheres.setColorAt(idx, testColor);


          }
        }
        spheres.instanceMatrix.needsUpdate = true;
        if (spheres.instanceColor) spheres.instanceColor.needsUpdate = true;

        frameInfoEl.textContent = `${new Date().toLocaleTimeString()} · ${W}x${H}`;
        framesReceived10s++;
      } catch (e) {
        console.error('[CSV] handle error:', e);
      }

      
      // nachdem alle setMatrixAt(...) und setColorAt(...) fertig sind:
      spheres.instanceMatrix.needsUpdate = true;
      if (spheres.instanceColor) spheres.instanceColor.needsUpdate = true;

      if (!window.__primed__) {
          spheres.material.needsUpdate = true; // <<< WICHTIG
          window.__primed__ = true;
      }

      
    }

    wsFrames.onmessage = (ev) => {
      if (typeof ev.data === 'string') handleCSV(ev.data);
      else if (ev.data instanceof Blob) ev.data.text().then(handleCSV, (e)=>console.error('[CSV] blob->text error', e));
      else if (ev.data instanceof ArrayBuffer) {
        const txt = new TextDecoder().decode(ev.data);
        handleCSV(txt);
      } else {
        console.warn('[WS Frames] unbekannter Datentyp', ev.data);
      }
    };

    // Renderloop + FPS
    let last = performance.now(), accum = 0, frames = 0;
    function loop(t){
      const dt = t - last; last = t; accum += dt; frames++;
      if (accum >= 1000) { fpsEl.textContent = Math.round(frames * 1000 / accum); frames = 0; accum = 0; }
      controls.update();
      renderer.render(scene, camera);
      requestAnimationFrame(loop);
    }
    requestAnimationFrame(loop);

    window.addEventListener('resize', resize); resize();
    /*
    document.getElementById('btnReset').addEventListener('click', () => {
      camera.position.set(0.5, 0.5, 1.2);
      controls.target.set(0, 0, 0.4);
      controls.update();
    });
    */

    setStatus('Viewer bereit – warte auf Frames …');
  })().catch(err => {
    console.error('[APP] fatal:', err);
    const statusEl = document.getElementById('status');
    if (statusEl) statusEl.textContent = 'Fehler: siehe Konsole';
  });
})();