// 커스텀 justified 가상 그리드 — 레이아웃은 워커에서, 렌더는 뷰포트 ±1화면만.
import {
  createEffect,
  createMemo,
  createSignal,
  For,
  on,
  onCleanup,
  onMount,
  Show,
  type Component,
} from "solid-js";
import { computeLayout, type LayoutResult } from "./layout";
import {
  anchorIndex,
  clearSelection,
  gridRowHeight,
  info,
  openLightbox,
  selected,
  selectSingle,
  setSelectionFromIds,
  toggleSelect,
  selectRange,
  snapshot,
  thumbVersions,
  zoomGrid,
} from "../../state/store";
import { fileDetails, setThumbPriority, thumbUrl, type Snapshot } from "../../ipc/commands";
import { ko } from "../../i18n/ko";
import {
  openContextMenu,
  openRenameDialog,
  performTrash,
  performUndo,
} from "../fileops/ContextMenu";

const GAP = 6;
const OVERSCAN_PX = 800;

/** rowTops에서 y 이상이 처음 나오는 행 인덱스 (이진탐색) */
function findRow(rowTops: Float32Array, y: number): number {
  let lo = 0;
  let hi = rowTops.length - 2; // 마지막 요소는 전체 높이
  while (lo < hi) {
    const mid = (lo + hi + 1) >> 1;
    if (rowTops[mid] <= y) lo = mid;
    else hi = mid - 1;
  }
  return lo;
}

