<template>
  <div class="relative w-full h-full min-h-full">
    <div id="forge-preview" ref="forgeDom" class="absolute inset-0"></div>

    <!-- Popovers：永遠顯示在每顆 sprite 上方（跟著相機更新） -->
    <div class="absolute inset-0 z-20 pointer-events-none opacity-90">
      <div
        v-for="L in filteredLabels"
        :key="L.id"
        class="absolute -translate-x-1/2 -translate-y-full cursor-pointer"
        :style="{
          left: L.x + 'px',
          top: L.y - 10 + 'px',
          zIndex: activeLabelId === L.id ? 40 : 30,
        }"
        @click="bringToFrontById(L.id)"
      >
        <div
          class="pointer-events-auto relative bg-white/95 border border-gray-300 rounded-md shadow px-3 py-2 text-sm cursor-pointer w-[180px] break-words leading-relaxed"
          style="will-change: transform"
          @click.stop="openResidentModal(L, $event)"
          role="button"
          tabindex="0"
          @keydown.enter.prevent.stop="openResidentModal(L, $event)"
          @keydown.space.prevent.stop="openResidentModal(L, $event)"
          :aria-label="`查看 ${L.data.name} 詳細資訊`"
        >
          <!-- 箭頭（下方置中，指向 sprite）  -->
          <span
            class="absolute left-1/2 -bottom-2 -translate-x-1/2 w-0 h-0 border-x-8 border-x-transparent border-t-8 border-t-white"
            aria-hidden="true"
          ></span>

          <ul class="list-none">
            <!-- 第一行：床號永遠顯示-->
            <li class="flex justify-between items-center gap-1">
              <!-- 左側：性別圖示 + 狀態符號 + 床號 -->
              <div class="flex items-center">
                <!-- 性別圖示 -->
                <span
                  v-if="(L.data.roomGender || L.data.residentsSex) === '男'"
                  class="inline-flex items-center justify-center mr-2 align-middle"
                  :style="{ color: BRAND_GREEN }"
                  aria-label="男生房"
                  title="男生房"
                >
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    width="17"
                    height="17"
                    viewBox="0 0 256 256"
                  >
                    <path
                      fill="currentColor"
                      d="M152 140a36 36 0 1 1-36-36a36 36 0 0 1 36 36m64-100v176a16 16 0 0 1-16 16H56a16 16 0 0 1-16-16V40a16 16 0 0 1 16-16h144a16 16 0 0 1 16 16m-24 32a8 8 0 0 0-8-8h-32a8 8 0 0 0 0 16h12.69l-18 18A52.08 52.08 0 1 0 158 109.35l18-18V104a8 8 0 0 0 16 0Z"
                    />
                  </svg>
                </span>
                <span
                  v-else-if="
                    (L.data.roomGender || L.data.residentsSex) === '女'
                  "
                  class="inline-flex items-center justify-center mr-2 align-middle"
                  :style="{ color: BRAND_RED }"
                  aria-label="女生房"
                  title="女生房"
                >
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    width="16"
                    height="16"
                    viewBox="0 0 256 256"
                  >
                    <path
                      fill="currentColor"
                      d="M128 144a40 40 0 1 1 40-40a40 40 0 0 1-40 40m88-104v176a16 16 0 0 1-16 16H56a16 16 0 0 1-16-16V40a16 16 0 0 1 16-16h144a16 16 0 0 1 16 16m-80 136v-16.58a56 56 0 1 0-16 0V176H96a8 8 0 0 0 0 16h24v16a8 8 0 0 0 16 0v-16h24a8 8 0 0 0 0-16Z"
                    />
                  </svg>
                </span>

                <!-- 狀態符號：hospitalized=黃三角；leave=灰三角；vacant=空心圓；occupied=dot -->
                <template v-if="L.data.status === 'hospitalized'">
                  <span
                    class="inline-flex items-center justify-center text-brand-yellow-dark mr-2 align-middle"
                    aria-label="住院中"
                  >
                    <svg
                      xmlns="http://www.w3.org/2000/svg"
                      width="12"
                      height="12"
                      viewBox="0 0 24 24"
                    >
                      <path
                        fill="currentColor"
                        d="M12 1.67a2.91 2.91 0 0 0-2.492 1.403L1.398 16.61a2.914 2.914 0 0 0 2.484 4.385h16.225a2.914 2.914 0 0 0 2.503-4.371L14.494 3.078A2.92 2.92 0 0 0 12 1.67"
                      />
                    </svg>
                  </span>
                </template>
                <template v-else-if="L.data.status === 'leave'">
                  <span
                    class="inline-flex items-center justify-center text-brand-gray mr-2 align-middle"
                    aria-label="請假中"
                  >
                    <svg
                      xmlns="http://www.w3.org/2000/svg"
                      width="12"
                      height="12"
                      viewBox="0 0 24 24"
                    >
                      <path
                        fill="currentColor"
                        d="M12 1.67a2.91 2.91 0 0 0-2.492 1.403L1.398 16.61a2.914 2.914 0 0 0 2.484 4.385h16.225a2.914 2.914 0 0 0 2.503-4.371L14.494 3.078A2.92 2.92 0 0 0 12 1.67"
                      />
                    </svg>
                  </span>
                </template>
                <template v-else-if="L.data.status === 'vacant'">
                  <!-- 空床：空心圓 SVG-->
                  <span
                    class="inline-flex items-center justify-center text-brand-gray mr-2 align-middle"
                    aria-label="空床"
                  >
                    <svg
                      xmlns="http://www.w3.org/2000/svg"
                      width="12"
                      height="12"
                      viewBox="0 0 24 24"
                    >
                      <path
                        fill="currentColor"
                        d="M11.5 3a9.5 9.5 0 0 1 9.5 9.5a9.5 9.5 0 0 1-9.5 9.5A9.5 9.5 0 0 1 2 12.5A9.5 9.5 0 0 1 11.5 3m0 1A8.5 8.5 0 0 0 3 12.5a8.5 8.5 0 0 0 8.5 8.5a8.5 8.5 0 0 0 8.5-8.5A8.5 8.5 0 0 0 11.5 4"
                      />
                    </svg>
                  </span>
                </template>
                <template v-else>
                  <!-- 有住民：顯示 dot；在「選到有住民」篩選時，dot 一律綠 -->
                  <span
                    class="inline-block w-2 h-2 rounded-full mr-2 align-middle"
                    :style="{
                      backgroundColor:
                        selectedInfo === 'occupied' &&
                        L.data.status === 'occupied'
                          ? BRAND_GREEN
                          : labelDotColor(L),
                    }"
                  ></span>
                </template>

                <!-- 床號 -->
                <span class="align-middle">{{ L.data.name }}</span>
              </div>

              <!-- 右側：眼睛 icon -->
              <div class="text-gray-400" title="查看詳細" aria-label="查看詳細">
                <span>
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    width="12"
                    height="12"
                    viewBox="0 0 24 24"
                  >
                    <path
                      fill="currentColor"
                      d="M12 9a3 3 0 0 0-3 3a3 3 0 0 0 3 3a3 3 0 0 0 3-3a3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5a5 5 0 0 1 5-5a5 5 0 0 1 5 5a5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5"
                    />
                  </svg>
                </span>
              </div>
            </li>

            <!-- 第二行：住民基本資料（只有空床才顯示 - ） -->
            <li>
              {{
                isVacantWithoutSpecial(L)
                  ? "-"
                  : `${L.data.residentsName}｜${L.data.residentsSex}｜${L.data.residentsAge}`
              }}
            </li>
          </ul>
        </div>
      </div>
    </div>

    <!-- 樓層切換 -->
    <div
      class="absolute top-4 left-4 text-sm flex justify-center items-center z-30 bg-white border rounded-md shadow"
    >
      <p class="ps-4 py-2">樓層｜</p>

      <button
        :class="getFloorBtnClass('1F')"
        @click="onClickFloor('1F')"
        :aria-pressed="activeFloor === '1F'"
      >
        1F
      </button>

      <button
        :class="getFloorBtnClass('2F')"
        @click="onClickFloor('2F')"
        :aria-pressed="activeFloor === '2F'"
      >
        2F
      </button>
    </div>

    <!-- 床位資訊篩選 -->
    <div
      class="absolute top-16 left-4 text-sm flex justify-center items-center z-30 bg-white border rounded-md shadow ps-4"
    >
      <p class="py-2">床位資訊｜</p>

      <div class="flex justify-start items-center">
        <!-- 無顯示 -->
        <button
          :class="getInfoBtnClass('none')"
          @click="selectInfo({ label: '無顯示', value: 'none' })"
          :aria-pressed="selectedInfo === 'none'"
        >
          <span
            class="inline-flex items-center justify-center leading-none align-middle [&>svg]:block shrink-0"
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="12"
              height="12"
              viewBox="0 0 32 32"
            >
              <path
                fill="currentColor"
                d="M24.879 2.879A3 3 0 1 1 29.12 7.12l-8.79 8.79a.125.125 0 0 0 0 .177l8.79 8.79a3 3 0 1 1-4.242 4.243l-8.79-8.79a.125.125 0 0 0-.177 0l-8.79 8.79a3 3 0 1 1-4.243-4.242l8.79-8.79a.125.125 0 0 0 0-.177l-8.79-8.79A3 3 0 0 1 7.12 2.878l8.79 8.79a.125.125 0 0 0 .177 0z"
              />
            </svg>
          </span>
          <span class="leading-none align-middle">不顯示</span>
        </button>

        <!-- 有住民（綠/紅） -->
        <button
          :class="getInfoBtnClass('occupied')"
          @click="selectInfo({ label: '有住民', value: 'occupied' })"
          :aria-pressed="selectedInfo === 'occupied'"
        >
          <span
            class="text-brand-green inline-flex items-center justify-center leading-none align-middle [&>svg]:block shrink-0"
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="18"
              height="18"
              viewBox="0 0 16 16"
            >
              <path fill="currentColor" d="M8 4a4 4 0 1 1 0 8a4 4 0 0 1 0-8" />
            </svg>
          </span>

          <span class="leading-none align-middle">有住民</span>
        </button>

        <!-- 空床（白色 sprite + 灰 dot） -->
        <button
          :class="getInfoBtnClass('vacant')"
          @click="selectInfo({ label: '空床', value: 'vacant' })"
          :aria-pressed="selectedInfo === 'vacant'"
        >
          <span
            class="text-brand-gray inline-flex items-center justify-center leading-none align-middle [&>svg]:block shrink-0"
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="12"
              height="12"
              viewBox="0 0 24 24"
            >
              <path
                fill="currentColor"
                d="M11.5 3a9.5 9.5 0 0 1 9.5 9.5a9.5 9.5 0 0 1-9.5 9.5A9.5 9.5 0 0 1 2 12.5A9.5 9.5 0 0 1 11.5 3m0 1A8.5 8.5 0 0 0 3 12.5a8.5 8.5 0 0 0 8.5 8.5a8.5 8.5 0 0 0 8.5-8.5A8.5 8.5 0 0 0 11.5 4"
              />
            </svg>
          </span>
          <span class="leading-none align-middle">空床</span>
        </button>

        <!-- 住院中（白色 sprite + 黃三角） -->
        <button
          :class="getInfoBtnClass('hospitalized')"
          @click="selectInfo({ label: '住院中', value: 'hospitalized' })"
          :aria-pressed="selectedInfo === 'hospitalized'"
        >
          <span
            class="inline-flex items-center justify-center leading-none align-middle [&>svg]:block shrink-0 text-brand-yellow-dark"
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="12"
              height="12"
              viewBox="0 0 24 24"
            >
              <path
                fill="currentColor"
                d="M12 1.67a2.91 2.91 0 0 0-2.492 1.403L1.398 16.61a2.914 2.914 0 0 0 2.484 4.385h16.225a2.914 2.914 0 0 0 2.503-4.371L14.494 3.078A2.92 2.92 0 0 0 12 1.67"
              />
            </svg>
          </span>
          <span class="leading-none align-middle">住院中</span>
        </button>

        <!-- 請假中（白色 sprite + 灰三角） -->
        <button
          :class="getInfoBtnClass('leave')"
          @click="selectInfo({ label: '請假中', value: 'leave' })"
          :aria-pressed="selectedInfo === 'leave'"
        >
          <span
            class="inline-flex items-center justify-center leading-none align-middle [&>svg]:block shrink-0 text-brand-gray"
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="12"
              height="12"
              viewBox="0 0 24 24"
            >
              <path
                fill="currentColor"
                d="M12 1.67a2.91 2.91 0 0 0-2.492 1.403L1.398 16.61a2.914 2.914 0 0 0 2.484 4.385h16.225a2.914 2.914 0 0 0 2.503-4.371L14.494 3.078A2.92 2.92 0 0 0 12 1.67"
              />
            </svg>
          </span>
          <span class="leading-none align-middle">請假中</span>
        </button>
      </div>
    </div>

    <!-- 分區切換（多選｜預設不選＝顯示全部） -->
    <div
      class="absolute bottom-12 left-4 text-sm flex justify-center items-center gap-2 z-30 bg-white border rounded-md shadow"
    >
      <p class="ps-4 py-2">查看分區｜</p>

      <button
        v-for="zone in zones"
        :key="zone"
        :class="getZoneBtnClass(zone)"
        @click="toggleZone(zone)"
        :aria-pressed="isZoneSelected(zone)"
        type="button"
      >
        {{ zone }}
      </button>
    </div>
  </div>

  <!-- 中央 Modal：病患資訊 -->
  <div
    v-if="modalOpen"
    class="fixed inset-0 z-[120] flex items-center justify-center"
    role="dialog"
    aria-modal="true"
  >
    <!-- 背景遮罩 -->
    <div class="absolute inset-0 bg-black/40" @click="closeResidentModal"></div>

    <!-- Modal 內容 -->
    <div
      class="relative z-[130] w-[min(92vw,500px)] max-h-[84vh] overflow-auto bg-white rounded-2xl shadow-xl p-6"
      @click.stop
    >
      <!-- 標題 -->
      <div class="flex items-center justify-between mb-4">
        <div>
          <h3 class="text-lg font-semibold tracking-widest">病患資訊</h3>
        </div>
        <div class="flex justify-end items-center gap-4">
          <a
            href="http://service2.humetrics.ai:3456/9054/?token=UARK"
            target="_blank"
          >
            <button
              class="btn btn-sm bg-brand-purple-dark text-white hover:bg-brand-purple border-none shadow rounded px-3 py-2 tracking-wider"
            >
              健康偵測系統
            </button>
          </a>

          <button
            class="px-3 py-1 rounded-md hover:bg-gray-100"
            @click="closeResidentModal"
            aria-label="關閉"
          >
            ✕
          </button>
        </div>
      </div>

      <!-- 內容清單 -->
      <ul class="list-none text-sm space-y-2">
        <li><span class="text-gray-500">姓名：</span>{{ modalData?.name }}</li>
        <li><span class="text-gray-500">性別：</span>{{ modalData?.sex }}</li>
        <li><span class="text-gray-500">年齡：</span>{{ modalData?.age }}</li>
        <li>
          <span class="text-gray-500">入住日期：</span
          >{{ modalData?.startTime }}
        </li>
        <li>
          <span class="text-gray-500">身體狀況：</span
          >{{ modalData?.healthStatus }}
        </li>
        <li>
          <span class="text-gray-500">服藥狀況：</span
          >{{ modalData?.medicationStatus }}
        </li>
        <li>
          <span class="text-gray-500">特殊事件：</span
          >{{ modalData?.specialEvent }}
        </li>
        <li class="whitespace-pre-wrap break-words">
          <span class="text-gray-500">備註：</span>{{ modalData?.note }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { useRoute } from "vue-router";
import useForgeSprite from "@/hooks/forge/useForgeSprite";

/* ---------------------- 工具：樹/幾何 ---------------------- */
function getDirectChildren(model, parentId) {
  const tree = model.getInstanceTree?.();
  if (!tree) return [];
  const out = [];
  tree.enumNodeChildren(parentId, (id) => out.push(id));
  return out;
}
function nodeHasFragments(model, dbId) {
  const tree = model.getInstanceTree?.();
  if (!tree) return false;
  let has = false;
  tree.enumNodeFragments(
    dbId,
    () => {
      has = true;
    },
    true
  );
  return has;
}

/* ---------------------- 基本變數 ---------------------- */
const route = useRoute();
const forgeDom = ref(null);
let viewer = null;
const viewerReady = ref(false);
const activeFloor = ref("1F");

let unbindSpriteClick = null;
let THREE = null;

const {
  updateDataVisualization: dvInit,
  createSprites,
  forgeClickListener,
  cardfitToView,
  clear: clearSprites,
  __staticDevices,
} = useForgeSprite();

/* ---------------------- 常數 / 主題色 ---------------------- */
const FLOOR_DBIDS = { "1F": null, "2F": null };
const BRAND_RED = "#FF8678"; // 女（佔床）
const BRAND_GREEN = "#34D5C8"; // 男（佔床）
const BRAND_GRAY = "#CACACA"; // 空床 dot
const BRAND_WHITE = "#FFFFFF"; // 不在院內（sprite 本體）
const MODEL_SVF_PATH = `/upload/forge/0.svf`;

const THEME_PURPLE = {
  r: 170 / 255,
  g: 90 / 255,
  b: 255 / 255,
  a: 0.9,
};
const THEME_YELLOW = { r: 250 / 255, g: 204 / 255, b: 21 / 255, a: 0.9 };

/* ---------------------- 分區映射（每層樓對半且不重疊） ---------------------- */
// 每層樓一個 Map： key = 3 碼房號, value = '一般' | '氧氣'
const ROOM_ZONE_MAP = { "1F": new Map(), "2F": new Map() };

function getZoneForRoom(floorKey, code3) {
  return ROOM_ZONE_MAP[floorKey]?.get(String(code3)) ?? null;
}

/* ======（可選）手動指定男女房；預設留空就會自動對半 ====== */
const FIXED_ROOM_GENDER = {
  "1F": {
    male: ["101", "103"],
    female: ["102", "105"],
  },
  "2F": {
    male: ["201", "203", "205", "207", "209", "211", "213", "215"],
    female: ["202", "206", "208", "210", "212", "216"],
  },
};

/* ---------------------- THREE 入口 ---------------------- */
function getThree() {
  if (THREE) return THREE;
  THREE =
    (globalThis.Autodesk &&
      globalThis.Autodesk.Viewing &&
      (globalThis.Autodesk.Viewing.THREE ||
        globalThis.Autodesk.Viewing.Private?.THREE)) ||
    globalThis.THREE ||
    null;
  if (!THREE) throw new Error("[Forge] THREE 尚未就緒");
  return THREE;
}

/* ---------------------- Resident Modal ---------------------- */
const modalOpen = ref(false);
const modalData = ref(null);
const activeLabelId = ref(null);
function bringToFrontById(id) {
  activeLabelId.value = id;
}

function buildResidentModalData(d) {
  const vacant = d?.status === "vacant";
  return {
    name: vacant ? "-" : d?.residentsName ?? "-",
    sex: vacant ? "-" : d?.residentsSex ?? "-",
    age: vacant ? "-" : d?.residentsAge ?? "-",
    startTime: vacant ? "-" : d?.startTime ?? "-",
    healthStatus: vacant ? "-" : d?.healthStatus ?? "一般",
    medicationStatus: vacant ? "-" : d?.medicationStatus ?? "規律服藥",
    specialEvent:
      d?.status === "hospitalized"
        ? "住院中"
        : d?.status === "leave"
        ? "請假中"
        : "-",
    note: vacant ? "-" : d?.note ?? "-",
  };
}
function openResidentModal(L, e) {
  e?.stopPropagation?.();
  bringToFrontById(L.id);
  modalData.value = buildResidentModalData(L.data);
  modalOpen.value = true;
}
function closeResidentModal() {
  modalOpen.value = false;
}

/* ---------------------- 初始樓層 ---------------------- */
function resolveInitialFloor() {
  const q = (route.query?.floor || "").toString().toUpperCase();
  if (q === "1F" || q === "2F") return q;
  const saved = localStorage.getItem("uark-floor");
  if (saved === "1F" || saved === "2F") return saved;
  return "1F";
}

/* ---------------------- Forge 初始化 ---------------------- */
function initViewer(container) {
  return new Promise((resolve) => {
    Autodesk.Viewing.Initializer({ env: "Local", language: "en" }, () => {
      const config = {
        extensions: ["Autodesk.DataVisualization", "Autodesk.DocumentBrowser"],
      };
      viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
      Autodesk.Viewing.Private.InitParametersSetting.alpha = true;
      viewer.start();
      window.v = viewer;
      getThree();
      viewer.setGroundShadow(true);
      const r = viewer.impl?.renderer?.();
      if (r?.setClearColor) r.setClearColor(0x000000, 0);
      if (r?.setClearAlpha) r.setClearAlpha(0);
      viewer.impl.invalidate(true);
      resolve();
    });
  });
}
function loadModel(filePath) {
  return new Promise((resolve, reject) => {
    viewer.loadModel(filePath, {}, (model) => resolve(model), reject);
  });
}
function hideViewCubeAndHome() {
  const tryHide = () => {
    const ext = viewer?.getExtension?.("Autodesk.ViewCubeUi");
    if (ext?.setVisible) ext.setVisible(false);
  };
  tryHide();
  viewer.addEventListener(Autodesk.Viewing.EXTENSION_LOADED_EVENT, (e) => {
    if (e.extensionId === "Autodesk.ViewCubeUi") tryHide();
  });
}
function waitObjectTree(model) {
  return new Promise((resolve) => {
    if (model.getData().instanceTree) return resolve();
    const onTree = () => {
      viewer.removeEventListener(
        Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT,
        onTree
      );
      resolve();
    };
    viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, onTree);
  });
}

