﻿<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
import Muuri from '@/vendor/muuri-src';
import { useLayoutStore } from '@/store/useLayoutStore';
import { useWidgetsStore } from '@/store/useWidgetsStore';
import { useFoldersStore, type Folder, type FolderItem } from '@/store/useFoldersStore';
import { useUIStore } from '@/store/useUIStore';
import { useSettingsStore } from '@/store/useSettingsStore';
import IconCard from '@/components/IconCard/index.vue';
import WidgetCard from '@/components/WidgetCard/index.vue';
import FolderCard from '@/components/FolderCard/index.vue';
import FolderDialog from '@/components/FolderDialog/index.vue';

type GridItemType = 'icon' | 'widget' | 'folder';
type GridItemSize = '1x1' | '1x2' | '2x1' | '2x2' | '2x4';

interface Icon {
  id: string;
  name: string;
  url: string;
  img?: string;
  text?: string;
  bgColor?: string;
  size?: GridItemSize;
  groupId?: string;
  folderId?: string;
}

type WidgetSize = GridItemSize;
interface Widget {
  id: string;
  component: string;
  size: WidgetSize;
  data?: any;
  groupId?: string;
  folderId?: string;
}

type FolderChild =
  | { type: 'icon'; id: string; icon: Icon }
  | { type: 'widget'; id: string; widget: Widget };

interface GridOrderEntry {
  id: string;
  type: GridItemType;
}

type OrderedGridItem =
  | { type: 'icon'; id: string; icon: Icon }
  | { type: 'widget'; id: string; widget: Widget }
  | { type: 'folder'; id: string; folder: Folder; children: FolderChild[] };

const GRID_ORDER_STORAGE_KEY = 'itab_grid_order';
const DEFAULT_GROUP_ID = 'home';
const DEFAULT_FOLDER_SIZE: GridItemSize = '1x1';

const props = defineProps<{
  activeGroupId?: string | null;
}>();

const layoutStore = useLayoutStore();
const widgetsStore = useWidgetsStore();
const foldersStore = useFoldersStore();
const uiStore = useUIStore();
const settingsStore = useSettingsStore();
const gridRef = ref<HTMLElement | null>(null);
const grid = ref<any | null>(null);
const isDragging = ref(false);
const suppressClick = ref(false);
const clickBlockUntil = ref(0);
const gridOrder = ref<GridOrderEntry[]>([]);
const resizedKeys = ref(new Set<string>());
const pendingResizedKeys = new Set<string>();
const widgetSizeMap = new Map<string, GridItemSize>();
const iconSizeMap = new Map<string, GridItemSize>();
const folderSizeMap = new Map<string, GridItemSize>();
const enteringKeys = ref(new Set<string>());
const previousIconIds = ref(new Set<string>());
const previousWidgetIds = ref(new Set<string>());
const openFolderId = ref<string | null>(null);
const hoverTargetEl = ref<HTMLElement | null>(null);
const draggingMeta = ref<{ id: string | null; type: GridItemType | null }>({
  id: null,
  type: null,
});
const draggingPointerBackup = ref<string | null>(null);
const draggingEl = ref<HTMLElement | null>(null);
const draggingItemRef = ref<any | null>(null);
const overlapRafId = ref<number | null>(null);
const overlapPendingEl = ref<HTMLElement | null>(null);
const disableSort = ref(false);
const mergeTargetEl = ref<HTMLElement | null>(null);
const mergeCandidateEl = ref<HTMLElement | null>(null);
const mergeCandidateAt = ref<number>(0);
const mergeActive = ref(false);
const mergeCommitted = ref(false);
const mergeOrderSnapshot = ref<GridOrderEntry[] | null>(null);
const mergePendingFolderId = ref<string | null>(null);
const mergePendingInsertIndex = ref<number | null>(null);
const OVERLAP_MERGE_THRESHOLD = 0.6;
const OVERLAP_HOLD_MS = 100;
const OVERLAP_DROP_THRESHOLD = 0.35;
const MERGE_HIT_INSET_RATIO = 0.18;
const MERGE_HIT_INSET_PX = 6;
const POINTER_ROW_TOLERANCE_RATIO = 0.6;
const POINTER_EDGE_PADDING = 6;
const lastPointer = ref<{ x: number; y: number } | null>(null);
const pointerTargetEl = ref<HTMLElement | null>(null);
const layoutAnimationMs = 240;
const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)';
const DELETE_ANIM_MS = 240;
const DELETE_ANIM_EASING = 'cubic-bezier(0.4, 0, 0.2, 1)';
const normalizeGroupId = (groupId?: string | null) => groupId ?? DEFAULT_GROUP_ID;
const activeGroupId = computed(() => normalizeGroupId(props.activeGroupId));

const normalizeGridOrder = (order: GridOrderEntry[]) => {
  const seen = new Set<string>();
  const normalized: GridOrderEntry[] = [];
  for (const entry of order) {
    if (!entry || typeof entry.id !== 'string') continue;
    if (entry.type !== 'icon' && entry.type !== 'widget' && entry.type !== 'folder') continue;
    const key = `${entry.type}:${entry.id}`;
    if (seen.has(key)) continue;
    seen.add(key);
    normalized.push(entry);
  }
  return normalized;
};

const buildDefaultOrder = (): GridOrderEntry[] => [
  ...widgetsStore.widgets.filter(widget => !widget.folderId).map(widget => ({ id: widget.id, type: 'widget' as const })),
  ...layoutStore.icons.filter(icon => !icon.folderId).map(icon => ({ id: icon.id, type: 'icon' as const })),
  ...foldersStore.folders.map(folder => ({ id: folder.id, type: 'folder' as const })),
];

const applyGridOrder = (order: GridOrderEntry[], syncStoreOrder = true) => {
  gridOrder.value = order;
  localStorage.setItem(GRID_ORDER_STORAGE_KEY, JSON.stringify(order));
  if (!syncStoreOrder) return;
  const iconOrder = order.filter(item => item.type === 'icon').map(item => item.id);
  const widgetOrder = order.filter(item => item.type === 'widget').map(item => item.id);
  if (iconOrder.length) {
    layoutStore.setIconOrder(iconOrder);
  }
  if (widgetOrder.length) {
    widgetsStore.setWidgetOrder(widgetOrder);
  }
};

const loadGridOrder = () => {
  const saved = localStorage.getItem(GRID_ORDER_STORAGE_KEY);
  if (saved) {
    try {
      const parsed = JSON.parse(saved) as GridOrderEntry[];
      if (Array.isArray(parsed)) {
        gridOrder.value = normalizeGridOrder(parsed);
      }
    } catch {
      gridOrder.value = [];
    }
  }
  if (!gridOrder.value.length) {
    gridOrder.value = buildDefaultOrder();
  }
  applyGridOrder(gridOrder.value, true);
};

