const nodes = [
  {
    id: "root",
    type: "root",
    title: "웹 개발",
    subtitle: "클릭해서 펼치기",
    image: "imgs/frontend_backend.png",
    category: "전체 개요",
    focus: "웹 개발이 프론트엔드, 백엔드, 통신, 데이터 흐름으로 나뉘는 큰 그림을 봅니다.",
    x: 1020,
    y: 700,
    children: ["structure", "frontend", "network", "backend", "workflow"],
  },
  {
    id: "structure",
    type: "category",
    title: "웹의 뼈대",
    subtitle: "브라우저와 화면 구성",
    image: "imgs/html_css.png",
    category: "웹의 뼈대",
    focus: "HTML, CSS, 브라우저, DOM이 화면을 구성하는 기본 흐름을 봅니다.",
    x: 1020,
    y: 420,
    children: ["browser", "html-css", "dom"],
  },
  {
    id: "frontend",
    type: "category",
    title: "프론트엔드",
    subtitle: "UI를 만드는 도구",
    image: "imgs/frontend_backend.png",
    category: "프론트엔드",
    focus: "사용자가 보는 화면과 백엔드가 만나는 경계를 먼저 파악합니다.",
    x: 1360,
    y: 560,
    children: ["javascript", "react", "front-back"],
  },
  {
    id: "network",
    type: "category",
    title: "통신",
    subtitle: "요청과 응답",
    image: "imgs/httpReq_Res.png",
    category: "통신",
    focus: "요청과 응답, 상태 코드, 헤더, 바디가 웹 통신에서 맡는 역할을 봅니다.",
    x: 1320,
    y: 900,
    children: ["auth", "cookie", "api", "http", "request-response"],
  },
  {
    id: "backend",
    type: "category",
    title: "백엔드·데이터",
    subtitle: "서버와 저장소",
    image: "imgs/database.png",
    category: "백엔드·데이터",
    focus: "서버가 요청을 처리하고 데이터를 저장·조회하는 큰 흐름을 봅니다.",
    x: 1020,
    y: 1030,
    children: ["web-server", "database", "postgres", "json"],
  },
  {
    id: "workflow",
    type: "category",
    title: "프레임워크·도구",
    subtitle: "개발 흐름",
    image: "imgs/nextJS.png",
    category: "프레임워크·도구",
    focus: "Next.js 같은 프레임워크가 라우팅, 렌더링, 개발 흐름을 어떻게 묶는지 봅니다.",
    x: 660,
    y: 900,
    children: ["next", "next-structure"],
  },
  {
    id: "browser",
    parent: "structure",
    type: "term",
    title: "브라우저",
    subtitle: "웹의 뼈대",
    image: "imgs/term_browser.png",
    category: "웹의 뼈대",
    focus: "URL 요청, 리소스 해석, 렌더링, 개발자 도구를 연결해서 봅니다.",
    x: 720,
    y: 330,
  },
  {
    id: "html-css",
    parent: "structure",
    type: "term",
    title: "HTML / CSS",
    subtitle: "웹의 뼈대",
    image: "imgs/html_css.png",
    category: "웹의 뼈대",
    focus: "HTML은 구조, CSS는 표현을 담당한다는 기본 분업을 익힙니다.",
    x: 1020,
    y: 240,
  },
  {
    id: "dom",
    parent: "structure",
    type: "term",
    title: "DOM",
    subtitle: "웹의 뼈대",
    image: "imgs/term_dom.png",
    category: "웹의 뼈대",
    focus: "HTML이 객체 트리로 바뀌고 JavaScript가 이를 조작하는 흐름을 봅니다.",
    x: 1320,
    y: 330,
  },
  {
    id: "javascript",
    parent: "frontend",
    type: "term",
    title: "JavaScript",
    subtitle: "프론트엔드",
    image: "imgs/term_javascript.png",
    category: "프론트엔드",
    focus: "이벤트, 상태, 비동기 처리로 화면이 움직이는 원리를 이해합니다.",
    x: 1530,
    y: 380,
  },
  {
    id: "react",
    parent: "frontend",
    type: "term",
    title: "React",
    subtitle: "프론트엔드",
    image: "imgs/React.png",
    category: "프론트엔드",
    focus: "컴포넌트, 상태, props를 중심으로 UI를 조립하는 방식을 봅니다.",
    x: 1640,
    y: 560,
  },
  {
    id: "front-back",
    parent: "frontend",
    type: "term",
    title: "프론트엔드와 백엔드",
    subtitle: "프론트엔드",
    image: "imgs/frontend_backend.png",
    category: "프론트엔드",
    focus: "화면 담당 영역과 서버 담당 영역이 어디서 만나는지 확인합니다.",
    x: 1530,
    y: 740,
  },
  {
    id: "auth",
    parent: "network",
    type: "term",
    title: "인증",
    subtitle: "통신",
    image: "imgs/term_authentication.png",
    category: "통신",
    focus: "로그인, 세션, 토큰, 권한 확인의 차이를 분리해서 봅니다.",
    x: 1510,
    y: 810,
  },
  {
    id: "cookie",
    parent: "network",
    type: "term",
    title: "쿠키",
    subtitle: "통신",
    image: "imgs/cookie.png",
    category: "통신",
    focus: "브라우저에 저장되는 작은 데이터가 세션과 추적에 쓰이는 방식을 익힙니다.",
    x: 1600,
    y: 940,
  },
  {
    id: "api",
    parent: "network",
    type: "term",
    title: "API",
    subtitle: "통신",
    image: "imgs/term_api.png",
    category: "통신",
    focus: "프론트엔드와 백엔드가 약속된 주소와 데이터 형식으로 대화하는 개념입니다.",
    x: 1460,
    y: 1080,
  },
  {
    id: "http",
    parent: "network",
    type: "term",
    title: "HTTP 프로토콜",
    subtitle: "통신",
    image: "imgs/http_protocol.png",
    category: "통신",
    focus: "웹에서 클라이언트와 서버가 약속된 방식으로 통신하는 규칙입니다.",
    x: 1190,
    y: 1160,
  },
  {
    id: "request-response",
    parent: "network",
    type: "term",
    title: "HTTP 요청/응답",
    subtitle: "통신",
    image: "imgs/httpReq_Res.png",
    category: "통신",
    focus: "메서드, 헤더, 바디, 상태 코드가 실제 흐름에서 어떤 역할인지 봅니다.",
    x: 930,
    y: 1180,
  },
  {
    id: "web-server",
    parent: "backend",
    type: "term",
    title: "웹서버 프로그램",
    subtitle: "백엔드·데이터",
    image: "imgs/web_server_program.png",
    category: "백엔드·데이터",
    focus: "요청을 받아 정적 파일이나 애플리케이션 응답을 돌려주는 서버 역할입니다.",
    x: 760,
    y: 1010,
  },
  {
    id: "database",
    parent: "backend",
    type: "term",
    title: "데이터베이스",
    subtitle: "백엔드·데이터",
    image: "imgs/database.png",
    category: "백엔드·데이터",
    focus: "애플리케이션 데이터가 테이블, 문서, 키-값 등으로 저장되는 방식을 봅니다.",
    x: 880,
    y: 1280,
  },
  {
    id: "postgres",
    parent: "backend",
    type: "term",
    title: "PostgreSQL",
    subtitle: "백엔드·데이터",
    image: "imgs/postgress.png",
    category: "백엔드·데이터",
    focus: "관계형 데이터베이스와 SQL, 트랜잭션, 확장성 개념을 연결합니다.",
    x: 1180,
    y: 1300,
  },
  {
    id: "json",
    parent: "backend",
    type: "term",
    title: "JSON",
    subtitle: "백엔드·데이터",
    image: "imgs/term_json.png",
    category: "백엔드·데이터",
    focus: "API에서 주고받는 데이터가 키-값, 배열, 객체로 표현되는 형식입니다.",
    x: 1420,
    y: 1210,
  },
  {
    id: "next",
    parent: "workflow",
    type: "term",
    title: "Next.js",
    subtitle: "프레임워크·도구",
    image: "imgs/nextJS.png",
    category: "프레임워크·도구",
    focus: "React 기반 프레임워크가 라우팅, 렌더링, API 경계를 어떻게 제공하는지 봅니다.",
    x: 390,
    y: 820,
  },
  {
    id: "next-structure",
    parent: "workflow",
    type: "term",
    title: "Next.js 디렉터리 구조",
    subtitle: "프레임워크·도구",
    image: "imgs/nextjs_directory_structure.png",
    category: "프레임워크·도구",
    focus: "app, page, layout, component, public 같은 폴더의 역할을 익힙니다.",
    x: 420,
    y: 1080,
  },
];