/* ---------------------- 依名稱解析樓層 dbId ---------------------- */
function findTopNodeByTest(model, tester) {
  const tree = model.getInstanceTree?.();
  if (!tree) return null;
  const root = tree.getRootId();
  let hit = null;
  tree.enumNodeChildren(root, (id) => {
    if (hit != null) return;
    const name = String(tree.getNodeName?.(id) || "");
    if (tester(name)) hit = id;
  });
  return hit;
}
function findDbIdByNameUnderExact(model, rootDbId, targetName) {
  const tree = model.getInstanceTree?.();
  if (!tree || rootDbId == null) return null;
  const target = String(targetName).trim().toUpperCase();
  let hit = null;
  const walk = (id) => {
    if (hit != null) return;
    const name = String(tree.getNodeName?.(id) || "")
      .trim()
      .toUpperCase();
    if (name === target) {
      hit = id;
      return;
    }
    tree.enumNodeChildren(id, walk);
  };
  walk(rootDbId);
  return hit;
}
function findDbIdByNameUnder(model, rootDbId, tester) {
  const tree = model.getInstanceTree?.();
  if (!tree || rootDbId == null) return null;
  let hit = null;
  const walk = (id) => {
    if (hit != null) return;
    const name = String(tree.getNodeName?.(id) || "");
    if (tester(name)) {
      hit = id;
      return;
    }
    tree.enumNodeChildren(id, walk);
  };
  walk(rootDbId);
  return hit;
}
function getNodeNameById(model, dbId) {
  const tree = model.getInstanceTree?.();
  return tree ? String(tree.getNodeName?.(dbId) || "") : "";
}
function resolveFloorDbIdsFromTree(model) {
  const arc1Root = findTopNodeByTest(model, (n) => /\b1F\.nwc\b/i.test(n));
  const arc2Root = findTopNodeByTest(model, (n) => /\b2F\.nwc\b/i.test(n));

  const id1Exact = arc1Root
    ? findDbIdByNameUnderExact(model, arc1Root, "1F(360)")
    : null;
  const id1Like =
    !id1Exact && arc1Root
      ? findDbIdByNameUnder(model, arc1Root, (n) =>
          /(?:^|[^A-Z0-9])1F\s*\(360\)(?:[^A-Z0-9]|$)/i.test(String(n || ""))
        )
      : null;

  const id2Exact = arc2Root
    ? findDbIdByNameUnderExact(model, arc2Root, "2F(360)")
    : null;
  const id2Like =
    !id2Exact && arc2Root
      ? findDbIdByNameUnder(model, arc2Root, (n) =>
          /(?:^|[^A-Z0-9])2F\s*\(360\)(?:[^A-Z0-9]|$)/i.test(String(n || ""))
        )
      : null;

  FLOOR_DBIDS["1F"] = id1Exact ?? id1Like ?? null;
  FLOOR_DBIDS["2F"] = id2Exact ?? id2Like ?? null;

  if (FLOOR_DBIDS["1F"] != null)
    console.log(
      "[Forge] 1F 命中名稱 =",
      getNodeNameById(model, FLOOR_DBIDS["1F"])
    );
  else console.warn("[Forge] 未在 1F.nwc 子樹找到 1F(360) 節點");
  if (FLOOR_DBIDS["2F"] != null)
    console.log(
      "[Forge] 2F 命中名稱 =",
      getNodeNameById(model, FLOOR_DBIDS["2F"])
    );
  else console.warn("[Forge] 未在 2F.nwc 子樹找到 2F(360) 節點");

  console.log("[Forge] 解析樓層 dbIds：", FLOOR_DBIDS);
}