const ensureOrderConsistency = () => {
  const iconIds = new Set(layoutStore.icons.filter(icon => !icon.folderId).map(icon => icon.id));
  const widgetIds = new Set(widgetsStore.widgets.filter(widget => !widget.folderId).map(widget => widget.id));
  const folderIds = new Set(foldersStore.folders.map(folder => folder.id));
  const seen = new Set<string>();
  const nextOrder: GridOrderEntry[] = [];

  const pushEntry = (entry: GridOrderEntry) => {
    const key = `${entry.type}:${entry.id}`;
    if (seen.has(key)) return;
    seen.add(key);
    nextOrder.push(entry);
  };

  for (const entry of gridOrder.value) {
    if (entry.type === 'icon' && iconIds.has(entry.id)) {
      pushEntry(entry);
    }
    if (entry.type === 'widget' && widgetIds.has(entry.id)) {
      pushEntry(entry);
    }
    if (entry.type === 'folder' && folderIds.has(entry.id)) {
      pushEntry(entry);
    }
  }

  for (const widget of widgetsStore.widgets) {
    const key = `widget:${widget.id}`;
    if (!widget.folderId && !seen.has(key)) {
      pushEntry({ id: widget.id, type: 'widget' });
    }
  }

  for (const icon of layoutStore.icons) {
    const key = `icon:${icon.id}`;
    if (!icon.folderId && !seen.has(key)) {
      pushEntry({ id: icon.id, type: 'icon' });
    }
  }

  for (const folder of foldersStore.folders) {
    const key = `folder:${folder.id}`;
    if (!seen.has(key)) {
      pushEntry({ id: folder.id, type: 'folder' });
    }
  }

  const orderChanged =
    nextOrder.length !== gridOrder.value.length ||
    nextOrder.some((entry, index) => {
      const current = gridOrder.value[index];
      return current?.id !== entry.id || current?.type !== entry.type;
    });

  if (orderChanged) {
    applyGridOrder(nextOrder, false);
  } else {
    gridOrder.value = nextOrder;
  }
};

const isInActiveGroup = (groupId?: string | null) =>
  normalizeGroupId(groupId) === activeGroupId.value;

  const orderedItems = computed<OrderedGridItem[]>(() => {
    const iconsById = new Map(layoutStore.icons.map(icon => [icon.id, icon]));
    const widgetsById = new Map(widgetsStore.widgets.map(widget => [widget.id, widget]));
    const foldersById = new Map(foldersStore.folders.map(folder => [folder.id, folder]));
    const folderedKeys = new Set<string>();
    const folderedById = new Map<string, FolderChild[]>();
    for (const icon of layoutStore.icons) {
      if (icon.folderId) {
        folderedKeys.add(`icon:${icon.id}`);
        const list = folderedById.get(icon.folderId) ?? [];
        list.push({ type: 'icon', id: icon.id, icon });
        folderedById.set(icon.folderId, list);
      }
    }
    for (const widget of widgetsStore.widgets) {
      if (widget.folderId) {
        folderedKeys.add(`widget:${widget.id}`);
        const list = folderedById.get(widget.folderId) ?? [];
        list.push({ type: 'widget', id: widget.id, widget });
        folderedById.set(widget.folderId, list);
      }
    }
    const used = new Set<string>();
    const items: OrderedGridItem[] = [];

    const buildChildren = (folder: Folder): FolderChild[] => {
      const children: FolderChild[] = [];
      const existing = new Set<string>();
      for (const child of folder.items) {
        if (child.type === 'icon') {
          const icon = iconsById.get(child.id);
          if (icon) {
            children.push({ type: 'icon', id: icon.id, icon });
            existing.add(`icon:${icon.id}`);
          }
        } else if (child.type === 'widget') {
          const widget = widgetsById.get(child.id);
          if (widget) {
            children.push({ type: 'widget', id: widget.id, widget });
            existing.add(`widget:${widget.id}`);
          }
        }
      }
      const extras = folderedById.get(folder.id);
      if (extras?.length) {
        for (const child of extras) {
          const key = `${child.type}:${child.id}`;
          if (!existing.has(key)) {
            children.push(child);
            existing.add(key);
          }
        }
      }
      return children;
    };

  for (const entry of gridOrder.value) {
      if (entry.type === 'icon') {
        const icon = iconsById.get(entry.id);
        if (
          icon &&
          !icon.folderId &&
          !folderedKeys.has(`icon:${icon.id}`) &&
          isInActiveGroup(icon.groupId)
        ) {
          items.push({ type: 'icon', id: icon.id, icon });
          used.add(`icon:${icon.id}`);
        }
      } else if (entry.type === 'widget') {
        const widget = widgetsById.get(entry.id);
        if (
          widget &&
          !widget.folderId &&
          !folderedKeys.has(`widget:${widget.id}`) &&
          isInActiveGroup(widget.groupId)
        ) {
          items.push({ type: 'widget', id: widget.id, widget });
          used.add(`widget:${widget.id}`);
        }
      } else if (entry.type === 'folder') {
      const folder = foldersById.get(entry.id);
      if (folder && isInActiveGroup(folder.groupId)) {
        items.push({ type: 'folder', id: folder.id, folder, children: buildChildren(folder) });
        used.add(`folder:${folder.id}`);
      }
    }
  }

    for (const widget of widgetsStore.widgets) {
      const key = `widget:${widget.id}`;
      if (
        !widget.folderId &&
        !folderedKeys.has(key) &&
        !used.has(key) &&
        isInActiveGroup(widget.groupId)
      ) {
        items.push({ type: 'widget', id: widget.id, widget });
      }
    }

    for (const icon of layoutStore.icons) {
      const key = `icon:${icon.id}`;
      if (
        !icon.folderId &&
        !folderedKeys.has(key) &&
        !used.has(key) &&
        isInActiveGroup(icon.groupId)
      ) {
        items.push({ type: 'icon', id: icon.id, icon });
      }
    }

  for (const folder of foldersStore.folders) {
    const key = `folder:${folder.id}`;
    if (!used.has(key) && isInActiveGroup(folder.groupId)) {
      items.push({ type: 'folder', id: folder.id, folder, children: buildChildren(folder) });
    }
  }

  return items;
});

const resolveFolderChildren = (folderId: string): FolderChild[] => {
  const folder = foldersStore.folders.find(f => f.id === folderId);
  if (!folder) return [];
  const iconsById = new Map(layoutStore.icons.map(icon => [icon.id, icon]));
  const widgetsById = new Map(widgetsStore.widgets.map(widget => [widget.id, widget]));
  const children: FolderChild[] = [];
  const existing = new Set<string>();
  for (const child of folder.items) {
    if (child.type === 'icon') {
      const icon = iconsById.get(child.id);
      if (icon) {
        children.push({ type: 'icon', id: icon.id, icon });
        existing.add(`icon:${icon.id}`);
      }
    } else if (child.type === 'widget') {
      const widget = widgetsById.get(child.id);
      if (widget) {
        children.push({ type: 'widget', id: widget.id, widget });
        existing.add(`widget:${widget.id}`);
      }
    }
  }
  for (const icon of layoutStore.icons) {
    if (icon.folderId === folderId && !existing.has(`icon:${icon.id}`)) {
      children.push({ type: 'icon', id: icon.id, icon });
      existing.add(`icon:${icon.id}`);
    }
  }
  for (const widget of widgetsStore.widgets) {
    if (widget.folderId === folderId && !existing.has(`widget:${widget.id}`)) {
      children.push({ type: 'widget', id: widget.id, widget });
      existing.add(`widget:${widget.id}`);
    }
  }
  return children;
};

