<script setup lang="ts">
import type { DocumentFileNode } from './types';

import { computed, ref } from 'vue';

import {
  ChevronRight,
  File,
  FileCode2,
  FileImage,
  FileText,
  FolderClosed,
  FolderOpen,
  Ellipsis,
  Plus,
} from '@easyflow/icons';

import { onClickOutside } from '@vueuse/core';

const props = defineProps<{
  currentPath?: string;
  dirtyPaths?: Set<string>;
  errorCounts?: Record<string, number>;
  focusedPath?: string;
  level?: number;
  node: DocumentFileNode;
  readonly?: boolean;
  warningCounts?: Record<string, number>;
}>();

const emit = defineEmits<{
  create: [node: DocumentFileNode];
  download: [node: DocumentFileNode];
  focusPath: [path: string];
  remove: [node: DocumentFileNode];
  rename: [node: DocumentFileNode];
  select: [node: DocumentFileNode];
  upload: [node: DocumentFileNode];
}>();

const expanded = ref(true);
const menuOpen = ref(false);
const menuRef = ref<HTMLElement>();
const isDirectory = computed(() => props.node.type === 'DIRECTORY');
const isRootSkill = computed(() => props.node.path === 'SKILL.md');
const isCurrent = computed(() => props.node.path === props.currentPath);
const isFocusTarget = computed(
  () =>
    props.node.path === props.focusedPath ||
    (isDirectory.value &&
      !expanded.value &&
      Boolean(props.focusedPath?.startsWith(`${props.node.path}/`))),
);
const icon = computed(() => {
  if (isDirectory.value) return expanded.value ? FolderOpen : FolderClosed;
  const extension = props.node.name.split('.').pop()?.toLowerCase();
  if (props.node.name === 'SKILL.md' || extension === 'md') return FileText;
  if (['js', 'py', 'sh'].includes(extension || '')) return FileCode2;
  if (['gif', 'jpeg', 'jpg', 'png', 'webp'].includes(extension || ''))
    return FileImage;
  return File;
});

onClickOutside(menuRef, () => {
  menuOpen.value = false;
});

function activate() {
  emit('focusPath', props.node.path);
  if (isDirectory.value) {
    expanded.value = !expanded.value;
  }
  emit('select', props.node);
}

function runAction(action: 'download' | 'remove' | 'rename') {
  menuOpen.value = false;
  if (action === 'download') emit('download', props.node);
  else if (action === 'remove') emit('remove', props.node);
  else emit('rename', props.node);
}

function focusTreeItem(target?: HTMLElement | null) {
  if (!target) return;
  const tree = target.closest<HTMLElement>('[role="tree"]');
  tree
    ?.querySelectorAll<HTMLElement>('[role="treeitem"]')
    .forEach((item) => (item.tabIndex = -1));
  target.tabIndex = 0;
  emit('focusPath', target.dataset.treePath || '');
  target.focus();
}

function handleKeydown(event: KeyboardEvent) {
  if (event.key === 'ArrowRight' && isDirectory.value) {
    event.preventDefault();
    if (!expanded.value) {
      expanded.value = true;
      emit('focusPath', props.node.path);
      return;
    }
    const row = event.currentTarget as HTMLElement;
    const nodeItem = row.closest('.file-tree-node');
    const group = [...(nodeItem?.children || [])].find(
      (element) => element.getAttribute('role') === 'group',
    );
    const firstNode = group?.children.item(0);
    const firstChild =
      firstNode?.querySelector<HTMLElement>('[role="treeitem"]');
    focusTreeItem(firstChild);
    return;
  }
  if (event.key === 'ArrowLeft') {
    event.preventDefault();
    if (isDirectory.value && expanded.value) {
      expanded.value = false;
      emit('focusPath', props.node.path);
      return;
    }
    const row = event.currentTarget as HTMLElement;
    const nodeItem = row.closest('.file-tree-node');
    const parentNode = nodeItem?.parentElement?.closest('.file-tree-node');
    const parentRow = [...(parentNode?.children || [])].find((element) =>
      element.classList.contains('file-tree-node__row-wrap'),
    );
    const parentItem =
      parentRow?.querySelector<HTMLElement>('[role="treeitem"]');
    focusTreeItem(parentItem);
    return;
  }
  if (event.key === 'Enter' || event.key === ' ') {
    event.preventDefault();
    activate();
  }
}
</script>

