const statusEl = document.getElementById("status");
const canvas = document.getElementById("scene-canvas");

function setStatus(message, level = "warn") {
  if (!statusEl) return;
  statusEl.textContent = message;
  statusEl.className = `status ${level}`;
}

function rowMajorToMatrix4(THREE, m) {
  const mat = new THREE.Matrix4();
  mat.set(
    m[0], m[1], m[2], m[3],
    m[4], m[5], m[6], m[7],
    m[8], m[9], m[10], m[11],
    m[12], m[13], m[14], m[15],
  );
  return mat;
}

function pandaRowMajorToMatrix4(THREE, m) {
  const mat = new THREE.Matrix4();
  // Panda's matrix data uses row-vector convention (translation on last row).
  // Three.js expects column-vector convention (translation on last column).
  mat.set(
    m[0], m[4], m[8], m[12],
    m[1], m[5], m[9], m[13],
    m[2], m[6], m[10], m[14],
    m[3], m[7], m[11], m[15],
  );
  return mat;
}

function convertNodeMatrix(THREE, sourceMatRowMajor, basis, basisInv, matrixConvention = "panda_row_vector_row_major") {
  const src = matrixConvention === "panda_row_vector_row_major"
    ? pandaRowMajorToMatrix4(THREE, sourceMatRowMajor)
    : rowMajorToMatrix4(THREE, sourceMatRowMajor);
  return basis.clone().multiply(src).multiply(basisInv);
}

function toColorArray(color, fallback = [1, 1, 1]) {
  if (!Array.isArray(color) || color.length < 3) return fallback;
  return [Number(color[0]) || 0, Number(color[1]) || 0, Number(color[2]) || 0];
}

function applyMaterialOverride(THREE, root, override) {
  if (!override) return;

  root.traverse((obj) => {
    if (!obj.isMesh || !obj.material) return;

    const list = Array.isArray(obj.material) ? obj.material : [obj.material];
    for (const mat of list) {
      if (mat.color && Array.isArray(override.base_color)) {
        const [r, g, b] = override.base_color;
        mat.color.setRGB(r ?? 1, g ?? 1, b ?? 1);
      }
      if (Object.prototype.hasOwnProperty.call(override, "roughness") && "roughness" in mat) {
        mat.roughness = Number(override.roughness);
      }
      if (Object.prototype.hasOwnProperty.call(override, "metallic") && "metalness" in mat) {
        mat.metalness = Number(override.metallic);
      }
      if (Object.prototype.hasOwnProperty.call(override, "opacity")) {
        const opacity = THREE.MathUtils.clamp(Number(override.opacity), 0, 1);
        const isTransparent = opacity < 0.999;
        mat.opacity = opacity;
        mat.transparent = isTransparent;
        // Prevent "see-through solid mesh" when source GLTF had transparent pipeline state.
        mat.depthWrite = !isTransparent;
        mat.depthTest = true;
        mat.blending = isTransparent ? THREE.NormalBlending : THREE.NoBlending;
        if (!isTransparent && "alphaTest" in mat) {
          mat.alphaTest = 0;
        }
        if (!isTransparent && "transmission" in mat) {
          mat.transmission = 0;
        }
      }
      mat.needsUpdate = true;
    }
  });
}

function textureSlotByStage(stageName) {
  const key = String(stageName || "").toLowerCase();
  if (key.includes("normal")) return "normalMap";
  if (key.includes("rough")) return "roughnessMap";
  if (key.includes("metal")) return "metalnessMap";
  if (key.includes("emission") || key.includes("emissive")) return "emissiveMap";
  if (key.includes("ao")) return "aoMap";
  if (key.includes("alpha") || key.includes("opacity")) return "alphaMap";
  return "map";
}

function applyTextureOverrides(THREE, root, textureOverrides, textureLoader) {
  if (!Array.isArray(textureOverrides) || textureOverrides.length === 0) return;

  const texBySlot = new Map();
  for (const item of textureOverrides) {
    if (!item || !item.uri) continue;
    const slot = textureSlotByStage(item.stage);
    if (texBySlot.has(slot)) continue;

    try {
      const tex = textureLoader.load(item.uri);
      tex.flipY = false;
      texBySlot.set(slot, tex);
    } catch (err) {
      console.warn("Texture load failed:", item.uri, err);
    }
  }

  if (texBySlot.size === 0) return;

  root.traverse((obj) => {
    if (!obj.isMesh || !obj.material) return;

    const list = Array.isArray(obj.material) ? obj.material : [obj.material];
    for (const mat of list) {
      for (const [slot, tex] of texBySlot.entries()) {
        if (slot in mat) {
          mat[slot] = tex;
        }
      }
      mat.needsUpdate = true;
    }
  });
}

