<template>
  <List
  	class="he-tree"
      :id="nodeKey"
      ref="virtualizationList"
      :items="visibleStats"
      :enabled="virtualization"
      :class="{'he-tree-rtl': rtl}"
  >
    <template #prepend>
      <slot name="prepend" :tree="self"></slot>
    </template>
    <template #default="{ item: stat, index }">
      <TreeNode :vt-index="index" :class="[
        stat.class,
        {
          'drag-placeholder-wrapper': stat.data === placeholderData,
          'dragging-node': stat === dragNode,
        },
      ]" :style="stat.style" :stat="stat" :rtl="rtl" :btt="btt" :indent="indent" :table="table" :treeLine="treeLine"
        :treeLineOffset="treeLineOffset" :processor="processor" @click="$emit('click:node', stat)"
        @open="$emit('open:node', $event)" @close="$emit('close:node', $event)" @check="$emit('check:node', $event)">
        <template #default="{ indentStyle }">
          <template v-if="stat.data === placeholderData">
            <view v-if="!table" class="drag-placeholder he-tree-drag-placeholder">
              <slot name="placeholder" :tree="self"></slot>
            </view>
            <td v-else :style="indentStyle" :colspan="placeholderColspan">
              <view class="drag-placeholder he-tree-drag-placeholder">
                <slot name="placeholder" :tree="self"></slot>
              </view>
            </td>
          </template>
          <slot v-else :node="stat.data" :stat="stat" :indentStyle="indentStyle" :tree="self">{{ stat.data[textKey] }}
          </slot>
        </template>
      </TreeNode>
    </template>
    <template #append>
      <slot name="append" :tree="self"></slot>
    </template>
  </List>
</template>

<script lang="ts">
// 如果遇到滚动不流畅的情况，不用处理，因为Dev tool造成的。
// If the scrolling is not smooth, do not deal with it, because it is caused by the Dev tool.
import { PropType, defineComponent, reactive } from "./vue";
import * as hp from "./helper-js";
import List from "./list";
import TreeNode from "./treeNode.vue";
import { vueMakeTreeProcessor, Stat, TreeProcessor } from "./TreeProcessorVue";

export { walkTreeData } from "./helper-js";

