<script setup lang="ts" generic="T extends { icon?: any; [key: string]: any }">
import type { Component } from 'vue';

import { computed, nextTick, ref, watch } from 'vue';

import { preferences } from '@easyflow/preferences';

import { MoreFilled } from '@element-plus/icons-vue';
import {
  ElButton,
  ElDropdown,
  ElDropdownItem,
  ElDropdownMenu,
  ElEmpty,
  ElIcon,
  ElTree,
} from 'element-plus';

import { getEmptyStateImageUrl } from '#/utils/assets';

interface ControlButton<Item> {
  disabled?: ((item: Item) => boolean) | boolean;
  icon?: any;
  label: string;
  onClick: (item: Item) => void;
  type?: any;
  visible?: (item: Item) => boolean;
}

interface Props {
  childrenKey?: string;
  controlBtns?: ControlButton<T>[];
  defaultExpandAll?: boolean;
  defaultSelected?: number | string;
  footerButton?: {
    disabled?: boolean;
    icon?: any;
    label: string;
    loading?: boolean;
    onClick: () => void;
  };
  iconSize?: number;
  labelKey: string;
  menus: T[];
  title?: string;
  valueKey: string;
}

const props = withDefaults(defineProps<Props>(), {
  childrenKey: '',
  controlBtns: () => [],
  defaultExpandAll: true,
  defaultSelected: '',
  footerButton: undefined,
  iconSize: 16,
  title: '',
});
const emits = defineEmits<{
  (e: 'change', item: T): void;
}>();

const panelWidth = 225;
const selected = ref<number | string>(props.defaultSelected ?? '');
const treeRef = ref<InstanceType<typeof ElTree>>();
const lastEmittedDefault = ref('');
const treeProps = computed(() => ({
  children: props.childrenKey,
  label: props.labelKey,
}));

function menuValue(item: T) {
  return item[props.valueKey] as number | string;
}

function normalizedValue(value: unknown) {
  return String(value ?? '');
}

function findMenu(items: T[], value: number | string): T | undefined {
  for (const item of items) {
    if (normalizedValue(menuValue(item)) === normalizedValue(value)) {
      return item;
    }
    if (props.childrenKey) {
      const children = item[props.childrenKey] as T[] | undefined;
      const matched = children?.length ? findMenu(children, value) : undefined;
      if (matched) return matched;
    }
  }
  return undefined;
}

function syncTreeSelection() {
  if (!props.childrenKey) return;
  void nextTick(() => treeRef.value?.setCurrentKey(selected.value));
}

function handleChange(item: T) {
  selected.value = menuValue(item);
  lastEmittedDefault.value = normalizedValue(selected.value);
  emits('change', item);
  syncTreeSelection();
}

function visibleActions(item: T) {
  return props.controlBtns.filter(
    (button) => !button.visible || button.visible(item),
  );
}

function hasActions(item: T) {
  const value = normalizedValue(menuValue(item));
  return !['', '0'].includes(value) && visibleActions(item).length > 0;
}

function actionDisabled(button: ControlButton<T>, item: T) {
  return typeof button.disabled === 'function'
    ? button.disabled(item)
    : Boolean(button.disabled);
}

function runAction(button: ControlButton<T>, item: T) {
  if (!actionDisabled(button, item)) button.onClick(item);
}

function isComponent(icon: any) {
  return typeof icon !== 'string';
}

function isSvgString(icon: any) {
  if (typeof icon !== 'string') return false;
  return icon.trim().startsWith('<svg') && icon.trim().endsWith('</svg>');
}

watch(
  [() => props.defaultSelected, () => props.menus],
  ([defaultSelected]) => {
    selected.value = defaultSelected ?? '';
    syncTreeSelection();
    if (defaultSelected === '' || defaultSelected === undefined) return;
    const matched = findMenu(props.menus, defaultSelected);
    const normalized = normalizedValue(defaultSelected);
    if (matched && lastEmittedDefault.value !== normalized) {
      lastEmittedDefault.value = normalized;
      emits('change', matched);
    }
  },
  { deep: true, immediate: true },
);
</script>