const sizeClassFor = (item: OrderedGridItem) => {
  if (item.type === 'widget') return `size-${item.widget.size}`;
  if (item.type === 'folder') return `size-${item.folder.size ?? DEFAULT_FOLDER_SIZE}`;
  return `size-${item.icon.size ?? '1x1'}`;
};

const currentFolder = computed(() => {
  if (!openFolderId.value) return null;
  return foldersStore.folders.find(folder => folder.id === openFolderId.value) ?? null;
});

const currentFolderChildren = computed(() =>
  currentFolder.value ? resolveFolderChildren(currentFolder.value.id) : []
);

const handleClick = (event: MouseEvent) => {
  if (suppressClick.value || isDragging.value) return;
  const target = event.target as HTMLElement;
  const itemEl = target.closest('.grid-item') as HTMLElement | null;
  if (!itemEl || itemEl.dataset.type !== 'icon') return;
  const link = target.closest('a') as HTMLAnchorElement | null;
  if (link && link.href) {
    const targetMode = settingsStore.openInNewTab ? '_blank' : '_self';
    window.open(link.href, targetMode);
  }
};

const handleClickCapture = (event: MouseEvent) => {
  if (isDragging.value || suppressClick.value || Date.now() < clickBlockUntil.value) {
    event.preventDefault();
    event.stopPropagation();
  }
};

const handleContextMenu = (event: MouseEvent) => {
  const target = event.target as HTMLElement;
  const itemEl = target.closest('.grid-item') as HTMLElement | null;
  if (!itemEl) {
    uiStore.openContextMenu(event.clientX, event.clientY, null, 'desktop');
    return;
  }
  const id = itemEl.dataset.id;
  const type = itemEl.dataset.type as GridItemType | undefined;
  if (id && (type === 'icon' || type === 'widget' || type === 'folder')) {
    uiStore.openContextMenu(event.clientX, event.clientY, id, type);
  } else {
    uiStore.openContextMenu(event.clientX, event.clientY, null, 'desktop');
  }
};

const setHoverTarget = (el: HTMLElement | null) => {
  if (hoverTargetEl.value === el) return;
  if (hoverTargetEl.value) {
    hoverTargetEl.value.classList.remove('is-drop-target');
  }
  hoverTargetEl.value = el;
  if (el) {
    el.classList.add('is-drop-target');
  }
};

const clearHoverTarget = () => setHoverTarget(null);

const activateMergeTarget = (el: HTMLElement) => {
  mergeActive.value = true;
  mergeTargetEl.value = el;
  mergeCandidateEl.value = null;
  mergeCandidateAt.value = 0;
  setHoverTarget(el);
};

const deactivateMergeTarget = (forceSort = true) => {
  if (!mergeActive.value) return;
  mergeActive.value = false;
  mergeTargetEl.value = null;
  clearHoverTarget();
  if (forceSort) {
    draggingItemRef.value?._drag?.sort?.(true);
  }
};

const applySortLock = (locked: boolean) => {
  const prev = disableSort.value;
  disableSort.value = locked;
  if (prev && !locked) {
    draggingItemRef.value?._drag?.sort?.(true);
  }
};

const calcOverlapArea = (a: DOMRect, b: DOMRect) => {
  const xOverlap = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
  const yOverlap = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
  return xOverlap * yOverlap;
};

const getPointerTargetFromRects = (pointer: { x: number; y: number }, dragEl: HTMLElement) => {
  if (!gridRef.value) return null;
  const elements = Array.from(gridRef.value.querySelectorAll('.grid-item')) as HTMLElement[];
  let hit: HTMLElement | null = null;
  let bestArea = Infinity;
  for (const el of elements) {
    if (el === dragEl) continue;
    const rect = el.getBoundingClientRect();
    const inset = Math.max(
      MERGE_HIT_INSET_PX,
      Math.min(rect.width, rect.height) * MERGE_HIT_INSET_RATIO
    );
    const left = rect.left + inset;
    const right = rect.right - inset;
    const top = rect.top + inset;
    const bottom = rect.bottom - inset;
    if (
      pointer.x >= left &&
      pointer.x <= right &&
      pointer.y >= top &&
      pointer.y <= bottom
    ) {
      const area = rect.width * rect.height;
      if (area < bestArea) {
        bestArea = area;
        hit = el;
      }
    }
  }
  return hit;
};

const resolvePointerFromEvent = (event: any) => {
  if (!event) return null;
  const x =
    typeof event.clientX === 'number'
      ? event.clientX
      : typeof event.pageX === 'number'
      ? event.pageX
      : typeof event.x === 'number'
      ? event.x
      : typeof event?.srcEvent?.clientX === 'number'
      ? event.srcEvent.clientX
      : typeof event?.srcEvent?.pageX === 'number'
      ? event.srcEvent.pageX
      : undefined;
  const y =
    typeof event.clientY === 'number'
      ? event.clientY
      : typeof event.pageY === 'number'
      ? event.pageY
      : typeof event.y === 'number'
      ? event.y
      : typeof event?.srcEvent?.clientY === 'number'
      ? event.srcEvent.clientY
      : typeof event?.srcEvent?.pageY === 'number'
      ? event.srcEvent.pageY
      : undefined;
  if (x === undefined || y === undefined) return null;
  return { x, y };
};

const buildPointerSortRows = (
  items: { item: any; rect: DOMRect }[]
) => {
  if (!items.length) return [];
  const sorted = [...items].sort((a, b) => {
    const topDiff = a.rect.top - b.rect.top;
    if (Math.abs(topDiff) > 1) return topDiff;
    return a.rect.left - b.rect.left;
  });
  const baseSize = Math.min(sorted[0].rect.width, sorted[0].rect.height);
  const rowTolerance = baseSize * POINTER_ROW_TOLERANCE_RATIO;
  const rows: { items: { item: any; rect: DOMRect }[]; top: number; bottom: number; mid: number }[] = [];
  let current = {
    items: [sorted[0]],
    top: sorted[0].rect.top,
    bottom: sorted[0].rect.bottom,
    mid: (sorted[0].rect.top + sorted[0].rect.bottom) / 2,
  };

  for (const entry of sorted.slice(1)) {
    if (Math.abs(entry.rect.top - current.top) <= rowTolerance) {
      current.items.push(entry);
      current.top = Math.min(current.top, entry.rect.top);
      current.bottom = Math.max(current.bottom, entry.rect.bottom);
      current.mid = (current.top + current.bottom) / 2;
    } else {
      current.items.sort((a, b) => a.rect.left - b.rect.left);
      rows.push(current);
      current = {
        items: [entry],
        top: entry.rect.top,
        bottom: entry.rect.bottom,
        mid: (entry.rect.top + entry.rect.bottom) / 2,
      };
    }
  }

  current.items.sort((a, b) => a.rect.left - b.rect.left);
  rows.push(current);
  return rows;
};

