<script setup>
import { ref, computed, provide, watch, shallowRef } from "vue";
import { useNamespace, useLocale } from "@yh-ui/hooks";
import { useComponentTheme } from "@yh-ui/theme";
import { treeProps, treeEmits, TREE_INJECTION_KEY } from "./tree";
import TreeNodeComponent from "./tree-node.vue";
defineOptions({ name: "YhTree" });
const props = defineProps(treeProps);
const emit = defineEmits(treeEmits);
const ns = useNamespace("tree");
const { t } = useLocale();
const { themeStyle } = useComponentTheme(
  "tree",
  computed(() => props.themeOverrides)
);
const expandedKeys = ref(new Set(props.defaultExpandedKeys));
const checkedKeys = ref(new Set(props.defaultCheckedKeys));
const currentNodeKey = ref(props.currentNodeKey);
const nodeMap = shallowRef(/* @__PURE__ */ new Map());
const treeNodes = ref([]);
const scrollTop = ref(0);
const handleScroll = (e) => {
  scrollTop.value = e.target.scrollTop;
};
const totalHeight = computed(() => flattenedNodes.value.length * props.itemHeight);
const startIndex = computed(() => Math.floor(scrollTop.value / props.itemHeight));
const visibleCount = computed(() => Math.ceil(props.height / props.itemHeight));
const endIndex = computed(() => startIndex.value + visibleCount.value);
const visibleNodes = computed(
  () => flattenedNodes.value.slice(startIndex.value, endIndex.value + 1)
);
const offset = computed(() => startIndex.value * props.itemHeight);
const buildTree = (data, parent = null, level = 0) => {
  const {
    label: labelKey = "label",
    children: childrenKey = "children",
    disabled: disabledKey = "disabled"
  } = props.props || {};
  return data.map((item) => {
    const key = item[props.nodeKey] ?? item.key ?? item.id;
    const node = {
      ...item,
      key,
      label: String(item[labelKey] ?? item.label ?? ""),
      disabled: !!item[disabledKey] || !!item.disabled,
      level,
      parent,
      expanded: props.defaultExpandAll || expandedKeys.value.has(key),
      checked: checkedKeys.value.has(key),
      indeterminate: false,
      visible: true,
      loading: false,
      loaded: !props.lazy,
      rawData: item,
      children: void 0
    };
    nodeMap.value.set(key, node);
    const childrenData = item[childrenKey];
    if (childrenData && childrenData.length > 0) {
      node.children = buildTree(childrenData, node, level + 1);
    }
    return node;
  });
};
watch(
  () => props.data,
  (newData) => {
    nodeMap.value.clear();
    treeNodes.value = buildTree(newData);
    if (currentNodeKey.value) {
      const node = nodeMap.value.get(currentNodeKey.value);
      if (node) {
      }
    }
  },
  { immediate: true, deep: true }
);
watch(
  () => props.currentNodeKey,
  (val) => {
    if (val !== currentNodeKey.value) {
      currentNodeKey.value = val;
    }
  }
);
const flattenedNodes = computed(() => {
  const result = [];
  const traverse = (nodes) => {
    nodes.forEach((node) => {
      if (!node.visible) return;
      result.push(node);
      if (node.expanded && node.children) {
        traverse(node.children);
      }
    });
  };
  traverse(treeNodes.value);
  return result;
});
const handleNodeClick = (node, e) => {
  if (dragNode.value) return;
  if (node.disabled) return;
  currentNodeKey.value = node.key;
  emit("update:currentNodeKey", node.key);
  emit("current-change", node);
  if (props.expandOnClickNode && !node.isLeaf && (node.children?.length || props.lazy)) {
    handleNodeExpand(node);
  }
  if (props.checkOnClickNode && props.showCheckbox) {
    handleNodeCheck(node, !node.checked);
  }
  emit("node-click", node, e);
};
const handleNodeExpand = async (node) => {
  if (node.isLeaf) return;
  if (props.lazy && !node.loaded && props.load) {
    if (node.loading) return;
    node.loading = true;
    try {
      const children = await props.load(node);
      node.children = buildTree(children, node, node.level + 1);
      node.loaded = true;
    } finally {
      node.loading = false;
    }
  }
  const newExpanded = !node.expanded;
  node.expanded = newExpanded;
  if (newExpanded) {
    expandedKeys.value.add(node.key);
    if (props.accordion) {
      const siblings = node.parent ? node.parent.children : treeNodes.value;
      siblings?.forEach((sibling) => {
        if (sibling.key !== node.key && sibling.expanded) {
          sibling.expanded = false;
          expandedKeys.value.delete(sibling.key);
        }
      });
    }
  } else {
    expandedKeys.value.delete(node.key);
  }
  emit("node-expand", node, newExpanded);
};
const handleNodeCheck = (node, checked) => {
  if (node.disabled) return;
  node.checked = checked;
  node.indeterminate = false;
  if (checked) {
    checkedKeys.value.add(node.key);
  } else {
    checkedKeys.value.delete(node.key);
  }
  if (!props.checkStrictly) {
    const updateChildren = (children) => {
      children?.forEach((child) => {
        if (!child.disabled) {
          child.checked = checked;
          child.indeterminate = false;
          if (checked) {
            checkedKeys.value.add(child.key);
          } else {
            checkedKeys.value.delete(child.key);
          }
          updateChildren(child.children);
        }
      });
    };
    updateChildren(node.children);
    const updateParent = (parent) => {
      if (!parent) return;
      const children = parent.children || [];
      const checkedCount = children.filter((c) => c.checked).length;
      const indeterminateCount = children.filter((c) => c.indeterminate).length;
      if (checkedCount === children.length) {
        parent.checked = true;
        parent.indeterminate = false;
        checkedKeys.value.add(parent.key);
      } else if (checkedCount > 0 || indeterminateCount > 0) {
        parent.checked = false;
        parent.indeterminate = true;
        checkedKeys.value.delete(parent.key);
      } else {
        parent.checked = false;
        parent.indeterminate = false;
        checkedKeys.value.delete(parent.key);
      }
      updateParent(parent.parent);
    };
    updateParent(node.parent);
  }
  emit("check", node, checked, Array.from(checkedKeys.value));
};
const setCurrentKey = (key) => {
  currentNodeKey.value = key;
  emit("update:currentNodeKey", key);
};
const filter = (query) => {
  const filterFn = props.filterMethod || ((q, node) => node.label.includes(q));
  const filterNodes = (nodes) => {
    let hasVisible = false;
    nodes.forEach((node) => {
      let visible = filterFn(query, node);
      if (node.children) {
        const childVisible = filterNodes(node.children);
        visible = visible || childVisible;
      }
      node.visible = visible;
      if (visible) hasVisible = true;
    });
    return hasVisible;
  };
  if (query) {
    filterNodes(treeNodes.value);
  } else {
    const resetVisible = (nodes) => {
      nodes.forEach((node) => {
        node.visible = true;
        if (node.children) resetVisible(node.children);
      });
    };
    resetVisible(treeNodes.value);
  }
};
const getCheckedNodes = () => {
  return flattenedNodes.value.filter((node) => node.checked);
};
const getCheckedKeys = () => {
  return Array.from(checkedKeys.value);
};
const setCheckedKeys = (keys) => {
  checkedKeys.value = new Set(keys);
  nodeMap.value.forEach((node, key) => {
    node.checked = keys.includes(key);
    node.indeterminate = false;
  });
};
const dragNode = ref(null);
const draggingNodeKey = ref(void 0);
const dropTargetNodeKey = ref(void 0);
const dropPosition = ref(void 0);
const handleDragStart = (node, e) => {
  dragNode.value = node;
  draggingNodeKey.value = node.key;
  if (e.dataTransfer) {
    e.dataTransfer.effectAllowed = "move";
    e.dataTransfer.setData("text/plain", "");
  }
  emit("node-drag-start", node, e);
};
const handleDragOver = (node, e) => {
  e.preventDefault();
  if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
  const el = e.currentTarget;
  if (!el) return;
  const rect = el.getBoundingClientRect();
  const offsetTop = e.clientY - rect.top;
  let position = "inner";
  if (offsetTop < rect.height * 0.25) {
    position = "before";
  } else if (offsetTop > rect.height * 0.75) {
    position = "after";
  }
  dropPosition.value = position;
  dropTargetNodeKey.value = node.key;
  emit("node-drag-over", node, e);
};
const handleDragEnter = (node, e) => {
  e.preventDefault();
  dropTargetNodeKey.value = node.key;
  emit("node-drag-enter", node, e);
};
const handleDragLeave = (node, e) => {
  if (dropTargetNodeKey.value === node.key) {
  }
  emit("node-drag-leave", node, e);
};
const handleDragEnd = (_node, _e) => {
  dragNode.value = null;
  draggingNodeKey.value = void 0;
  dropTargetNodeKey.value = void 0;
  dropPosition.value = void 0;
};
const handleDrop = (node, e) => {
  e.preventDefault();
  if (!dragNode.value) return;
  let curr = node.parent;
  while (curr) {
    if (curr.key === dragNode.value.key) return;
    curr = curr.parent;
  }
  emit("node-drop", dragNode.value, node, dropPosition.value || "inner", e);
  draggingNodeKey.value = void 0;
  dropTargetNodeKey.value = void 0;
  dropPosition.value = void 0;
};
watch(
  () => props.currentNodeKey,
  (val) => {
    currentNodeKey.value = val;
  }
);
provide(TREE_INJECTION_KEY, {
  props,
  currentNodeKey,
  expandedKeys,
  checkedKeys,
  draggingNodeKey,
  dropTargetNodeKey,
  dropPosition,
  handleNodeClick,
  handleNodeExpand,
  handleNodeCheck,
  setCurrentKey,
  handleDragStart,
  handleDragOver,
  handleDragEnter,
  handleDragLeave,
  handleDragEnd,
  handleDrop
});
const getNode = (key) => {
  return nodeMap.value.get(key);
};
const getHalfCheckedNodes = () => {
  const result = [];
  nodeMap.value.forEach((node) => {
    if (node.indeterminate) result.push(node);
  });
  return result;
};
const getHalfCheckedKeys = () => {
  return getHalfCheckedNodes().map((node) => node.key);
};
const getCurrentKey = () => currentNodeKey.value;
const getCurrentNode = () => currentNodeKey.value ? nodeMap.value.get(currentNodeKey.value) : void 0;
const setExpandedKeys = (keys) => {
  expandedKeys.value = new Set(keys);
  nodeMap.value.forEach((node, key) => {
    node.expanded = keys.includes(key);
  });
};
const expandNode = (key) => {
  const node = nodeMap.value.get(key);
  if (node && !node.expanded) handleNodeExpand(node);
};
const collapseNode = (key) => {
  const node = nodeMap.value.get(key);
  if (node && node.expanded) handleNodeExpand(node);
};
const setChecked = (key, checked, deep = true) => {
  const node = nodeMap.value.get(key);
  if (!node || node.disabled) return;
  if (deep && !props.checkStrictly) {
    handleNodeCheck(node, checked);
  } else {
    node.checked = checked;
    if (checked) checkedKeys.value.add(key);
    else checkedKeys.value.delete(key);
    emit("check", node, checked, Array.from(checkedKeys.value));
  }
};
const innerScrollRef = ref();
const scrollTo = (options) => {
  if (props.virtual) {
    innerScrollRef.value?.scrollTo(typeof options === "number" ? { top: options } : options);
  }
};
const scrollToNode = (key) => {
  if (!props.virtual) return;
  const index = flattenedNodes.value.findIndex((n) => n.key === key);
  if (index !== -1) {
    scrollTo(index * props.itemHeight);
  }
};
const setCheckedNodes = (nodes) => {
  const keys = nodes.map(
    (node) => node[props.nodeKey] ?? node.key ?? node.id
  );
  setCheckedKeys(keys);
};
const setData = (data) => {
  nodeMap.value.clear();
  treeNodes.value = buildTree(data);
};
defineExpose({
  filter,
  getNode,
  getCheckedNodes,
  getCheckedKeys,
  getHalfCheckedNodes,
  getHalfCheckedKeys,
  getCurrentKey,
  getCurrentNode,
  setChecked,
  setCheckedKeys,
  setCheckedNodes,
  setCurrentKey,
  setExpandedKeys,
  setData,
  expandNode,
  collapseNode,
  scrollTo,
  scrollToNode,
  expandedKeys,
  checkedKeys
});
</script>

