<script setup>
import { computed, watch, watchEffect } from "vue";
import { useTreeState } from "./useTreeState";
import TreeNode from "./TreeNode.vue";
defineOptions({ name: "UiTree" });
const props = defineProps({
  roots: { type: Array, required: true },
  getKey: { type: Function, required: true },
  getChildren: { type: Function, required: false, default: void 0 },
  loadChildren: { type: Function, required: false, default: void 0 },
  selectionMode: { type: String, required: false, default: "none" },
  cascade: { type: String, required: false, default: "independent" },
  modelValue: { type: null, required: false, default: void 0 },
  selectable: { type: Function, required: false, default: () => true },
  draggable: { type: [Function, Boolean], required: false, default: false },
  canDrop: { type: Function, required: false, default: void 0 },
  filterPredicate: { type: Function, required: false, default: void 0 },
  filterQuery: { type: String, required: false, default: "" },
  initialExpanded: { type: Array, required: false, default: () => [] },
  showFocusedRow: { type: Boolean, required: false, default: true }
});
const emit = defineEmits(["update:modelValue", "update:roots", "update:expanded", "move"]);
defineSlots();
function readInitialSelection() {
  if (props.selectionMode === "none") return [];
  if (props.selectionMode === "single") {
    const v2 = props.modelValue;
    return typeof v2 === "string" ? [v2] : [];
  }
  const v = props.modelValue;
  return Array.isArray(v) ? v.slice() : [];
}
const rootsRef = computed(() => props.roots);
function setRoots(next) {
  emit("update:roots", next);
}
let lastEmittedSelection = readInitialSelection();
function onSelectionChange(next) {
  lastEmittedSelection = next;
  if (props.selectionMode === "single") {
    emit("update:modelValue", next[0]);
    return;
  }
  emit("update:modelValue", next);
}
const state = useTreeState({
  rootsRef,
  setRoots,
  getKey: props.getKey,
  getChildren: props.getChildren,
  loadChildren: props.loadChildren,
  selectionMode: () => props.selectionMode,
  cascade: () => props.cascade,
  selectable: (n, d) => props.selectable(n, d),
  initialSelection: readInitialSelection,
  initialExpanded: () => props.initialExpanded ?? [],
  onSelectionChange
});
watch(rootsRef, () => state.syncRoots());
watchEffect(() => {
  const next = [];
  for (const k of state.expanded) next.push(k);
  emit("update:expanded", next);
});
watch(
  () => props.modelValue,
  (v) => {
    const desired = props.selectionMode === "single" ? typeof v === "string" ? [v] : [] : Array.isArray(v) ? v.slice() : [];
    if (sameSet(desired, lastEmittedSelection)) return;
    state.selected.clear();
    for (const k of desired) state.selected.add(k);
    lastEmittedSelection = desired;
  }
);
function sameSet(a, b) {
  if (a.length !== b.length) return false;
  const s = new Set(a);
  for (const k of b) if (!s.has(k)) return false;
  return true;
}
const tree = {
  insert: (parentKey, node, index) => state.insert(parentKey, node, index),
  remove: (key) => state.remove(key),
  move: (key, newParentKey, index) => {
    const loc = state.locate(key);
    if (!loc) return;
    const payload = {
      key,
      fromParentKey: loc.parent ? props.getKey(loc.parent) : null,
      fromIndex: loc.index,
      toParentKey: newParentKey,
      toIndex: index
    };
    state.move(key, newParentKey, index);
    emit("move", payload);
  },
  update: (key, patch) => state.update(key, patch),
  expand: (key) => state.expand(key),
  collapse: (key) => state.collapse(key),
  select: (key) => state.selectKey(key),
  deselect: (key) => state.deselectKey(key),
  invalidate: (key) => state.invalidate(key),
  retry: (key) => state.retry(key),
  findPath: (key) => state.findPath(key),
  snapshot: () => state.snapshot()
};
defineExpose(tree);
const isExpanded = (k) => state.expanded.has(k);
const isSelectedKey = (k) => state.selected.has(k);
const isVisuallySelected = (n, d) => state.isVisuallySelected(n, d);
const isIndeterminate = (n, d) => state.isIndeterminate(n, d);
const resolvedChildren = (n) => state.resolvedChildren(n);
const loadStateOf = (k) => state.loadState.get(k);
const isExpandable = (n) => state.isExpandable(n);
const draggableFn = (n, d) => typeof props.draggable === "function" ? props.draggable(n, d) : props.draggable;
function handleToggleExpand(key) {
  state.toggleExpand(key);
}
function handleToggleSelect(key) {
  if (props.selectionMode === "none") return;
  if (props.selectionMode === "single") {
    const loc = state.locate(key);
    if (!loc || !props.selectable(loc.node, loc.depth)) return;
    state.selectKey(key);
    return;
  }
  state.toggleMulti(key);
}
function handleFocusRow(key) {
  state.focusedKey.value = key;
}
function flatVisible() {
  return state.flattenVisible(
    props.filterPredicate && props.filterQuery ? (n) => {
      if (props.filterPredicate(n, props.filterQuery)) return true;
      const kids = resolvedChildren(n);
      if (!kids || kids.length === 0) return false;
      const stack = [...kids];
      while (stack.length) {
        const x = stack.pop();
        if (props.filterPredicate(x, props.filterQuery)) return true;
        const k = resolvedChildren(x);
        if (k && k.length) stack.push(...k);
      }
      return false;
    } : void 0
  );
}
function moveFocus(delta) {
  const list = flatVisible();
  if (!list.length) return;
  const cur = state.focusedKey.value;
  let idx = list.findIndex((r) => r.key === cur);
  if (idx === -1) idx = delta > 0 ? -1 : list.length;
  const next = Math.max(0, Math.min(list.length - 1, idx + delta));
  state.focusedKey.value = list[next].key;
}
function focusEdge(end) {
  const list = flatVisible();
  if (!list.length) return;
  state.focusedKey.value = (end === "home" ? list[0] : list[list.length - 1]).key;
}
function onKeydown(e) {
  if (props.selectionMode === "none" && !["ArrowUp", "ArrowDown", "Home", "End"].includes(e.key)) {
  }
  switch (e.key) {
    case "ArrowDown":
      e.preventDefault();
      moveFocus(1);
      break;
    case "ArrowUp":
      e.preventDefault();
      moveFocus(-1);
      break;
    case "ArrowRight": {
      e.preventDefault();
      const k = state.focusedKey.value;
      if (!k) return;
      const loc = state.locate(k);
      if (!loc) return;
      if (state.isExpandable(loc.node) && !state.expanded.has(k)) {
        state.expand(k);
      } else {
        moveFocus(1);
      }
      break;
    }
    case "ArrowLeft": {
      e.preventDefault();
      const k = state.focusedKey.value;
      if (!k) return;
      if (state.expanded.has(k)) {
        state.collapse(k);
        return;
      }
      const loc = state.locate(k);
      if (loc?.parent) state.focusedKey.value = props.getKey(loc.parent);
      break;
    }
    case "Home":
      e.preventDefault();
      focusEdge("home");
      break;
    case "End":
      e.preventDefault();
      focusEdge("end");
      break;
    case " ":
    // Space
    case "Enter": {
      e.preventDefault();
      const k = state.focusedKey.value;
      if (!k) return;
      handleToggleSelect(k);
      break;
    }
  }
}
const isEmpty = computed(() => {
  if (state.rootsView.value.length > 0) return false;
  return true;
});
const selectedPanePayload = computed(() => ({
  selected: state.selectedNodes.value,
  tree
}));
</script>

