<script lang="ts">
export default {
  name: "c-tree",
};
</script>

<script setup lang="ts">
import { ref, watch, toRaw, computed, useAttrs } from "vue";
import { Tree, InputSearch, Button, Input, Cascader } from "ant-design-vue";
import Tag from "../components/TreeTag.vue";
import type { AntTreeNodeDropEvent, TreeProps } from "ant-design-vue/es/tree";
import {
  Key,
  EventDataNode,
  DataNode,
  FieldNames,
} from "ant-design-vue/lib/vc-tree/interface";
import { CheckInfo } from "ant-design-vue/lib/vc-tree/props";
import {
  ArrowDownOutlined,
  ArrowUpOutlined,
  DeleteOutlined,
  EditOutlined,
  PlusOutlined,
  ReloadOutlined,
  PlusSquareFilled,
  PlusCircleFilled,
} from "@ant-design/icons-vue";
import { vOnClickOutside } from "@vueuse/components";

export type IAntCheckedKeys = Key[] | { checked: Key[]; halfChecked: Key[] };

export interface IAntSelectedInfo {
  event: "select";
  selected: boolean;
  node: EventDataNode;
  selectedNodes: DataNode[];
  nativeEvent: MouseEvent;
}

export interface IMoveTarget {
  current: DataNode;
  parent: DataNode | undefined;
}

export interface CTreeProps extends TreeProps {
  actions?: ("add" | "edit" | "refresh" | "remove" | "add-solid")[];
  checkable?: boolean;
  checkedKeys?: string[];
  fieldNames?: FieldNames;
  showSearch?: boolean;
  singleSelect?: boolean;
  checkStrictly?: boolean;
  floatAction?: (
    | "add"
    | "add-solid"
    | "edit"
    | "up"
    | "down"
    | "add-solid"
    | "remove"
  )[];
  onCheck?: (checked: IAntCheckedKeys, info: CheckInfo) => void;
  onSelect?: (selected: Key[], info: IAntSelectedInfo) => void;
}

// eslint-disable-next-line vue/no-setup-props-destructure
const {
  actions,
  checkable = false,
  checkedKeys = [],
  fieldNames,
  showSearch,
  checkStrictly = false,
  singleSelect = false,
  floatAction = ["add", "edit", "up", "down", "remove"],
  onCheck,
  onSelect,
} = defineProps<CTreeProps>();

const emit = defineEmits<{
  (e: "add", v: DataNode): void;
  (e: "add-solid", v: DataNode): void;
  (e: "add-done", v: DataNode): void;
  (e: "search", v: Key): void;
  (e: "refresh"): void;
  (e: "edit", v: DataNode): void;
  (e: "remove", v: DataNode): void;
  (e: "drag", v: AntTreeNodeDropEvent): void;
  (e: "up", v: IMoveTarget): void;
  (e: "down", v: IMoveTarget): void;
  (e: "update:checkedKeys", v: IAntCheckedKeys): void;
  (e: "update:selectedKeys", v: Key[]): void;
}>();

const $attr: object = useAttrs();

const searchValue = ref<string>("");

watch(searchValue, (value) => {
  emit("search", value);
});

// ant-tree's select item

const selectedKeys = ref<Key[]>([]);

// handle ant-tree's field name

const defaultFieldNames: Required<FieldNames> = {
  children: "children",
  title: "title",
  key: "key",
  _title: [],
};

const mergedFieldNames = computed<Required<FieldNames>>(() => ({
  ...defaultFieldNames,
  ...fieldNames,
}));

const keyName = computed(() => mergedFieldNames.value.key);

const treeKey = ref<number>(1);

// current active node

const current = ref<DataNode | undefined>(undefined);
const parent = ref<DataNode | undefined>(undefined);

// when manual add node

const addNodeName = ref<string | undefined>(undefined);

const addNodeSelect = ref<Array<number | string> | undefined>(undefined);

// action button and little floating window function

const add = () => {
  if (current.value) {
    emit("add", toRaw(current.value));
  }
};

const addSolid = () => {
  if (current.value) {
    emit("add-solid", toRaw(current.value));
  }
};

const addDone = (item) => {
  emit("add-done", {
    ...item,
    [mergedFieldNames.value.title]: addNodeName.value || addNodeSelect.value,
  });
  addNodeName.value = undefined;
  addNodeSelect.value = undefined;
};

const refresh = () => {
  emit("refresh");
};

const edit = () => {
  if (current.value) {
    emit("edit", toRaw(current.value));
  }
};

const remove = () => {
  if (current.value) {
    emit("remove", toRaw(current.value));
  }
};

const editMode = ref(false);

const onDrop = (info: AntTreeNodeDropEvent) => {
  emit("drag", info);
};