/* ---------------------- 名稱搜尋 ---------------------- */
function collectDbIdsByNameUnder(model, rootDbId, keyword) {
  const tree = model.getInstanceTree?.();
  if (!tree) return [];
  const out = [];
  const walk = (id) => {
    const name = tree.getNodeName ? tree.getNodeName(id) : "";
    if (typeof name === "string" && name.includes(keyword)) out.push(id);
    tree.enumNodeChildren(id, walk);
  };
  walk(rootDbId);
  return out;
}

/* ---------------------- 幾何工具（單一定義） ---------------------- */
function getNodeWorldBounds(model, dbId) {
  const THREE = getThree();
  const tree = model.getInstanceTree?.();
  const fragList = model.getFragmentList?.();
  const box = new THREE.Box3();
  if (!tree || !fragList) return box;
  tree.enumNodeFragments(
    dbId,
    (fragId) => {
      const fb = new THREE.Box3();
      fragList.getWorldBounds(fragId, fb);
      box.union(fb);
    },
    true
  );
  return box;
}

/* ---------------------- 找「房間」容器與掃描 ---------------------- */
function code3ForFloor(name, floorKey) {
  const m = String(name || "").match(/\b(\d{3})\b/);
  if (!m) return null;
  const code = m[1];
  const wantPrefix = String(floorKey || "").charAt(0); // '1' or '2'
  if (wantPrefix && code.charAt(0) !== wantPrefix) return null;
  if (code === "360") return null; // 排除 1F(360)/2F(360) 之類
  return code;
}
function findRoomContainerId(model, floorRootId) {
  const tree = model.getInstanceTree?.();
  if (!tree || floorRootId == null) return null;

  const candidates = [];
  const walk = (id) => {
    const name = String(tree.getNodeName?.(id) || "").trim();
    const looksLikeRoomGroup = /房|病房|room|ROOM/i.test(name);
    if (looksLikeRoomGroup) {
      const children = [];
      tree.enumNodeChildren(id, (cid) => children.push(cid));
      const numericKids = children.filter((cid) =>
        /\b\d{3}\b/.test(String(tree.getNodeName?.(cid) || ""))
      );
      candidates.push({
        id,
        total: children.length,
        numeric: numericKids.length,
      });
    }
    tree.enumNodeChildren(id, walk);
  };
  walk(floorRootId);

  if (!candidates.length) return null;

  candidates.sort((a, b) => b.numeric - a.numeric || b.total - a.total);
  const hit = candidates[0];

  if (!hit || hit.numeric < 4) return null; // 不可靠 → 改用掃描
  return hit.id;
}
function scanRoomsUnderFloor(model, floorRootId, floorKey) {
  const tree = model.getInstanceTree?.();
  if (!tree || floorRootId == null) return [];
  const out = [];
  const seen = new Set();
  const walk = (id) => {
    const name = String(tree.getNodeName?.(id) || "");
    let has = false;
    tree.enumNodeFragments(
      id,
      () => {
        has = true;
      },
      true
    );
    const code3 = code3ForFloor(name, floorKey);
    if (code3 && has) {
      if (!seen.has(id)) {
        out.push({ id, name, code3 });
        seen.add(id);
      }
      return;
    }
    tree.enumNodeChildren(id, walk);
  };
  walk(floorRootId);
  return out;
}
function roomCodeOf(name) {
  const m = String(name || "").match(/\b(\d{3})\b/);
  return m ? m[1] : null;
}
function collectRoomsForFloor(model, floorKey, floorRootId) {
  const tree = model.getInstanceTree?.();
  if (!tree || floorRootId == null) return [];

  const containerId = findRoomContainerId(model, floorRootId);

  let roomNodes = [];
  if (containerId != null) {
    const children = [];
    tree.enumNodeChildren(containerId, (id) => children.push(id));
    roomNodes = children
      .map((cid) => {
        const n = String(tree.getNodeName?.(cid) || "");
        const code3 = code3ForFloor(n, floorKey);
        if (!code3) return null;
        let has = false;
        tree.enumNodeFragments(
          cid,
          () => {
            has = true;
          },
          true
        );
        return has ? { id: cid, name: n, code3 } : null;
      })
      .filter(Boolean);
  }
  if (!roomNodes.length) {
    roomNodes = scanRoomsUnderFloor(model, floorRootId, floorKey);
  }
  if (!roomNodes.length) {
    console.warn("[Forge] 此層未找到任何符合 3 碼房號的節點");
    return [];
  }

  const rooms = roomNodes.map(({ id, name, code3 }) => {
    const box = getNodeWorldBounds(model, id);
    return { id, name, code3, box, gender: null };
  });

  const sets = FIXED_ROOM_GENDER[floorKey] || {};
  const maleSet = new Set(sets.male || []);
  const femaleSet = new Set(sets.female || []);
  rooms.forEach((r) => {
    if (maleSet.has(r.code3)) r.gender = "男";
    if (femaleSet.has(r.code3)) r.gender = "女";
  });

  const total = rooms.length;
  const targetMale = Math.floor(total / 2);
  const targetFemale = total - targetMale;

  let maleCount = rooms.filter((r) => r.gender === "男").length;
  let femaleCount = rooms.filter((r) => r.gender === "女").length;

  const unresolved = rooms
    .filter((r) => !r.gender)
    .sort((a, b) => (a.code3 || "").localeCompare(b.code3 || ""));

  for (const r of unresolved) {
    if (maleCount < targetMale && femaleCount < targetFemale) {
      if (maleCount <= femaleCount) {
        r.gender = "男";
        maleCount++;
      } else {
        r.gender = "女";
        femaleCount++;
      }
    } else if (maleCount < targetMale) {
      r.gender = "男";
      maleCount++;
    } else if (femaleCount < targetFemale) {
      r.gender = "女";
      femaleCount++;
    } else {
      r.gender = "男";
      maleCount++;
    }
  }

  console.table(
    rooms.map((r) => ({ floor: floorKey, code: r.code3, gender: r.gender }))
  );

  // === 對半分區（一般/氧氣），兩組不重疊 ===
  ROOM_ZONE_MAP[floorKey].clear();
  const sorted = [...rooms]
    .filter((r) => r.code3) // 只分配有 3 碼房號的
    .sort((a, b) => String(a.code3).localeCompare(String(b.code3)));

  // 偶數位 -> 一般；奇數位 -> 氧氣（確保不重疊、幾乎對半）
  sorted.forEach((r, idx) => {
    ROOM_ZONE_MAP[floorKey].set(r.code3, idx % 2 === 0 ? "一般" : "氧氣");
  });

  return rooms;
}