<template>
  <div
    role="tree"
    :aria-multiselectable="selectionMode === 'multi' ? 'true' : void 0"
    tabindex="0"
    data-slot="tree"
    class="outline-none"
    @keydown="onKeydown"
  >
    <div
      v-if="$slots['selected-pane']"
      data-slot="tree-selected-pane"
    >
      <slot
        name="selected-pane"
        v-bind="selectedPanePayload"
      />
    </div>

    <template v-if="isEmpty">
      <div data-slot="tree-empty">
        <slot name="empty" />
      </div>
    </template>
    <template v-else>
      <ul
        role="group"
        class="m-0 list-none p-0"
      >
        <TreeNode
          v-for="(node, idx) in state.rootsView.value"
          :key="getKey(node)"
          :node="node"
          :depth="0"
          :pos-in-set="idx + 1"
          :set-size="state.rootsView.value.length"
          :parent-key="null"
          :get-key="getKey"
          :get-children="getChildren"
          :has-loader="!!loadChildren"
          :selection-mode="selectionMode"
          :cascade="cascade"
          :selectable="selectable"
          :draggable="draggableFn"
          :is-expanded="isExpanded"
          :is-selected-key="isSelectedKey"
          :is-visually-selected="isVisuallySelected"
          :is-indeterminate="isIndeterminate"
          :resolved-children="resolvedChildren"
          :load-state-of="loadStateOf"
          :filter-predicate="filterPredicate"
          :filter-query="filterQuery"
          :is-expandable="isExpandable"
          :tree="tree"
          :focused-key="state.focusedKey.value"
          :show-focused-row="showFocusedRow"
          @toggle-expand="handleToggleExpand"
          @toggle-select="handleToggleSelect"
          @focus-row="handleFocusRow"
        >
          <!-- Forward node/actions/error/loading-children explicitly.
               (`empty` and `selected-pane` are root-only and not propagated
               down through child rows.) -->
          <template
            v-if="$slots.node"
            #node="slotProps"
          >
            <slot
              name="node"
              v-bind="slotProps"
            />
          </template>
          <template
            v-if="$slots.actions"
            #actions="slotProps"
          >
            <slot
              name="actions"
              v-bind="slotProps"
            />
          </template>
          <template
            v-if="$slots.error"
            #error="slotProps"
          >
            <slot
              name="error"
              v-bind="slotProps"
            />
          </template>
          <template
            v-if="$slots['loading-children']"
            #loading-children="slotProps"
          >
            <slot
              name="loading-children"
              v-bind="slotProps"
            />
          </template>
        </TreeNode>
      </ul>
    </template>
  </div>
</template>
