<template>
  <div
    :class="containerClass"
  >
    <VcLoading
      :active="isDisabled"
    />
    <div
      v-if="filterable"
      class="vc-tree__filter-container"
    >
      <VcInput
        v-model="filterValue"
        :placeholder="filterPlaceholder"
        :disabled="isDisabled"
        :color="color"
        clearable
        max-width="100%"
      />
    </div>
    <div
      class="vc-tree__wrapper"
    >
      <ul
        v-show="filteredNodes.length"
        :style="{ maxHeight: maxHeightWrapper }"
        class="vc-tree__container"
        role="tree"
      >
        <VcTreeNode
          v-for="node in filteredNodes"
          :key="node[nodeKey]"
          :node="node"
          :config="config"
          :expanded-keys="localExpandedKeys"
          :selected-keys="selectedKeys"
          :selectable="selectable"
          :disabled="isDisabled"
          is-root
          @node-toggle="onToggleNode"
          @node-change="onChangeNode"
        >
          <template
            v-if="$slots['node-label']"
            #label="data"
          >
            <slot
              name="node-label"
              :node="data.node"
            />
          </template>
        </VcTreeNode>
      </ul>
      <div
        v-if="!filteredNodes.length"
        class="text-center text-inactive-elements pt-2"
      >
        {{ emptyTextMessage }}
      </div>
    </div>
  </div>
</template>

<script lang="ts">
/* eslint-disable @typescript-eslint/no-explicit-any */
import { defineComponent, PropType } from 'vue'
import { convertToUnit, resolveFieldData } from '@/utils/helpers'
import {
  VcSelectedKeyData,
  VcSelectedKeys,
  VcTreeActionKeys,
  VcTreeChangeEvent,
  VcTreeConfig,
  VcTreeNodeType
} from '@/components/VcTree/VcTree.types'
import VcLoading from '@/components/VcLoading/VcLoading.vue'
import VcInput from '@/components/VcInput/VcInput.vue'
import VcTreeNode from '@/components/VcTree/VcTreeNode.vue'
import cloneDeep from 'lodash/cloneDeep'
import color from '@/mixins/input/color.mixin'

const BASE_CLASS = 'vc-tree'
const DEFAULT_CONFIG = {
  valueKey: 'id',
  labelKey: 'label',
  iconKey: 'icon',
  childrenKey: 'children'
}
const FILTERED_KEYS = ['parent', 'isRoot', 'checked', '_keyPath', 'children']