function directionToThree(THREE, direction, basis) {
  const d = new THREE.Vector3(
    Number(direction?.[0] ?? 0),
    Number(direction?.[1] ?? 0),
    Number(direction?.[2] ?? -1),
  );
  d.applyMatrix4(basis);
  if (d.lengthSq() < 1e-6) d.set(0, 0, -1);
  return d.normalize();
}

async function bootstrap() {
  setStatus("Loading WebGL dependencies...");

  let THREE;
  let OrbitControls;
  let GLTFLoader;

  try {
    THREE = await import("../vendor/three.module.min.js");
    ({ OrbitControls } = await import("../vendor/OrbitControls.js"));
    ({ GLTFLoader } = await import("../vendor/GLTFLoader.js"));
  } catch (err) {
    setStatus(
      [
        "Failed to load local Three.js vendor files.",
        "Please replace vendor placeholders with official files:",
        "- vendor/three.module.min.js",
        "- vendor/OrbitControls.js",
        "- vendor/GLTFLoader.js",
        "",
        String(err),
      ].join("\n"),
      "error",
    );
    throw err;
  }

  setStatus("Loading scene manifest...");

  const response = await fetch("../scene/scene_webgl.json", { cache: "no-cache" });
  if (!response.ok) {
    throw new Error(`Failed to load scene manifest: HTTP ${response.status}`);
  }

  const data = await response.json();

  const renderer = new THREE.WebGLRenderer({
    canvas,
    antialias: true,
    alpha: false,
  });
  renderer.setPixelRatio(window.devicePixelRatio || 1);
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.outputColorSpace = THREE.SRGBColorSpace;

  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x11151c);

  const cameraData = data.camera || {};
  const camera = new THREE.PerspectiveCamera(
    Number(cameraData.fov_deg ?? 80),
    window.innerWidth / Math.max(1, window.innerHeight),
    Number(cameraData.near ?? 0.1),
    Number(cameraData.far ?? 10000),
  );

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

  const basis = rowMajorToMatrix4(
    THREE,
    Array.isArray(data.coordinate?.basis_matrix)
      ? data.coordinate.basis_matrix
      : [1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1],
  );
  const basisInv = basis.clone().invert();
  const matrixConvention = String(data.coordinate?.matrix_convention || "panda_row_vector_row_major");

  if (Array.isArray(cameraData.matrix_local_row_major) && cameraData.matrix_local_row_major.length === 16) {
    const camMat = convertNodeMatrix(
      THREE,
      cameraData.matrix_local_row_major,
      basis,
      basisInv,
      matrixConvention,
    );
    camera.matrix.copy(camMat);
    camera.matrix.decompose(camera.position, camera.quaternion, camera.scale);
    camera.matrixAutoUpdate = true;
    camera.updateMatrix();
  } else {
    camera.position.set(0, -50, 20);
    camera.lookAt(0, 0, 0);
  }

  const env = data.environment || {};

  if (env.ambient_light) {
    const c = toColorArray(env.ambient_light.color, [0.2, 0.2, 0.2]);
    const amb = new THREE.AmbientLight(new THREE.Color(c[0], c[1], c[2]), Number(env.ambient_light.intensity ?? 1));
    scene.add(amb);
  }

  if (env.directional_light) {
    const c = toColorArray(env.directional_light.color, [0.8, 0.8, 0.8]);
    const dirLight = new THREE.DirectionalLight(
      new THREE.Color(c[0], c[1], c[2]),
      Number(env.directional_light.intensity ?? 1),
    );

    const dir = directionToThree(THREE, env.directional_light.direction, basis);
    dirLight.position.copy(dir.clone().multiplyScalar(-40));
    dirLight.target.position.set(0, 0, 0);

    scene.add(dirLight);
    scene.add(dirLight.target);
  }

  const nodeMap = new Map();
  const pendingModelLoads = [];

  const gltfLoader = new GLTFLoader();
  const textureLoader = new THREE.TextureLoader();

  for (const node of Array.isArray(data.nodes) ? data.nodes : []) {
    let obj;

    if (node.kind === "point_light") {
      const light = node.light || {};
      const c = toColorArray(light.color, [1, 1, 1]);
      obj = new THREE.PointLight(
        new THREE.Color(c[0], c[1], c[2]),
        Number(light.intensity ?? 1),
        Number(light.range ?? 0),
      );
    } else if (node.kind === "spot_light") {
      const light = node.light || {};
      const c = toColorArray(light.color, [1, 1, 1]);
      const angle = THREE.MathUtils.degToRad(Number(light.spot_angle_deg ?? 45));
      const spot = new THREE.SpotLight(
        new THREE.Color(c[0], c[1], c[2]),
        Number(light.intensity ?? 1),
        Number(light.range ?? 0),
        angle,
        1 - Number(light.inner_cone_ratio ?? 0.4),
      );
      spot.target.position.set(0, 0, -1);
      obj = spot;
    } else if (node.kind === "ground") {
      const g = node.ground || {};
      const width = Number(g.width ?? 100);
      const height = Number(g.height ?? 100);
      const m = node.material_override || {};
      const bc = Array.isArray(m.base_color) ? m.base_color : [0.8, 0.8, 0.8, 1];
      const mat = new THREE.MeshStandardMaterial({
        color: new THREE.Color(Number(bc[0] ?? 0.8), Number(bc[1] ?? 0.8), Number(bc[2] ?? 0.8)),
        roughness: Number(m.roughness ?? 1),
        metalness: Number(m.metallic ?? 0),
        transparent: Number(m.opacity ?? 1) < 1,
        opacity: Number(m.opacity ?? 1),
        side: THREE.DoubleSide,
      });
      obj = new THREE.Mesh(new THREE.PlaneGeometry(width, height), mat);
      obj.receiveShadow = true;
    } else {
      obj = new THREE.Group();
      const modelUri = node.model?.uri;
      if (modelUri) {
        const p = new Promise((resolve) => {
          gltfLoader.load(
            modelUri,
            (gltf) => {
              const root = gltf.scene || (Array.isArray(gltf.scenes) ? gltf.scenes[0] : null);
              if (root) {
                applyMaterialOverride(THREE, root, node.material_override || null);
                applyTextureOverrides(THREE, root, node.texture_overrides || [], textureLoader);
                obj.add(root);
              }
              resolve();
            },
            undefined,
            (err) => {
              console.warn(`Failed to load model ${modelUri}:`, err);
              resolve();
            },
          );
        });
        pendingModelLoads.push(p);
      }
    }

    obj.name = node.name || node.id || "node";

    if (Array.isArray(node.matrix_local_row_major) && node.matrix_local_row_major.length === 16) {
      const converted = convertNodeMatrix(THREE, node.matrix_local_row_major, basis, basisInv, matrixConvention);
      obj.matrixAutoUpdate = false;
      obj.matrix.copy(converted);
      obj.matrix.decompose(obj.position, obj.quaternion, obj.scale);
    }

    nodeMap.set(node.id, obj);
  }

  for (const node of Array.isArray(data.nodes) ? data.nodes : []) {
    const obj = nodeMap.get(node.id);
    if (!obj) continue;

    const parent = node.parent_id ? nodeMap.get(node.parent_id) : null;
    if (parent) {
      parent.add(obj);
    } else {
      scene.add(obj);
    }

    if (obj.isSpotLight && obj.target) {
      obj.add(obj.target);
    }
  }

  await Promise.all(pendingModelLoads);

  const resize = () => {
    const w = window.innerWidth;
    const h = Math.max(1, window.innerHeight);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
    renderer.setSize(w, h);
  };

  window.addEventListener("resize", resize);
  resize();

  setStatus(
    `Scene ready. Nodes: ${(data.nodes || []).length}.\nUse mouse to orbit, wheel to zoom.`,
    "ok",
  );

  const clock = new THREE.Clock();
  const tick = () => {
    requestAnimationFrame(tick);
    const dt = clock.getDelta();
    if (dt >= 0) {
      controls.update();
      renderer.render(scene, camera);
    }
  };

  tick();
}

bootstrap().catch((err) => {
  console.error(err);
  setStatus(`Viewer bootstrap failed:\n${String(err)}`, "error");
});