<template>
  <div class="page-side" :style="{ width: `${panelWidth}px` }">
    <div class="page-side__main">
      <h3 v-if="title" class="page-side__title">{{ title }}</h3>

      <div class="page-side__scroll">
        <ElTree
          v-if="childrenKey"
          ref="treeRef"
          class="page-side__tree"
          :data="menus"
          :node-key="valueKey"
          :props="treeProps"
          :current-node-key="selected"
          :default-expand-all="defaultExpandAll"
          :expand-on-click-node="false"
          highlight-current
          @node-click="handleChange"
        >
          <template #default="{ data }">
            <div class="page-side__tree-node">
              <div class="page-side__label">
                <div v-if="data.icon" class="page-side__icon">
                  <!-- eslint-disable vue/no-v-html -->
                  <div
                    v-if="isSvgString(data.icon)"
                    v-html="data.icon"
                    class="page-side__svg"
                    :style="{
                      width: `${iconSize}px`,
                      height: `${iconSize}px`,
                    }"
                  ></div>
                  <!-- eslint-enable vue/no-v-html -->
                  <img
                    v-else-if="
                      typeof data.icon === 'string' && !isComponent(data.icon)
                    "
                    :src="data.icon"
                    :style="{
                      width: `${iconSize}px`,
                      height: `${iconSize}px`,
                    }"
                    alt=""
                  />
                  <ElIcon v-else>
                    <component :is="data.icon as Component" />
                  </ElIcon>
                </div>
                <slot name="label" :item="data">
                  <span class="page-side__label-text">
                    {{ data[labelKey] }}
                  </span>
                </slot>
              </div>

              <ElDropdown v-if="hasActions(data)" trigger="click" @click.stop>
                <ElButton
                  class="page-side__more"
                  :aria-label="`管理${data[labelKey]}`"
                  :icon="MoreFilled"
                  text
                  @click.stop
                />
                <template #dropdown>
                  <ElDropdownMenu>
                    <ElDropdownItem
                      v-for="button in visibleActions(data)"
                      :key="button.label"
                      :disabled="actionDisabled(button, data)"
                      @click="runAction(button, data)"
                    >
                      <ElButton
                        :type="button.type"
                        :icon="button.icon"
                        :disabled="actionDisabled(button, data)"
                        link
                      >
                        {{ button.label }}
                      </ElButton>
                    </ElDropdownItem>
                  </ElDropdownMenu>
                </template>
              </ElDropdown>
            </div>
          </template>
        </ElTree>

        <template v-else>
          <div
            v-for="item in menus"
            :key="menuValue(item)"
            class="page-side__item"
            :class="{
              'is-selected':
                normalizedValue(selected) === normalizedValue(menuValue(item)),
            }"
            role="button"
            tabindex="0"
            @click="handleChange(item)"
            @keydown.enter.prevent="handleChange(item)"
            @keydown.space.prevent="handleChange(item)"
          >
            <div class="page-side__label">
              <div v-if="item.icon" class="page-side__icon">
                <!-- eslint-disable vue/no-v-html -->
                <div
                  v-if="isSvgString(item.icon)"
                  v-html="item.icon"
                  class="page-side__svg"
                  :style="{
                    width: `${iconSize}px`,
                    height: `${iconSize}px`,
                  }"
                ></div>
                <!-- eslint-enable vue/no-v-html -->
                <img
                  v-else-if="
                    typeof item.icon === 'string' && !isComponent(item.icon)
                  "
                  :src="item.icon"
                  :style="{
                    width: `${iconSize}px`,
                    height: `${iconSize}px`,
                  }"
                  alt=""
                />
                <ElIcon v-else>
                  <component :is="item.icon as Component" />
                </ElIcon>
              </div>
              <slot name="label" :item="item">
                <span class="page-side__label-text">
                  {{ item[labelKey] }}
                </span>
              </slot>
            </div>

            <ElDropdown v-if="hasActions(item)" trigger="click" @click.stop>
              <ElButton
                class="page-side__more"
                :aria-label="`管理${item[labelKey]}`"
                :icon="MoreFilled"
                text
                @click.stop
              />
              <template #dropdown>
                <ElDropdownMenu>
                  <ElDropdownItem
                    v-for="button in visibleActions(item)"
                    :key="button.label"
                    :disabled="actionDisabled(button, item)"
                    @click="runAction(button, item)"
                  >
                    <ElButton
                      :type="button.type"
                      :icon="button.icon"
                      :disabled="actionDisabled(button, item)"
                      link
                    >
                      {{ button.label }}
                    </ElButton>
                  </ElDropdownItem>
                </ElDropdownMenu>
              </template>
            </ElDropdown>
          </div>
        </template>

        <ElEmpty
          v-if="menus.length === 0"
          :image="getEmptyStateImageUrl(preferences.theme.mode)"
        />
      </div>
    </div>

    <ElButton
      v-if="footerButton"
      class="page-side__footer"
      :disabled="footerButton.disabled"
      :icon="footerButton.icon"
      :loading="footerButton.loading"
      plain
      @click="footerButton.onClick"
    >
      {{ footerButton.label }}
    </ElButton>
  </div>