export default defineComponent({
  name: 'VcTree',
  components: { VcTreeNode, VcInput, VcLoading },
  mixins: [color],
  props: {
    loading: {
      type: Boolean,
      default: false
    },
    config: {
      type: Object as PropType<VcTreeConfig>,
      default: () => ({ ...DEFAULT_CONFIG })
    },
    options: {
      type: Array as PropType<VcTreeNodeType[]>,
      default: () => []
    },
    scrollHeight: {
      type: [String, Number],
      default: undefined
    },
    expandedKeys: {
      type: Object as PropType<VcTreeActionKeys>,
      default: () => ({})
    },
    filterPlaceholder: {
      type: String,
      default: ''
    },
    selectable: {
      type: Boolean,
      default: true
    },
    returnedValue: {
      type: String,
      default: ''
    },
    defaultCheckedKeys: {
      type: Array,
      default: () => []
    },
    emptyText: {
      type: String,
      default: ''
    }
  },
  emits: [
    'node-collapse',
    'node-expand',
    'update:expandedKeys',
    'update:selectedKeys',
    'node-select',
    'node-unselect'
  ],
  data () {
    return {
      isReady: false,
      filterValue: '',
      selectedKeys: {} as VcSelectedKeys,
      localExpandedKeys: this.expandedKeys as VcTreeActionKeys,
      localOptions: [] as VcTreeNodeType[]
    }
  },
  computed: {
    isDisabled () {
      return this.loading || !this.isReady
    },
    childrenKey () {
      return this.config?.childrenKey || DEFAULT_CONFIG.childrenKey
    },
    nodeKey () {
      return this.config.valueKey || DEFAULT_CONFIG.valueKey
    },
    filterable () {
      return !!this.filterBy
    },
    filterBy () {
      return this.config.filterBy
    },
    containerClass () {
      return [
        BASE_CLASS
      ]
    },
    filteredValue () {
      if (!this.filterBy) {
        return this.localOptions
      }
      const filteredNodes = []
      const searchFields = this.filterBy.split(',')
      const filterText = this.filterValue.trim().toLocaleLowerCase(this.config?.filterLocale)

      for (const node of this.localOptions) {
        const _node = { ...node }
        const paramsWithoutNode = { searchFields, filterText }
        if (this.findFilteredNodes(_node, paramsWithoutNode) || this.isFilterMatched(_node, paramsWithoutNode)) {
          filteredNodes.push(_node)
        }
      }

      return filteredNodes
    },
    filteredNodes () {
      if (!this.filterable || !this.filterValue.trim().length) {
        return this.localOptions
      }
      return this.filteredValue
    },
    maxHeightWrapper () {
      if (!this.scrollHeight) {
        return ''
      }
      return convertToUnit(this.scrollHeight)
    },
    emptyTextMessage () {
      return this.emptyText ? this.emptyText : this.$tVU.t('general.no.data')
    }
  },
  watch: {
    expandedKeys (newVal) {
      this.localExpandedKeys = newVal
    },
    selectedKeys (val) {
      this.setValue(val)
    }
  },
  created () {
    this.initData()
  },
  mounted () {
    this.$nextTick(() => {
      this.isReady = true
    })
  },
  methods: {
    initData () {
      const res = [] as VcTreeNodeType[]
      const _selectedData = {}
      const _options = cloneDeep(this.options)
      for (let option of _options) {
        option = this.initOptions(option, '', _selectedData)
        res.push(option)
      }
      this.localOptions = [...res]
      this.selectedKeys = _selectedData
    },
    setValue (val: VcSelectedKeys): void {
      const _resData = Object.values(val)
      if (!_resData.length) {
        this.$emit('update:selectedKeys', [])
      } else {
        if (this.returnedValue) {
          const res = _resData.map((item: VcSelectedKeyData) => item.node[this.returnedValue])
          this.$emit('update:selectedKeys', res)
        } else {
          const res = _resData.map((item: VcSelectedKeyData) => {
            const _res = { ...item.node }
            FILTERED_KEYS.forEach(i => delete _res[i])
            return _res
          })
          this.$emit('update:selectedKeys', res)
        }
      }
    },
    initOptions (data: VcTreeNodeType, keyOld = '', selectedData: VcSelectedKeys): VcTreeNodeType {
      const _key = keyOld ? keyOld + '.' + data[this.nodeKey] : data[this.nodeKey]
      if (data[this.childrenKey]?.length) {
        const children = []
        for (let child of data[this.childrenKey] || []) {
          child = this.initOptions(child, _key, selectedData)
          children.push(child)
        }
        data[this.childrenKey] = children
      }
      let checked = false
      if (this.defaultCheckedKeys?.length) {
        checked = this.defaultCheckedKeys.includes(data[this.nodeKey])
        if (checked) {
          selectedData[data[this.nodeKey]] = { checked: true, partialChecked: false, node: data }
          this.updateChildren(data, true, selectedData)
        }
      }
      return {
        ...data,
        _keyPath: _key,
        checked
      }
    },
    findFilteredNodes (node: VcTreeNodeType, params: any): boolean | undefined {
      if (node) {
        let matched = false
        if (node[this.childrenKey]) {
          const childNodes = [...node[this.childrenKey]]
          node[this.childrenKey] = []
          for (const childNode of childNodes) {
            const copyChildNode = { ...childNode }
            if (this.isFilterMatched(copyChildNode, params)) {
              matched = true
              node[this.childrenKey].push(copyChildNode)
            }
          }
        }
        return matched
      }
    },
    isFilterMatched (node: VcTreeNodeType, params: any): boolean {
      let matched = false
      for (const field of params.searchFields) {
        const fieldValue = String(resolveFieldData(node, field)).toLocaleLowerCase(this.config?.filterLocale)
        if (fieldValue.indexOf(params.filterText) > -1) {
          matched = true
        }
      }

      if (!matched) {
        matched = this.findFilteredNodes(node, params) || matched
      }

      return matched
    },
    onToggleNode (event: VcTreeNodeType): void {
      const key = event[this.nodeKey]
      if (this.localExpandedKeys[key]) {
        delete this.localExpandedKeys[key]
        this.$emit('node-collapse', event)
      } else {
        this.localExpandedKeys[key] = true
        this.$emit('node-expand', event)
      }
      this.localExpandedKeys = { ...this.localExpandedKeys }
      this.$emit('update:expandedKeys', this.localExpandedKeys)
    },
    updateChildren (data: VcTreeNodeType, val: boolean, selectedData: VcSelectedKeys): void {
      if (!data[this.childrenKey] || !data[this.childrenKey].length) {
        return
      }
      data[this.childrenKey].forEach((item: VcTreeNodeType) => {
        item.checked = val
        if (selectedData[item[this.nodeKey]]) {
          delete selectedData[item[this.nodeKey]]
        }
        this.updateChildren(item, val, selectedData)
      })
    },
    updateParent (node: VcTreeNodeType | undefined, selectedData: VcSelectedKeys): void {
      if (!node) {
        return
      }
      const checkedChildren = node[this.childrenKey].filter((i: VcTreeNodeType) => i.checked)
      if (checkedChildren.length > 0 && checkedChildren.length === node[this.childrenKey]?.length) {
        node.checked = true
        selectedData[node[this.nodeKey]] = { checked: true, partialChecked: false, node: node }
        checkedChildren.forEach((item: VcTreeNodeType) => {
          delete selectedData[item[this.nodeKey]]
        })
      } else if (checkedChildren.length > 0) {
        node.checked = false
        delete selectedData[node[this.nodeKey]]
        checkedChildren.forEach((item: VcTreeNodeType) => {
          selectedData[item[this.nodeKey]] = { checked: true, partialChecked: false, node: item }
        })
      } else {
        node.checked = false
        delete selectedData[node[this.nodeKey]]
      }
      if (node.parent) {
        this.updateParent(node.parent, selectedData)
      }
    },
    onChangeNode (event: VcTreeChangeEvent): void {
      const _selectedData = { ...this.selectedKeys }
      const _node = event.node
      if (event.check) {
        _selectedData[_node[this.nodeKey]] = { checked: true, partialChecked: false, node: _node }
        this.$emit('node-select', event.node)
      } else {
        delete _selectedData[_node[this.nodeKey]]
        this.$emit('node-unselect', event.node)
      }
      _node.checked = event.check
      this.updateChildren(_node, event.check, _selectedData)
      this.updateParent(_node.parent, _selectedData)
      this.selectedKeys = _selectedData
    }
  }
})
</script>

<style lang="scss" scoped>
.vc-tree {
  &__container {
    overflow-y: auto;
  }
  ::-webkit-scrollbar {
    @apply w-1.5 h-1.5;
  }
  ::-webkit-scrollbar-track {
    @apply rounded-lg shadow-vertical-tab-scroll mt-2;
  }
  ::-webkit-scrollbar-thumb {
    @apply bg-secondary-actions rounded-0.875;
  }
  ::-webkit-scrollbar-thumb:hover {
    @apply bg-secondary-actions--focus;
  }
}
</style>