const viewport = document.querySelector("#viewport");
const world = document.querySelector("#world");
const nodeLayer = document.querySelector("#nodeLayer");
const connectorLayer = document.querySelector("#connectorLayer");
const visibleCount = document.querySelector("#visibleCount");
const lightbox = document.querySelector("#lightbox");
const lightboxImage = document.querySelector("#lightboxImage");
const lightboxCaption = document.querySelector("#lightboxCaption");
const lightboxCategory = document.querySelector("#lightboxCategory");
const lightboxFocus = document.querySelector("#lightboxFocus");
const zoomStage = document.querySelector("#zoomStage");
const zoomLevel = document.querySelector("#zoomLevel");
const zoomInButton = document.querySelector("#zoomIn");
const zoomOutButton = document.querySelector("#zoomOut");
const zoomResetButton = document.querySelector("#zoomReset");
const fullscreenToggle = document.querySelector("#fullscreenToggle");

const nodeMap = new Map(nodes.map((node) => [node.id, node]));
const expanded = new Set();
let pan = { x: window.innerWidth / 2 - 1136, y: window.innerHeight / 2 - 764 };
let activePointer = null;
let zoomState = { scale: 1, x: 0, y: 0, dragging: false, pointerId: null, startX: 0, startY: 0, startOffsetX: 0, startOffsetY: 0 };

