<script lang="ts">
  import { onMount } from "svelte";
  import { invoke } from "@tauri-apps/api/core";
  import { listen, type UnlistenFn } from "@tauri-apps/api/event";
  import { LogicalSize, currentMonitor, getCurrentWindow } from "@tauri-apps/api/window";
  import HudPanel from "$lib/components/HudPanel.svelte";
  import CenterStage from "$lib/components/CenterStage.svelte";
  import FileExplorerModal from "$lib/components/FileExplorerModal.svelte";
  import { pressureColorPalettes } from "$lib/config/color-map";
  import "$lib/styles/theme.css";
  import type {
    ConnectionState,
    FileExplorerEntry,
    FileExplorerListResult,
    FileExplorerRoot,
    HudColorMapOption,
    HudCopy,
    HudConfigLink,
    HudNoticeTone,
    HudPacket,
    PressureColorMapPreset,
    HudSignalPanel,
    HudSignalSeries,
    HudSummary,
    LocaleCode,
    MatrixDisplayMode,
    SerialConnectResult,
    SerialExportResult,
    SerialRecordStateResult,
    SerialImportResult,
    SignalTone,
    WindowControlAction
  } from "$lib/types/hud";

  type SignalPanelTemplate = Pick<HudSignalPanel, "id" | "code" | "title" | "side">;
  type FileExplorerMode = "open" | "save";

  interface ReplayFrame {
    values: number[];
    dtsMs: number;
  }

  const copyByLocale: Record<LocaleCode, HudCopy> = {
    "zh-CN": {
      appName: "JE-Skin",
      suiteName: "v0.3.1",
      stageTitle: "WebGL2 主渲染区",
      stageHint: "底图与三维操作将在此区域加载",
      configPanelTitle: "参数配置",
      configPanelHint: "矩阵规模与颜色映射范围会实时作用到主舞台。",
      matrixSizeLabel: "点阵数量",
      matrixRowsLabel: "行数",
      matrixColsLabel: "列数",
      rangeLabel: "映射范围",
      rangeMinLabel: "最小值",
      rangeMaxLabel: "最大值",
      colorMapLabel: "映射颜色",
      matrixViewLabel: "矩阵模式",
      matrixViewNumericLabel: "数字矩阵",
      matrixViewDotsLabel: "点矩阵",
      resetConfigLabel: "恢复默认",
      applyLiveHint: "实时生效 / 矩阵尺寸变更将重建 viewer",
      runtimeReady: "WEBGL2 READY",
      runtimeFallback: "WEBGL2 N/A",
      controlArea: "控制区",
      serialPortLabel: "串口",
      connectionLabel: "连接状态",
      deviceLabel: "设备",
      sampleRateLabel: "采样率",
      channelsLabel: "通道",
      configLinksLabel: "配置链接",
      refreshPortsLabel: "刷新",
      connectActionLabel: "连接",
      disconnectActionLabel: "断开",
      exportActionLabel: "导出 CSV",
      exportingActionLabel: "导出中",
      importActionLabel: "导入 CSV",
      fileExplorerImportTitle: "导入 CSV 文件",
      fileExplorerExportTitle: "导出 CSV 文件",
      fileExplorerPathLabel: "路径",
      fileExplorerNameLabel: "文件名",
      fileExplorerCancelLabel: "取消",
      fileExplorerOpenLabel: "打开",
      fileExplorerSaveLabel: "保存",
      fileExplorerEmptyHint: "当前目录下没有可用条目",
      fileExplorerCsvHint: "仅显示 *.csv 文件",
      fileExplorerLoadingLabel: "处理中...",
      fileExplorerUpLabel: "↑ 上一级",
      fileExplorerNameColumnLabel: "名称",
      fileExplorerSizeColumnLabel: "大小",
      fileExplorerModifiedColumnLabel: "修改时间",
      replaySectionLabel: "回放",
      replayPlayLabel: "播放",
      replayPauseLabel: "暂停",
      replayStopLabel: "停止",
      replaySpeedLabel: "速度",
      replayProgressLabel: "进度",
      replayEmptyHint: "未加载回放文件",
      connectedLabel: "已连接",
      connectingLabel: "连接中",
      disconnectedLabel: "未连接"
    },
    "en-US": {
      appName: "JE-Skin",
      suiteName: "v0.3.1",
      stageTitle: "WebGL2 Main Surface",
      stageHint: "Map texture and 3D interactions will render here",
      configPanelTitle: "Config Panel",
      configPanelHint: "Matrix dimensions and color-mapping range update the stage live.",
      matrixSizeLabel: "Matrix Density",
      matrixRowsLabel: "Rows",
      matrixColsLabel: "Cols",
      rangeLabel: "Color Range",
      rangeMinLabel: "Min",
      rangeMaxLabel: "Max",
      colorMapLabel: "Color Map",
      matrixViewLabel: "Matrix Mode",
      matrixViewNumericLabel: "Numeric",
      matrixViewDotsLabel: "Dots",
      resetConfigLabel: "Reset",
      applyLiveHint: "Live apply / size changes recreate the viewer",
      runtimeReady: "WEBGL2 READY",
      runtimeFallback: "WEBGL2 N/A",
      controlArea: "Control Area",
      serialPortLabel: "Port",
      connectionLabel: "Connection",
      deviceLabel: "Device",
      sampleRateLabel: "Sample Rate",
      channelsLabel: "Channels",
      configLinksLabel: "Config Links",
      refreshPortsLabel: "Refresh",
      connectActionLabel: "Connect",
      disconnectActionLabel: "Disconnect",
      exportActionLabel: "Export CSV",
      exportingActionLabel: "Exporting",
      importActionLabel: "Import CSV",
      fileExplorerImportTitle: "Import CSV File",
      fileExplorerExportTitle: "Export CSV File",
      fileExplorerPathLabel: "Path",
      fileExplorerNameLabel: "File Name",
      fileExplorerCancelLabel: "Cancel",
      fileExplorerOpenLabel: "Open",
      fileExplorerSaveLabel: "Save",
      fileExplorerEmptyHint: "No entries in this directory",
      fileExplorerCsvHint: "Only *.csv files are listed",
      fileExplorerLoadingLabel: "Processing...",
      fileExplorerUpLabel: "↑ Up",
      fileExplorerNameColumnLabel: "Name",
      fileExplorerSizeColumnLabel: "Size",
      fileExplorerModifiedColumnLabel: "Modified",
      replaySectionLabel: "Replay",
      replayPlayLabel: "Play",
      replayPauseLabel: "Pause",
      replayStopLabel: "Stop",
      replaySpeedLabel: "Speed",
      replayProgressLabel: "Progress",
      replayEmptyHint: "No replay file loaded",
      connectedLabel: "Connected",
      connectingLabel: "Connecting",
      disconnectedLabel: "Offline"
    }
  };

  const pointsPerSeries = 28;
  const summaryPointsPerSeries = 42;
  const signalRenderTickMs = 1200;
  const replayDefaultFrameMs = 40;
  const showSignalPanels = false;
  const mockToneCycle: SignalTone[] = ["cyan", "lime", "orange", "violet", "gold", "rose"];

  const signalPanelTemplates: SignalPanelTemplate[] = [
    {
      id: "m2258",
      code: "M2258",
      title: "Pressure / Ch-A",
      side: "left"
    },
    {
      id: "m2036",
      code: "M2036",
      title: "Pressure / Ch-B",
      side: "left"
    },
    {
      id: "s16104",
      code: "S16104",
      title: "Signal / Right-A",
      side: "right"
    },
    {
      id: "s2716",
      code: "S2716",
      title: "Signal / Right-B",
      side: "right"
    }
  ];

  let locale: LocaleCode = "zh-CN";
  let connectionState: ConnectionState = "offline";
  let serialPortValue = "COM14";
  let serialPortOptions = ["COM4", "COM9", "COM14", "COM16"];
  let isRefreshingPorts = false;
  let connectionNotice = "";
  let connectionNoticeTone: HudNoticeTone = "info";
  let isExporting = false;
  let deviceValue = "JE-Skin-F";
  let sampleRateValue = "100Hz";
  let channelsValue = "84";
  let isWindowMaximized = false;
  let activeConfigLinkId = "stream-on";
  let isConfigPanelOpen = false;
  let isPrecisionTestOpen = false;
  let hasSignalData = false;
  let signalPanels: HudSignalPanel[] = buildInactivePanels();
  let summary: HudSummary = buildEmptySummary();
  let pressureMatrix: number[] | null = null;
  let matrixRows = 12;
  let matrixCols = 7;
  let rangeMin = 0;
  let rangeMax = 16000;
  let colorMapPreset: PressureColorMapPreset = "emerald";
  let matrixDisplayMode: MatrixDisplayMode = "dots";
  let replayFrames: ReplayFrame[] = [];
  let replayCurrentIndex = 0;
  let replayHasDisplayedFrame = false;
  let replayIsPlaying = false;
  let replaySpeed = 1;
  let replayProgress = 0;
  let replayFileName = "";
  let replayTimerId: number | null = null;
  let fileExplorerOpen = false;
  let fileExplorerMode: FileExplorerMode = "open";
  let fileExplorerBusy = false;
  let fileExplorerCurrentPath = "";
  let fileExplorerParentPath: string | null = null;
  let fileExplorerEntries: FileExplorerEntry[] = [];
  let fileExplorerRoots: FileExplorerRoot[] = [];
  let fileExplorerSelectedPath = "";
  let fileExplorerFileName = "";

  $: uiCopy = copyByLocale[locale];
  $: configLinks = buildConfigLinks(locale, activeConfigLinkId, isConfigPanelOpen, isPrecisionTestOpen);
  $: leftSignalPanels = signalPanels.filter((panel) => panel.side === "left");
  $: rightSignalPanels = signalPanels.filter((panel) => panel.side === "right");
  $: rangeTicks = buildRangeTicks(rangeMin, rangeMax);
  $: colorMapOptions = buildColorMapOptions(locale);
  $: rangeScaleStyle = buildRangeScaleStyle(colorMapPreset);
  $: replayHasData = replayFrames.length > 0;
  $: replayFrameInfo = replayHasData ? `${replayHasDisplayedFrame ? replayCurrentIndex + 1 : 0}/${replayFrames.length}` : "";
  $: fileExplorerTitle =
    fileExplorerMode === "open" ? uiCopy.fileExplorerImportTitle : uiCopy.fileExplorerExportTitle;
  $: fileExplorerConfirmLabel =
    fileExplorerMode === "open" ? uiCopy.fileExplorerOpenLabel : uiCopy.fileExplorerSaveLabel;

  function isTauriRuntime(): boolean {
    return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
  }

  function clamp(value: number, min: number, max: number): number {
    return Math.min(max, Math.max(min, value));
  }

  function randomBetween(min: number, max: number): number {
    return Math.random() * (max - min) + min;
  }

  function formatRangeTick(value: number): string {
    if (Math.abs(value) >= 1000) {
      return Number.isInteger(value / 1000) ? `${value / 1000}k` : `${(value / 1000).toFixed(1)}k`;
    }

    return Number.isInteger(value) ? String(value) : value.toFixed(1);
  }

  function buildRangeTicks(min: number, max: number): string[] {
    const safeMax = Math.max(max, min + 1);
    const steps = 10;
    const ticks: string[] = [];

    for (let index = 0; index <= steps; index += 1) {
      const value = min + ((safeMax - min) * index) / steps;
      ticks.push(formatRangeTick(Math.round(value * 10) / 10));
    }

    return ticks;
  }

  function buildColorMapOptions(currentLocale: LocaleCode): HudColorMapOption[] {
    const localizedLabels: Record<PressureColorMapPreset, string> =
      currentLocale === "zh-CN"
        ? {
            emerald: "翡翠",
            arctic: "极光",
            ember: "热焰"
          }
        : {
            emerald: "Emerald",
            arctic: "Arctic",
            ember: "Ember"
          };

    return (Object.keys(localizedLabels) as PressureColorMapPreset[]).map((id) => {
      const palette = pressureColorPalettes[id];

      return {
        id,
        label: localizedLabels[id],
        previewStops: [palette.rangeStops[1], palette.rangeStops[3], palette.rangeStops[5]]
      };
    });
  }

  function buildRangeScaleStyle(preset: PressureColorMapPreset): string {
    const palette = pressureColorPalettes[preset] ?? pressureColorPalettes.emerald;
    const [range0, range1, range2, range3, range4, range5] = palette.rangeStops;
    const [glow0, glow1, glow2] = palette.rangeGlow;
    const {
      bg00,
      bg10,
      bg20,
      bg30,
      textMainRgb,
      textDimRgb,
      borderRgb,
      borderStrongRgb,
      surfaceRgb,
      surfaceAltRgb,
      surfaceDeepRgb,
      glowRgb,
      glowAltRgb,
      cyanRgb,
      limeRgb,
      orangeRgb,
      infoRgb
    } = palette.uiTheme;

    return [
      `--hud-bg-00: ${bg00}`,
      `--hud-bg-10: ${bg10}`,
      `--hud-bg-20: ${bg20}`,
      `--hud-bg-30: ${bg30}`,
      `--hud-text-main-rgb: ${textMainRgb}`,
      `--hud-text-dim-rgb: ${textDimRgb}`,
      `--hud-text-main: rgb(${textMainRgb})`,
      `--hud-text-dim: rgb(${textDimRgb})`,
      `--hud-border-rgb: ${borderRgb}`,
      `--hud-border-strong-rgb: ${borderStrongRgb}`,
      `--hud-surface-rgb: ${surfaceRgb}`,
      `--hud-surface-alt-rgb: ${surfaceAltRgb}`,
      `--hud-surface-deep-rgb: ${surfaceDeepRgb}`,
      `--hud-glow-rgb: ${glowRgb}`,
      `--hud-glow-alt-rgb: ${glowAltRgb}`,
      `--hud-cyan-rgb: ${cyanRgb}`,
      `--hud-lime-rgb: ${limeRgb}`,
      `--hud-orange-rgb: ${orangeRgb}`,
      `--hud-info-rgb: ${infoRgb}`,
      `--hud-cyan: rgb(${cyanRgb})`,
      `--hud-lime: rgb(${limeRgb})`,
      `--hud-orange: rgb(${orangeRgb})`,
      `--hud-range-0: ${range0}`,
      `--hud-range-1: ${range1}`,
      `--hud-range-2: ${range2}`,
      `--hud-range-3: ${range3}`,
      `--hud-range-4: ${range4}`,
      `--hud-range-5: ${range5}`,
      `--hud-range-glow-0: ${glow0}`,
      `--hud-range-glow-1: ${glow1}`,
      `--hud-range-glow-2: ${glow2}`
    ].join("; ");
  }

  function unquoteCsvCell(cell: string): string {
    const trimmed = cell.trim();
    if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
      return trimmed.slice(1, -1).replaceAll("\"\"", "\"").trim();
    }
    return trimmed;
  }

  function splitCsvLine(line: string): string[] {
    const cells: string[] = [];
    let current = "";
    let inQuotes = false;

    for (let index = 0; index < line.length; index += 1) {
      const char = line[index];
      if (char === "\"") {
        if (inQuotes && line[index + 1] === "\"") {
          current += "\"";
          index += 1;
        } else {
          inQuotes = !inQuotes;
        }
        continue;
      }

      if (char === "," && !inQuotes) {
        cells.push(unquoteCsvCell(current));
        current = "";
        continue;
      }

      current += char;
    }

    cells.push(unquoteCsvCell(current));
    return cells;
  }

  function parseReplayCsv(text: string): ReplayFrame[] {
    const lines = text
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter((line) => line.length > 0);

    if (lines.length < 2) {
      return [];
    }

    const headerCells = splitCsvLine(lines[0]).map((item) => item.toLowerCase());
    let dtsIndex = headerCells.findIndex((item) => item === "dts" || item === "dts_ms" || item === "timestamp");
    if (dtsIndex < 0) {
      dtsIndex = Math.max(headerCells.length - 1, 0);
    }

    const frames: ReplayFrame[] = [];
    for (let rowIndex = 1; rowIndex < lines.length; rowIndex += 1) {
      const cells = splitCsvLine(lines[rowIndex]);
      if (cells.length === 0) {
        continue;
      }

      const values: number[] = [];
      for (let columnIndex = 0; columnIndex < cells.length; columnIndex += 1) {
        if (columnIndex === dtsIndex) {
          continue;
        }
        const parsed = Number(cells[columnIndex]);
        values.push(Number.isFinite(parsed) ? parsed : 0);
      }

      const fallbackDts = frames.length ? frames[frames.length - 1].dtsMs + replayDefaultFrameMs : 0;
      const parsedDts = Number(cells[dtsIndex]);
      const resolvedDts = Number.isFinite(parsedDts) ? Math.max(Math.round(parsedDts), fallbackDts) : fallbackDts;
      frames.push({ values, dtsMs: resolvedDts });
    }

    return frames;
  }

  function buildDefaultExportName(): string {
    const now = new Date();
    const pad = (value: number) => String(value).padStart(2, "0");
    return `joyson_export_${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.csv`;
  }

  function ensureCsvSuffix(fileName: string): string {
    const trimmed = fileName.trim();
    if (!trimmed) {
      return "";
    }

    return trimmed.toLowerCase().endsWith(".csv") ? trimmed : `${trimmed}.csv`;
  }

  function inferPathSeparator(path: string): string {
    return path.includes("\\") ? "\\" : "/";
  }

  function joinPath(parent: string, fileName: string): string {
    const safeParent = parent.trim();
    if (!safeParent) {
      return fileName;
    }

    const separator = inferPathSeparator(safeParent);
    if (safeParent.endsWith(separator)) {
      return `${safeParent}${fileName}`;
    }

    return `${safeParent}${separator}${fileName}`;
  }

  function isCsvPath(path: string): boolean {
    return path.toLowerCase().endsWith(".csv");
  }

  function applyImportedFrames(fileName: string, frames: ReplayFrame[], frameCount: number, channelCount: number): void {
    if (!frames.length) {
      throw new Error("EmptyReplayData");
    }

    replayFrames = frames;
    replayFileName = fileName;
    replayCurrentIndex = 0;
    replayHasDisplayedFrame = false;
    replayProgress = 0;
    resetReplayVisualState();

    connectionNotice =
      locale === "zh-CN"
        ? `已导入 ${fileName}，共 ${frameCount} 帧 / ${channelCount} 通道，可开始回放。`
        : `${fileName} loaded (${frameCount} frames / ${channelCount} channels). Ready to replay.`;
    connectionNoticeTone = "ok";
  }

  async function loadFileExplorerDirectory(path?: string): Promise<void> {
    if (!isTauriRuntime()) {
      return;
    }

    fileExplorerBusy = true;
    try {
      const result = await invoke<FileExplorerListResult>("file_explorer_list", {
        path,
        extensions: fileExplorerMode === "open" ? ["csv"] : undefined
      });

      fileExplorerCurrentPath = result.currentPath;
      fileExplorerParentPath = result.parentPath;
      fileExplorerRoots = result.roots;
      fileExplorerEntries = result.entries;

      const selectedExists = fileExplorerEntries.some((entry) => entry.path === fileExplorerSelectedPath);
      if (!selectedExists) {
        fileExplorerSelectedPath = "";
      }
    } catch (error) {
      connectionNotice =
        locale === "zh-CN" ? "文件浏览器加载失败，请检查目录权限。" : "File explorer failed to load. Check directory permissions.";
      connectionNoticeTone = "warn";
      console.error("File explorer load failed:", error);
    } finally {
      fileExplorerBusy = false;
    }
  }

  async function openFileExplorer(mode: FileExplorerMode): Promise<void> {
    if (!isTauriRuntime()) {
      if (mode === "open") {
        await importViaBrowserInput();
        return;
      }

      await runSerialExport();
      return;
    }

    fileExplorerMode = mode;
    fileExplorerOpen = true;
    fileExplorerBusy = false;
    fileExplorerSelectedPath = "";
    if (mode === "save") {
      fileExplorerFileName = buildDefaultExportName();
    } else {
      fileExplorerFileName = "";
    }

    await loadFileExplorerDirectory(fileExplorerCurrentPath || undefined);
  }

  function closeFileExplorer(): void {
    if (fileExplorerBusy) {
      return;
    }

    fileExplorerOpen = false;
  }

  async function importViaBrowserInput(): Promise<void> {
    if (typeof document === "undefined") {
      return;
    }

    const input = document.createElement("input");
    input.type = "file";
    input.accept = ".csv,text/csv";

    const selectedFile = await new Promise<File | null>((resolve) => {
      input.onchange = () => resolve(input.files?.[0] ?? null);
      input.click();
    });

    if (!selectedFile) {
      return;
    }

    await importReplayFromFile(selectedFile);
  }

  async function importReplayFromFile(file: File): Promise<boolean> {
    if (!file) {
      return false;
    }

    pauseReplayPlayback();

    try {
      const text = await file.text();
      let frames: ReplayFrame[] = [];
      let importedFrameCount = 0;
      let importedChannelCount = 0;

      if (isTauriRuntime()) {
        const result = await invoke<SerialImportResult>("serial_import_csv", {
          fileName: file.name,
          csvContent: text
        });
        frames = result.frames.map((frame) => ({
          values: frame.data,
          dtsMs: frame.dtsMs
        }));
        importedFrameCount = result.frameCount;
        importedChannelCount = result.channelCount;
      } else {
        frames = parseReplayCsv(text);
        importedFrameCount = frames.length;
        importedChannelCount = frames[0]?.values.length ?? 0;
      }

      applyImportedFrames(file.name, frames, importedFrameCount, importedChannelCount);
      return true;
    } catch (error) {
      connectionNotice = resolveImportNotice(error);
      connectionNoticeTone = "warn";
      console.error("Replay import failed:", error);
      return false;
    }
  }

  async function importReplayFromPath(path: string): Promise<boolean> {
    pauseReplayPlayback();

    try {
      const result = await invoke<SerialImportResult>("serial_import_csv_from_path", {
        filePath: path
      });

      const frames = result.frames.map((frame) => ({
        values: frame.data,
        dtsMs: frame.dtsMs
      }));

      applyImportedFrames(result.fileName, frames, result.frameCount, result.channelCount);
      return true;
    } catch (error) {
      connectionNotice = resolveImportNotice(error);
      connectionNoticeTone = "warn";
      console.error("Replay import failed:", error);
      return false;
    }
  }

  function stopReplayTimer(): void {
    if (replayTimerId == null || typeof window === "undefined") {
      return;
    }
    window.clearTimeout(replayTimerId);
    replayTimerId = null;
  }

  function frameValuesToMatrix(values: number[]): number[] {
    const totalCells = Math.max(matrixRows * matrixCols, 1);
    const matrix = new Array<number>(totalCells).fill(0);

    for (let index = 0; index < totalCells; index += 1) {
      const value = Number(values[index] ?? 0);
      matrix[index] = Number.isFinite(value) ? value : 0;
    }

    return matrix;
  }

  function buildZeroMatrix(): number[] {
    const totalCells = Math.max(matrixRows * matrixCols, 1);
    return new Array<number>(totalCells).fill(0);
  }

  function resetReplayVisualState(): void {
    pressureMatrix = buildZeroMatrix();
    signalPanels = buildInactivePanels();
    summary = buildEmptySummary();
    hasSignalData = false;
  }

  function replayFrameTotal(frame: ReplayFrame): number {
    return frame.values.reduce((sum, value) => sum + (Number.isFinite(value) ? value : 0), 0);
  }

  function buildReplaySummaryAt(index: number): HudSummary {
    if (!replayFrames.length) {
      return buildEmptySummary();
    }

    const safeIndex = clamp(index, 0, replayFrames.length - 1);
    const startIndex = Math.max(0, safeIndex - summaryPointsPerSeries + 1);
    const points: number[] = [];
    const frameIds: number[] = [];
    for (let cursor = startIndex; cursor <= safeIndex; cursor += 1) {
      points.push(replayFrameTotal(replayFrames[cursor]));
      frameIds.push(cursor + 1);
    }
    return buildSummary(points, frameIds);
  }

  function applyReplayFrame(index: number): void {
    if (!replayFrames.length) {
      return;
    }

    const safeIndex = clamp(Math.round(index), 0, replayFrames.length - 1);
    replayCurrentIndex = safeIndex;
    replayHasDisplayedFrame = true;
    replayProgress = replayFrames.length > 1 ? safeIndex / (replayFrames.length - 1) : 1;
    pressureMatrix = frameValuesToMatrix(replayFrames[safeIndex].values);
    signalPanels = buildInactivePanels();
    summary = buildReplaySummaryAt(safeIndex);
    hasSignalData = true;
  }

  function getReplayFrameDelay(frameIndex: number): number {
    if (frameIndex >= replayFrames.length - 1) {
      return replayDefaultFrameMs;
    }

    const currentDts = replayFrames[frameIndex].dtsMs;
    const nextDts = replayFrames[frameIndex + 1].dtsMs;
    const rawGap = Math.max(nextDts - currentDts, replayDefaultFrameMs);
    return clamp(Math.round(rawGap / Math.max(replaySpeed, 0.25)), 16, 2500);
  }

  function stepReplayPlayback(): void {
    if (!replayIsPlaying) {
      return;
    }

    if (!replayFrames.length || replayCurrentIndex >= replayFrames.length - 1) {
      replayIsPlaying = false;
      stopReplayTimer();
      return;
    }

    const delay = getReplayFrameDelay(replayCurrentIndex);
    applyReplayFrame(replayCurrentIndex + 1);

    if (typeof window !== "undefined") {
      replayTimerId = window.setTimeout(stepReplayPlayback, delay);
    }
  }

  function pauseReplayPlayback(): void {
    replayIsPlaying = false;
    stopReplayTimer();
  }

  function startReplayPlayback(): void {
    if (!replayFrames.length) {
      return;
    }

    if (!replayHasDisplayedFrame) {
      applyReplayFrame(replayCurrentIndex);
    }

    if (replayCurrentIndex >= replayFrames.length - 1) {
      applyReplayFrame(0);
    }

    replayIsPlaying = true;
    stopReplayTimer();
    if (typeof window !== "undefined") {
      replayTimerId = window.setTimeout(stepReplayPlayback, getReplayFrameDelay(replayCurrentIndex));
    }
  }

  function createSeriesPoints(seedValue: number): number[] {
    let current = seedValue;
    const points: number[] = [];

    for (let index = 0; index < pointsPerSeries; index += 1) {
      current = clamp(current + randomBetween(-5.5, 5.5), 5, 95);
      points.push(Math.round(current * 10) / 10);
    }

    return points;
  }

  function buildSeriesForPanel(panelId: string): HudSignalSeries[] {
    return [
      {
        id: `${panelId}-series-1`,
        tone: "cyan",
        points: createSeriesPoints(randomBetween(80, 220))
      }
    ];
  }

  function evolveSeries(points: number[], tone: SignalTone): number[] {
    const drift =
      tone === "cyan"
        ? 12
        : tone === "lime"
          ? 4.9
          : tone === "orange"
            ? 4.4
            : tone === "violet"
              ? 5.1
              : tone === "gold"
                ? 4.6
                : 5.3;
    const previous = points.length ? points[points.length - 1] : randomBetween(80, 220);
    const next = Math.round(clamp(previous + randomBetween(-drift, drift), 0, 500) * 10) / 10;
    const nextPoints = points.length >= pointsPerSeries ? points.slice(1) : points.slice();
    nextPoints.push(next);
    return nextPoints;
  }

  function buildIcons(panelCode: string, count: number) {
    return Array.from({ length: count }, (_, index) => ({
      id: `${panelCode}-icon-${index + 1}`,
      label: count === 1 ? "TOTAL" : `${panelCode}-${index + 1}`,
      tone: mockToneCycle[index % mockToneCycle.length]
    }));
  }

  function buildPanelStats(series: HudSignalSeries[]) {
    const latestValues = series
      .map((entry) => entry.points[entry.points.length - 1])
      .filter((value): value is number => value != null);
    const allPoints = series.flatMap((entry) => entry.points);

    return {
      latest: latestValues.length ? latestValues.reduce((sum, value) => sum + value, 0) / latestValues.length : null,
      min: allPoints.length ? Math.min(...allPoints) : null,
      max: allPoints.length ? Math.max(...allPoints) : null
    };
  }

  function buildMockPanels(activeIds: string[]): HudSignalPanel[] {
    return signalPanelTemplates
      .filter((panel) => activeIds.includes(panel.id))
      .map((panel) => {
        const series = buildSeriesForPanel(panel.id);

        return {
          ...panel,
          active: true,
          series,
          icons: buildIcons(panel.code, series.length),
          ...buildPanelStats(series)
        };
      });
  }

  function evolvePanels(panels: HudSignalPanel[]): HudSignalPanel[] {
    return panels.map((panel) => {
      const series = panel.series.map((series) => ({
        ...series,
        points: evolveSeries(series.points, series.tone)
      }));

      return {
        ...panel,
        active: true,
        series,
        icons: buildIcons(panel.code, series.length),
        ...buildPanelStats(series)
      };
    });
  }

  function buildEmptySummary(): HudSummary {
    return {
      label: "Resultant Force",
      xValues: [],
      points: [],
      latest: null,
      min: null,
      max: null
    };
  }

  function isZeroLikeValue(value: number): boolean {
    return !Number.isFinite(value) || Math.abs(value) < 0.0001;
  }

  function shouldHideSummary(points: number[]): boolean {
    return points.length === 0 || points.every((value) => isZeroLikeValue(value));
  }

  function normalizeSummary(summaryValue: HudSummary): HudSummary {
    return shouldHideSummary(summaryValue.points) ? buildEmptySummary() : summaryValue;
  }

  function buildSummary(points: number[], xValues: number[] = []): HudSummary {
    if (points.length === 0) {
      return buildEmptySummary();
    }

    const resolvedXValues = points.map((_, index) => {
      const x = xValues[index];
      return Number.isFinite(x) ? Number(x) : index + 1;
    });

    return {
      label: "Resultant Force",
      xValues: resolvedXValues,
      points,
      latest: points[points.length - 1],
      min: Math.min(...points),
      max: Math.max(...points)
    };
  }

  function createSummaryPoints(seedValue: number): number[] {
    let current = seedValue;
    const points: number[] = [];

    for (let index = 0; index < summaryPointsPerSeries; index += 1) {
      current = clamp(current + randomBetween(-140, 140), 180, 2200);
      points.push(Math.round(current * 10) / 10);
    }

    return points;
  }

  function evolveSummary(summaryValue: HudSummary): HudSummary {
    const previous = summaryValue.points.length
      ? summaryValue.points[summaryValue.points.length - 1]
      : randomBetween(280, 1600);
    const next = Math.round(clamp(previous + randomBetween(-160, 160), 120, 2400) * 10) / 10;
    const previousXValues =
      summaryValue.xValues && summaryValue.xValues.length === summaryValue.points.length
        ? summaryValue.xValues
        : summaryValue.points.map((_, index) => index + 1);
    const points =
      summaryValue.points.length >= summaryPointsPerSeries
        ? summaryValue.points.slice(1)
        : summaryValue.points.slice();
    const xValues =
      previousXValues.length >= summaryPointsPerSeries ? previousXValues.slice(1) : previousXValues.slice();

    points.push(next);
    xValues.push((xValues[xValues.length - 1] ?? 0) + 1);
    return buildSummary(points, xValues);
  }

  function buildInactivePanels(): HudSignalPanel[] {
    return [];
  }

  function applyPacket(packet: HudPacket): void {
    if (replayHasData) {
      return;
    }
    signalPanels = showSignalPanels ? packet.panels : buildInactivePanels();
    summary = packet.summary;
    pressureMatrix = packet.pressureMatrix;
    hasSignalData = signalPanels.length > 0 || packet.summary.points.length > 0;
  }

  function clearHudPanels(): void {
    hasSignalData = false;
    signalPanels = buildInactivePanels();
    summary = buildEmptySummary();
    pressureMatrix = null;
  }

  function startMockFeed(push: (packet: HudPacket) => void): () => void {
    let panels = buildInactivePanels();
    let summaryValue = buildSummary(createSummaryPoints(randomBetween(480, 1440)));
    push({ ts: Date.now(), panels, summary: summaryValue, pressureMatrix: null });

    const timerId = window.setInterval(() => {
      summaryValue = evolveSummary(summaryValue);

      push({ ts: Date.now(), panels, summary: summaryValue, pressureMatrix: null });
    }, signalRenderTickMs);

    return () => {
      window.clearInterval(timerId);
    };
  }

  async function startTauriHudStream(push: (packet: HudPacket) => void): Promise<UnlistenFn> {
    return listen<HudPacket>("hud_stream", (event) => {
      push(event.payload);
    });
  }

  function buildConfigLinks(
    currentLocale: LocaleCode,
    activeId: string,
    isSettingsOpen: boolean,
    isPrecisionOpen: boolean
  ): HudConfigLink[] {
    const labels =
      currentLocale === "zh-CN"
        ? {
            streamOn: "打开",
            streamOff: "关闭",
            calibrate: "校准",
            precisionTest: "游戏",
            settings: "参数"
          }
        : {
            streamOn: "Open",
            streamOff: "Close",
            calibrate: "Calib",
            precisionTest: "Game",
            settings: "Setup"
          };

    return [
      {
        id: "stream-on",
        label: labels.streamOn,
        tone: "lime",
        active: activeId === "stream-on"
      },
      {
        id: "stream-off",
        label: labels.streamOff,
        tone: "orange",
        active: activeId === "stream-off"
      },
      {
        id: "calibrate",
        label: labels.calibrate,
        tone: "cyan",
        active: activeId === "calibrate"
      },
      {
        id: "precision-test",
        label: labels.precisionTest,
        tone: "lime",
        active: isPrecisionOpen
      },
      {
        id: "settings",
        label: labels.settings,
        tone: "neutral",
        active: isSettingsOpen
      }
    ];
  }

  async function ensureDefaultWindowSize(): Promise<void> {
    if (!isTauriRuntime()) {
      return;
    }

    try {
      const monitor = await currentMonitor();
      if (!monitor) {
        return;
      }

      const scaleFactor = monitor.scaleFactor || 1;
      const targetWidth = Math.floor((monitor.size.width * 0.75) / scaleFactor);
      const targetHeight = Math.floor((monitor.size.height * 0.75) / scaleFactor);

      const currentWindow = getCurrentWindow();
      const currentSize = await currentWindow.innerSize();
      const currentWidth = Math.floor(currentSize.width / scaleFactor);
      const currentHeight = Math.floor(currentSize.height / scaleFactor);
      const shouldResize = currentWidth < targetWidth * 0.9 || currentHeight < targetHeight * 0.9;

      if (shouldResize) {
        await currentWindow.setSize(new LogicalSize(targetWidth, targetHeight));
        await currentWindow.center();
      }
    } catch (error) {
      console.error("Default window sizing failed:", error);
    }
  }

  async function syncWindowState(): Promise<void> {
    if (!isTauriRuntime()) {
      return;
    }

    try {
      isWindowMaximized = await getCurrentWindow().isMaximized();
    } catch {
      isWindowMaximized = false;
    }
  }

  async function probeWebgl2(): Promise<void> {
    if (typeof window === "undefined") {
      return;
    }

    const canvas = document.createElement("canvas");
    const context = canvas.getContext("webgl2");
  }

  function handleLocaleChange(event: CustomEvent<LocaleCode>): void {
    locale = event.detail;
  }

  function handlePortChange(event: CustomEvent<string>): void {
    serialPortValue = event.detail;
    connectionState = "offline";
    connectionNotice = "";
    clearHudPanels();
    console.info("[hud] port changed:", event.detail);
  }

  function normalizeInvokeError(error: unknown): string {
    if (typeof error === "string") {
      return error;
    }

    if (error && typeof error === "object") {
      if ("message" in error && typeof error.message === "string") {
        return error.message;
      }

      return JSON.stringify(error);
    }

    return "UnknownError";
  }

  function resolveSerialNotice(error: unknown, action: "connect" | "disconnect"): string {
    const errorCode = normalizeInvokeError(error);

    if (locale === "zh-CN") {
      if (action === "connect") {
        switch (errorCode) {
          case "OpenError":
            return "串口连接失败，请确认端口存在且未被占用。";
          case "AlreadyConnected":
            return "当前已存在活动连接，请先断开。";
          case "InvalidConfig":
            return "当前串口配置无效，请重新选择端口。";
          default:
            return "串口连接失败，请稍后重试。";
        }
      }

      switch (errorCode) {
        case "CloseError":
          return "串口断开失败，请稍后重试。";
        default:
          return "串口断开失败，请稍后重试。";
      }
    }

    if (action === "connect") {
      switch (errorCode) {
        case "OpenError":
          return "Connection failed. Check whether the port exists or is already in use.";
        case "AlreadyConnected":
          return "A serial connection is already active. Disconnect it first.";
        case "InvalidConfig":
          return "The selected serial port is invalid. Choose another port.";
        default:
          return "Connection failed. Please try again.";
      }
    }

    return "Disconnect failed. Please try again.";
  }

  function resolveRefreshNotice(error: unknown): string {
    const errorCode = normalizeInvokeError(error);

    if (locale === "zh-CN") {
      return errorCode === "ScanError"
        ? "串口列表刷新失败，请确认系统串口服务正常。"
        : "刷新串口列表失败，请稍后重试。";
    }

    return errorCode === "ScanError"
      ? "Refreshing serial ports failed. Check whether the OS serial service is available."
      : "Refreshing serial ports failed. Please try again.";
  }

  function resolveExportNotice(error: unknown): string {
    const errorCode = normalizeInvokeError(error);

    if (locale === "zh-CN") {
      if (errorCode === "NoRecordedData") {
        return "暂无可导出的采样数据，请先连接并采集。";
      }

      return errorCode === "ExportError"
        ? "CSV 导出失败，请确认目标目录可写。"
        : "CSV 导出失败，请稍后重试。";
    }

    if (errorCode === "NoRecordedData") {
      return "No recorded data is available. Connect and collect samples first.";
    }

    return errorCode === "ExportError"
      ? "CSV export failed. Verify that the output directory is writable."
      : "CSV export failed. Please try again.";
  }

  function resolveImportNotice(error: unknown): string {
    const errorCode = normalizeInvokeError(error);

    if (locale === "zh-CN") {
      if (errorCode === "NoRecordedData") {
        return "CSV 文件没有可回放的数据帧。";
      }

      return errorCode === "ImportError"
        ? "CSV 导入失败，请确认数据格式正确。"
        : "CSV 导入失败，请稍后重试。";
    }

    if (errorCode === "NoRecordedData") {
      return "The CSV file does not contain replayable frames.";
    }

    return errorCode === "ImportError"
      ? "CSV import failed. Please verify the data format."
      : "CSV import failed. Please try again.";
  }

  async function refreshSerialPorts(): Promise<void> {
    if (!isTauriRuntime()) {
      return;
    }

    isRefreshingPorts = true;

    try {
      const ports = await invoke<string[]>("serial_enum");
      serialPortOptions = ports;

      if (ports.includes(serialPortValue)) {
        return;
      }

      serialPortValue = ports[0] ?? "";
      if (!serialPortValue) {
        connectionState = "offline";
        clearHudPanels();
      }
    } catch (error) {
      connectionNotice = resolveRefreshNotice(error);
      connectionNoticeTone = "warn";
      console.error("Serial port refresh failed:", error);
    } finally {
      isRefreshingPorts = false;
    }
  }

  async function handleSerialRefresh(): Promise<void> {
    await refreshSerialPorts();
  }

  async function handleSerialConnect(event: CustomEvent<string>): Promise<void> {
    if (!isTauriRuntime()) {
      console.warn("[serial] Connect is only available inside Tauri.");
      return;
    }

    if (connectionState === "online") {
      await handleSerialDisconnect();
      return;
    }

    connectionState = "connecting";
    connectionNotice = "";

    try {
      const result = await invoke<SerialConnectResult>("serial_connect", { port: event.detail });
      connectionState = result.connected ? "online" : "offline";
      serialPortValue = result.port;
      connectionNotice = "";
      connectionNoticeTone = "info";
      clearHudPanels();
      console.info("[serial] connect result:", result.message);
    } catch (error) {
      connectionState = "offline";
      connectionNotice = resolveSerialNotice(error, "connect");
      connectionNoticeTone = "warn";
      clearHudPanels();
      console.error("Serial connect failed:", error);
    }
  }

  async function handleSerialDisconnect(): Promise<void> {
    try {
      const result = await invoke<SerialConnectResult>("serial_disconnect");
      connectionState = result.connected ? "online" : "offline";
      connectionNotice = "";
      connectionNoticeTone = "info";
      clearHudPanels();
    } catch (error) {
      connectionNotice = resolveSerialNotice(error, "disconnect");
      connectionNoticeTone = "warn";
      console.error("Serial disconnect failed:", error);
    }
  }

  async function runSerialExport(filePath?: string): Promise<boolean> {
    if (!isTauriRuntime()) {
      connectionNotice =
        locale === "zh-CN" ? "当前环境不支持导出到本地路径。" : "Current runtime cannot export to local paths.";
      connectionNoticeTone = "warn";
      return false;
    }

    isExporting = true;
    fileExplorerBusy = true;

    try {
      const result = filePath
        ? await invoke<SerialExportResult>("serial_export_csv_to_path", { filePath })
        : await invoke<SerialExportResult>("serial_export_csv");

      connectionNotice =
        locale === "zh-CN"
          ? `CSV 导出成功（${result.frameCount} 帧）：${result.path}`
          : `CSV exported (${result.frameCount} frames): ${result.path}`;
      connectionNoticeTone = "ok";
      return true;
    } catch (error) {
      connectionNotice = resolveExportNotice(error);
      connectionNoticeTone = "warn";
      console.error("Serial export failed:", error);
      return false;
    } finally {
      isExporting = false;
      fileExplorerBusy = false;
    }
  }

  async function precheckExportRecordData(): Promise<boolean> {
    if (!isTauriRuntime()) {
      return true;
    }

    try {
      const result = await invoke<SerialRecordStateResult>("serial_has_record_data");
      if (result.hasData) {
        return true;
      }

      connectionNotice = resolveExportNotice("NoRecordedData");
      connectionNoticeTone = "warn";
      return false;
    } catch (error) {
      connectionNotice = resolveExportNotice(error);
      connectionNoticeTone = "warn";
      console.error("Export precheck failed:", error);
      return false;
    }
  }

  async function handleSerialExportRequest(): Promise<void> {
    const hasData = await precheckExportRecordData();
    if (!hasData) {
      return;
    }

    await openFileExplorer("save");
  }

  async function handleReplayImportRequest(): Promise<void> {
    await openFileExplorer("open");
  }

  async function handleFileExplorerNavigate(event: CustomEvent<string>): Promise<void> {
    await loadFileExplorerDirectory(event.detail);
  }

  async function handleFileExplorerConfirm(): Promise<void> {
    if (fileExplorerBusy) {
      return;
    }

    if (fileExplorerMode === "open") {
      const selected = fileExplorerEntries.find((entry) => entry.path === fileExplorerSelectedPath);
      if (!selected) {
        return;
      }
      if (selected.isDir) {
        await loadFileExplorerDirectory(selected.path);
        return;
      }
      if (!isCsvPath(selected.path)) {
        connectionNotice = locale === "zh-CN" ? "请先选择 CSV 文件。" : "Please choose a CSV file.";
        connectionNoticeTone = "warn";
        return;
      }

      fileExplorerBusy = true;
      const ok = await importReplayFromPath(selected.path);
      fileExplorerBusy = false;
      if (ok) {
        fileExplorerOpen = false;
      }
      return;
    }

    const selected = fileExplorerEntries.find((entry) => entry.path === fileExplorerSelectedPath);
    const targetDir = selected?.isDir ? selected.path : fileExplorerCurrentPath;
    const csvName = ensureCsvSuffix(fileExplorerFileName || buildDefaultExportName());
    if (!csvName) {
      return;
    }

    const targetPath = joinPath(targetDir, csvName);
    const ok = await runSerialExport(targetPath);
    if (ok) {
      fileExplorerOpen = false;
    }
  }

  function handleReplayToggle(): void {
    if (!replayHasData) {
      return;
    }

    if (replayIsPlaying) {
      pauseReplayPlayback();
      return;
    }

    startReplayPlayback();
  }

  function handleReplayStop(): void {
    pauseReplayPlayback();
    if (replayHasData) {
      applyReplayFrame(0);
    }
  }

  function handleReplaySeek(event: CustomEvent<number>): void {
    if (!replayHasData) {
      return;
    }

    const ratio = clamp(Number.isFinite(event.detail) ? event.detail : 0, 0, 1);
    const targetIndex = Math.round(ratio * Math.max(replayFrames.length - 1, 0));
    applyReplayFrame(targetIndex);

    if (replayIsPlaying && typeof window !== "undefined") {
      stopReplayTimer();
      replayTimerId = window.setTimeout(stepReplayPlayback, 0);
    }
  }

  function handleReplaySpeed(event: CustomEvent<number>): void {
    const nextSpeed = clamp(Number.isFinite(event.detail) ? event.detail : 1, 0.5, 2);
    replaySpeed = Math.round(nextSpeed * 100) / 100;

    if (replayIsPlaying && typeof window !== "undefined") {
      stopReplayTimer();
      replayTimerId = window.setTimeout(stepReplayPlayback, 0);
    }
  }

  function handleReplayClose(): void {
    pauseReplayPlayback();
    replayFrames = [];
    replayCurrentIndex = 0;
    replayHasDisplayedFrame = false;
    replaySpeed = 1;
    replayProgress = 0;
    replayFileName = "";
    resetReplayVisualState();
  }

  function handleConfigLink(event: CustomEvent<string>): void {
    if (event.detail === "precision-test") {
      isPrecisionTestOpen = !isPrecisionTestOpen;
      isConfigPanelOpen = false;
      return;
    }

    if (event.detail === "settings") {
      isPrecisionTestOpen = false;
      isConfigPanelOpen = !isConfigPanelOpen;
      return;
    }

    isPrecisionTestOpen = false;
    isConfigPanelOpen = false;
    activeConfigLinkId = event.detail;
    console.info("[hud] config link clicked:", event.detail);
  }

  async function handleWindowControl(event: CustomEvent<WindowControlAction>): Promise<void> {
    if (!isTauriRuntime()) {
      return;
    }

    try {
      if (event.detail === "minimize") {
        await invoke("win_minimize");
      } else if (event.detail === "toggle-maximize") {
        await invoke("win_toggle_maximize");
        await syncWindowState();
      } else {
        await invoke("win_close");
      }
    } catch (error) {
      console.error("Window control failed:", error);
    }
  }

  function handleMatrixDisplayToggle(event: CustomEvent<boolean>): void {
    matrixDisplayMode = event.detail ? "dots" : "numeric";
  }

  onMount(() => {
    let disposed = false;
    let unlistenHudStream: UnlistenFn | null = null;
    let stopMockFeed: (() => void) | null = null;

    void ensureDefaultWindowSize();
    void syncWindowState();
    void probeWebgl2();

    if (isTauriRuntime()) {
      void refreshSerialPorts();
      void startTauriHudStream(applyPacket)
        .then((unlisten) => {
          if (disposed) {
            unlisten();
            return;
          }

          unlistenHudStream = unlisten;
        })
        .catch((error) => {
          console.error("Failed to listen for hud_stream:", error);
        });
    } else {
      stopMockFeed = startMockFeed(applyPacket);
    }

    return () => {
      disposed = true;
      pauseReplayPlayback();
      stopMockFeed?.();
      unlistenHudStream?.();
    };
  });