<template>
  <li class="file-tree-node" role="none">
    <div
      class="file-tree-node__row-wrap"
      :style="{ '--tree-level': level || 0 }"
    >
      <button
        type="button"
        role="treeitem"
        class="file-tree-node__row"
        :class="{ 'is-current': isCurrent }"
        :data-tree-path="node.path"
        :aria-current="isCurrent ? 'page' : undefined"
        :aria-expanded="isDirectory ? expanded : undefined"
        :aria-level="(level || 0) + 1"
        :aria-selected="isCurrent"
        :tabindex="isFocusTarget ? 0 : -1"
        @click="activate"
        @focus="emit('focusPath', node.path)"
        @keydown="handleKeydown"
      >
        <ChevronRight
          v-if="isDirectory"
          class="file-tree-node__chevron"
          :class="{ 'is-expanded': expanded }"
          :size="14"
          aria-hidden="true"
        />
        <component
          :is="icon"
          class="file-tree-node__icon"
          :size="15"
          aria-hidden="true"
        />
        <span class="file-tree-node__name">{{ node.name }}</span>
        <span
          v-if="dirtyPaths?.has(node.path)"
          class="file-tree-node__dirty"
          title="未保存"
        >
          <span aria-hidden="true">●</span>
          <span class="file-tree-node__sr-only">未保存</span>
        </span>
        <span
          v-if="errorCounts?.[node.path]"
          class="file-tree-node__count is-error"
          :aria-label="`${errorCounts[node.path]} 个错误`"
        >
          {{ errorCounts[node.path] }}
        </span>
        <span
          v-else-if="warningCounts?.[node.path]"
          class="file-tree-node__count is-warning"
          :aria-label="`${warningCounts[node.path]} 个警告`"
        >
          {{ warningCounts[node.path] }}
        </span>
      </button>
      <div class="file-tree-node__actions">
        <button
          v-if="isDirectory && !readonly"
          type="button"
          aria-label="在此目录新建文件"
          title="在此目录新建文件"
          @click.stop="emit('create', node)"
        >
          <Plus :size="14" aria-hidden="true" />
        </button>
        <div
          v-if="!isDirectory || !readonly"
          ref="menuRef"
          class="file-tree-node__menu-wrap"
        >
          <button
            type="button"
            aria-label="更多操作"
            title="更多操作"
            aria-haspopup="menu"
            :aria-expanded="menuOpen"
            @click.stop="menuOpen = !menuOpen"
          >
            <Ellipsis :size="14" aria-hidden="true" />
          </button>
          <div v-if="menuOpen" class="file-tree-node__menu" role="menu">
            <button
              v-if="isDirectory && !readonly"
              type="button"
              role="menuitem"
              @click="
                menuOpen = false;
                emit('upload', node);
              "
            >
              上传资源
            </button>
            <button
              v-if="!isDirectory"
              type="button"
              role="menuitem"
              @click="runAction('download')"
            >
              下载
            </button>
            <template v-if="!readonly && !isDirectory && !isRootSkill">
              <button
                type="button"
                role="menuitem"
                @click="runAction('rename')"
              >
                重命名
              </button>
              <button
                type="button"
                role="menuitem"
                class="is-danger"
                @click="runAction('remove')"
              >
                删除
              </button>
            </template>
          </div>
        </div>
      </div>
    </div>
    <ul
      v-if="isDirectory && expanded && node.children?.length"
      class="file-tree-node__children"
      role="group"
    >
      <FileTreeNode
        v-for="child in node.children"
        :key="child.path"
        :node="child"
        :level="(level || 0) + 1"
        :current-path="currentPath"
        :focused-path="focusedPath"
        :dirty-paths="dirtyPaths"
        :error-counts="errorCounts"
        :warning-counts="warningCounts"
        :readonly="readonly"
        @create="emit('create', $event)"
        @download="emit('download', $event)"
        @focus-path="emit('focusPath', $event)"
        @remove="emit('remove', $event)"
        @rename="emit('rename', $event)"
        @select="emit('select', $event)"
        @upload="emit('upload', $event)"
      />
    </ul>
  </li>