connectorLayer.setAttribute("viewBox", "0 0 2400 1600");
connectorLayer.setAttribute("width", "2400");
connectorLayer.setAttribute("height", "1600");

function visibleNodeIds() {
  const visible = ["root"];
  for (const node of nodes) {
    if (node.id === "root") continue;
    const parent = node.parent ?? "root";
    if (expanded.has(parent)) visible.push(node.id);
  }
  return visible;
}

function setWorldTransform() {
  world.style.transform = `translate(${pan.x}px, ${pan.y}px)`;
}

function render() {
  const visible = new Set(visibleNodeIds());
  nodeLayer.innerHTML = "";

  for (const node of nodes) {
    if (!visible.has(node.id)) continue;
    const element = document.createElement("div");
    element.className = `node ${node.type} node-enter${node.image ? " has-image" : ""}`;
    element.dataset.id = node.id;
    element.tabIndex = 0;
    element.setAttribute("role", node.image ? "button" : "group");
    element.setAttribute("aria-label", node.image ? `${node.title} 이미지 열기` : node.title);
    element.style.left = `${node.x}px`;
    element.style.top = `${node.y}px`;
    element.innerHTML = `
      <span class="node-title">${node.title}</span>
      <span class="node-subtitle">${node.subtitle}</span>
      ${
        node.children
          ? `<button class="node-badge" type="button" aria-label="${node.title} ${expanded.has(node.id) ? "접기" : "펼치기"}">${expanded.has(node.id) ? "−" : "+"}</button>`
          : `<span class="node-badge" aria-hidden="true">↗</span>`
      }
    `;
    if (expanded.has(node.id)) element.classList.add("is-open");
    element.addEventListener("pointerdown", onNodePointerDown);
    element.addEventListener("keydown", onNodeKeyDown);
    const toggleButton = element.querySelector("button.node-badge");
    if (toggleButton) {
      toggleButton.addEventListener("pointerdown", (event) => event.stopPropagation());
      toggleButton.addEventListener("click", (event) => {
        event.stopPropagation();
        toggleNode(node.id);
      });
    }
    nodeLayer.appendChild(element);
  }

  requestAnimationFrame(() => drawConnectors(visible));
  visibleCount.textContent = `${visible.size}개 노드`;
}

function nodeBox(node) {
  const element = nodeLayer.querySelector(`[data-id="${node.id}"]`);
  const width = element?.offsetWidth ?? (node.type === "root" ? 232 : node.type === "term" ? 176 : 184);
  const height = element?.offsetHeight ?? (node.type === "root" ? 128 : 76);
  return {
    left: node.x,
    top: node.y,
    width,
    height,
    cx: node.x + width / 2,
    cy: node.y + height / 2,
  };
}

function boxEdgePoint(box, toward) {
  const dx = toward.cx - box.cx;
  const dy = toward.cy - box.cy;
  if (dx === 0 && dy === 0) return { x: box.cx, y: box.cy };

  const halfW = box.width / 2 + 8;
  const halfH = box.height / 2 + 8;
  const scale = Math.min(Math.abs(halfW / dx || Infinity), Math.abs(halfH / dy || Infinity));
  return {
    x: box.cx + dx * scale,
    y: box.cy + dy * scale,
  };
}