export const JustifiedGrid: Component = () => {
  let containerRef: HTMLDivElement | undefined;
  const [layout, setLayout] = createSignal<LayoutResult | undefined>();
  const [scrollTop, setScrollTop] = createSignal(0);
  const [viewportH, setViewportH] = createSignal(600);
  // 마퀴(드래그 사각형) 선택 — 콘텐츠 좌표계 [x1,y1,x2,y2]
  const [marquee, setMarquee] = createSignal<[number, number, number, number] | null>(null);

  // 레이아웃 재계산 — 컨테이너 폭을 DOM에서 직접 읽고 메인 스레드에서 동기 계산.
  const recompute = () => {
    const snap = snapshot();
    if (!snap || !containerRef) {
      setLayout(undefined);
      return;
    }
    const w = containerRef.clientWidth - 16; // 패딩 보정
    if (w <= 0) return;
    const ratios = snap.aspectRatios();
    setLayout(computeLayout(ratios, w, gridRowHeight(), GAP));
  };

  // 데이터(스냅샷) 변경 시 즉시 재계산.
  createEffect(on(snapshot, recompute));

  onMount(() => {
    const el = containerRef!;
    let lastW = -1;
    let lastRh = -1;
    // 폭과 줌(gridRowHeight)을 함께 감시 — 어느 쪽이 바뀌든 재배치.
    // (이벤트/이펙트 누락 환경에 견고하도록 폴링을 단일 진실원으로 사용)
    const tick = () => {
      const w = el.clientWidth;
      const rh = gridRowHeight();
      setViewportH(el.clientHeight);
      if (Math.abs(w - lastW) > 0.5 || rh !== lastRh) {
        lastW = w;
        lastRh = rh;
        recompute();
      }
    };
    tick();
    [50, 200, 500].forEach((ms) => setTimeout(tick, ms));
    const ro = new ResizeObserver(tick);
    ro.observe(el);
    const onWinResize = () => requestAnimationFrame(tick);
    window.addEventListener("resize", onWinResize);
    const poll = setInterval(tick, 150);
    onCleanup(() => {
      ro.disconnect();
      window.removeEventListener("resize", onWinResize);
      clearInterval(poll);
    });
  });

  // Ctrl+휠 → 그리드 줌 (썸네일 크기). 즉시 재계산.
  const onWheel = (e: WheelEvent) => {
    if (!e.ctrlKey && !e.metaKey) return;
    e.preventDefault();
    zoomGrid(e.deltaY < 0 ? 1.12 : 1 / 1.12);
    recompute();
  };

  // rAF 스로틀 스크롤
  let rafPending = false;
  const onScroll = () => {
    if (rafPending) return;
    rafPending = true;
    requestAnimationFrame(() => {
      rafPending = false;
      if (containerRef) setScrollTop(containerRef.scrollTop);
    });
  };

  /** 가시 항목 인덱스 범위 */
  const visibleRange = createMemo<[number, number]>(() => {
    const l = layout();
    const snap = snapshot();
    if (!l || !snap || l.rowFirst.length === 0) return [0, -1];
    const top = Math.max(0, scrollTop() - OVERSCAN_PX);
    const bottom = scrollTop() + viewportH() + OVERSCAN_PX;
    const firstRow = findRow(l.rowTops, top);
    let lastRow = firstRow;
    while (lastRow + 1 < l.rowFirst.length && l.rowTops[lastRow + 1] < bottom) lastRow++;
    const first = l.rowFirst[firstRow];
    const last =
      lastRow + 1 < l.rowFirst.length ? l.rowFirst[lastRow + 1] - 1 : snap.count - 1;
    return [first, last];
  });

  // 뷰포트 항목 중 썸네일 없는 것 우선 생성 요청 (디바운스 200ms)
  let prioTimer: ReturnType<typeof setTimeout> | undefined;
  createEffect(() => {
    const [first, last] = visibleRange();
    const snap = snapshot();
    if (!snap || last < first) return;
    clearTimeout(prioTimer);
    prioTimer = setTimeout(() => {
      const ids: number[] = [];
      for (let i = first; i <= last && ids.length < 512; i++) {
        if (!snap.hasThumb(i)) ids.push(snap.id(i));
      }
      if (ids.length > 0) void setThumbPriority(ids);
    }, 200);
  });
  onCleanup(() => clearTimeout(prioTimer));

  const indices = createMemo(() => {
    const [first, last] = visibleRange();
    const out: number[] = [];
    for (let i = first; i <= last; i++) out.push(i);
    return out;
  });

  const onCellClick = (e: MouseEvent, snap: Snapshot, index: number) => {
    const id = snap.id(index);
    if (e.shiftKey) selectRange(snap, index);
    else if (e.ctrlKey || e.metaKey) toggleSelect(id, index);
    else selectSingle(id, index);
  };

  /** 위/아래 화살표: 레이아웃 기하 기반 — 인접 행에서 x중심이 가장 가까운 셀 */
  const verticalNeighbor = (from: number, dir: -1 | 1): number | undefined => {
    const l = layout();
    const snap = snapshot();
    if (!l || !snap) return undefined;
    const cx = l.boxes[from * 4] + l.boxes[from * 4 + 2] / 2;
    const cy = l.boxes[from * 4 + 1];
    let best: number | undefined;
    let bestDist = Infinity;
    // 최대 3행 거리까지 탐색 (마지막 행 채움 편차 대응)
    const step = dir === 1 ? 1 : -1;
    for (let i = from + step; i >= 0 && i < snap.count; i += step) {
      const y = l.boxes[i * 4 + 1];
      if (dir === 1 ? y <= cy : y >= cy) continue; // 같은 행 스킵
      const nx = l.boxes[i * 4] + l.boxes[i * 4 + 2] / 2;
      const d = Math.abs(nx - cx);
      if (d < bestDist) {
        bestDist = d;
        best = i;
      } else if (best !== undefined && Math.abs(y - l.boxes[best * 4 + 1]) > 1) {
        break; // 다음 행으로 넘어가면 종료
      }
    }
    return best;
  };

  const onKeyDown = (e: KeyboardEvent) => {
    const snap = snapshot();
    if (!snap || snap.count === 0) return;
    const anchor = anchorIndex();

    if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "z") {
      e.preventDefault();
      void performUndo();
      return;
    }

    switch (e.key) {
      case "Enter":
      case " ":
        if (anchor >= 0) {
          e.preventDefault();
          openLightbox(anchor);
        }
        break;
      case "Delete":
        if (selected().size > 0) {
          e.preventDefault();
          void performTrash();
        }
        break;
      case "F2":
        if (anchor >= 0 && selected().size === 1) {
          e.preventDefault();
          const id = snap.id(anchor);
          void fileDetails([id]).then((d) => {
            if (d[0]) openRenameDialog(id, d[0].name);
          });
        }
        break;
      case "ArrowLeft":
        if (anchor > 0) {
          e.preventDefault();
          selectSingle(snap.id(anchor - 1), anchor - 1);
        }
        break;
      case "ArrowRight":
        if (anchor >= 0 && anchor < snap.count - 1) {
          e.preventDefault();
          selectSingle(snap.id(anchor + 1), anchor + 1);
        }
        break;
      case "ArrowUp":
      case "ArrowDown": {
        if (anchor < 0) break;
        e.preventDefault();
        const next = verticalNeighbor(anchor, e.key === "ArrowDown" ? 1 : -1);
        if (next !== undefined) selectSingle(snap.id(next), next);
        break;
      }
    }
  };

  const onCellContextMenu = (e: MouseEvent, snap: Snapshot, index: number) => {
    e.preventDefault();
    const id = snap.id(index);
    if (!selected().has(id)) selectSingle(id, index);
    void fileDetails([id]).then((d) => {
      openContextMenu({ x: e.clientX, y: e.clientY, fileId: id, fileName: d[0]?.name });
    });
  };

  const onCellDragStart = (e: DragEvent, snap: Snapshot, index: number) => {
    const id = snap.id(index);
    if (!selected().has(id)) selectSingle(id, index);
    e.dataTransfer?.setData("application/x-archive-move", "1");
    if (e.dataTransfer) e.dataTransfer.effectAllowed = "move";
  };

  // ── 마퀴(드래그 사각형) 선택 ──
  // 콘텐츠 좌표 = clientX - 컨테이너rect - 패딩(8) (+ y는 scrollTop)
  const toContent = (clientX: number, clientY: number): [number, number] => {
    const rect = containerRef!.getBoundingClientRect();
    return [
      clientX - rect.left - 8,
      clientY - rect.top - 8 + containerRef!.scrollTop,
    ];
  };

  const onBgMouseDown = (e: MouseEvent) => {
    // 배경(빈 공간) 좌클릭에서만 시작 — 셀 클릭은 제외
    if (e.button !== 0 || !(e.target as HTMLElement).dataset.gridbg) return;
    const [sx, sy] = toContent(e.clientX, e.clientY);
    setMarquee([sx, sy, sx, sy]);
    if (!e.ctrlKey && !e.metaKey) clearSelection();

    const onMove = (me: MouseEvent) => {
      const [cx, cy] = toContent(me.clientX, me.clientY);
      const m = marquee();
      if (m) setMarquee([m[0], m[1], cx, cy]);
    };
    const onUp = () => {
      document.removeEventListener("mousemove", onMove);
      document.removeEventListener("mouseup", onUp);
      commitMarquee();
      setMarquee(null);
    };
    document.addEventListener("mousemove", onMove);
    document.addEventListener("mouseup", onUp);
  };

  const commitMarquee = () => {
    const m = marquee();
    const l = layout();
    const snap = snapshot();
    if (!m || !l || !snap) return;
    const [x1, y1, x2, y2] = [Math.min(m[0], m[2]), Math.min(m[1], m[3]), Math.max(m[0], m[2]), Math.max(m[1], m[3])];
    // 너무 작은 드래그는 무시(클릭으로 간주)
    if (x2 - x1 < 4 && y2 - y1 < 4) return;
    const ids: number[] = [];
    for (let i = 0; i < snap.count; i++) {
      const bx = l.boxes[i * 4];
      const by = l.boxes[i * 4 + 1];
      const bw = l.boxes[i * 4 + 2];
      const bh = l.boxes[i * 4 + 3];
      // AABB 교차
      if (bx < x2 && bx + bw > x1 && by < y2 && by + bh > y1) ids.push(snap.id(i));
    }
    setSelectionFromIds(ids);
  };

  const marqueeRect = createMemo(() => {
    const m = marquee();
    if (!m) return null;
    return {
      left: Math.min(m[0], m[2]),
      top: Math.min(m[1], m[3]),
      width: Math.abs(m[2] - m[0]),
      height: Math.abs(m[3] - m[1]),
    };
  });

  return (
    <div
      ref={containerRef}
      onScroll={onScroll}
      onWheel={onWheel}
      onKeyDown={onKeyDown}
      onMouseDown={onBgMouseDown}
      tabindex={0}
      class="relative min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-2 py-2 outline-none"
    >
      <Show
        when={snapshot() && snapshot()!.count > 0}
        fallback={
          <div class="grid h-full place-items-center text-sm text-[var(--text-muted)]">
            {ko.grid.empty}
          </div>
        }
      >
        <div
          class="relative"
          data-gridbg="1"
          style={{ height: `${layout()?.totalHeight ?? 0}px` }}
        >
          <Show when={marqueeRect()}>
            {(r) => (
              <div
                class="pointer-events-none absolute z-10 border border-[var(--accent)] bg-[var(--accent-soft)]"
                style={{
                  left: `${r().left}px`,
                  top: `${r().top}px`,
                  width: `${r().width}px`,
                  height: `${r().height}px`,
                }}
              />
            )}
          </Show>
          <For each={indices()}>
            {(i) => {
              // 모든 파생값은 반응형으로 — For가 같은 인덱스 값에 대해 DOM을 재사용해도
              // 뷰(폴더) 전환 시 스냅샷이 바뀌면 id/썸네일/선택상태가 즉시 갱신되도록 한다.
              const box = () => {
                const l = layout();
                return l
                  ? [l.boxes[i * 4], l.boxes[i * 4 + 1], l.boxes[i * 4 + 2], l.boxes[i * 4 + 3]]
                  : [0, 0, 0, 0];
              };
              const id = () => {
                const s = snapshot();
                return s ? s.id(i) : -1;
              };
              const version = () => thumbVersions[id()] ?? 0;
              const showImg = () => {
                const s = snapshot();
                return !!s && (s.hasThumb(i) || version() > 0);
              };
              // 썸네일 미준비 상태: 실패(state 2)면 정적 표시, 그 외엔 '생성 중' 스켈레톤.
              const failed = () => {
                const s = snapshot();
                return !!s && s.thumbFailed(i);
              };
              const isVideo = () => {
                const s = snapshot();
                return !!s && s.kind(i) === 1;
              };
              return (
                <div
                  class="absolute cursor-pointer overflow-hidden rounded-[3px] bg-[var(--grid-cell-bg)] outline-offset-[-2px]"
                  classList={{
                    "outline outline-2 outline-[var(--accent)]": selected().has(id()),
                  }}
                  style={{
                    transform: `translate(${box()[0]}px, ${box()[1]}px)`,
                    width: `${box()[2]}px`,
                    height: `${box()[3]}px`,
                  }}
                  onClick={(e) => {
                    const s = snapshot();
                    if (s) onCellClick(e, s, i);
                  }}
                  onDblClick={() => openLightbox(i)}
                  onContextMenu={(e) => {
                    const s = snapshot();
                    if (s) onCellContextMenu(e, s, i);
                  }}
                  draggable={true}
                  onDragStart={(e) => {
                    const s = snapshot();
                    if (s) onCellDragStart(e, s, i);
                  }}
                >
                  <Show
                    when={showImg() && info()}
                    fallback={
                      // 준비 전: 실패면 정적 아이콘, 대기면 은은한 스켈레톤(생성 중)
                      <Show
                        when={!failed()}
                        fallback={
                          <div class="grid h-full w-full place-items-center text-[var(--text-muted)]">
                            <span class="text-base opacity-60">🖼</span>
                          </div>
                        }
                      >
                        <div class="h-full w-full animate-pulse bg-[var(--bg-hover)]" />
                      </Show>
                    }
                  >
                    <img
                      src={`${thumbUrl(info()!, id())}&v=${version()}`}
                      class="h-full w-full object-cover"
                      decoding="async"
                      draggable={false}
                      onLoad={(e) => {
                        (e.currentTarget as HTMLImageElement).style.display = "";
                      }}
                      onError={(e) => {
                        (e.currentTarget as HTMLImageElement).style.display = "none";
                      }}
                    />
                  </Show>
                  <Show when={isVideo()}>
                    <span class="absolute bottom-1 right-1 rounded bg-black/60 px-1 text-[10px] text-white">
                      영상
                    </span>
                  </Show>
                </div>
              );
            }}
          </For>
        </div>
      </Show>
    </div>
  );
};