</script>

<main class="hud-screen" style={rangeScaleStyle}>
  <div class="hud-background" aria-hidden="true">
    <div class="hud-gradient"></div>
    <div class="hud-vignette"></div>
    <div class="hud-noise"></div>
  </div>

  <div class="hud-layout">
    <HudPanel
      appName={uiCopy.appName}
      suiteName={uiCopy.suiteName}
      controlAreaLabel={uiCopy.controlArea}
      locale={locale}
      connectionState={connectionState}
      connectionLabel={uiCopy.connectionLabel}
      connectedLabel={uiCopy.connectedLabel}
      connectingLabel={uiCopy.connectingLabel}
      disconnectedLabel={uiCopy.disconnectedLabel}
      serialPortLabel={uiCopy.serialPortLabel}
      {serialPortValue}
      {serialPortOptions}
      deviceLabel={uiCopy.deviceLabel}
      deviceValue={deviceValue}
      sampleRateLabel={uiCopy.sampleRateLabel}
      sampleRateValue={sampleRateValue}
      channelsLabel={uiCopy.channelsLabel}
      channelsValue={channelsValue}
      configLinksLabel={uiCopy.configLinksLabel}
      refreshPortsLabel={uiCopy.refreshPortsLabel}
      matrixViewLabel={uiCopy.matrixViewLabel}
      matrixViewNumericLabel={uiCopy.matrixViewNumericLabel}
      matrixViewDotsLabel={uiCopy.matrixViewDotsLabel}
      {matrixDisplayMode}
      connectActionLabel={uiCopy.connectActionLabel}
      disconnectActionLabel={uiCopy.disconnectActionLabel}
      exportActionLabel={uiCopy.exportActionLabel}
      exportingActionLabel={uiCopy.exportingActionLabel}
      importActionLabel={uiCopy.importActionLabel}
      {connectionNotice}
      {connectionNoticeTone}
      {configLinks}
      {isRefreshingPorts}
      {isExporting}
      isConnectDisabled={!serialPortValue || connectionState === "connecting"}
      isExportDisabled={isExporting || connectionState === "connecting"}
      isWindowMaximized={isWindowMaximized}
      on:windowcontrol={handleWindowControl}
      on:localechange={handleLocaleChange}
      on:portchange={handlePortChange}
      on:configlink={handleConfigLink}
      on:matrixdisplaytoggle={handleMatrixDisplayToggle}
      on:serialrefresh={handleSerialRefresh}
      on:serialconnect={handleSerialConnect}
      on:serialexport={handleSerialExportRequest}
      on:csvimport={handleReplayImportRequest}
      on:noticeclear={() => (connectionNotice = "")}
    />

    <CenterStage
      {locale}
      bind:matrixRows
      bind:matrixCols
      bind:rangeMin
      bind:rangeMax
      bind:colorMapPreset
      bind:matrixDisplayMode
      configPanelTitle={uiCopy.configPanelTitle}
      configPanelHint={uiCopy.configPanelHint}
      matrixSizeLabel={uiCopy.matrixSizeLabel}
      matrixRowsLabel={uiCopy.matrixRowsLabel}
      matrixColsLabel={uiCopy.matrixColsLabel}
      rangeLabel={uiCopy.rangeLabel}
      rangeMinLabel={uiCopy.rangeMinLabel}
      rangeMaxLabel={uiCopy.rangeMaxLabel}
      colorMapLabel={uiCopy.colorMapLabel}
      {colorMapOptions}
      replaySectionLabel={uiCopy.replaySectionLabel}
      replayPlayLabel={uiCopy.replayPlayLabel}
      replayPauseLabel={uiCopy.replayPauseLabel}
      replayStopLabel={uiCopy.replayStopLabel}
      replaySpeedLabel={uiCopy.replaySpeedLabel}
      replayProgressLabel={uiCopy.replayProgressLabel}
      {replayHasData}
      {replayIsPlaying}
      {replaySpeed}
      {replayProgress}
      {replayFileName}
      {replayFrameInfo}
      resetConfigLabel={uiCopy.resetConfigLabel}
      applyLiveHint={uiCopy.applyLiveHint}
      leftPanels={leftSignalPanels}
      rightPanels={rightSignalPanels}
      {pressureMatrix}
      showConfigPanel={isConfigPanelOpen}
      showPrecisionTestPanel={isPrecisionTestOpen}
      {summary}
      on:replaytoggle={handleReplayToggle}
      on:replaystop={handleReplayStop}
      on:replayseek={handleReplaySeek}
      on:replayspeed={handleReplaySpeed}
      on:replayclose={handleReplayClose}
      on:configclose={() => (isConfigPanelOpen = false)}
    >
      {#if !isPrecisionTestOpen}
        <section class="range-scale" aria-label="Signal Range">
          <p class="range-label">Range</p>
          <div class="range-track">
            {#each rangeTicks as tick}
              <span class="range-tick">{tick}</span>
            {/each}
          </div>
        </section>
      {/if}
    </CenterStage>
  </div>

  <FileExplorerModal
    open={fileExplorerOpen}
    mode={fileExplorerMode}
    title={fileExplorerTitle}
    currentPath={fileExplorerCurrentPath}
    parentPath={fileExplorerParentPath}
    roots={fileExplorerRoots}
    entries={fileExplorerEntries}
    bind:selectedPath={fileExplorerSelectedPath}
    bind:fileName={fileExplorerFileName}
    pathLabel={uiCopy.fileExplorerPathLabel}
    fileNameLabel={uiCopy.fileExplorerNameLabel}
    cancelLabel={uiCopy.fileExplorerCancelLabel}
    confirmLabel={fileExplorerConfirmLabel}
    emptyHint={uiCopy.fileExplorerEmptyHint}
    csvHint={uiCopy.fileExplorerCsvHint}
    busyLabel={uiCopy.fileExplorerLoadingLabel}
    upLabel={uiCopy.fileExplorerUpLabel}
    nameColumnLabel={uiCopy.fileExplorerNameColumnLabel}
    sizeColumnLabel={uiCopy.fileExplorerSizeColumnLabel}
    modifiedColumnLabel={uiCopy.fileExplorerModifiedColumnLabel}
    isBusy={fileExplorerBusy}
    on:close={closeFileExplorer}
    on:navigate={handleFileExplorerNavigate}
    on:confirm={handleFileExplorerConfirm}
  />
</main>

<style>
  .hud-screen {
    position: relative;
    isolation: isolate;
    height: 100dvh;
    min-height: 100dvh;
    overflow: clip;
    background: var(--hud-bg-00);
  }

  .hud-background {
    position: absolute;
    inset: 0;
    pointer-events: none;
    z-index: 0;
  }

  .hud-gradient {
    position: absolute;
    inset: 0;
    background:
      radial-gradient(circle at 14% 6%, rgb(var(--hud-glow-rgb) / 0.07), transparent 36%),
      radial-gradient(circle at 86% 14%, rgb(var(--hud-glow-alt-rgb) / 0.05), transparent 32%),
      linear-gradient(170deg, var(--hud-bg-20) 0%, var(--hud-bg-10) 56%, var(--hud-bg-30) 100%);
  }

  .hud-vignette {
    position: absolute;
    inset: 0;
    background: radial-gradient(circle at center, transparent 41%, rgb(0 0 0 / 0.66) 100%);
  }

  .hud-noise {
    position: absolute;
    inset: -12%;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 140 140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
    opacity: 0.025;
    mix-blend-mode: soft-light;
  }

  .hud-layout {
    position: relative;
    z-index: 1;
    display: grid;
    height: 100%;
    min-height: 0;
    grid-template-rows: auto minmax(0, 1fr);
    gap: clamp(0.5rem, 1.2vw, 0.95rem);
    padding: clamp(0.65rem, 1.75vw, 1.3rem);
    border: 1px solid rgb(var(--hud-border-rgb) / 0.2);
    border-radius: 0.9rem;
    background:
      linear-gradient(
        176deg,
        rgb(var(--hud-surface-alt-rgb) / 0.9) 0%,
        rgb(var(--hud-surface-deep-rgb) / 0.94) 56%,
        rgb(var(--hud-surface-rgb) / 0.9) 100%
      ),
      radial-gradient(circle at 18% 0%, rgb(var(--hud-glow-rgb) / 0.05), transparent 40%),
      radial-gradient(circle at 84% 8%, rgb(var(--hud-glow-alt-rgb) / 0.04), transparent 36%);
    box-shadow:
      inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.08),
      inset 0 -28px 60px rgb(0 0 0 / 0.34);
    overflow: hidden;
  }

  .range-scale {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    gap: 0.75rem;
    border: 1px solid rgb(var(--hud-border-rgb) / 0.24);
    border-radius: 0.48rem;
    padding: 0.34rem 0.52rem;
    background:
      linear-gradient(180deg, rgb(var(--hud-surface-rgb) / 0.72), rgb(var(--hud-surface-deep-rgb) / 0.56)),
      radial-gradient(circle at 50% 0, rgb(var(--hud-glow-rgb) / 0.06), transparent 52%);
    box-shadow:
      inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.06),
      0 0 12px rgb(var(--hud-glow-rgb) / 0.08);
    pointer-events: none;
  }

  .range-label {
    margin: 0;
    color: rgb(var(--hud-text-dim-rgb) / 0.82);
    font-size: 0.56rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
  }

  .range-track {
    position: relative;
    isolation: isolate;
    display: grid;
    grid-template-columns: repeat(11, minmax(0, 1fr));
    gap: 0.26rem;
    padding: 0.28rem 0.36rem 0.16rem;
    border: 1px solid rgb(var(--hud-border-strong-rgb) / 0.14);
    border-radius: 999px;
    background: rgb(var(--hud-surface-rgb) / 0.34);
    overflow: hidden;
  }

  .range-track::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    border-radius: inherit;
    background:
      linear-gradient(
        90deg,
        color-mix(in srgb, var(--hud-range-0) 92%, black) 0%,
        color-mix(in srgb, var(--hud-range-1) 96%, black) 12.5%,
        color-mix(in srgb, var(--hud-range-1) 92%, black) 25%,
        color-mix(in srgb, var(--hud-range-2) 96%, black) 37.5%,
        color-mix(in srgb, var(--hud-range-2) 92%, black) 50%,
        color-mix(in srgb, var(--hud-range-3) 96%, black) 62.5%,
        color-mix(in srgb, var(--hud-range-3) 92%, black) 75%,
        color-mix(in srgb, var(--hud-range-4) 96%, black) 87.5%,
        color-mix(in srgb, var(--hud-range-5) 94%, black) 100%
      ),
      linear-gradient(180deg, rgb(var(--hud-text-main-rgb) / 0.06), transparent 42%);
    box-shadow:
      inset 0 1px 0 rgb(var(--hud-text-main-rgb) / 0.1),
      inset 0 -10px 18px rgb(0 0 0 / 0.18);
    opacity: 0.94;
  }

  .range-track::after {
    content: "";
    position: absolute;
    inset-inline: 0.32rem;
    inset-block-start: 0.22rem;
    block-size: 0.18rem;
    z-index: 0;
    border-radius: 999px;
    background: linear-gradient(
      90deg,
      var(--hud-range-glow-0) 0%,
      var(--hud-range-glow-1) 52%,
      var(--hud-range-glow-2) 100%
    );
    filter: blur(0.18rem);
  }

  .range-tick {
    position: relative;
    z-index: 1;
    padding-block-start: 0.36rem;
    color: rgb(var(--hud-text-main-rgb) / 0.96);
    font-size: 0.56rem;
    text-align: center;
    text-shadow:
      0 1px 0 rgb(0 0 0 / 0.46),
      0 0 12px rgb(var(--hud-surface-alt-rgb) / 0.4);
  }

  .range-tick::before {
    content: "";
    position: absolute;
    inset-block-start: 0;
    inset-inline: 50%;
    inline-size: 1px;
    block-size: 0.24rem;
    transform: translateX(-50%);
    background: rgb(var(--hud-text-main-rgb) / 0.74);
    box-shadow: 0 0 8px rgb(var(--hud-glow-rgb) / 0.22);
  }

  @media (max-width: 760px) {
    .range-scale {
      grid-template-columns: 1fr;
    }
  }
</style>