const cpt = defineComponent({
  components: { List, TreeNode },
  props: {
    // for vue3
    modelValue: { required: true, type: Array as PropType<any[]> },
    updateBehavior: {
      type: String as PropType<"modify" | "new" | "disabled">,
      default: "modify",
    },
    processor: {
      type: Object as PropType<TreeProcessor>,
      default: () =>
        vueMakeTreeProcessor([], {
          noInitialization: true,
        }),
    },
    childrenKey: { type: String, default: "children" },
    /**
     * for default slot. 用于默认插槽
     */
    textKey: { type: String, default: "text" },
    /**
     * node indent. 节点缩进
     */
    indent: { type: Number, default: 20 },
    /**
     * Enable virtual list. 启用虚拟列表
     */
    virtualization: { type: Boolean, default: false },
    /**
     * Render count for virtual list at start. 虚拟列表初始渲染数量.
     */
    virtualizationPrerenderCount: { type: Number, default: 20 },
    /**
     * Open all nodes by default. 默认打开所有节点.
     */
    defaultOpen: { type: Boolean, default: true },
    statHandler: { type: Function as PropType<(stat: Stat<any>) => Stat<any>> },
    /**
     * From right to left. 由右向左显示.
     */
    rtl: { type: Boolean, default: false },
    /**
     * From bottom to top. 由下向上显示
     */
    btt: { type: Boolean, default: false },
    /**
     * Display as table
     */
    table: { type: Boolean, default: false },
    watermark: { type: Boolean, default: false },
    nodeKey: {
      type: [String, Function] as PropType<
        "index" | ((stat: Stat<any>, index: number) => any)
      >,
      default: 'index',
    },
    treeLine: { type: Boolean, default: false },
    treeLineOffset: { type: Number, default: 8 },
  },
  emits: [
    "update:modelValue",
    "click:node",
    "open:node",
    "close:node",
    "check:node",
    "beforeDragStart",
    "before-drag-start",
    "after-drop",
    "change",
    "enter",
    "leave",
  ],
  data() {
    return {
	  treeID: hp.randString(),
      stats: [],
      statsFlat: [],
      dragNode: null,
      dragOvering: false,
      placeholderData: {},
      placeholderColspan: 1,
      batchUpdateWaiting: false,
      self: this,
      _ignoreValueChangeOnce: false,
    } as {
	  treeID: any,
      stats: Exclude<TreeProcessor["stats"], null>;
      statsFlat: Exclude<TreeProcessor["statsFlat"], null>;
      dragNode: Stat<any> | null;
      dragOvering: boolean;
      placeholderData: {};
      placeholderColspan: number;
      batchUpdateWaiting: boolean;
      self: any;
      _ignoreValueChangeOnce: boolean;
    };
  },
  computed: {
    valueComputed() {
      return (this.modelValue) || [];
    },
    visibleStats() {
      const { statsFlat, isVisible } = this;
      let items = statsFlat;
      if (this.btt) {
        items = items.slice();
        items.reverse();
      }
      return items.filter((stat) => isVisible(stat));
    },
    rootChildren() {
      return this.stats
    },
  },
  methods: {
    _emitValue(value: any[]) {
      // @ts-ignore
      this.$emit("update:modelValue", value);
    },
    /**
     * private method
     * @param value
     */
    _updateValue(value: any[]) {
      if (this.updateBehavior === "disabled") {
        return false;
      }
      // if value changed, ignore change once
      if (value !== this.valueComputed) {
        this._ignoreValueChangeOnce = true;
      }
      this._emitValue(value);
      return true;
    },
    getStat: reactiveFirstArg(
      processorMethodProxy("getStat")
    ) as TreeProcessor["getStat"],
    has: reactiveFirstArg(processorMethodProxy("has")) as TreeProcessor["has"],
    updateCheck: processorMethodProxy(
      "updateCheck"
    ) as TreeProcessor["updateCheck"],
    getChecked: processorMethodProxy(
      "getChecked"
    ) as TreeProcessor["getChecked"],
    getUnchecked: processorMethodProxy(
      "getUnchecked"
    ) as TreeProcessor["getUnchecked"],
    openAll: processorMethodProxy("openAll") as TreeProcessor["openAll"],
    closeAll: processorMethodProxy("closeAll") as TreeProcessor["closeAll"],
    openNodeAndParents: processorMethodProxy("openNodeAndParents") as TreeProcessor["openNodeAndParents"],
    isVisible: processorMethodProxy("isVisible") as TreeProcessor["isVisible"],
    move: processorMethodProxyWithBatchUpdate("move") as TreeProcessor["move"],
    add: reactiveFirstArg(
      processorMethodProxyWithBatchUpdate("add")
    ) as TreeProcessor["add"],
    addMulti(
      dataArr: any[],
      parent?: Stat<any> | null,
      startIndex?: number | null
    ) {
      this.batchUpdate(() => {
        let index = startIndex;
        for (const data of dataArr) {
          this.add(data, parent, index);
          if (index != null) {
            index++;
          }
        }
      });
    },
    remove: processorMethodProxy("remove") as TreeProcessor["remove"],
    removeMulti(dataArr: any[]) {
      let cloned = [...dataArr];
      this.batchUpdate(() => {
        for (const data of cloned) {
          this.remove(data);
        }
      });
    },
    iterateParent: processorMethodProxy(
      "iterateParent"
    ) as TreeProcessor["iterateParent"],
    getSiblings: processorMethodProxy(
      "getSiblings"
    ) as TreeProcessor["getSiblings"],
    getData: processorMethodProxy("getData") as hp.ReplaceReturnType<
      TreeProcessor["getData"],
      any[]
    >,
    getRootEl() {
      // @ts-ignore
      return this.$refs.vtlist.listElRef as HTMLElement;
    },
    batchUpdate(task: () => any | Promise<any>) {
      const r = this.ignoreUpdate(task);
      if (!this.batchUpdateWaiting) {
        this._updateValue(
          this.updateBehavior === "new" ? this.getData() : this.valueComputed
        );
      }
      return r;
    },
    ignoreUpdate(task: () => any | Promise<any>) {
      const old = this.batchUpdateWaiting;
      this.batchUpdateWaiting = true;
      const r = task();
      this.batchUpdateWaiting = old;
      return r;
    },
  },
  watch: {
    processor: {
      immediate: true,
      handler(processor: typeof this.processor) {
        if (processor) {
          // hook
          const getNodeDataChildren = (nodeData: any): any[] => {
            if (!nodeData) {
              return this.valueComputed;
            } else {
              const { childrenKey } = this;
              if (!nodeData[childrenKey]) {
                nodeData[childrenKey] = [];
              }
              return nodeData[childrenKey];
            }
          };
          processor["_statHandler2"] = this.statHandler
            ? (stat) => {
              if (stat.data === this.placeholderData) {
                return stat;
              }
              return this.statHandler!(stat);
            }
            : null;
          processor.afterSetStat = (stat, parent, index) => {
            const { childrenKey, updateBehavior } = this;
            let value = this.valueComputed;
            if (updateBehavior === "new") {
              if (this.batchUpdateWaiting) {
                return;
              }
              value = this.getData();
            } else if (updateBehavior === "modify") {
              const siblings = getNodeDataChildren(parent?.data);
              if (siblings.includes(stat.data)) {
                // when call add -> add child -> _setPositionm ignore because the child already in parent.children
              } else {
                siblings.splice(index, 0, stat.data);
              }
            } else if (updateBehavior === "disabled") {
            }
            if (this.batchUpdateWaiting) {
              return;
            }
            this._updateValue(value);
          };
          processor.afterRemoveStat = (stat) => {
            const { childrenKey, updateBehavior } = this;
            let value = this.valueComputed;
            if (updateBehavior === "new") {
              if (this.batchUpdateWaiting) {
                return;
              }
              value = this.getData();
            } else if (updateBehavior === "modify") {
              const siblings = getNodeDataChildren(stat.parent?.data);
              hp.arrayRemove(siblings, stat.data);
            } else if (updateBehavior === "disabled") {
            }
            if (this.batchUpdateWaiting) {
              return;
            }
            this._updateValue(value);
          };
        }
        if (!processor.initialized) {
          processor.data = this.valueComputed;
          Object.assign(
            processor,
            hp.objectOnly(this, ["childrenKey", "defaultOpen"])
          );
          processor.init();
          processor.updateCheck();
        }
        this.stats = processor.stats!;
        this.statsFlat = processor.statsFlat!;
        if (processor.data !== this.valueComputed) {
          this._updateValue(processor.data);
        }
      },
    },
    valueComputed: {
      handler(value) {
        // isDragging triggered in Vue2 because its array is not same with Vue3
        const isDragging = this.dragOvering || this.dragNode
        if (isDragging || this._ignoreValueChangeOnce) {
          this._ignoreValueChangeOnce = false;
        } else {
          const { processor } = this;
          processor.data = value;
          processor.init();
          this.stats = processor.stats!;
          this.statsFlat = processor.statsFlat!;
        }
      },
    },
  },
  created() { },
  mounted() {

  },
});

export default cpt;
export type BaseTreeType = InstanceType<typeof cpt>;

function processorMethodProxy(name: string) {
  return function (...args) {
    // @ts-ignore
    return this.processor[name](...args);
  };
}
function processorMethodProxyWithBatchUpdate(name: string) {
  return function (...args) {
    // @ts-ignore
    return this.batchUpdate(() => {
      // @ts-ignore
      return this.processor[name](...args);
    });
  };
}
function reactiveFirstArg(func: any) {
  return function (arg1, ...args) {
    if (arg1) {
      arg1 = reactive(arg1);
    }
    // @ts-ignore
    return func.call(this, arg1, ...args);
  };
}
</script>

<style>
.he-tree--rtl {
  direction: rtl;
}

.he-tree-drag-placeholder {
  background: #ddf2f9;
  border: 1px dashed #00d9ff;
  height: 22px;
  width: 100%;
}
</style>