const getPointerInsertIndex = (
  pointer: { x: number; y: number },
  rows: { items: { item: any; rect: DOMRect }[]; top: number; bottom: number; mid: number }[]
) => {
  if (!rows.length) return 0;
  const total = rows.reduce((sum, row) => sum + row.items.length, 0);
  const first = rows[0];
  const last = rows[rows.length - 1];

  if (pointer.y < first.top - POINTER_EDGE_PADDING) {
    return 0;
  }
  if (pointer.y > last.bottom + POINTER_EDGE_PADDING) {
    return total;
  }

  let rowIndex = rows.length - 1;
  for (let i = 0; i < rows.length; i += 1) {
    if (pointer.y < rows[i].mid) {
      rowIndex = i;
      break;
    }
  }

  const row = rows[rowIndex];
  let colIndex = row.items.length;
  for (let i = 0; i < row.items.length; i += 1) {
    const rect = row.items[i].rect;
    const midX = rect.left + rect.width / 2;
    if (pointer.x < midX) {
      colIndex = i;
      break;
    }
  }

  const baseIndex = rows.slice(0, rowIndex).reduce((sum, r) => sum + r.items.length, 0);
  return baseIndex + colIndex;
};

const getOverlapRatio = (dragEl: HTMLElement, targetEl: HTMLElement) => {
  const dragRect = dragEl.getBoundingClientRect();
  const targetRect = targetEl.getBoundingClientRect();
  const dragArea = Math.max(1, dragRect.width * dragRect.height);
  const targetArea = Math.max(1, targetRect.width * targetRect.height);
  const baseArea = Math.max(1, Math.min(dragArea, targetArea));
  return calcOverlapArea(dragRect, targetRect) / baseArea;
};

const findTargetStats = (dragEl: HTMLElement) => {
  if (!gridRef.value) return null;
  const dragRect = dragEl.getBoundingClientRect();
  const dragArea = Math.max(1, dragRect.width * dragRect.height);
  let best: { el: HTMLElement; ratio: number } | null = null;
  const elements = Array.from(gridRef.value.querySelectorAll('.grid-item')) as HTMLElement[];
  for (const el of elements) {
    if (el === dragEl) continue;
    const rect = el.getBoundingClientRect();
    const targetArea = Math.max(1, rect.width * rect.height);
    const baseArea = Math.max(1, Math.min(dragArea, targetArea));
    const ratio = calcOverlapArea(dragRect, rect) / baseArea;
    if (!best || ratio > best.ratio) {
      best = { el, ratio };
    }
  }
  return { best };
};

const resolveGroupIdByItem = (item: { id: string; type: GridItemType }) => {
  if (item.type === 'icon') {
    return layoutStore.icons.find(icon => icon.id === item.id)?.groupId ?? activeGroupId.value;
  }
  if (item.type === 'widget') {
    return widgetsStore.widgets.find(widget => widget.id === item.id)?.groupId ?? activeGroupId.value;
  }
  if (item.type === 'folder') {
    return foldersStore.folders.find(folder => folder.id === item.id)?.groupId ?? activeGroupId.value;
  }
  return activeGroupId.value;
};

const addItemToFolderState = (folderId: string, item: { id: string; type: 'icon' | 'widget' }) => {
  const folderGroup = foldersStore.folders.find(f => f.id === folderId)?.groupId ?? activeGroupId.value;
  if (item.type === 'icon') {
    layoutStore.updateIcon(item.id, { folderId, groupId: folderGroup });
  } else {
    widgetsStore.updateWidgetFolder(item.id, folderId, folderGroup);
  }
  foldersStore.addItem(folderId, { id: item.id, type: item.type });
};

const removeItemFromFolderState = (folderId: string, item: { id: string; type: 'icon' | 'widget' }) => {
  if (item.type === 'icon') {
    layoutStore.updateIcon(item.id, { folderId: undefined });
  } else {
    widgetsStore.updateWidgetFolder(item.id, undefined);
  }
  foldersStore.removeItem(folderId, item.id, item.type);
};

const removeEntryFromOrder = (order: GridOrderEntry[], item: { id: string; type: GridItemType }) =>
  order.filter(entry => !(entry.id === item.id && entry.type === item.type));

const entryKey = (entry: GridOrderEntry) => `${entry.type}:${entry.id}`;

const getVisibleOrderEntries = (): GridOrderEntry[] => {
  if (!grid.value) return [];
  const items = grid.value.getItems();
  const nextOrder: GridOrderEntry[] = [];
  for (const item of items) {
    const element = item.getElement() as HTMLElement;
    const id = element?.dataset?.id;
    const type = element?.dataset?.type as GridItemType | undefined;
    if (id && (type === 'icon' || type === 'widget' || type === 'folder')) {
      nextOrder.push({ id, type });
    }
  }
  return nextOrder;
};

const mergeVisibleOrder = (visibleOrder: GridOrderEntry[], baseOrder: GridOrderEntry[]) => {
  const visibleKeys = new Set(visibleOrder.map(entryKey));
  const visibleQueue = [...visibleOrder];
  const merged: GridOrderEntry[] = [];

  for (const entry of baseOrder) {
    if (visibleKeys.has(entryKey(entry))) {
      const nextEntry = visibleQueue.shift();
      if (nextEntry) {
        merged.push(nextEntry);
      }
    } else {
      merged.push(entry);
    }
  }

  const mergedKeys = new Set(merged.map(entryKey));
  for (const entry of visibleQueue) {
    const key = entryKey(entry);
    if (!mergedKeys.has(key)) {
      merged.push(entry);
      mergedKeys.add(key);
    }
  }

  return merged;
};

  const createFolderFromItems = (
    source: { id: string; type: 'icon' | 'widget' },
    target: { id: string; type: 'icon' | 'widget' }
  ) => {
    const groupId = resolveGroupIdByItem(target);
    const folder = foldersStore.addFolder({
      name: '文件夹',
      groupId,
      size: DEFAULT_FOLDER_SIZE,
      items: [
        { id: target.id, type: target.type },
        { id: source.id, type: source.type },
      ],
    });
    addItemToFolderState(folder.id, target);
    addItemToFolderState(folder.id, source);
    markGridEntriesHidden([
      { id: target.id, type: target.type },
      { id: source.id, type: source.type },
    ]);

      const visibleOrder = getVisibleOrderEntries();
      const sourceIndex = visibleOrder.findIndex(entry => entry.id === source.id && entry.type === source.type);
      const targetIndex = visibleOrder.findIndex(entry => entry.id === target.id && entry.type === target.type);
      let insertIndex = targetIndex >= 0 ? targetIndex : visibleOrder.length;
      if (sourceIndex !== -1 && sourceIndex < insertIndex) {
        insertIndex -= 1;
      }

      const nextVisible = visibleOrder.filter(
        entry =>
          !(entry.id === source.id && entry.type === source.type) &&
          !(entry.id === target.id && entry.type === target.type)
      );
      if (insertIndex < 0) insertIndex = 0;
      if (insertIndex > nextVisible.length) insertIndex = nextVisible.length;
      nextVisible.splice(insertIndex, 0, { id: folder.id, type: 'folder' });

      const finalOrder = mergeVisibleOrder(nextVisible, gridOrder.value);
      applyGridOrder(finalOrder, true);
      mergeOrderSnapshot.value = finalOrder.slice();
      mergePendingFolderId.value = folder.id;
      mergePendingInsertIndex.value = insertIndex;
      removeGridEntries([
        { id: target.id, type: target.type },
        { id: source.id, type: source.type },
      ]);
    };

  const moveItemIntoFolder = (folderId: string, item: { id: string; type: 'icon' | 'widget' }) => {
    const folder = foldersStore.folders.find(f => f.id === folderId);
    if (!folder) return;
    addItemToFolderState(folderId, item);
    markGridEntriesHidden([{ id: item.id, type: item.type }]);
    removeGridEntries([{ id: item.id, type: item.type }]);
    const nextOrder = removeEntryFromOrder(gridOrder.value, item);
    applyGridOrder(nextOrder, true);
    refreshLayout(true);
  };