/* ---------------------- Box 幫手 & 由床盒對應到房 ---------------------- */
function expandBox(box, eps = 0.05) {
  const THREE = getThree();
  const b = box.clone();
  b.min.sub(new THREE.Vector3(eps, eps, eps));
  b.max.add(new THREE.Vector3(eps, eps, eps));
  return b;
}
function resolveRoomByBox(rooms, bedBox) {
  if (!rooms.length) return null;
  const eb = expandBox(bedBox, 0.02);

  for (const r of rooms) {
    const rb = expandBox(r.box, 0.02);
    if (rb.intersectsBox(eb) || rb.containsBox(eb)) return r;
  }

  const THREE = getThree();
  const c = new THREE.Vector3();
  bedBox.getCenter(c);
  let best = null,
    bestD2 = Infinity;
  for (const r of rooms) {
    const rc = new THREE.Vector3();
    r.box.getCenter(rc);
    const dx = c.x - rc.x,
      dy = c.y - rc.y;
    const d2 = dx * dx + dy * dy;
    if (d2 < bestD2) {
      bestD2 = d2;
      best = r;
    }
  }
  return best;
}

/* ---------------------- 分區 ---------------------- */
const ZONE_BY_NAME = { 一般: ["101", "102"], 氧氣: ["103", "105"] };