const getMoveTarget = (): IMoveTarget | undefined => {
  if (current.value) {
    return {
      current: toRaw(current.value),
      parent: toRaw(parent.value),
    };
  }
};

const up = () => {
  const target = getMoveTarget();
  if (target) {
    emit("up", target);
  }
};

const down = () => {
  const target = getMoveTarget();
  if (target) {
    emit("down", target);
  }
};

const customCheck = (val: IAntCheckedKeys, e: CheckInfo) => {
  const valList = Array.isArray(val) ? val : val.checked;

  if (onCheck) {
    onCheck(valList, e);
  }

  if (singleSelect) {
    const currentItem = e.node[keyName.value];
    const checkList = checkedKeys.includes(currentItem) ? [] : [currentItem];
    emit("update:checkedKeys", checkList);
    return;
  }

  emit("update:checkedKeys", valList);
};

const customSelect = (val: Key[], e: IAntSelectedInfo) => {
  if (onSelect) {
    onSelect(val, e);
  }

  current.value = e.selectedNodes[0];
  parent.value = e.node.parent?.node;

  emit("update:selectedKeys", val);
};

const activeMenuKey = ref<Key | null>(null);

const refreshTree = () => {
  treeKey.value = Math.random();
};

// handle right mounse key

const findNodeInList = (currentItem, callback) => {
  const deepFind = (item, val, parent) => {
    const result = item[keyName.value] === val[keyName.value];
    const child = item[mergedFieldNames.value.children] || [];
    if (result) {
      callback(item, parent);
    } else if (child.length) {
      child.forEach((cld) => deepFind(cld, val, item));
    }
  };

  $attr["tree-data"].forEach((item) => deepFind(item, currentItem, null));
};

const handleSelectKey = (currentItem) => {
  const keyValue = currentItem[keyName.value];

  let currentIndex;

  let isExistKey = false;

  selectedKeys.value.forEach((item, index) => {
    if (item === keyValue) {
      currentIndex = index;
      isExistKey = true;
    }
  });

  const multipleMode = Object.keys($attr).includes("multiple");

  if (isExistKey) {
    selectedKeys.value.splice(currentIndex, 1);
  } else if (multipleMode) {
    selectedKeys.value.push(keyValue);
  } else {
    selectedKeys.value[0] = keyValue;
  }

  emit("update:selectedKeys", selectedKeys.value);
};

const showContextMenu = (e: MouseEvent, item: object) => {
  e.preventDefault();

  findNodeInList(item, (currentItem, parentItem) => {
    current.value = currentItem;
    parent.value = parentItem;
    handleSelectKey(currentItem);
  });

  activeMenuKey.value = item[keyName.value];
};

const closeContextMenu = () => {
  activeMenuKey.value = null;
};

defineExpose({ refreshTree, addNodeSelect });
</script>