const handleMergeDrop = () => {
  const dragId = draggingMeta.value.id;
  const dragType = draggingMeta.value.type;
  if (!dragId || !dragType) return;
  let targetEl = mergeTargetEl.value;
  if (!mergeActive.value || !targetEl) {
    targetEl = hoverTargetEl.value;
    const dragEl = draggingEl.value;
    if (!targetEl && dragEl) {
      const stats = findTargetStats(dragEl);
      const best = stats?.best ?? null;
      if (best && best.ratio >= OVERLAP_MERGE_THRESHOLD) {
        targetEl = best.el;
      }
    }
  }
  if (!targetEl) {
    clearHoverTarget();
    return;
  }
  if (!mergeActive.value && draggingEl.value) {
    const ratio = getOverlapRatio(draggingEl.value, targetEl);
    if (ratio < OVERLAP_DROP_THRESHOLD) {
      clearHoverTarget();
      return;
    }
  }
  const targetId = targetEl.dataset.id;
  const targetType = targetEl.dataset.type as GridItemType | undefined;
  if (!targetId || !targetType) return;
  if (targetId === dragId && targetType === dragType) return;
  if (dragType === 'folder') return; // no nesting folders for now

  if (targetType === 'folder') {
    moveItemIntoFolder(targetId, { id: dragId, type: dragType });
    mergeCommitted.value = true;
    return;
  }
  if (targetType === 'icon' || targetType === 'widget') {
    if (dragType === 'icon' || dragType === 'widget') {
      createFolderFromItems({ id: dragId, type: dragType }, { id: targetId, type: targetType });
      mergeCommitted.value = true;
    }
  }
};

const openFolder = (folderId: string) => {
  openFolderId.value = folderId;
};

const handleRenameFolder = (payload: { id: string; name: string }) => {
  foldersStore.renameFolder(payload.id, payload.name);
};

const handleOpenChild = (child: FolderChild) => {
  if (child.type === 'icon') {
    const targetMode = settingsStore.openInNewTab ? '_blank' : '_self';
    window.open(child.icon.url, targetMode);
  }
};

const handleRemoveFromFolder = (payload: { folderId: string; child: FolderChild }) => {
  const { folderId, child } = payload;
  const baseOrder = gridOrder.value.slice();
  removeItemFromFolderState(folderId, { id: child.id, type: child.type });
  let nextOrder = removeEntryFromOrder(baseOrder, { id: child.id, type: child.type });
  const folderAfter = foldersStore.folders.find(f => f.id === folderId);
  const folderIndex = baseOrder.findIndex(entry => entry.id === folderId && entry.type === 'folder');
  const safeFolderIndex = folderIndex >= 0 ? folderIndex : nextOrder.length;
  if (!folderAfter) {
    nextOrder = removeEntryFromOrder(nextOrder, { id: folderId, type: 'folder' });
    nextOrder.splice(safeFolderIndex, 0, { id: child.id, type: child.type });
    applyGridOrder(nextOrder, true);
    refreshLayout(true);
    return;
  }
  if (folderAfter.items.length === 1) {
    const remaining = folderAfter.items[0];
    removeItemFromFolderState(folderId, { id: remaining.id, type: remaining.type });
    nextOrder = removeEntryFromOrder(nextOrder, { id: folderId, type: 'folder' });
    nextOrder = removeEntryFromOrder(nextOrder, { id: remaining.id, type: remaining.type });
    nextOrder.splice(safeFolderIndex, 0, { id: remaining.id, type: remaining.type });
    nextOrder.splice(safeFolderIndex + 1, 0, { id: child.id, type: child.type });
    const nextEntering = new Set(enteringKeys.value);
    nextEntering.add(`${remaining.type}:${remaining.id}`);
    nextEntering.add(`${child.type}:${child.id}`);
    enteringKeys.value = nextEntering;
    applyGridOrder(nextOrder, true);
    refreshLayout(true);
    return;
  }
  const insertIndex = safeFolderIndex + 1;
  nextOrder.splice(insertIndex, 0, { id: child.id, type: child.type });
  applyGridOrder(nextOrder, true);
  refreshLayout(true);
};

const clearFolderChildren = (folderId: string) => {
  const released: { id: string; type: 'icon' | 'widget' }[] = [];
  for (const icon of layoutStore.icons) {
    if (icon.folderId === folderId) {
      layoutStore.updateIcon(icon.id, { folderId: undefined });
      released.push({ id: icon.id, type: 'icon' });
    }
  }
  for (const widget of widgetsStore.widgets) {
    if (widget.folderId === folderId) {
      widgetsStore.updateWidgetFolder(widget.id, undefined);
      released.push({ id: widget.id, type: 'widget' });
    }
  }
  const folder = foldersStore.folders.find(item => item.id === folderId);
  if (folder) {
    for (const child of folder.items) {
      if (child.type === 'icon') {
        layoutStore.updateIcon(child.id, { folderId: undefined });
        if (!released.some(entry => entry.id === child.id && entry.type === 'icon')) {
          released.push({ id: child.id, type: 'icon' });
        }
      } else {
        widgetsStore.updateWidgetFolder(child.id, undefined);
        if (!released.some(entry => entry.id === child.id && entry.type === 'widget')) {
          released.push({ id: child.id, type: 'widget' });
        }
      }
    }
  }
  return released;
};

const animateDeleteEntry = (id: string, type: GridItemType) => {
  const el = gridRef.value?.querySelector(
    `.grid-item[data-type="${type}"][data-id="${id}"]`
  ) as HTMLElement | null;
  if (!el) return false;
  const content = el.querySelector('.grid-item-content') as HTMLElement | null;
  const target = content ?? el;
  el.classList.add('is-deleting');
  el.style.pointerEvents = 'none';
  target.style.transition = `transform ${DELETE_ANIM_MS}ms ${DELETE_ANIM_EASING}, opacity ${DELETE_ANIM_MS}ms ${DELETE_ANIM_EASING}`;
  target.style.transform = 'scale(1)';
  target.style.opacity = '1';
  // Force reflow so the transition kicks in.
  void target.offsetWidth;
  requestAnimationFrame(() => {
    target.style.transform = 'scale(0)';
    target.style.opacity = '0';
  });
  return true;
};

watch(
  () => uiStore.pendingDelete,
  payload => {
    if (!payload) return;
    const { id, type } = payload;
    const hasAnimation = animateDeleteEntry(id, type);
    const finalize = () => {
      if (type === 'icon') {
        layoutStore.deleteIcon(id);
      } else if (type === 'widget') {
        widgetsStore.deleteWidget(id);
      } else if (type === 'folder') {
        const released = clearFolderChildren(id);
        foldersStore.deleteFolder(id);
        if (released.length) {
          const nextEntering = new Set(enteringKeys.value);
          for (const entry of released) {
            nextEntering.add(`${entry.type}:${entry.id}`);
          }
          enteringKeys.value = nextEntering;
        }
      }
      uiStore.clearPendingDelete();
      refreshLayout(true);
    };
    if (hasAnimation) {
      window.setTimeout(finalize, DELETE_ANIM_MS);
    } else {
      finalize();
    }
  }
);