</template>

<style scoped>
.page-side {
  display: flex;
  flex: none;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  padding: var(--space-2);
  overflow: hidden;
  background: hsl(var(--surface-panel));
  border: 1px solid hsl(var(--line-subtle));
  border-radius: var(--radius-panel);
}

.page-side__main {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--space-5);
  min-height: 0;
  overflow: hidden;
}

.page-side__title {
  margin: 0;
  font-size: 16px;
  font-weight: 500;
}

.page-side__scroll {
  flex: 1;
  min-height: 0;
  overflow: auto;
}

.page-side__item,
.page-side__tree-node {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  justify-content: space-between;
  min-width: 0;
}

.page-side__item {
  min-height: 40px;
  padding: 0 var(--space-3);
  margin-bottom: var(--space-1);
  font-size: 14px;
  color: hsl(var(--nav-item-muted-foreground));
  cursor: pointer;
  border-radius: var(--radius-control);
  outline: none;
  transition:
    color var(--motion-duration-fast) var(--motion-ease-standard),
    background-color var(--motion-duration-fast) var(--motion-ease-standard);
}

.page-side__item:hover {
  color: hsl(var(--foreground));
  background: hsl(var(--accent));
}

.page-side__item.is-selected {
  color: hsl(var(--nav-item-active-foreground));
  background: hsl(var(--nav-item-active));
}

.page-side__item:focus-visible {
  outline: 2px solid hsl(var(--primary) / 72%);
  outline-offset: -2px;
}

.page-side__label {
  display: flex;
  flex: 1;
  gap: var(--space-1);
  align-items: center;
  min-width: 0;
}

.page-side__label-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.page-side__icon {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  margin-left: -3px;
}

.page-side__icon img {
  object-fit: contain;
}

.page-side__svg {
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.page-side__more {
  flex: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
}

.page-side__item:hover .page-side__more,
.page-side__item:focus-within .page-side__more,
.page-side__tree-node:hover .page-side__more,
.page-side__tree-node:focus-within .page-side__more {
  opacity: 1;
  pointer-events: auto;
}

.page-side__footer {
  flex: none;
  width: 100%;
  margin-top: var(--space-2);
}

.page-side__tree {
  color: hsl(var(--nav-item-muted-foreground));
  background: transparent;
}

.page-side__tree :deep(.el-tree-node__content) {
  height: 40px;
  margin-bottom: var(--space-1);
  font-size: 14px;
  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);
}

.page-side__tree :deep(.el-tree-node__content:hover) {
  color: hsl(var(--foreground));
  background: hsl(var(--accent));
}

.page-side__tree :deep(.el-tree-node.is-current > .el-tree-node__content) {
  color: hsl(var(--nav-item-active-foreground));
  background: hsl(var(--nav-item-active));
}

.page-side__tree-node {
  flex: 1;
  height: 100%;
  padding-right: var(--space-1);
}

.page-side__svg :deep(svg) {
  width: 100%;
  max-width: 100%;
  height: 100%;
  max-height: 100%;
  object-fit: contain;
}
</style>