<template>
  <InputSearch
    v-if="showSearch"
    v-model:value="searchValue"
    style="margin-bottom: 8px"
    placeholder="请输入内容"
    allowClear
  />
  <div class="tree-node__actions" v-if="actions && actions.length">
    <Button
      v-if="actions.includes('remove')"
      class="tree__action-button"
      @click="remove"
    >
      <template #icon><DeleteOutlined /></template>
      {{ $cyt("删除") }}
    </Button>
    <Button
      v-if="actions.includes('refresh')"
      class="tree__action-button"
      @click="refresh"
    >
      <template #icon><ReloadOutlined /></template>
      {{ $cyt("刷新") }}
    </Button>
    <Button
      v-if="actions.includes('add')"
      class="tree__action-button"
      type="primary"
      @click="add"
    >
      <template #icon><PlusOutlined /></template>
      {{ $cyt("新增") }}
    </Button>
    <Button
      v-if="actions.includes('edit')"
      class="tree__action-button"
      :type="editMode ? 'primary' : 'default'"
      @click="editMode = !editMode"
    >
      <template #icon><EditOutlined /></template>
      {{ $cyt("编辑") }}
    </Button>
  </div>
  <Tree
    class="tree"
    :fieldNames="mergedFieldNames"
    :class="{ 'tree--single-select': singleSelect }"
    v-model:selectedKeys="selectedKeys"
    v-bind="$attrs"
    :checkable="singleSelect || checkable"
    :checkStrictly="checkStrictly || singleSelect"
    :checkedKeys="checkedKeys"
    @select="customSelect"
    @check="customCheck"
    @drop="onDrop"
    :key="treeKey"
  >
    <template #title="item">
      <div
        class="tree-node"
        @contextmenu="(e) => showContextMenu(e, item)"
        v-if="!item.addNodeMode"
      >
        <a-radio
          v-if="singleSelect"
          class="tree-node__checkbox-hack"
          :checked="checkedKeys[0] === item[keyName]"
          :disabled="item.disabled"
        />
        <div class="tree-node__title">
          <div class="truncate" v-html="item[mergedFieldNames.title]"></div>
        </div>
        <template v-for="tag in item.tags" :key="tag.text">
          <Tag v-if="typeof tag === 'string'" class="tree-node__tag">
            {{ tag }}
          </Tag>
          <Tag
            v-else
            :color="tag.color"
            :light="tag.light"
            class="tree-node__tag"
          >
            {{ tag.text }}
          </Tag>
        </template>
        <div class="tree-node__title-actions-placeholder">
          <div
            class="tree-node__title-actions"
            v-if="!item.disabled && activeMenuKey === item[keyName]"
            v-on-click-outside="closeContextMenu"
          >
            <arrow-up-outlined
              v-if="floatAction.includes('up')"
              class="tree-node__action-icon"
              @click.prevent.stop="up"
            />
            <arrow-down-outlined
              v-if="floatAction.includes('down')"
              class="tree-node__action-icon"
              @click.prevent.stop="down"
            />
            <plus-square-filled
              v-if="floatAction.includes('add-solid')"
              class="tree-node__action-icon"
              @click.prevent.stop="addSolid"
            />
            <plus-outlined
              v-if="floatAction.includes('add')"
              class="tree-node__action-icon"
              @click.prevent.stop="add"
            />
            <edit-outlined
              v-if="floatAction.includes('edit')"
              class="tree-node__action-icon"
              @click.prevent.stop="edit"
            />
            <delete-outlined
              v-if="floatAction.includes('remove')"
              class="tree-node__action-icon"
              @click.prevent.stop="remove"
            />
          </div>
        </div>
      </div>
      <div class="tree-node" v-else-if="item.addNodeMode === 'input'">
        <Input v-model:value="addNodeName" size="small" style="width: 120px" />
        <plus-circle-filled
          class="tree-node__action-icon"
          @click.prevent.stop="addDone(item)"
        />
      </div>
      <div class="tree-node" v-else-if="item.addNodeMode === 'cascader'">
        <cascader
          v-model:value="addNodeSelect"
          size="small"
          :max-tag-count="1"
          multiple
          style="width: 120px"
          :options="item.selectOptions"
          placeholder="请选择节点"
        />
        <plus-circle-filled
          class="tree-node__action-icon"
          @click.prevent.stop="addDone(item)"
        />
      </div>
    </template>
  </Tree>
</template>

<style scope lang="less">
@title-height: 32;

.tree {
  margin-top: 10px !important;
  overflow: hidden;
  background-color: transparent;
}

.tree__action-button {
  &:not(:first-child) {
    margin-left: 10px;
  }
}

.tree-node {
  display: flex;
  width: 20em;
  align-items: center;
}

.tree-node__title {
  height: @title-height * 1px;
  display: flex;
  align-items: center;
  overflow: hidden;
}

.tree-node__tag {
  font-size: 12px;
  height: 16px;
  line-height: 16px;
  margin-top: -8px;
  margin-left: 4px;
}

.tree-node__title-actions-placeholder {
  margin-left: 10px;
  height: @title-height * 1px;
  flex-grow: 1;
  position: relative;
  display: flex;
  align-items: center;
}

.tree-node__title-actions {
  height: 26px;
  display: flex;
  flex: 0;
  align-items: center;
  padding: 0 10px;
  position: fixed;
  background: #fff;
  box-shadow: 0px 9px 28px 8px rgba(0, 0, 0, 0.05),
    0px 6px 16px 0px rgba(0, 0, 0, 0.08), 0px 3px 6px -4px rgba(0, 0, 0, 0.12);
  border-radius: 4px;
  z-index: 1000;
}

.tree-node__actions {
  display: flex;
  margin-top: 10px;
}

.tree-node__action-icon {
  color: var(--color-primary);

  &:not(:first-child) {
    margin-left: 10px;
  }
}

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

// single select checkbox hack
.tree-node__checkbox-hack {
  position: absolute;
  left: -24px;
}

.tree--single-select {
  .tree-node__checkbox-hack {
    pointer-events: none;
  }

  .ant-tree-checkbox {
    opacity: 0;
  }
}

/* ant-hack */
.ant-tree-switcher {
  top: ((@title-height - 24) / 2) * 1px;
}

.ant-tree .ant-tree-treenode {
  align-items: center !important;
}

.ant-tree .ant-tree-node-content-wrapper.ant-tree-node-selected {
  background-color: var(--color-tree-node-selected);
}

.ant-tree-checkbox {
  margin-top: 0px !important;
}
</style>