const persistOrderFromGrid = async () => {
  if (!grid.value) return;
  const items = grid.value.getItems();
  const nextOrder: GridOrderEntry[] = [];
  for (const item of items) {
    const element = item.getElement() as HTMLElement;
    const id = element.dataset.id;
    const type = element.dataset.type as GridItemType | undefined;
    if (id && (type === 'icon' || type === 'widget' || type === 'folder')) {
      nextOrder.push({ id, type });
    }
  }
  if (nextOrder.length) {
    if (!gridOrder.value.length) {
      applyGridOrder(nextOrder, true);
    } else {
      const entryKey = (entry: GridOrderEntry) => `${entry.type}:${entry.id}`;
      const visibleKeys = new Set(nextOrder.map(entryKey));
      const visibleQueue = [...nextOrder];
      const merged: GridOrderEntry[] = [];

      for (const entry of gridOrder.value) {
        if (visibleKeys.has(entryKey(entry))) {
          const nextEntry = visibleQueue.shift();
          if (nextEntry) {
            merged.push(nextEntry);
          }
        } else {
          merged.push(entry);
        }
      }

      const mergedKeys = new Set(merged.map(entryKey));
      for (const entry of visibleQueue) {
        const key = entryKey(entry);
        if (!mergedKeys.has(key)) {
          merged.push(entry);
          mergedKeys.add(key);
        }
      }

      applyGridOrder(merged, true);
    }
  }
  await nextTick();
  grid.value?.synchronize();
  grid.value?.layout();
};

const syncGridItems = () => {
  if (!grid.value || !gridRef.value) return;
  const existingItems = grid.value.getItems() ?? [];
  const existingElements = new Set(existingItems.map((item: any) => item.getElement()));
  const domElements = Array.from(gridRef.value.querySelectorAll('.grid-item'));
  const toAdd = domElements.filter(element => !existingElements.has(element));
  if (toAdd.length) {
    let pendingFolderEl: HTMLElement | null = null;
    const pendingFolderId = mergePendingFolderId.value;
    const insertIndex = mergePendingInsertIndex.value;
    if (pendingFolderId) {
      pendingFolderEl =
        toAdd.find(
          element =>
            element.dataset?.type === 'folder' &&
            element.dataset?.id === pendingFolderId
        ) ?? null;
    }
    if (pendingFolderEl && insertIndex !== null && insertIndex !== undefined) {
      grid.value.add([pendingFolderEl], { layout: false, index: insertIndex });
      mergePendingFolderId.value = null;
      mergePendingInsertIndex.value = null;
      const remaining = toAdd.filter(element => element !== pendingFolderEl);
      if (remaining.length) {
        grid.value.add(remaining, { layout: false });
      }
    } else {
      grid.value.add(toAdd, { layout: false });
    }
  }
  const toRemove = existingItems.filter((item: any) => !gridRef.value?.contains(item.getElement()));
  if (toRemove.length) {
    grid.value.remove(toRemove, { removeElements: false });
  }
};

  const refreshLayout = async (instant = false) => {
    await nextTick();
    syncGridItems();
    grid.value?.synchronize();
    grid.value?.refreshItems();
  if (grid.value?._settings?.layout) {
    grid.value._settings.layout.fillGaps = true;
  }
    if (instant) {
      grid.value?.layout(true);
    } else {
      grid.value?.layout();
    }
  };

  const sortGridToOrder = (order: GridOrderEntry[], instant = false) => {
    if (!grid.value) return;
    syncGridItems();
    grid.value.synchronize();
    grid.value.refreshItems();
    const items = grid.value.getItems() ?? [];
    if (!items.length) return;
    const itemMap = new Map<string, any>();
    for (const item of items) {
      const el = item.getElement() as HTMLElement | null;
      const id = el?.dataset?.id;
      const type = el?.dataset?.type;
      if (id && type) {
        itemMap.set(`${type}:${id}`, item);
      }
    }
    const ordered: any[] = [];
    for (const entry of order) {
      const item = itemMap.get(`${entry.type}:${entry.id}`);
      if (item) ordered.push(item);
    }
    const orderedSet = new Set(ordered);
    for (const item of items) {
      if (!orderedSet.has(item)) ordered.push(item);
    }
    grid.value.sort(ordered, { layout: instant ? 'instant' : true });
  };

  const getGridItemsByEntries = (entries: { id: string; type: GridItemType }[]) => {
    if (!grid.value) return [];
    const items = grid.value.getItems() ?? [];
    const targetKeys = new Set(entries.map(entry => `${entry.type}:${entry.id}`));
    const matches: any[] = [];
    for (const item of items) {
      const el = item.getElement() as HTMLElement | null;
      const id = el?.dataset?.id;
      const type = el?.dataset?.type;
      if (id && type && targetKeys.has(`${type}:${id}`)) {
        matches.push(item);
      }
    }
    return matches;
  };

  const hideGridEntries = (entries: { id: string; type: GridItemType }[]) => {
    if (!grid.value) return;
    const items = getGridItemsByEntries(entries);
    if (items.length) {
      grid.value.hide(items, { instant: true, layout: false, syncWithLayout: false });
    }
  };

  const markGridEntriesHidden = (entries: { id: string; type: GridItemType }[]) => {
    const keySet = new Set(entries.map(entry => `${entry.type}:${entry.id}`));
    for (const entry of entries) {
      const el = gridRef.value?.querySelector(
        `.grid-item[data-type="${entry.type}"][data-id="${entry.id}"]`
      ) as HTMLElement | null;
      if (el) {
        el.dataset.mergeHidden = '1';
        el.style.setProperty('display', 'none', 'important');
      }
    }
    const dragEl = draggingEl.value;
    if (dragEl) {
      const dragKey = `${dragEl.dataset?.type}:${dragEl.dataset?.id}`;
      if (keySet.has(dragKey)) {
        dragEl.dataset.mergeHidden = '1';
        dragEl.style.setProperty('display', 'none', 'important');
      }
    }
    hideGridEntries(entries);
  };

  const removeGridEntries = (entries: { id: string; type: GridItemType }[]) => {
    if (!grid.value) return;
    const items = getGridItemsByEntries(entries);
    if (items.length) {
      grid.value.remove(items, { removeElements: true, layout: false });
    }
  };

  const waitForNextFrame = () =>
    new Promise<void>(resolve => {
      requestAnimationFrame(() => resolve());
    });

  const waitForFolderElement = async (folderId?: string | null, attempts = 8) => {
    if (!folderId) return;
    for (let i = 0; i < attempts; i += 1) {
      const el = gridRef.value?.querySelector(
        `.grid-item[data-type="folder"][data-id="${folderId}"]`
      );
      if (el) return;
      await waitForNextFrame();
    }
  };

  const pruneGridToOrder = (order: GridOrderEntry[]) => {
    if (!grid.value) return;
    const allowed = new Set(order.map(entry => `${entry.type}:${entry.id}`));
    const items = grid.value.getItems() ?? [];
    const toRemove: any[] = [];
    for (const item of items) {
      const el = item.getElement() as HTMLElement | null;
      const id = el?.dataset?.id;
      const type = el?.dataset?.type;
      if (id && type && !allowed.has(`${type}:${id}`)) {
        toRemove.push(item);
      }
    }
    if (toRemove.length) {
      grid.value.remove(toRemove, { removeElements: true });
    }
  };

  const reconcileGridAfterMerge = async (order: GridOrderEntry[], folderId?: string | null) => {
    await nextTick();
    await waitForFolderElement(folderId);
    pruneGridToOrder(order);
    sortGridToOrder(order, true);
  };