</template>

<style scoped>
.file-tree-node,
.file-tree-node__children {
  padding: 0;
  margin: 0;
  list-style: none;
}

.file-tree-node__row-wrap {
  position: relative;
  display: flex;
  align-items: center;
  width: calc(100% - 16px);
  margin: 1px 8px;
  border-radius: var(--radius-control);
}

.file-tree-node__row {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  width: 100%;
  min-height: 36px;
  padding: 0 var(--radius-control) 0
    calc(var(--radius-control) + var(--tree-level) * 14px);
  margin: 0;
  color: hsl(var(--nav-item-muted-foreground));
  text-align: left;
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-control);
  transition:
    color var(--motion-duration-fast) var(--motion-ease-standard),
    background-color var(--motion-duration-fast) var(--motion-ease-standard);
}

.file-tree-node__row-wrap:hover .file-tree-node__row {
  color: hsl(var(--foreground));
  background: hsl(var(--nav-item-hover));
}

.file-tree-node__row-wrap:hover .file-tree-node__row,
.file-tree-node__row-wrap:focus-within .file-tree-node__row {
  padding-right: 66px;
}

.file-tree-node__row.is-current {
  font-weight: 600;
  color: hsl(var(--nav-item-active-foreground));
  background: hsl(var(--nav-item-active));
}

.file-tree-node__row:focus-visible {
  outline: 2px solid hsl(var(--primary) / 72%);
  outline-offset: 1px;
}

.file-tree-node__icon {
  flex: none;
  width: 15px;
  color: hsl(var(--text-muted));
}

.file-tree-node__chevron {
  flex: none;
  color: hsl(var(--text-muted));
  transition: transform var(--motion-duration-fast) var(--motion-ease-standard);
}

.file-tree-node__chevron.is-expanded {
  transform: rotate(90deg);
}

.file-tree-node__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.file-tree-node__actions {
  position: absolute;
  right: var(--space-1);
  display: flex;
  gap: 2px;
  visibility: hidden;
  opacity: 0;
}

.file-tree-node__row-wrap:hover .file-tree-node__actions,
.file-tree-node__row-wrap:focus-within .file-tree-node__actions {
  visibility: visible;
  opacity: 1;
}

.file-tree-node__actions button {
  display: grid;
  place-items: center;
  min-width: 26px;
  height: 26px;
  padding: 0 var(--space-1);
  color: hsl(var(--text-muted));
  cursor: pointer;
  background: hsl(var(--surface-elevated) / 92%);
  border: 0;
  border-radius: var(--radius-control);
}

.file-tree-node__actions button:hover {
  color: hsl(var(--foreground));
  background: hsl(var(--nav-tool-hover));
}

.file-tree-node__actions button:focus-visible {
  outline: 2px solid hsl(var(--primary) / 72%);
  outline-offset: 1px;
}

.file-tree-node__menu-wrap {
  position: relative;
}

.file-tree-node__menu {
  position: absolute;
  top: calc(100% + 2px);
  right: 0;
  z-index: 10;
  display: grid;
  width: 112px;
  padding: var(--space-1);
  background: hsl(var(--surface-elevated));
  border: 1px solid hsl(var(--line-subtle));
  border-radius: var(--radius-control);
  box-shadow: var(--shadow-float);
}

.file-tree-node__menu button {
  display: flex;
  justify-content: flex-start;
  width: 100%;
  padding: 0 var(--space-2);
}

.file-tree-node__menu button.is-danger {
  color: hsl(var(--destructive));
}

.file-tree-node__dirty {
  font-size: 8px;
  color: hsl(var(--warning));
}

.file-tree-node__sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  white-space: nowrap;
  border: 0;
  clip: rect(0, 0, 0, 0);
}

.file-tree-node__count {
  display: inline-grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  font-size: 11px;
  border-radius: 999px;
}

.file-tree-node__count.is-error {
  color: hsl(var(--destructive));
  background: hsl(var(--destructive) / 12%);
}

.file-tree-node__count.is-warning {
  color: hsl(var(--warning));
  background: hsl(var(--warning) / 14%);
}
</style>