function judgeZoneByName(roomName) {
  const code = roomCodeOf(roomName); // 抓 3 碼房號
  if (!code) return null;

  // 依房號前綴推回樓層（1xx -> 1F、2xx -> 2F），預設用當前樓層
  const floorKey = code.startsWith("1")
    ? "1F"
    : code.startsWith("2")
    ? "2F"
    : activeFloor.value;

  return getZoneForRoom(floorKey, code);
}

let lastThemedIds = new Set();
function clearOurTheming(model) {
  try {
    viewer.clearThemingColors(model);
  } catch (e) {
    console.warn("[Forge] clearThemingColors 失敗", e);
  }
  lastThemedIds.clear();
}
function setRoomTheming(id, rgba) {
  const THREE = getThree();
  const c = new THREE.Vector4(rgba.r, rgba.g, rgba.b, rgba.a);
  viewer.setThemingColor(id, c, viewer.model, true);
  lastThemedIds.add(id);
}

function rebuildZoneThemingForCurrentFloor() {
  if (!viewer || !viewer.model) return;

  // 先清乾淨我們加過的房間主題色（不動 sprites）
  clearOurTheming(viewer.model);

  // 沒選任何分區＝顯示全部（不上主題色）
  if (selectedZones.value.size === 0) {
    viewer.impl.invalidate(true);
    return;
  }

  const floorKey = activeFloor.value; // "1F" | "2F"
  const floorRootId = FLOOR_DBIDS[floorKey];
  if (floorRootId == null) return;

  const tree = viewer.model.getInstanceTree?.();
  if (!tree) return;

  // --- 幫手：從名稱抓 3 碼房號，且需符合該樓層前綴 ---
  const code3For = (name) => {
    const m = String(name || "").match(/\b(\d{3})\b/);
    if (!m) return null;
    const code = m[1];
    if (code.charAt(0) !== floorKey.charAt(0)) return null; // 1xx/2xx
    if (code === "360") return null; // 排除 1F(360)/2F(360)
    return code;
  };

  // --- 找出本層所有「房間」節點（需有幾何） ---
  let roomsGroupId =
    findDbIdByNameUnderExact(viewer.model, floorRootId, "房間") ??
    findDbIdByNameUnder(viewer.model, floorRootId, (n) =>
      /房間/i.test(String(n || ""))
    );

  const rooms = [];
  const seen = new Set();
  const pushIfRoom = (id) => {
    const name = String(tree.getNodeName?.(id) || "");
    let has = false;
    tree.enumNodeFragments(
      id,
      () => {
        has = true;
      },
      true
    );
    if (!has) return;
    const code = code3For(name);
    if (!code) return;
    if (!seen.has(id)) {
      rooms.push({ id, name, code3: code, codeN: parseInt(code, 10) });
      seen.add(id);
    }
  };

  if (roomsGroupId != null) {
    tree.enumNodeChildren(roomsGroupId, (cid) => pushIfRoom(cid));
  } else {
    const walk = (id) => {
      pushIfRoom(id);
      tree.enumNodeChildren(id, walk);
    };
    walk(floorRootId);
  }

  if (!rooms.length) {
    console.warn(`[Forge] ${floorKey} 未找到任何房間節點，無法分區上色`);
    viewer.impl.invalidate(true);
    return;
  }

  // --- 依「數值」排序（避免字串排序陷阱） ---
  const sortedRooms = [...rooms].sort((a, b) => a.codeN - b.codeN);

  // --- 重新建立本層分區表（每次重建，避免舊 Map 汙染） ---
  const zoneMap = new Map();
  // ① 主要策略：交錯分配（偶數→一般；奇數→氧氣）
  sortedRooms.forEach((r, idx) => {
    zoneMap.set(r.code3, idx % 2 === 0 ? "一般" : "氧氣");
  });

  // ② 驗證：若不小心全落同組（理論上不會，但保險）
  const cntGeneralA = sortedRooms.reduce(
    (n, r, i) => n + (zoneMap.get(r.code3) === "一般"),
    0
  );
  const cntO2A = sortedRooms.length - cntGeneralA;

  if (cntGeneralA === 0 || cntO2A === 0) {
    // ③ 後備策略：前半「一般」、後半「氧氣」
    zoneMap.clear();
    const half = Math.floor(sortedRooms.length / 2);
    sortedRooms.forEach((r, idx) => {
      zoneMap.set(r.code3, idx < half ? "一般" : "氧氣");
    });
  }

  // 存回全域對應表
  ROOM_ZONE_MAP[floorKey] = zoneMap;

  // --- 套用主題色（只對被勾選的分區著色） ---
  const chosen = selectedZones.value; // Set{"一般","氧氣"}
  let countGeneral = 0,
    countO2 = 0;

  for (const r of sortedRooms) {
    const z = zoneMap.get(r.code3);
    if (!z || !chosen.has(z)) continue;
    if (z === "一般") {
      setRoomTheming(r.id, THEME_PURPLE);
      countGeneral++;
    } else if (z === "氧氣") {
      setRoomTheming(r.id, THEME_YELLOW);
      countO2++;
    }
  }

  // ---- 除錯輸出 ----
  console.table(
    sortedRooms.map((r, i) => ({
      floor: floorKey,
      idx: i,
      code: r.code3,
      zone: zoneMap.get(r.code3),
    }))
  );
  console.log(
    `[Forge] ${floorKey} theming -> 一般:${countGeneral} | 氧氣:${countO2} | total:${
      sortedRooms.length
    } | chosen=${[...chosen].join(",")}`
  );

  viewer.impl.invalidate(true, true, true);
}