const markResized = (type: GridItemType, id: string) => {
  pendingResizedKeys.add(`${type}:${id}`);
};

const syncSizeMap = (
  items: { id: string; size: GridItemSize }[],
  map: Map<string, GridItemSize>
) => {
  const changed: string[] = [];
  const seen = new Set<string>();
  for (const item of items) {
    const prev = map.get(item.id);
    if (prev && prev !== item.size) {
      changed.push(item.id);
    }
    map.set(item.id, item.size);
    seen.add(item.id);
  }
  for (const id of Array.from(map.keys())) {
    if (!seen.has(id)) {
      map.delete(id);
    }
  }
  return changed;
};

onMounted(async () => {
  loadGridOrder();
  ensureOrderConsistency();
  previousIconIds.value = new Set(layoutStore.icons.map(icon => icon.id));
  previousWidgetIds.value = new Set(widgetsStore.widgets.map(widget => widget.id));
  await nextTick();
  if (!gridRef.value) return;
  grid.value = new Muuri(gridRef.value, {
    items: '.grid-item',
    dragEnabled: true,
    dragStartPredicate: {
      distance: 5,
      delay: 0,
    },
    dragSort: true,
    dragSortLock: () => disableSort.value,
    dragSortPredicate: (item: any, event: any) => {
      if (disableSort.value) return null;
      const predicate = (Muuri as any).ItemDrag?.defaultSortPredicate;
      return predicate
        ? predicate(item, { threshold: 30, action: 'move', migrateAction: 'move' }, event)
        : null;
    },
    dragSortHeuristics: {
      sortInterval: 60,
      minDragDistance: 2,
      minBounceBackAngle: 0.5,
    },
    layout: {
      fillGaps: true,
      rounding: true,
    },
    layoutDuration: layoutAnimationMs,
    layoutEasing,
  });

  grid.value.on('dragStart', (item: any) => {
    isDragging.value = true;
    suppressClick.value = true;
    const el = item.getElement() as HTMLElement;
    draggingEl.value = el;
    draggingItemRef.value = item;
    draggingMeta.value = {
      id: el?.dataset?.id ?? null,
      type: (el?.dataset?.type as GridItemType) ?? null,
    };
    draggingPointerBackup.value = el.style.pointerEvents || null;
    el.style.pointerEvents = 'none';
    disableSort.value = false;
    mergeCommitted.value = false;
    mergeOrderSnapshot.value = null;
    mergePendingInsertIndex.value = null;
    mergePendingFolderId.value = null;
  });

  grid.value.on('dragMove', (item: any, event: any) => {
    const el = item.getElement() as HTMLElement;
    const nextPointer = resolvePointerFromEvent(event);
    if (nextPointer) {
      lastPointer.value = nextPointer;
    }
    const pointer = lastPointer.value;
    if (pointer) {
      pointerTargetEl.value = getPointerTargetFromRects(pointer, el);
    } else {
      pointerTargetEl.value = null;
    }

    overlapPendingEl.value = el;
    if (overlapRafId.value === null) {
      overlapRafId.value = window.requestAnimationFrame(() => {
        overlapRafId.value = null;
        const pending = overlapPendingEl.value;
        overlapPendingEl.value = null;
        if (!pending) return;
        const now = performance.now();
        const pointerTarget = pointerTargetEl.value;

        if (mergeActive.value) {
          const locked = mergeTargetEl.value;
          if (!locked || !locked.isConnected) {
            deactivateMergeTarget(true);
          } else if (pointerTarget && pointerTarget !== locked) {
            deactivateMergeTarget(true);
            mergeCandidateEl.value = pointerTarget;
            mergeCandidateAt.value = now;
            setHoverTarget(pointerTarget);
          } else if (!pointerTarget) {
            deactivateMergeTarget(true);
          } else {
            setHoverTarget(locked);
          }
        } else {
          if (pointerTarget && pointerTarget.isConnected) {
            setHoverTarget(pointerTarget);
            if (mergeCandidateEl.value === pointerTarget) {
              if (now - mergeCandidateAt.value >= OVERLAP_HOLD_MS) {
                activateMergeTarget(pointerTarget);
              }
            } else {
              mergeCandidateEl.value = pointerTarget;
              mergeCandidateAt.value = now;
            }
          } else {
            mergeCandidateEl.value = null;
            mergeCandidateAt.value = 0;
            clearHoverTarget();
          }
        }

        applySortLock(mergeActive.value);
      });
    }
  });

  grid.value.on('dragEnd', () => {
    isDragging.value = false;
    handleMergeDrop();
    deactivateMergeTarget(false);
    clickBlockUntil.value = Date.now() + 180;
    window.setTimeout(() => {
      suppressClick.value = false;
    }, 0);
    draggingMeta.value = { id: null, type: null };
    const el = draggingEl.value;
    if (el) {
      el.style.pointerEvents = draggingPointerBackup.value ?? '';
    }
    draggingPointerBackup.value = null;
    draggingEl.value = null;
    draggingItemRef.value = null;
    if (overlapRafId.value !== null) {
      cancelAnimationFrame(overlapRafId.value);
      overlapRafId.value = null;
      overlapPendingEl.value = null;
    }
    applySortLock(false);
    mergeCandidateEl.value = null;
    mergeCandidateAt.value = 0;
    lastPointer.value = null;
    pointerTargetEl.value = null;
  });

  grid.value.on('dragReleaseEnd', () => {
    if (mergeCommitted.value) {
      mergeCommitted.value = false;
      const order = mergeOrderSnapshot.value ?? gridOrder.value;
      const folderId = mergePendingFolderId.value;
      mergeOrderSnapshot.value = null;
      mergePendingFolderId.value = null;
      mergePendingInsertIndex.value = null;
      void reconcileGridAfterMerge(order, folderId);
    } else {
      persistOrderFromGrid();
    }
    clearHoverTarget();
  });

  grid.value.on('layoutStart', () => {
    if (!pendingResizedKeys.size) return;
    resizedKeys.value = new Set(pendingResizedKeys);
  });

  grid.value.on('layoutEnd', () => {
    if (pendingResizedKeys.size) {
      pendingResizedKeys.clear();
      resizedKeys.value = new Set<string>();
    }
    if (enteringKeys.value.size) {
      enteringKeys.value = new Set<string>();
    }
  });

  grid.value.layout(true);
});