function drawConnectors(visible) {
  connectorLayer.innerHTML = "";
  for (const node of nodes) {
    if (node.id === "root" || !visible.has(node.id)) continue;
    const parent = nodeMap.get(node.parent ?? "root");
    if (!parent || !visible.has(parent.id)) continue;

    const parentBox = nodeBox(parent);
    const childBox = nodeBox(node);
    const from = boxEdgePoint(parentBox, childBox);
    const to = boxEdgePoint(childBox, parentBox);
    const dx = to.x - from.x;
    const dy = to.y - from.y;
    const horizontalPull = Math.max(120, Math.abs(dx) * 0.48);
    const verticalPull = Math.max(40, Math.abs(dy) * 0.16);
    const c1 = {
      x: from.x + Math.sign(dx || 1) * horizontalPull,
      y: from.y + Math.sign(dy || 1) * verticalPull,
    };
    const c2 = {
      x: to.x - Math.sign(dx || 1) * horizontalPull,
      y: to.y - Math.sign(dy || 1) * verticalPull,
    };
    const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
    path.setAttribute("d", `M ${from.x} ${from.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${to.x} ${to.y}`);
    connectorLayer.appendChild(path);
  }
}

function toggleNode(id) {
  const node = nodeMap.get(id);
  if (!node) return;
  if (node.children) {
    if (expanded.has(id)) collapseBranch(id);
    else expanded.add(id);
    render();
    return;
  }
  openLightbox(node);
}

function collapseBranch(id) {
  const node = nodeMap.get(id);
  expanded.delete(id);
  for (const childId of node.children ?? []) {
    collapseBranch(childId);
  }
}

function onNodePointerDown(event) {
  if (event.target.closest(".node-badge")) return;
  event.preventDefault();
  event.stopPropagation();
  const element = event.currentTarget;
  const node = nodeMap.get(element.dataset.id);
  element.setPointerCapture(event.pointerId);
  element.classList.add("is-dragging");
  activePointer = {
    mode: "node",
    pointerId: event.pointerId,
    node,
    element,
    startClientX: event.clientX,
    startClientY: event.clientY,
    startX: node.x,
    startY: node.y,
    moved: false,
  };
}

function onViewportPointerDown(event) {
  if (event.target.closest(".node, .hud, .canvas-help, .lightbox")) return;
  viewport.setPointerCapture(event.pointerId);
  viewport.classList.add("is-panning");
  activePointer = {
    mode: "pan",
    pointerId: event.pointerId,
    startClientX: event.clientX,
    startClientY: event.clientY,
    startPanX: pan.x,
    startPanY: pan.y,
    moved: false,
  };
}

function onPointerMove(event) {
  if (!activePointer || activePointer.pointerId !== event.pointerId) return;
  const dx = event.clientX - activePointer.startClientX;
  const dy = event.clientY - activePointer.startClientY;
  if (Math.hypot(dx, dy) > 5) activePointer.moved = true;

  if (activePointer.mode === "pan") {
    pan = { x: activePointer.startPanX + dx, y: activePointer.startPanY + dy };
    setWorldTransform();
    return;
  }

  activePointer.node.x = activePointer.startX + dx;
  activePointer.node.y = activePointer.startY + dy;
  activePointer.element.style.left = `${activePointer.node.x}px`;
  activePointer.element.style.top = `${activePointer.node.y}px`;
  drawConnectors(new Set(visibleNodeIds()));
}

function onPointerUp(event) {
  if (!activePointer || activePointer.pointerId !== event.pointerId) return;
  if (activePointer.mode === "node") {
    activePointer.element.classList.remove("is-dragging");
    if (!activePointer.moved && activePointer.node.image) openLightbox(activePointer.node);
  }
  viewport.classList.remove("is-panning");
  activePointer = null;
}

function onNodeKeyDown(event) {
  const node = nodeMap.get(event.currentTarget.dataset.id);
  if (!node?.image || (event.key !== "Enter" && event.key !== " ")) return;
  event.preventDefault();
  openLightbox(node);
}

function openLightbox(node) {
  resetZoom();
  lightboxImage.src = encodeURI(node.image);
  lightboxImage.alt = `${node.title} 인포그래픽`;
  lightboxCaption.textContent = node.title;
  lightboxCategory.textContent = node.category;
  lightboxFocus.textContent = node.focus;
  lightbox.hidden = false;
}

function closeLightbox() {
  setFullscreenMode(false);
  lightbox.hidden = true;
}

function setFullscreenMode(enabled) {
  lightbox.classList.toggle("is-fullscreen", enabled);
  fullscreenToggle.textContent = enabled ? "전체화면 종료" : "전체화면";
}

function toggleFullscreen() {
  setFullscreenMode(!lightbox.classList.contains("is-fullscreen"));
  resetZoom();
}