/* ---------------------- Sprites 顏色規則 ---------------------- */
function spriteColorBy(status, gender) {
  // 在院內：依性別著色；不在院內(含空床/住院/請假)：白色
  if (status === "occupied") return gender === "男" ? BRAND_GREEN : BRAND_RED;
  return BRAND_WHITE;
}
function labelDotColor(L) {
  const s = L?.data?.status;
  const g = L?.data?.residentsSex;
  if (s === "occupied") return g === "男" ? BRAND_GREEN : BRAND_RED;
  // 空床 / 住院中 / 請假中 → 灰色 dot
  return BRAND_GRAY;
}

/* ---------------------- 樓層顯示（隔離） ---------------------- */
function collectLeafsUnder(model, rootDbId) {
  const tree = model.getInstanceTree();
  const leafs = [];
  const walk = (id) => {
    const c = tree.getChildCount(id);
    if (c === 0) leafs.push(id);
    else tree.enumNodeChildren(id, walk);
  };
  walk(rootDbId);
  return leafs.length ? leafs : [rootDbId];
}
function allLeafIds(model) {
  const tree = model.getInstanceTree();
  const root = tree.getRootId();
  const out = [];
  const walk = (id) => {
    const c = tree.getChildCount(id);
    if (c === 0) out.push(id);
    else tree.enumNodeChildren(id, walk);
  };
  walk(root);
  return out;
}
async function showLevel(levelKey) {
  if (!viewer || !viewer.model) return;
  const levelDbId = FLOOR_DBIDS[levelKey];
  if (levelDbId == null) {
    console.warn(`[Forge] 找不到樓層 '${levelKey}' 對應的 dbId，改為顯示全景`);
    viewer.showAll();
    viewer.fitToView();
    viewer.impl.invalidate(true);
    return;
  }
  const ids = collectLeafsUnder(viewer.model, levelDbId);
  viewer.showAll();
  try {
    if (typeof viewer.setAggregateIsolation === "function") {
      viewer.setAggregateIsolation([{ model: viewer.model, ids }]);
      viewer.fitToView?.(ids, viewer.model);
    } else {
      viewer.isolate(ids, viewer.model);
      viewer.fitToView?.(ids, viewer.model);
    }
    viewer.setGhosting(false);
    viewer.impl.invalidate(true);
  } catch (e) {
    console.warn("[Forge] aggregate isolate 失敗，改用 hide-rest 方案", e);
    const all = new Set(allLeafIds(viewer.model));
    ids.forEach((id) => all.delete(id));
    const toHide = Array.from(all);
    viewer.showAll();
    toHide.forEach((id) => viewer.hide(id));
    viewer.fitToView?.(ids, viewer.model);
    viewer.setGhosting(false);
    viewer.impl.invalidate(true);
  }
}

/* ---------------------- 假資料生成 ---------------------- */
function seededRand(seed) {
  const s = Math.sin(seed * 9301 + 49297) * 233280;
  return s - Math.floor(s);
}
function pick(arr, seed) {
  return arr[Math.floor(seededRand(seed) * arr.length)];
}
function ageFromSeed(seed) {
  return 65 + Math.floor(seededRand(seed + 7) * 31);
}
function startDateFromSeed(seed) {
  const now = new Date();
  const daysAgo = 30 + Math.floor(seededRand(seed + 13) * 365);
  const d = new Date(now.getTime() - daysAgo * 86400000);
  const mm = String(d.getMonth() + 1).padStart(2, "0");
  const dd = String(d.getDate()).padStart(2, "0");
  return `${d.getFullYear()}/${mm}/${dd}`;
}

const NAME_POOL_MALE = [
  "王建中",
  "張家豪",
  "劉俊傑",
  "黃柏翰",
  "吳宗翰",
  "蔡宗倫",
  "楊偉傑",
  "何彥廷",
  "邱柏宇",
  "鄭智凱",
  "陳志明",
  "林冠廷",
  "李建宏",
  "周育賢",
  "許庭瑋",
  "郭子豪",
  "曾博翔",
  "簡志豪",
  "蘇冠宇",
  "謝承翰",
  "羅庭瑋",
  "傅宥辰",
  "潘柏睿",
  "洪偉倫",
];

