// static/main.js

// ================= 摄像头 + ASR =================
(() => {
  const $camStatus = document.getElementById('camStatus');
  const $asrStatus = document.getElementById('asrStatus');
  const $partial = document.getElementById('partial');
  const $finalList = document.getElementById('finalList');
  const $btnClear = document.getElementById('btnClear');
  const $btnRe = document.getElementById('btnReconnect');
  const $fps = document.getElementById('fps');
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');

  // === 获取/创建聊天容器（关键补丁） ===
  let chatContainer = document.getElementById('chatContainer');

  function ensureChatContainer() {
    // 已缓存且仍在文档中
    if (chatContainer && document.body.contains(chatContainer)) return chatContainer;

    // 重新获取，防热更新或 DOM 移动
    chatContainer = document.getElementById('chatContainer');
    if (!chatContainer) {
      chatContainer = document.createElement('div');
      chatContainer.id = 'chatContainer';

      // 优先挂到 finalList 的父容器；否则挂到 partial 的父容器；再否则挂到 body 兜底
      if ($finalList && $finalList.parentElement) {
        // 隐藏原来的 finalList
        $finalList.style.display = 'none';
        // 将聊天容器挂载到 finals div 内
        $finalList.parentElement.appendChild(chatContainer);
        console.log('[chat] 创建并挂载 #chatContainer 到 finalList 区域');
      } else if ($partial && $partial.parentElement) {
        $partial.parentElement.appendChild(chatContainer);
        console.log('[chat] 创建并挂载 #chatContainer 到 partial 区域');
      } else {
        document.body.appendChild(chatContainer);
        console.warn('[chat] 未找到合适锚点，已挂到 <body>');
      }
    }
    return chatContainer;
  }

  // === 注入聊天样式（左右两侧气泡 + 时间戳，增加权重）===
  (function injectChatStyles() {
    if (document.getElementById('chat-style-injected')) return;
    const s = document.createElement('style');
    s.id = 'chat-style-injected';
    s.textContent = `
      #chatContainer{
        position: relative !important;
        overflow-y: auto !important;
        flex: 1 !important;  /* 改为使用 flex: 1 占满剩余空间 */
        min-height: 0 !important;  /* 确保 flex 子元素能正确收缩 */
        padding: 12px 12px 4px !important;
        background: #0b1020 !important;
        border: 1px solid #1d2438 !important;
        border-radius: 10px !important;
        margin-top: 12px !important;
      }
      
      /* 自定义滚动条样式 */
      #chatContainer::-webkit-scrollbar {
        width: 8px !important;
      }
      
      #chatContainer::-webkit-scrollbar-track {
        background: #0d1420 !important;
        border-radius: 4px !important;
      }
      
      #chatContainer::-webkit-scrollbar-thumb {
        background: #2a3446 !important;
        border-radius: 4px !important;
        transition: background 0.2s !important;
      }
      
      #chatContainer::-webkit-scrollbar-thumb:hover {
        background: #3a4556 !important;
      }
      
      /* Firefox 滚动条 */
      #chatContainer {
        scrollbar-width: thin !important;
        scrollbar-color: #2a3446 #0d1420 !important;
      }
      .timestamp{
        text-align:center !important;
        font-size:12px !important;
        color:#8a93a5 !important;
        margin:10px 0 !important;
        user-select:none !important;
      }
      .message{
        display:flex !important;
        gap:8px !important;
        margin:6px 0 !important;
        align-items:flex-end !important;
      }
      .message.ai{ justify-content:flex-start !important; }
      .message.user{ justify-content:flex-end !important; }

      .avatar{
        width:28px !important; height:28px !important; border-radius:50% !important;
        background:#232a3d !important; flex:0 0 28px !important;
        display:flex !important; align-items:center !important; justify-content:center !important;
        color:#9fb0c3 !important; font-size:12px !important; user-select:none !important;
        border:1px solid #29314a !important;
      }
      .message.user .avatar{ display:none !important; }

      .bubble{
        max-width: 72% !important;
        padding:10px 12px !important;
        line-height:1.45 !important;
        border-radius:14px !important;
        word-break:break-word !important;
        white-space:pre-wrap !important;
        border:1px solid transparent !important;
        box-shadow:0 2px 8px rgba(0,0,0,0.15) !important;
        font-size:14px !important;
      }
      .message.ai .bubble{
        background:#111a2e !important;
        color:#e6edf3 !important;
        border-color:#1e2740 !important;
        border-top-left-radius:6px !important;
      }
      .message.user .bubble{
        background:#2a6df4 !important;
        color:#fff !important;
        border-color:#2a6df4 !important;
        border-top-right-radius:6px !important;
      }
    `;
    document.head.appendChild(s);
  })();

  // 聊天消息管理
  let lastTimestamp = 0;
  const TIMESTAMP_INTERVAL = 5 * 60 * 1000; // 5分钟

  function shouldShowTimestamp() {
    const now = Date.now();
    if (now - lastTimestamp > TIMESTAMP_INTERVAL) {
      lastTimestamp = now;
      return true;
    }
    return false;
  }

  function formatTime(timestamp = Date.now()) {
    const date = new Date(timestamp);
    const hours = date.getHours().toString().padStart(2, '0');
    const minutes = date.getMinutes().toString().padStart(2, '0');
    return `${hours}:${minutes}`;
  }

  function addTimestamp() {
    const container = ensureChatContainer();
    const timestampDiv = document.createElement('div');
    timestampDiv.className = 'timestamp';
    timestampDiv.textContent = formatTime();
    container.appendChild(timestampDiv);
  }

  function addMessage(text, isUser = false) {
    // 时间戳
    if (shouldShowTimestamp()) addTimestamp();

    const container = ensureChatContainer();

    // 行容器
    const messageDiv = document.createElement('div');
    messageDiv.className = `message ${isUser ? 'user' : 'ai'}`;

    // 左侧头像（AI）
    const avatar = document.createElement('div');
    avatar.className = 'avatar';
    avatar.textContent = isUser ? '' : 'AI';

    // 气泡
    const bubbleDiv = document.createElement('div');
    bubbleDiv.className = 'bubble';
    bubbleDiv.textContent = text;

    if (isUser) {
      // 右侧：气泡在右
      messageDiv.appendChild(bubbleDiv);
    } else {
      // 左侧：头像 + 气泡
      messageDiv.appendChild(avatar);
      messageDiv.appendChild(bubbleDiv);
    }

    container.appendChild(messageDiv);

    // 滚动到底部
    container.scrollTop = container.scrollHeight;
  }

  // Day 20: 更新 badge 样式，支持 connecting 状态动画
  function setBadge(el, status, text) {
    el.textContent = text;
    // status: 'ok', 'err', 'connecting'
    if (status === true) status = 'ok';
    if (status === false) status = 'err';
    el.className = 'badge ' + (status || '');
  }

  function navLabelAndText(raw) {
    // 去掉前缀 “[导航] ”
    const t = raw.startsWith('[导航]') ? raw.substring(4).trim() : raw;
    // 粗略判断：含“斑马线/绿灯/红灯/黄灯/过马路”归为斑马线导航，否则盲道导航
    const crossHints = ['斑马线', '绿灯', '红灯', '黄灯', '过马路'];
    const isCross = crossHints.some(k => t.includes(k));
    const label = isCross ? '【斑马线导航】' : '【盲道导航】';
    return { label, text: `${label} ${t}` };
  }

  // 改进的 fitCanvas: 支持移动端尺寸计算
  function fitCanvas() {
    const rect = canvas.getBoundingClientRect();
    // 使用容器实际宽高，添加最小值保护
    const w = Math.max(320, Math.floor(rect.width) || 320);
    let h = Math.floor(rect.height) || 0;
    // 如果容器高度太小或为0，使用4:3比例回退
    if (h < 100) {
      h = Math.max(240, Math.floor(w * 3 / 4));
    }
    if (canvas.width !== w || canvas.height !== h) {
      canvas.width = w;
      canvas.height = h;
      console.log('[Canvas] 尺寸调整:', w, 'x', h);
    }
  }
  window.addEventListener('resize', fitCanvas);
  // 延迟初始化，确保布局完成
  setTimeout(fitCanvas, 100);
  fitCanvas();

  let wsCam, wsUI, frames = 0, fpsTimer = 0;

  function drawBlob(buf) {
    const blob = new Blob([buf], { type: 'image/jpeg' });
    if ('createImageBitmap' in window) {
      createImageBitmap(blob).then(bmp => {
        fitCanvas();
        ctx.drawImage(bmp, 0, 0, canvas.width, canvas.height);
      }).catch(() => { });
    } else {
      const img = new Image();
      img.onload = () => { fitCanvas(); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); URL.revokeObjectURL(img.src); };
      img.src = URL.createObjectURL(blob);
    }
    frames++;
    const now = performance.now();
    if (!fpsTimer) fpsTimer = now;
    if (now - fpsTimer >= 1000) {
      $fps.textContent = 'FPS: ' + frames;
      frames = 0; fpsTimer = now;
    }
  }

  function connectCamera() {
    try { if (wsCam) wsCam.close(); } catch (e) { }
    const proto = location.protocol === 'https:' ? 'wss' : 'ws';
    wsCam = new WebSocket(`${proto}://${location.host}/ws/viewer`);
    setBadge($camStatus, 'connecting', '📷 连接中...');
    wsCam.binaryType = 'arraybuffer';
    wsCam.onopen = () => setBadge($camStatus, 'ok', '📷 已连接');
    wsCam.onclose = () => setBadge($camStatus, 'err', '📷 已断开');
    wsCam.onerror = () => setBadge($camStatus, 'err', '📷 连接错误');
    wsCam.onmessage = (ev) => drawBlob(ev.data);
  }

  function connectASR() {
    try { if (wsUI) wsUI.close(); } catch (e) { }
    const proto = location.protocol === 'https:' ? 'wss' : 'ws';
    wsUI = new WebSocket(`${proto}://${location.host}/ws_ui`);
    setBadge($asrStatus, 'connecting', '🎤 连接中...');
    wsUI.onopen = () => setBadge($asrStatus, 'ok', '🎤 已连接');
    wsUI.onclose = () => setBadge($asrStatus, 'err', '🎤 已断开');
    wsUI.onerror = () => setBadge($asrStatus, 'err', '🎤 连接错误');
    wsUI.onmessage = (ev) => {
      const s = ev.data || '';
      if (s.startsWith('INIT:')) {
        try {
          const data = JSON.parse(s.slice(5));
          $partial.textContent = data.partial || '（等待音频…）';

          // 初始化时加载历史消息（识别 [AI] 与 [导航]）
          if (data.finals && data.finals.length > 0) {
            data.finals.forEach(text => {
              if (text.startsWith('[AI]')) {
                addMessage(text.substring(4).trim(), false);
              } else if (text.startsWith('[导航]')) {
                const { text: show } = navLabelAndText(text);
                addMessage(show, false);
              } else {
                addMessage(text, true);
              }
            });
          }
        } catch (e) { }
        return;
      }
      if (s.startsWith('PARTIAL:')) {
        $partial.textContent = s.slice(8);
        return;
      }
      if (s.startsWith('FINAL:')) {
        const text = s.slice(6);
        if (text.startsWith('[AI]')) {
          addMessage(text.substring(4).trim(), false);
        } else if (text.startsWith('[导航]')) {
          const { text: show } = navLabelAndText(text);
          addMessage(show, false); // 左侧 AI
        } else {
          addMessage(text, true);  // 其它仍按右侧
        }
        $partial.textContent = '（等待音频…）';
        return;
      }
    }
  }

  $btnClear.onclick = () => {
    const container = ensureChatContainer();
    // 清空聊天记录
    const messages = container.querySelectorAll('.message, .timestamp');
    messages.forEach(msg => msg.remove());
    lastTimestamp = 0; // 重置时间戳计数
  };
  $btnRe.onclick = () => { connectCamera(); connectASR(); };

  connectCamera();
  connectASR();
})();