function clampZoom(value) {
  return Math.min(5, Math.max(0.5, value));
}

function applyZoom() {
  zoomState.scale = clampZoom(zoomState.scale);
  lightboxImage.style.transform = `translate(${zoomState.x}px, ${zoomState.y}px) scale(${zoomState.scale})`;
  zoomLevel.textContent = `${Math.round(zoomState.scale * 100)}%`;
}

function resetZoom() {
  zoomState = { scale: 1, x: 0, y: 0, dragging: false, pointerId: null, startX: 0, startY: 0, startOffsetX: 0, startOffsetY: 0 };
  zoomStage?.classList.remove("is-dragging");
  if (lightboxImage) applyZoom();
}

function setZoom(nextScale) {
  const previous = zoomState.scale;
  zoomState.scale = clampZoom(nextScale);
  if (zoomState.scale <= 1 && !lightbox.classList.contains("is-fullscreen")) {
    zoomState.x = 0;
    zoomState.y = 0;
  } else if (previous <= 1) {
    zoomState.x = 0;
    zoomState.y = 0;
  }
  applyZoom();
}

function onZoomWheel(event) {
  event.preventDefault();
  const direction = event.deltaY > 0 ? -1 : 1;
  setZoom(zoomState.scale + direction * 0.15);
}

function onZoomPointerDown(event) {
  const canPanImage = zoomState.scale > 1 || lightbox.classList.contains("is-fullscreen");
  if (!canPanImage || event.button !== 0) return;
  event.preventDefault();
  zoomStage.setPointerCapture(event.pointerId);
  zoomStage.classList.add("is-dragging");
  zoomState.dragging = true;
  zoomState.pointerId = event.pointerId;
  zoomState.startX = event.clientX;
  zoomState.startY = event.clientY;
  zoomState.startOffsetX = zoomState.x;
  zoomState.startOffsetY = zoomState.y;
}

function onZoomPointerMove(event) {
  if (!zoomState.dragging || zoomState.pointerId !== event.pointerId) return;
  zoomState.x = zoomState.startOffsetX + event.clientX - zoomState.startX;
  zoomState.y = zoomState.startOffsetY + event.clientY - zoomState.startY;
  applyZoom();
}

function onZoomPointerUp(event) {
  if (!zoomState.dragging || zoomState.pointerId !== event.pointerId) return;
  zoomState.dragging = false;
  zoomState.pointerId = null;
  zoomStage.classList.remove("is-dragging");
}

function resetView() {
  pan = { x: window.innerWidth / 2 - 1136, y: window.innerHeight / 2 - 764 };
  setWorldTransform();
}

function foldAll() {
  expanded.clear();
  render();
}

viewport.addEventListener("pointerdown", onViewportPointerDown);
window.addEventListener("pointermove", onPointerMove);
window.addEventListener("pointerup", onPointerUp);
window.addEventListener("pointercancel", onPointerUp);
document.querySelector("#resetView").addEventListener("click", resetView);
document.querySelector("#foldAll").addEventListener("click", foldAll);
document.querySelector(".lightbox-backdrop").addEventListener("click", closeLightbox);
document.querySelector("#closeLightbox").addEventListener("click", closeLightbox);
zoomInButton.addEventListener("click", () => setZoom(zoomState.scale + 0.25));
zoomOutButton.addEventListener("click", () => setZoom(zoomState.scale - 0.25));
zoomResetButton.addEventListener("click", resetZoom);
fullscreenToggle.addEventListener("click", toggleFullscreen);
zoomStage.addEventListener("wheel", onZoomWheel, { passive: false });
zoomStage.addEventListener("dblclick", () => setZoom(zoomState.scale > 1 ? 1 : 2));
zoomStage.addEventListener("pointerdown", onZoomPointerDown);
zoomStage.addEventListener("pointermove", onZoomPointerMove);
zoomStage.addEventListener("pointerup", onZoomPointerUp);
zoomStage.addEventListener("pointercancel", onZoomPointerUp);
document.addEventListener("keydown", (event) => {
  if (event.key === "Escape") closeLightbox();
  if (lightbox.hidden) return;
  if (event.key === "+" || event.key === "=") setZoom(zoomState.scale + 0.25);
  if (event.key === "-") setZoom(zoomState.scale - 0.25);
  if (event.key === "0") resetZoom();
  if (event.key.toLowerCase() === "f") toggleFullscreen();
});
window.addEventListener("resize", resetView);

setWorldTransform();
render();