const NAME_POOL_FEMALE = [
  "王雅雯",
  "張淑芬",
  "黃子涵",
  "吳佩珊",
  "趙雅筑",
  "郭怡君",
  "洪嘉文",
  "陳怡君",
  "林怡婷",
  "李佳穎",
  "周品萱",
  "徐庭瑄",
  "鄭雅婷",
  "劉郁涵",
  "楊靜怡",
  "許淑華",
  "蔡佩玲",
  "何依婷",
  "邱雅筑",
  "曾婉婷",
  "簡詩涵",
  "謝采靈",
  "羅琬淳",
  "潘俞安",
];

const HEALTH_POOL = ["一般", "良好", "需觀察", "虛弱"];
const MEDICATION_POOL = ["規律服藥", "偶爾忘記", "暫停服藥", "需家屬協助"];
const NOTE_POOL = ["-", "對花生過敏", "家屬每週三探視", "夜間需加護理巡房"];

// 依種子回傳床位狀態（可調整機率）
function bedStatusFromSeed(seed) {
  const r = seededRand(seed + 17);
  // 分佈：佔床 62%｜住院 10%｜請假 8%｜空床 20%
  if (r < 0.62) return "occupied";
  if (r < 0.72) return "hospitalized";
  if (r < 0.8) return "leave";
  return "vacant";
}

/* ---------------------- 依樓層建立 sprites（佔床=性別色；不在院內=白） ---------------------- */
async function rebuildSpritesForFloor(floorKey) {
  const THREE = getThree();
  const floorRootId = FLOOR_DBIDS[floorKey];
  if (floorRootId == null) {
    console.warn("[Forge] 此樓層沒有對應的 root dbId：", floorKey);
    __staticDevices.length = 0;
    await createSprites();
    return;
  }

  // 1) 收集此層「所有房」並決定性別（平均對半）
  const ROOMS = collectRoomsForFloor(viewer.model, floorKey, floorRootId);

  // 2) 找「【崇恩護家】床位」群組，再拉出每一張床
  const BED_GROUP_KEYWORD = "【崇恩護家】床位";
  const bedGroupIds = collectDbIdsByNameUnder(
    viewer.model,
    floorRootId,
    BED_GROUP_KEYWORD
  );

  let bedNodes = [];
  bedGroupIds.forEach((groupId) => {
    const children = getDirectChildren(viewer.model, groupId);
    const directBeds = children.filter((id) =>
      nodeHasFragments(viewer.model, id)
    );
    if (directBeds.length) {
      bedNodes.push(...directBeds);
    } else {
      children.forEach((cId) => {
        const grand = getDirectChildren(viewer.model, cId);
        bedNodes.push(
          ...grand.filter((id) => nodeHasFragments(viewer.model, id))
        );
      });
    }
  });
  bedNodes = Array.from(new Set(bedNodes));

  const floorBox = getNodeWorldBounds(viewer.model, floorRootId);

  __staticDevices.length = 0;
  let globalIndex = 0;

  for (const dbId of bedNodes) {
    const bedBox = getNodeWorldBounds(viewer.model, dbId);
    if (bedBox.isEmpty()) continue;

    const c = new (getThree().Vector3)();
    bedBox.getCenter(c);
    const zOk = c.z >= floorBox.min.z - 0.5 && c.z <= floorBox.max.z + 0.5;
    if (!zOk) continue;

    const lift = Math.max(0.25, (bedBox.max.z - bedBox.min.z) * 0.2);
    const pos = new THREE.Vector3(c.x, c.y, bedBox.max.z + lift);

    // 用床的 box 對到房；若失敗，用交錯 fallback，避免全回同色
    const roomMatch = resolveRoomByBox(ROOMS, bedBox);
    const fallbackGen = globalIndex % 2 === 0 ? "男" : "女";
    const roomGender = roomMatch?.gender ?? fallbackGen;
    const roomLabel = roomMatch?.name ?? "?";

    // 四態：occupied / hospitalized / leave / vacant
    const spriteDbId = (floorKey === "1F" ? 91100 : 92100) + globalIndex++;
    const status = bedStatusFromSeed(spriteDbId);

    const hasResident = status !== "vacant"; // 住院/請假仍有住民資訊
    const color = spriteColorBy(status, roomGender);

    // 假資料（有住民才生成）
    const namePool = roomGender === "男" ? NAME_POOL_MALE : NAME_POOL_FEMALE;
    const residentsName = hasResident ? pick(namePool, spriteDbId) : "-";
    const residentsAge = hasResident ? ageFromSeed(spriteDbId) : "-";
    const startTime = hasResident ? startDateFromSeed(spriteDbId) : "-";
    const healthStatus = hasResident ? pick(HEALTH_POOL, spriteDbId + 5) : "-";
    const medicationStatus = hasResident
      ? pick(MEDICATION_POOL, spriteDbId + 6)
      : "-";
    const note = hasResident ? pick(NOTE_POOL, spriteDbId + 8) : "-";

    const zone =
      getZoneForRoom(floorKey, roomMatch?.code3 ?? roomCodeOf(roomLabel)) ??
      null;

    __staticDevices.push({
      name: `床號 ${roomLabel}`,
      forge_dbid: floorRootId,
      spriteDbId,
      device_coordinate_3d: { x: pos.x, y: pos.y, z: pos.z },
      device_normal_color: color,
      device_close_color: color,
      status, // "occupied" | "hospitalized" | "leave" | "vacant"

      // 房間性別（不論是否有住民都存在）
      roomGender: roomGender,

      // 住民資料（vacant 時維持 "-"）
      residentsName,
      residentsSex: hasResident ? roomGender : "-",
      residentsAge,
      startTime,
      healthStatus,
      medicationStatus,
      specialEvent:
        status === "hospitalized"
          ? "住院中"
          : status === "leave"
          ? "請假中"
          : "-",
      note,
      zone,
    });
  }

  await createSprites(); // 每張床都有 sprite；不在院內者＝白色
}

/* ---------------------- Popover（跟相機更新） ---------------------- */
const labels = ref([]);
let camEvtCleanup = null;

function worldToScreen(pos3) {
  const p = viewer.worldToClient(pos3);
  return { x: p.x, y: p.y };
}
function rebuildLabelsForCurrentFloor() {
  labels.value = __staticDevices.map((d) => {
    const { x, y } = worldToScreen(
      new (getThree().Vector3)(
        d.device_coordinate_3d.x,
        d.device_coordinate_3d.y,
        d.device_coordinate_3d.z
      )
    );
    return { id: d.spriteDbId, x, y, data: d };
  });
}
function updateLabelPositions() {
  if (!labels.value.length) return;
  labels.value = labels.value.map((L) => {
    const { x, y } = worldToScreen(
      new (getThree().Vector3)(
        L.data.device_coordinate_3d.x,
        L.data.device_coordinate_3d.y,
        L.data.device_coordinate_3d.z
      )
    );
    return { ...L, x, y };
  });
}
function attachCameraEventsForLabels() {
  camEvtCleanup?.();
  const onUpdate = () => requestAnimationFrame(updateLabelPositions);
  viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, onUpdate);
  viewer.addEventListener(Autodesk.Viewing.RENDER_PRESENTED_EVENT, onUpdate);
  viewer.addEventListener(Autodesk.Viewing.VIEWER_RESIZE_EVENT, onUpdate);
  camEvtCleanup = () => {
    viewer.removeEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, onUpdate);
    viewer.removeEventListener(
      Autodesk.Viewing.RENDER_PRESENTED_EVENT,
      onUpdate
    );
    viewer.removeEventListener(Autodesk.Viewing.VIEWER_RESIZE_EVENT, onUpdate);
  };
}
function rebuildLabelsAfterNextRender() {
  const once = () => {
    rebuildLabelsForCurrentFloor();
    updateLabelPositions();
    viewer.removeEventListener(Autodesk.Viewing.RENDER_PRESENTED_EVENT, once);
  };
  viewer.addEventListener(Autodesk.Viewing.RENDER_PRESENTED_EVENT, once);
}