<template>
  <div :class="[ns.b(), showLine && ns.m('show-line')]" :style="themeStyle" role="tree">
    <div
      v-if="props.virtual"
      ref="innerScrollRef"
      :style="{
  height: props.height + 'px',
  overflowY: 'auto'
}"
      @scroll="handleScroll"
    >
      <div :style="{
  height: totalHeight + 'px',
  position: 'relative'
}">
        <div :style="{
  transform: `translateY(${offset}px)`
}">
          <TreeNodeComponent v-for="node in visibleNodes" :key="node.key" :node="node">
            <template #default="{ node: childNode, data: childData }">
              <slot name="default" :node="childNode" :data="childData" />
            </template>
            <template #icon="{ node: childNode, data: childData }">
              <slot name="icon" :node="childNode" :data="childData" />
            </template>
            <template #prefix="{ node: childNode, data: childData }">
              <slot name="prefix" :node="childNode" :data="childData" />
            </template>
            <template #suffix="{ node: childNode, data: childData }">
              <slot name="suffix" :node="childNode" :data="childData" />
            </template>
          </TreeNodeComponent>
        </div>
      </div>
    </div>
    <template v-else-if="treeNodes.length > 0">
      <TreeNodeComponent v-for="node in treeNodes" :key="node.key" :node="node">
        <template #default="{ node: childNode, data: childData }">
          <slot name="default" :node="childNode" :data="childData" />
        </template>
        <template #icon="{ node: childNode, data: childData }">
          <slot name="icon" :node="childNode" :data="childData" />
        </template>
        <template #prefix="{ node: childNode, data: childData }">
          <slot name="prefix" :node="childNode" :data="childData" />
        </template>
        <template #suffix="{ node: childNode, data: childData }">
          <slot name="suffix" :node="childNode" :data="childData" />
        </template>
      </TreeNodeComponent>
    </template>
    <div v-else :class="ns.e('empty')">
      <slot name="empty">{{ emptyText || t("tree.emptyText") }}</slot>
    </div>
  </div>