// ================= IMU 3D（无虚线框、无滚动条、上下对齐、自适应） =================
import * as THREE from 'three';
import { GLTFLoader } from 'https://unpkg.com/three@0.155.0/examples/jsm/loaders/GLTFLoader.js';

// Day 20: IMU 浮窗折叠功能 - 修复：兼容模块延迟加载
// Day 23: 移动端优化 - 默认折叠
function initImuToggle() {
  const imuFloat = document.getElementById('imuFloat');
  const imuToggle = document.getElementById('imuToggle');
  console.log('[IMU] 初始化折叠功能, imuFloat:', !!imuFloat, 'imuToggle:', !!imuToggle);

  if (imuFloat && imuToggle) {
    // 检测移动端 - 默认折叠
    const isMobile = window.innerWidth < 1100;
    if (isMobile) {
      imuFloat.classList.add('collapsed');
      imuFloat.classList.remove('expanded');
      imuToggle.textContent = '+';
      console.log('[IMU] 移动端检测，默认折叠');
    } else {
      imuFloat.classList.add('expanded');
    }

    imuToggle.onclick = function (e) {
      e.preventDefault();
      e.stopPropagation();
      const isCollapsed = imuFloat.classList.toggle('collapsed');
      imuFloat.classList.toggle('expanded', !isCollapsed);
      this.textContent = isCollapsed ? '+' : '−';
      console.log('[IMU] 折叠状态:', isCollapsed);
    };
    console.log('[IMU] 折叠按钮事件已绑定');
  }
}