watch(
  () => [layoutStore.icons.map(icon => icon.id), widgetsStore.widgets.map(widget => widget.id)],
  ([iconIds, widgetIds]) => {
    ensureOrderConsistency();
    const nextIconIds = new Set(iconIds);
    const nextWidgetIds = new Set(widgetIds);
    const addedIcons = iconIds.filter(id => !previousIconIds.value.has(id));
    const addedWidgets = widgetIds.filter(id => !previousWidgetIds.value.has(id));
    if (addedIcons.length || addedWidgets.length) {
      const nextEntering = new Set(enteringKeys.value);
      for (const id of addedIcons) nextEntering.add(`icon:${id}`);
      for (const id of addedWidgets) nextEntering.add(`widget:${id}`);
      enteringKeys.value = nextEntering;
    }
    previousIconIds.value = nextIconIds;
    previousWidgetIds.value = nextWidgetIds;
    const instant = addedIcons.length > 0 || addedWidgets.length > 0;
    refreshLayout(instant);
  }
);

watch(
  () => widgetsStore.widgets.map(widget => ({ id: widget.id, size: widget.size })),
  async () => {
    const changed = syncSizeMap(
      widgetsStore.widgets.map(widget => ({ id: widget.id, size: widget.size })),
      widgetSizeMap
    );
    for (const id of changed) {
      markResized('widget', id);
    }
    await refreshLayout();
  }
);

watch(
  () => layoutStore.icons.map(icon => ({ id: icon.id, size: icon.size ?? '1x1' })),
  async () => {
    const changed = syncSizeMap(
      layoutStore.icons.map(icon => ({ id: icon.id, size: icon.size ?? '1x1' })),
      iconSizeMap
    );
    for (const id of changed) {
      markResized('icon', id);
    }
    await refreshLayout();
  }
);

watch(
  () => foldersStore.folders.map(folder => ({ id: folder.id, size: folder.size ?? DEFAULT_FOLDER_SIZE })),
  async () => {
    const changed = syncSizeMap(
      foldersStore.folders.map(folder => ({ id: folder.id, size: folder.size ?? DEFAULT_FOLDER_SIZE })),
      folderSizeMap
    );
    for (const id of changed) {
      markResized('folder', id);
    }
    await refreshLayout();
  }
);

watch(
  () => foldersStore.folders.map(folder => ({ id: folder.id, items: folder.items.length, groupId: folder.groupId })),
  () => {
    ensureOrderConsistency();
    refreshLayout(true);
  },
  { deep: true }
);

watch(
  () => props.activeGroupId,
  () => {
    if (!grid.value) return;
    refreshLayout(true);
  }
);

watch(
  () => [
    settingsStore.layoutDensity,
    settingsStore.iconDensity,
    settingsStore.compactSidebar,
  ],
  () => {
    if (!grid.value) return;
    refreshLayout(true);
  }
);

onUnmounted(() => {
  pendingResizedKeys.clear();
  grid.value?.destroy();
  grid.value = null;
});
</script>

<template>
  <div
    ref="gridRef"
    class="grid-canvas"
    :style="{ '--layout-anim-ms': `${layoutAnimationMs}ms` }"
    @click.capture="handleClickCapture"
    @click="handleClick"
    @contextmenu.prevent="handleContextMenu"
  >
    <div
      v-for="item in orderedItems"
      :key="`${item.type}-${item.id}`"
      class="grid-item"
      :class="[
        sizeClassFor(item),
        {
          'is-resized': resizedKeys.has(`${item.type}:${item.id}`),
          'is-entering': enteringKeys.has(`${item.type}:${item.id}`),
          'is-drop-target': hoverTargetEl && hoverTargetEl.dataset?.id === item.id
        }
      ]"
      :data-id="item.id"
      :data-type="item.type"
    >
      <div class="grid-item-content">
        <WidgetCard v-if="item.type === 'widget'" :widget="item.widget" />
        <IconCard v-else-if="item.type === 'icon'" :icon="item.icon" />
        <FolderCard
          v-else
          :folder="item.folder"
          :children="item.children"
          @open="openFolder(item.folder.id)"
        />
      </div>
    </div>
  </div>
  <FolderDialog
    :open="!!currentFolder"
    :folder="currentFolder"
    :children="currentFolderChildren"
    @close="openFolderId = null"
    @rename="handleRenameFolder"
    @remove="handleRemoveFromFolder"
    @open-item="handleOpenChild"
  />
</template>

<style lang="scss" scoped>
@import '@/styles/tokens.scss';

.grid-canvas {
  width: 100%;
  position: relative;
  user-select: none;          /* 鏍囧噯 */
  -webkit-user-select: none;  /* Safari 娴忚鍣?*/
  -moz-user-select: none;     /* Firefox 娴忚鍣?*/
  -ms-user-select: none;      /* IE 10+/Edge 娴忚鍣?*/
  --cell-size: var(--grid-cell-size);
  --cell-gap: var(--grid-gap);
  --cell-gap-padding: max(var(--cell-gap), 0px);
  padding: calc(var(--cell-gap-padding) / 2);
  box-sizing: border-box;
}

.grid-item {
  position: absolute;
  width: var(--cell-size);
  height: var(--cell-size);
  margin: calc(var(--cell-gap) / 2);
  cursor: grab;
  will-change: transform;
  transition: opacity 160ms ease;
}

.grid-item.is-resized {
  overflow: visible;
}

.grid-item.is-entering {
  opacity: 0;
}

.grid-item.is-entering .grid-item-content {
  animation: grid-item-enter 220ms $motion-easing-standard;
}

@keyframes grid-item-enter {
  0% {
    transform: scale(0.7);
    opacity: 0;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

.grid-item.size-1x2 {
  height: calc(var(--cell-size) * 2 + var(--cell-gap));
}

.grid-item.size-2x1 {
  width: calc(var(--cell-size) * 2 + var(--cell-gap));
}

.grid-item.size-2x2 {
  width: calc(var(--cell-size) * 2 + var(--cell-gap));
  height: calc(var(--cell-size) * 2 + var(--cell-gap));
}

.grid-item.size-2x4 {
  width: calc(var(--cell-size) * 2 + var(--cell-gap));
  height: calc(var(--cell-size) * 4 + var(--cell-gap) * 3);
}

.grid-item-content {
  width: 100%;
  height: 100%;
  transform-origin: center;
  border-radius: var(--icon-radius, #{$border-radius-small});
  transform: scale(1);
  opacity: 1;
  transition: transform 240ms $motion-easing-standard, opacity 240ms $motion-easing-standard;
  position: relative;
}

.grid-item.is-deleting .grid-item-content {
  transform: scale(0);
  opacity: 0;
}

.grid-item.is-deleting {
  pointer-events: none;
}

.grid-item.is-resized .grid-item-content {
  animation: grid-item-resize-zoom var(--layout-anim-ms) $motion-easing-standard;
}

@keyframes grid-item-resize-zoom {
  0% {
    transform: scale(0.97);
  }
  100% {
    transform: scale(1);
  }
}

.grid-item.muuri-item-dragging {
  z-index: $z-index-menu;
  cursor: grabbing;
  transition: none;
}

.grid-item-content::before {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: var(--icon-radius, #{$border-radius-small});
  background: rgba(255, 255, 255, 0.32);
  box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18), inset 0 0 0 1px rgba(255, 255, 255, 0.5);
  opacity: 0;
  transform: scale(0.96);
  transition: opacity 160ms $motion-easing-standard, transform 160ms $motion-easing-standard;
  pointer-events: none;
}

.grid-item.is-drop-target .grid-item-content::before {
  opacity: 1;
  transform: scale(1);
}
</style>