</template>

<style>
@charset "UTF-8";
/**
 * YH-UI Sass Variables
 * 原具体的 CSS 变量输出规则 (:root, html.dark 等) 已移至 root.scss，以防止各组件样式编译时产生重复的全局变量 CSS 冗余。
 */
.yh-tree {
  --yh-tree-node-height: var(--yh-tree-node-height, 28px);
  --yh-tree-text-color: var(--yh-tree-label-text-color, var(--yh-text-color-primary, #1d1d1f));
  --yh-tree-hover-bg: var(--yh-tree-node-hover-bg-color, var(--yh-color-primary-light-9, #ecf5ff));
  --yh-tree-current-bg: var(
    --yh-tree-node-selected-bg-color,
    var(--yh-color-primary-light-8, #d9ecff)
  );
}
html.dark .yh-tree {
  --yh-tree-hover-bg: var(--yh-tree-node-hover-bg-color, rgba(255, 255, 255, 0.06));
  --yh-tree-current-bg: var(--yh-tree-node-selected-bg-color, rgba(255, 255, 255, 0.12));
}
.yh-tree {
  font-size: 14px;
  color: var(--yh-tree-text-color);
  user-select: none;
}
.yh-tree__node {
  position: relative;
}
.yh-tree__node.is-disabled .yh-tree__label {
  color: var(--yh-text-color-disabled, #c0c4cc);
  cursor: not-allowed;
}

.yh-tree__node.is-dragging {
  opacity: 0.5;
}

.yh-tree__node.is-drop-inner {
  background-color: var(--yh-color-primary-light-9, #ecf5ff);
}
.yh-tree__node.is-drop-inner .yh-tree__content {
  background-color: var(--yh-color-primary-light-8, #d9ecff);
}

.yh-tree__node.is-drop-before::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background-color: var(--yh-color-primary, #409eff);
  z-index: 10;
}

.yh-tree__node.is-drop-after::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 2px;
  background-color: var(--yh-color-primary, #409eff);
  z-index: 10;
}

.yh-tree__content {
  display: flex;
  align-items: center;
  height: var(--yh-tree-node-height);
  padding-right: 8px;
  cursor: pointer;
  border-radius: 4px;
  transition: background-color 0.2s ease, color 0.2s ease;
}
.yh-tree__content:hover {
  background-color: var(--yh-tree-hover-bg);
}
.yh-tree__node.is-current > .yh-tree__content {
  background-color: var(--yh-tree-current-bg);
  color: var(--yh-color-primary, #409eff);
  font-weight: 500;
}

.yh-tree__expand-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  color: var(--yh-text-color-placeholder, #c0c4cc);
  cursor: pointer;
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), color 0.3s ease;
}
.yh-tree__expand-icon:hover {
  color: var(--yh-color-primary, #409eff);
}
.yh-tree__expand-icon.is-expanded {
  color: var(--yh-text-color-secondary, #86868b);
}

.yh-tree__expand-icon.is-loading {
  animation: yh-tree-rotate 1s linear infinite;
  color: var(--yh-color-primary, #409eff) !important;
}

.yh-tree__expand-icon svg {
  transition: fill 0.3s ease;
}

.yh-tree__expand-placeholder {
  display: inline-block;
  width: 24px;
}

.yh-tree__label {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: var(--yh-tree-node-height);
}

.yh-tree__icon {
  margin-right: 4px;
  color: var(--yh-text-color-secondary);
}

.yh-tree__empty {
  padding: 20px;
  text-align: center;
  color: var(--yh-text-color-secondary);
  font-size: 13px;
}

.yh-tree--show-line .yh-tree__node {
  position: relative;
}
.yh-tree--show-line .yh-tree__node::before {
  content: "";
  position: absolute;
  left: 11px;
  top: 0;
  height: 100%;
  width: 1px;
  background-color: var(--yh-border-color-light, #dcdfe6);
}
.yh-tree--show-line .yh-tree__node:last-child::before {
  height: 14px;
}
.yh-tree--show-line .yh-tree__content::before {
  content: "";
  position: absolute;
  left: 11px;
  top: 14px;
  width: 12px;
  height: 1px;
  background-color: var(--yh-border-color-light, #dcdfe6);
}
.yh-tree--show-line > .yh-tree__node::before {
  display: none;
}
.yh-tree--show-line > .yh-tree__node > .yh-tree__content::before {
  display: none;
}

.yh-tree .yh-checkbox {
  margin-right: 8px;
}

@keyframes yh-tree-rotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
</style>