// 确保 DOM 加载后执行（兼容模块延迟加载）
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', initImuToggle);
} else {
  // DOM 已加载完成，直接执行
  initImuToggle();
}

(() => {
  const container = document.getElementById('imu_view'); // 左侧3D容器
  const hud = document.getElementById('imu_hud');  // 右侧IMU容器

  // 左右窗口统一半透明底色
  if (container) container.style.background = 'rgba(0,0,0,0.2)';
  if (hud) {
    // 关键：右侧容器作为定位参考，同时禁止滚动、清理边框
    Object.assign(hud.style, {
      position: 'relative',
      overflow: 'hidden',
      border: 'none',
      outline: 'none',
      background: 'rgba(0,0,0,0.2)', // 右侧也给统一底色（整块），干净无额外面板底色
      borderRadius: '10px'
    });
  }

  // —— 彻底去掉“虚线框”和一切边框/阴影（含可能的外层壳）——
  (function killFraming() {
    const s = document.createElement('style');
    s.textContent = `
      #imu_view, #imu_hud, #data-panel, #imu_dock,
      .imu-card, .imu-wrap, .panel, .card, .window {
        border: none !important;
        outline: none !important;
        box-shadow: none !important;
        background-image: none !important;
      }
      /* 兜底：清除任何内联 dashed/ dotted */
      [style*="dashed"], [style*="dotted"] {
        border-style: none !important;
        outline: none !important;
      }
    `;
    document.head.appendChild(s);

    // 同时清理父级（最多向上两层）里的边框与滚动条，避免外层虚线框和滚动条
    [container, hud].forEach(el => {
      let p = el ? el.parentElement : null;
      for (let i = 0; i < 2 && p; i++, p = p.parentElement) {
        p.style.border = 'none';
        p.style.outline = 'none';
        p.style.boxShadow = 'none';
        p.style.overflow = 'hidden';
        p.style.backgroundImage = 'none';
      }
    });
  })();

  // 右侧：不再额外创建 dock 背板（直接用 hud 当整块背景）
  // 数据面板只负责显示文字，不再自带背景与边框

  // three.js 渲染器
  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(70, 1, 0.1, 1000);

  // 画质相关
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.outputColorSpace = THREE.SRGBColorSpace;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.0;
  renderer.setClearColor(0x000000, 0); // 透明背景

  // ——— 核心：左右窗口“上下齐+自适应等比” ———
  let syncRaf = 0;
  function syncHeights() {
    if (!container || !hud) return;
    const w = container.clientWidth || 300;

    // 恢复合理高度设置
    const padding = 20;
    const contentH = (document.getElementById('data-panel')?.offsetHeight || 0) + padding;
    let targetH = Math.max(180, contentH);  // 最小高度 180px

    hud.style.height = `${targetH}px`;
    hud.style.maxHeight = 'none';
    hud.style.overflow = 'hidden';

    container.style.height = `${targetH}px`;
    renderer.setSize(w, targetH);
    camera.aspect = w / targetH;
    camera.updateProjectionMatrix();
  }

  function requestSync() {
    cancelAnimationFrame(syncRaf);
    syncRaf = requestAnimationFrame(syncHeights);
  }

  // 初次与窗口变化时，同步左右高度
  requestSync();
  window.addEventListener('resize', requestSync);

  // 数据变化时也同步（放在 updateDataPanel 内）
  function updateDataPanel(roll, pitch, yaw, gx, gy, gz, ax, ay, az) {
    document.getElementById('panel-roll').textContent = roll.toFixed(1) + '°';
    document.getElementById('panel-pitch').textContent = pitch.toFixed(1) + '°';
    document.getElementById('panel-yaw').textContent = yaw.toFixed(1) + '°';
    document.getElementById('panel-gx').textContent = gx.toFixed(1);
    document.getElementById('panel-gy').textContent = gy.toFixed(1);
    document.getElementById('panel-gz').textContent = gz.toFixed(1);
    document.getElementById('panel-ax').textContent = ax.toFixed(2);
    document.getElementById('panel-ay').textContent = ay.toFixed(2);
    document.getElementById('panel-az').textContent = az.toFixed(2);

    requestSync(); // 数据刷新后同步高度
  }


  container.appendChild(renderer.domElement);

  // ========== 场景 ==========
  const group = new THREE.Group();
  scene.add(group);

  const axesHelper = new THREE.AxesHelper(4);
  scene.add(axesHelper);

  function createAxisLabel(text, position, color) {
    const c = document.createElement('canvas');
    const ctx = c.getContext('2d');
    c.width = 128; c.height = 64;
    ctx.fillStyle = color;
    ctx.font = 'Bold 24px Arial';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(text, 64, 32);
    const tex = new THREE.CanvasTexture(c);
    const mat = new THREE.SpriteMaterial({ map: tex });
    const spr = new THREE.Sprite(mat);
    spr.position.copy(position);
    spr.scale.set(0.8, 0.4, 1);
    return spr;
  }
  scene.add(createAxisLabel('X', new THREE.Vector3(4.5, 0, 0), '#ff0000'));
  scene.add(createAxisLabel('Y', new THREE.Vector3(0, 4.5, 0), '#00ff00'));
  scene.add(createAxisLabel('Z', new THREE.Vector3(0, 0, 4.5), '#0000ff'));

  function createScale() {
    const g = new THREE.Group();
    for (let i = 1; i <= 4; i++) {
      const geo = new THREE.SphereGeometry(0.05, 8, 6);
      const mk = (c) => new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ color: c }));
      const mx = mk(0xff4444); mx.position.set(i, 0, 0); g.add(mx);
      const my = mk(0x44ff44); my.position.set(0, i, 0); g.add(my);
      const mz = mk(0x4444ff); mz.position.set(0, 0, i); g.add(mz);
    }
    return g;
  }
  scene.add(createScale());

  function createDirectionLabels() {
    [
      { t: '前', p: new THREE.Vector3(0, 0, 5), c: '#00ffff' },
      { t: '后', p: new THREE.Vector3(0, 0, -5), c: '#00ffff' },
      { t: '左', p: new THREE.Vector3(-5, 0, 0), c: '#ffff00' },
      { t: '右', p: new THREE.Vector3(5, 0, 0), c: '#ffff00' },
      { t: '上', p: new THREE.Vector3(0, 5, 0), c: '#ff00ff' },
      { t: '下', p: new THREE.Vector3(0, -5, 0), c: '#ff00ff' },
    ].forEach(d => scene.add(createAxisLabel(d.t, d.p, d.c)));
  }
  createDirectionLabels();

  camera.position.set(4, 4, 6);
  camera.lookAt(0, 0, 0);

  // ========== 右侧 IMU 数据展示 ==========
  function createDataPanel() {
    const panel = document.createElement('div');
    panel.id = 'data-panel';
    panel.style.cssText = `
      position: relative;
      background: transparent;
      border: none;
      padding: 10px;
      width: 100%;
      color: #e6edf3;
      font-family: 'Consolas','Monaco',monospace;
      font-size: 11px;
      box-shadow: none;
      overflow: hidden;
    `;
    panel.innerHTML = `
      <div style="margin-bottom:8px;font-weight:bold;color:#61dafb;font-size:12px;border-bottom:1px solid #2a3446;padding-bottom:4px;">IMU 实时数据</div>
      <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px;">
        <div><div style="color:#9fb0c3;font-size:9px;">翻滚角 (Roll)</div>
             <div id="panel-roll" style="color:#ff6b6b;font-size:14px;font-weight:bold;">0.0°</div></div>
        <div><div style="color:#9fb0c3;font-size:9px;">俯仰角 (Pitch)</div>
             <div id="panel-pitch" style="color:#4ecdc4;font-size:14px;font-weight:bold;">0.0°</div></div>
      </div>
      <div style="margin-bottom:8px;">
        <div style="color:#9fb0c3;font-size:9px;">偏航角 (Yaw)</div>
        <div id="panel-yaw" style="color:#45b7d1;font-size:14px;font-weight:bold;">0.0°</div>
      </div>
      <div style="border-top:1px solid #2a3446;padding-top:6px;margin-top:6px;">
        <div style="color:#9fb0c3;font-size:9px;margin-bottom:4px;">角速度 (°/s)</div>
        <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;">
          <div><span style="color:#ff9999;font-size:9px;">gX:</span><span id="panel-gx" style="color:#ff9999;font-size:10px;">0.0</span></div>
          <div><span style="color:#99ff99;font-size:9px;">gY:</span><span id="panel-gy" style="color:#99ff99;font-size:10px;">0.0</span></div>
          <div><span style="color:#9999ff;font-size:9px;">gZ:</span><span id="panel-gz" style="color:#9999ff;font-size:10px;">0.0</span></div>
        </div>
      </div>
      <div style="border-top:1px solid #2a3446;padding-top:6px;margin-top:6px;">
        <div style="color:#9fb0c3;font-size:9px;margin-bottom:4px;">加速度 (m/s²)</div>
        <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;">
          <div><span style="color:#ff9999;font-size:9px;">aX:</span><span id="panel-ax" style="color:#ff9999;font-size:10px;">0.0</span></div>
          <div><span style="color:#99ff99;font-size:9px;">aY:</span><span id="panel-ay" style="color:#99ff99;font-size:10px;">0.0</span></div>
          <div><span style="color:#9999ff;font-size:9px;">aZ:</span><span id="panel-az" style="color:#9999ff;font-size:10px;">0.0</span></div>
        </div>
      </div>
    `;
    hud.appendChild(panel);
    return panel;
  }
  const dataPanel = createDataPanel();

  function updateDataPanel(roll, pitch, yaw, gx, gy, gz, ax, ay, az) {
    document.getElementById('panel-roll').textContent = roll.toFixed(1) + '°';
    document.getElementById('panel-pitch').textContent = pitch.toFixed(1) + '°';
    document.getElementById('panel-yaw').textContent = yaw.toFixed(1) + '°';
    document.getElementById('panel-gx').textContent = gx.toFixed(1);
    document.getElementById('panel-gy').textContent = gy.toFixed(1);
    document.getElementById('panel-gz').textContent = gz.toFixed(1);
    document.getElementById('panel-ax').textContent = ax.toFixed(2);
    document.getElementById('panel-ay').textContent = ay.toFixed(2);
    document.getElementById('panel-az').textContent = az.toFixed(2);
  }

  // ========== 灯光 ==========
  const ambientLight = new THREE.AmbientLight(0x404080, 0.3);
  scene.add(ambientLight);

  const mainLight = new THREE.DirectionalLight(0x00aaff, 1.2);
  mainLight.position.set(5, 8, 5);
  mainLight.castShadow = true;
  mainLight.shadow.mapSize.width = 2048;
  mainLight.shadow.mapSize.height = 2048;
  mainLight.shadow.camera.near = 0.5;
  mainLight.shadow.camera.far = 50;
  scene.add(mainLight);

  const fillLight = new THREE.DirectionalLight(0xff6633, 0.8);
  fillLight.position.set(-5, 3, -3);
  scene.add(fillLight);

  const rimLight = new THREE.DirectionalLight(0x66ffff, 0.6);
  rimLight.position.set(0, -5, 8);
  scene.add(rimLight);

  const pointLight1 = new THREE.PointLight(0x00ff88, 0.5, 20);
  pointLight1.position.set(3, 2, 4);
  scene.add(pointLight1);

  const pointLight2 = new THREE.PointLight(0xff3388, 0.4, 15);
  pointLight2.position.set(-3, -2, 2);
  scene.add(pointLight2);

  const spotLight = new THREE.SpotLight(0xffffff, 1.0, 30, Math.PI / 6, 0.3, 1);
  spotLight.position.set(0, 10, 8);
  spotLight.target.position.set(0, 0, 0);
  spotLight.castShadow = true;
  scene.add(spotLight);
  scene.add(spotLight.target);

  let lightTime = 0;
  function updateLighting() {
    lightTime += 0.01;
    mainLight.intensity = 1.2 + Math.sin(lightTime * 2) * 0.2;
    pointLight1.intensity = 0.5 + Math.sin(lightTime * 3) * 0.2;
    pointLight2.intensity = 0.4 + Math.cos(lightTime * 2.5) * 0.2;
    const hue = (Math.sin(lightTime * 0.5) + 1) * 0.3;
    rimLight.color.setHSL(0.5 + hue, 1.0, 0.7);
  }

  // ========== 模型 ==========
  let glassModel = null;
  const loader = new GLTFLoader();
  loader.load(
    '/static/models/aiglass.glb',
    (gltf) => {
      glassModel = gltf.scene;
      glassModel.scale.set(2, 2, 2);
      glassModel.position.set(0, 0, 0);
      glassModel.traverse((child) => {
        if (child.isMesh) {
          child.castShadow = true;
          child.receiveShadow = true;
          if (child.material) {
            if (child.material.transparent || child.material.opacity < 1) {
              child.material.envMapIntensity = 1.5;
              child.material.roughness = 0.1;
              child.material.metalness = 0.8;
            }
          }
        }
      });
      group.add(glassModel);
    },
    undefined,
    (error) => {
      console.error('GLB加载失败:', error);
      const fallbackCube = new THREE.Mesh(
        new THREE.BoxGeometry(2, 2, 2),
        new THREE.MeshStandardMaterial({ color: 0x00aaff, metalness: 0.7, roughness: 0.3, envMapIntensity: 1.0 })
      );
      fallbackCube.castShadow = true;
      fallbackCube.receiveShadow = true;
      group.add(fallbackCube);
    }
  );

  // 渲染循环
  (function animate() {
    requestAnimationFrame(animate);
    updateLighting();
    renderer.render(scene, camera);
  })();

  // ===== IMU 数学与数据通道（原逻辑保持） =====
  // 安装补偿
  const MOUNT_RX = 0, MOUNT_RY = -90, MOUNT_RZ = 0;
  const qMount = new THREE.Quaternion()
    .multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(MOUNT_RY)))
    .multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(MOUNT_RZ)))
    .multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), THREE.MathUtils.degToRad(MOUNT_RX)));

  const FOLLOW = 0.85;
  const $ = id => document.getElementById(id);
  const updateSlider = (idBase, v) => { const sl = $(`${idBase}_sl`), tv = $(`${idBase}_val`); if (sl) { const min = +sl.min, max = +sl.max; sl.value = Math.max(min, Math.min(max, v)); } if (tv) tv.textContent = (typeof v === 'number' ? v.toFixed(2) : '-'); };

  let MED_N = Number($('medn').value);
  $('medn').onchange = e => MED_N = Number(e.target.value);

  let STILL_W = Number($('still_w').value);
  $('still_w').onchange = e => STILL_W = Number(e.target.value);

  let ANG_EMA = Number($('ang_ema').value);
  $('ang_ema').onchange = e => ANG_EMA = Number(e.target.value);

  let GRAV_BETA = Number($('grav_beta').value);
  $('grav_beta').onchange = e => GRAV_BETA = Number(e.target.value);

  let YAW_DB = Number($('yaw_db').value);
  $('yaw_db').onchange = e => YAW_DB = Number(e.target.value);

  let YAW_LEAK = Number($('yaw_leak').value);
  $('yaw_leak').onchange = e => YAW_LEAK = Number(e.target.value);

  let autoRezero = true;
  $('auto_rezero').onchange = e => { autoRezero = e.target.checked; };

  let autoBias = true;
  $('auto_bias').onchange = e => { autoBias = e.target.checked; };

  let useProj = true;
  $('use_proj').onchange = e => { useProj = e.target.checked; };

  let freezeStill = true;
  $('freeze_still').onchange = e => { freezeStill = e.target.checked; };

  const mkMed = () => ({ buf: [], push(v) { this.buf.push(v); if (this.buf.length > MED_N) this.buf.shift(); const arr = [...this.buf].sort((a, b) => a - b); const m = arr[Math.floor(arr.length / 2)]; return { median: m, valid: this.buf.length === MED_N }; } });
  const fx = mkMed(), fy = mkMed(), fz = mkMed();
  const gx = mkMed(), gy = mkMed(), gz = mkMed();

  const rad2deg = r => r * 180 / Math.PI;
  const wrap180 = a => { a %= 360; if (a >= 180) a -= 360; if (a < -180) a += 360; return a; };

  let lastTS = 0;
  let yaw = 0;
  let ref = { roll: 0, pitch: 0, yaw: 0 };
  let holdStart = 0, isStill = false;

  let gLP = { x: 0, y: 0, z: 0 };
  const G = 9.807, A_TOL = 0.08 * G;

  let gOff = { x: 0, y: 0, z: 0 };
  const BIAS_ALPHA = 0.002;

  let Rf = 0, Pf = 0, Yf = 0;

  document.getElementById('btn_zero').onclick = () => { ref = { roll: Rf, pitch: Pf, yaw: Yf }; };
  document.getElementById('btn_reset').onclick = () => { ref = { roll: 0, pitch: 0, yaw: 0 }; yaw = 0; Rf = 0; Pf = 0; Yf = 0; };
  document.getElementById('btn_bias_now').onclick = () => { gOff = { ...lastGy }; };

  let lastGy = { x: 0, y: 0, z: 0 };

  const imu_ws_state = document.getElementById('imu_ws_state');
  function setImuBadge(ok, text) {
    imu_ws_state.textContent = text;
    imu_ws_state.className = 'badge ' + (ok ? 'ok' : 'err');
  }

  const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
  setImuBadge(false, 'connecting…');
  ws.onopen = () => setImuBadge(true, 'connected');
  ws.onclose = () => setImuBadge(false, 'disconnected');
  ws.onerror = () => setImuBadge(false, 'error');
  ws.onmessage = (ev) => {
    try {
      const d = JSON.parse(ev.data);
      const t = (typeof d.ts === 'number') ? d.ts : performance.now();
      let dt = (!lastTS || (t - lastTS) <= 0 || (t - lastTS) > 300) ? 0.02 : (t - lastTS) / 1000;
      lastTS = t;

      let ax = Number(d?.accel?.x) || 0, ay = Number(d?.accel?.y) || 0, az = Number(d?.accel?.z) || 0;
      let wx = Number(d?.gyro?.x) || 0, wy = Number(d?.gyro?.y) || 0, wz = Number(d?.gyro?.z) || 0;

      const fxr = fx.push(ax), fyr = fy.push(ay), fzr = fz.push(az);
      const gxr = gx.push(wx), gyr = gy.push(wy), gzr = gz.push(wz);
      if (fxr.valid) { ax = fxr.median; ay = fyr.median; az = fzr.median; }
      if (gxr.valid) { wx = gxr.median; wy = gyr.median; wz = gzr.median; }

      lastGy = { x: wx, y: wy, z: wz };

      gLP.x = GRAV_BETA * gLP.x + (1 - GRAV_BETA) * ax;
      gLP.y = GRAV_BETA * gLP.y + (1 - GRAV_BETA) * ay;
      gLP.z = GRAV_BETA * gLP.z + (1 - GRAV_BETA) * az;
      const gmag = Math.hypot(gLP.x, gLP.y, gLP.z) || 1;
      const gHat = { x: gLP.x / gmag, y: gLP.y / gmag, z: gLP.z / gmag };

      const roll = rad2deg(Math.atan2(az, ay));
      const pitch = rad2deg(Math.atan2(-ax, ay));

      const aNorm = Math.hypot(ax, ay, az);
      const wNorm = Math.hypot(wx, wy, wz);
      const nearFlat = Math.abs(roll) < 2.0 && Math.abs(pitch) < 2.0;
      const stillCond = (Math.abs(aNorm - G) < A_TOL) && (wNorm < STILL_W);

      if (stillCond) {
        if (!holdStart) holdStart = t;
        if (!isStill && (t - holdStart) > 350) isStill = true;
        if (autoBias) {
          gOff.x = (1 - BIAS_ALPHA) * gOff.x + BIAS_ALPHA * wx;
          gOff.y = (1 - BIAS_ALPHA) * gOff.y + BIAS_ALPHA * wy;
          gOff.z = (1 - BIAS_ALPHA) * gOff.z + BIAS_ALPHA * wz;
        }
      } else { holdStart = 0; isStill = false; }

      let yawdot = useProj
        ? ((wx - gOff.x) * gHat.x + (wy - gOff.y) * gHat.y + (wz - gOff.z) * gHat.z)
        : (wy - gOff.y);

      if (Math.abs(yawdot) < YAW_DB) yawdot = 0;
      if (freezeStill && stillCond) yawdot = 0;

      yaw = wrap180(yaw + yawdot * dt);

      if (YAW_LEAK > 0 && nearFlat && stillCond && Math.abs(yaw) > 0) {
        const step = YAW_LEAK * dt * Math.sign(-yaw);
        if (Math.abs(yaw) <= Math.abs(step)) yaw = 0; else yaw += step;
      }

      const alpha = ANG_EMA;
      Rf = alpha * roll + (1 - alpha) * Rf;
      Pf = alpha * pitch + (1 - alpha) * Pf;
      Yf = alpha * yaw + (1 - alpha) * Yf;

      if (autoRezero && nearFlat && wNorm < STILL_W) {
        if (!holdStart) holdStart = t;
        if (!isStill && (t - holdStart) > 350) {
          ref = { roll: Rf, pitch: Pf, yaw: Yf };
          isStill = true;
        }
      }

      const R = wrap180(Rf - ref.roll);
      const P = wrap180(Pf - ref.pitch);
      const Y = wrap180(Yf - ref.yaw);

      const qBody = new THREE.Quaternion()
        .multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), THREE.MathUtils.degToRad(Y)))
        .multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(P)))
        .multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), THREE.MathUtils.degToRad(R)));
      const q = qMount.clone().multiply(qBody);

      if (FOLLOW >= 0.999) group.setRotationFromQuaternion(q);
      else group.quaternion.slerp(q, FOLLOW);

      updateSlider('roll', R);
      updateSlider('pitch', P);
      updateSlider('yaw', Y);

      updateSlider('gx', wx); updateSlider('gy', wy); updateSlider('gz', wz);
      updateSlider('ax', ax); updateSlider('ay', ay); updateSlider('az', az);

      // 更新右侧数据
      updateDataPanel(R, P, Y, wx, wy, wz, ax, ay, az);
    } catch (e) { }
  };

  // 初次与窗口改变时，保持左右上下对齐
  window.addEventListener('resize', resize);
  resize();
})();