/* ---------------------- 分區選擇（多選） ---------------------- */
const zones = ["一般", "氧氣"];
const selectedZones = ref(new Set());
const toggleZone = (zone) => {
  const set = new Set(selectedZones.value);
  if (set.has(zone)) set.delete(zone);
  else set.add(zone);
  selectedZones.value = set;
  rebuildZoneThemingForCurrentFloor();
};
const isZoneSelected = (zone) => selectedZones.value.has(zone);

/* ---------------------- 床位資訊 選單（只影響 Popover，不影響 sprites） ---------------------- */
const soloSpriteId = ref(null);
const infoOptions = [
  { label: "無顯示", value: "none" },
  { label: "有住民", value: "occupied" },
  { label: "空床", value: "vacant" },
  { label: "住院中", value: "hospitalized" },
  { label: "請假中", value: "leave" },
];
const infoOpen = ref(false);
const selectedInfo = ref(infoOptions[0].value);

const getFloorBtnClass = (val) => {
  const isActive = activeFloor.value === val;
  return [
    // 共同樣式（不含固定 hover）
    "px-6 py-2 rounded-md",
    // 依狀態切換底色
    isActive ? "bg-brand-green-light" : "bg-white",
    // 依狀態切換 hover 顏色
    isActive ? "hover:bg-brand-green-light" : "hover:bg-gray-100",
  ];
};

const getInfoBtnClass = (val) => {
  const isActive = selectedInfo.value === val;
  return [
    // 共同樣式（不含 hover，避免與動態 hover 類別打架）
    "inline-flex items-center gap-2 px-4 py-3 rounded-md",
    // 依狀態切換底色/透明度
    isActive ? "bg-brand-green-light bg-opacity-50" : "bg-white",
    // 依狀態切換 hover 顏色（此處避免固定放 hover:bg-gray-100）
    isActive
      ? "hover:bg-brand-green-light hover:bg-opacity-50"
      : "hover:bg-gray-100",
  ];
};

const getZoneBtnClass = (zone) => {
  const isActive = isZoneSelected(zone);
  return [
    // 共同樣式（不含固定 hover，避免撞到動態 hover）
    "px-4 py-2 rounded-md",
    // 依狀態切換底色/透明度
    isActive ? "bg-brand-green-light bg-opacity-50" : "bg-white",
    // 依狀態切換 hover 類別
    isActive
      ? "hover:bg-brand-green-light hover:bg-opacity-50"
      : "hover:bg-gray-100",
  ];
};

const filteredLabels = computed(() => {
  if (soloSpriteId.value != null) {
    if (selectedInfo.value === "none") return [];
    const L = labels.value.find((L) => L.id === soloSpriteId.value);
    if (!L) return [];
    return selectedInfo.value === L.data.status ? [L] : [];
  }
  if (selectedInfo.value === "none") return [];
  const selectedSet = selectedZones.value;
  return labels.value.filter((L) => {
    const inZone =
      selectedSet.size === 0
        ? true
        : L.data?.zone
        ? selectedSet.has(L.data.zone)
        : true;
    return inZone && L.data.status === selectedInfo.value;
  });
});
const isVacantWithoutSpecial = (L) => L?.data?.status === "vacant";

const emit = defineEmits(["change"]);
const selectInfo = (opt) => {
  soloSpriteId.value = null;
  selectedInfo.value = opt.value;
  infoOpen.value = false;
  emit("change", opt.value);
};

const infoTriggerRef = ref(null);
const infoPanelRef = ref(null);
const onClickOutsideInfo = (e) => {
  const t = e.target;
  if (!infoTriggerRef.value || !infoPanelRef.value) return;
  const insideTrigger = infoTriggerRef.value.contains(t);
  const insidePanel = infoPanelRef.value.contains(t);
  if (!insideTrigger && !insidePanel) infoOpen.value = false;
};
const onKeydownInfo = (e) => {
  if (e.key === "Escape") infoOpen.value = false;
};

/* ---------------------- 套用樓層/切換 ---------------------- */
async function applyFloor(next) {
  soloSpriteId.value = null;
  activeFloor.value = next;
  localStorage.setItem("uark-floor", next);
  await showLevel(next);
  await rebuildSpritesForFloor(next);
  viewer.impl.invalidate(true);
  rebuildLabelsAfterNextRender();
  rebuildZoneThemingForCurrentFloor();
}
async function onClickFloor(next) {
  if (next === activeFloor.value) return;
  if (!viewerReady.value) return;
  await applyFloor(next);
}

/* ---------------------- 生命週期 ---------------------- */
onMounted(async () => {
  activeFloor.value = resolveInitialFloor();

  await initViewer(forgeDom.value);
  const model = await loadModel(MODEL_SVF_PATH);
  await waitObjectTree(model);
  hideViewCubeAndHome();

  await dvInit(viewer);
  resolveFloorDbIdsFromTree(model);

  unbindSpriteClick = forgeClickListener(async ({ data }) => {
    soloSpriteId.value =
      soloSpriteId.value === data.spriteDbId ? null : data.spriteDbId;
    bringToFrontById(data.spriteDbId);
    await cardfitToView({
      forge_dbid: data.forge_dbid,
      spriteDbId: data.spriteDbId,
      back: 30,
    });
  });
  attachCameraEventsForLabels();

  await applyFloor("1F");
  viewerReady.value = true;
});
onMounted(() => {
  document.addEventListener("click", onClickOutsideInfo);
  document.addEventListener("keydown", onKeydownInfo);
});
watch(
  () => route.query?.floor,
  async (v) => {
    if (!viewerReady.value) return;
    const q = (v || "").toString().toUpperCase();
    if (q === "1F" || q === "2F") await applyFloor(q);
    attachCameraEventsForLabels();
  }
);
onUnmounted(() => {
  try {
    clearOurTheming(viewer.model);
  } catch {}
  try {
    unbindSpriteClick?.();
    camEvtCleanup?.();
  } catch {}
  try {
    clearSprites();
  } catch {}
  if (viewer) {
    try {
      const ext = viewer.getExtension?.("Autodesk.DataVisualization");
      ext?.removeAllViewables?.();
      viewer.unloadExtension?.("Autodesk.DataVisualization");
    } catch {}
    viewer.tearDown?.();
    viewer.finish?.();
    viewer = null;
  }
  document.removeEventListener("click", onClickOutsideInfo);
  document.removeEventListener("keydown", onKeydownInfo);
});
</script>

<style>
.adsk-viewing-viewer {
  background-color: transparent !important;
}
#guiviewer3d-toolbar {
  display: none;
}
.adsk-viewcube {
  display: none !important;
}
</style>
