<template>
  <view class="ml-tree-box" :style="{ height: useHeight }">
    <view class="ml-tree-tool">
      <scroll-view :scroll-x="true" class="ml-tree-breadcrumb-scroll" v-if="breadcrumb">
        <view class="ml-tree-breadcrumb">
          <view class="breadcrumb-item" @click="handleToBreadcrumb()">
            <text class="breadcrumb-text">全部</text>
            <ml-icon v-if="cache && cache.length" name="arrow-right"></ml-icon>
          </view>
          <view class="breadcrumb-item" v-for="(item, index) in cache" :key="index" @click="handleToBreadcrumb(index)">
            <text class="breadcrumb-text">{{ item[useProps.label] || '' }}</text>
            <ml-icon v-if="index !== (cache.length - 1)" name="arrow-right"></ml-icon>
          </view>
        </view>
      </scroll-view>
    </view>
    <view class="ml-tree-container" :style="{ height: useContainerHeight }">
      <scroll-view :scroll-y="true" class="ml-tree-scroll">
        <view class="ml-tree-content">
          <view class="ml-tree-node" v-if="cache && cache.length" @click="handleBack">
            <view class="ml-tree-node__title back">
              <ml-icon name="direction-left"></ml-icon>
              <text class="ml-tree-node__text">返回</text>
            </view>
          </view>
          <template v-if="showTree && showTree.length">
            <view class="ml-tree-node" v-for="(item, index) in showTree" :key="index">
              <view class="ml-tree-node__action" v-if="check">
                <ml-checkbox
                  :value="checkKey.indexOf(item[useProps.key]) > -1"
                  :indeterminate="indeterminate.indexOf(item[useProps.key]) > -1"
                  v-if="multiple"
                  @tap.stop
                  @change="handleChangeCheckbox(item)"></ml-checkbox>
                <ml-radio
                  @tap.stop
                  :value="checkIndex"
                  :label="item[useProps.key]"
                  v-else
                  @change="handleChangeRadio"></ml-radio>
              </view>
              <MlTreeIcon
                :margin-right="15"
                @tap-icon="handleClickIcon($event, item)"
                :info="getPrefixIcon(item)"
              />
              <view class="ml-tree-node__title" :class="{ 'hasExplain': item[useProps.explain] }" @tap.stop="handleIntoChildren(item)">
                <view class="nowrap">{{ item[useProps.label] || '' }}<ml-text v-if="item.showExtra" :status="item.extraStatus">{{ item.extraText }}</ml-text></view>
                <view class="ml-tree-node__explain" v-if="item[useProps.explain]">{{ item[useProps.explain] }}</view>
              </view>
              <view class="ml-tree-node__extra">
                <MlTreeIcon
                  :margin-right="15"
                  :info="getSuffixIcon(item)"
                  @tap-icon="handleClickIcon($event, item)"
                />
                <MlTreeIcon
                  v-if="(item.children && item.children.length) || (lazy && !item.isLeaf)"
                  :margin-right="15"
                  :info="{ icon: 'arrow-right' }"
                  @tap-icon="handleIntoChildren(item)"
                />
              </view>
            </view>
          </template>
          <template v-else>
            <view class="ml-tree-node" v-if="loading">
              <view class="ml-tree-loading-tip">
                <ml-icon name="loading" roll />
                <text class="ml-tree-loading-text">加载中</text>
              </view>
            </view>
            <ml-not-data
              v-else
              :emptyUrl="useEmpty.emptyUrl"
              :emptyText="useEmpty.emptyText"
              :emptyHint="useEmpty.emptyHint"
            ></ml-not-data>
          </template>
        </view>
      </scroll-view>
    </view>
  </view>
</template>

<script>
import MlTreeIcon from './ml-tree-icon.vue'
import { findNodesByKeys, findTreeByKey } from './utils'

export default {
  name: 'ml-tree-box',
  components: { MlTreeIcon },
  options: {
    virtualHost: true
  },
  props: {
    value: [Array, String, Number],
    check: Boolean,
    autoload: Boolean,
    height: [String, Number],
    multiple: Boolean,
    breadcrumb: Boolean,
    lazy: Boolean,
    lazyLoad: Function,
    strictly: Boolean,
    arbitrary: Boolean,
    props: {
      type: Object,
      default: () => ({})
    },
    empty: {
      type: Object,
      default: () => ({})
    },
    tree: {
      type: Array,
      default: () => []
    },
    prefixIcon: [String, Object, Function],
    suffixIcon: [String, Object, Function]
  },
  watch: {
    value (val) {
      const { multiple } = this
      if (multiple) {
        this.checkKey = val
      } else {
        this.checkIndex = val
      }
    }
  },
  data () {
    return {
      cache: [],
      checkIndex: 0,
      loading: false,
      checkKey: [],
      indeterminate: [],
      defaultProps: {
        key: 'key',
        label: 'label',
        children: 'children',
        explain: 'explain',
        user: 'user',
        isLeaf: 'isLeaf'
      },
      defaultEmpty: {
        emptyUrl: '',
        emptyText: '',
        emptyHint: ''
      }
    }
  },
  inject: {
    currVM: {
      from: '$pageVM',
      default: null
    }
  },
  computed: {
    showTree () {
      const { current, tree } = this
      if (current) {
        return current.children
      }
      return tree
    },
    current () {
      const { cache } = this
      if (cache && cache.length) {
        return cache[cache.length - 1]
      }
      return null
    },
    useProps () {
      return { ...this.defaultProps, ...this.props }
    },
    useEmpty () {
      return { ...this.defaultEmpty, ...this.empty }
    },
    useContainerHeight () {
      const { breadcrumb, useHeight: height } = this
      const h = height.indexOf('px') > -1 || height.indexOf('%') > -1 ? height : `${height}rpx`
      if (breadcrumb) {
        return `calc(${h} - 50rpx)`
      }
      return `calc(${h})`
    },
    useHeight () {
      const { height } = this
      if (typeof height === 'string') {
        return height.indexOf('px') > -1 || height.indexOf('%') > -1 ? height : `${height}rpx`
      }
      return `${height}rpx`
    }
  },
  mounted () {
    const { autoload, requestLazy } = this
    if (autoload) {
      requestLazy()
    }
  },
  methods: {
    async handleIntoChildren (item) {
      const { requestLazy, lazy } = this
      if (lazy && !(item.children && item.children.length) && !item.isLeaf) {
        item.children = []
        this.cache.push(item)
        this.$emit('into', item)
        // this.lazyLoading = true
        await requestLazy(item)
        // this.lazyLoading = false
        this.cache.splice(this.cache.length - 1, 1, item)
      } else {
        if (item.children && item.children.length) {
          this.cache.push(item)
          this.$emit('into', this.cache[this.cache.length - 1])
        }
      }
      this.reloadLazy()
    },
    reloadLazy () {
      const { current } = this
      this.requestLazy(current)
    },
    async requestLazy (current) {
      const { lazyLoad, lazy, useProps } = this
      if (lazy && lazyLoad) {
        this.loading = true
        const list = await lazyLoad.call(this.currVM, current)
        this.loading = false
        if (current) {
          current[useProps.children] = list
        } else {
          this.$emit('update:tree', list)
        }
      }
    },
    handleBack () {
      const { cache } = this
      cache.splice(cache.length - 1, 1)
      if (cache.length) {
        this.$emit('back', cache[cache.length - 1])
      } else {
        this.$emit('back')
      }
    },
    handleToBreadcrumb (index) {
      const { cache } = this
      if (index === (cache.length - 1)) return false
      if (isNaN(+index)) {
        this.cache = []
      } else {
        cache.splice(index + 1)
      }
    },
    handleChangeRadio (e) {
      this.checkIndex = e.value
      this.handleEvent()
    },
    handleChangeCheckbox (row) {
      const { checkKey, useProps } = this
      const key = row[useProps.key]
      if (key) {
        const findIndex = checkKey.findIndex(item => item === key)
        if (findIndex > -1) {
          this.handleCheckOff(key)
        } else {
          this.handleCheckOn(key)
        }
      }
      this.handleEvent()
    },
    handleCheckOn (key, down = true) {
      const { tree, useProps, checkKey, strictly, arbitrary } = this
      const find = findTreeByKey(tree, key, null, useProps)
      if (find) {
        const { node, parent } = find
        this.checkKey.push(key)
        const children = node.children
        if (children && children.length && down && !arbitrary) {
          for (let i = 0; i < children.length; i++) {
            const item = children[i]
            const findKey = item[useProps.key]
            const findIndex = checkKey.findIndex(fd => fd === findKey)
            if (findIndex === -1) {
              this.handleCheckOn(findKey, true)
            }
          }
        }
        if (parent && !arbitrary) {
          if (strictly) {
            this.handleCheckOn(parent[useProps.key], false)
          } else {
            this.handleIndeterminateOn(parent)
          }
        }
      }
    },
    handleCheckOff (key) {
      const { tree, useProps, checkKey, arbitrary } = this
      const find = findTreeByKey(tree, key, null, useProps)
      const { node, parent } = find
      if (find) {
        const findIndex = checkKey.findIndex(item => item === key)
        this.checkKey.splice(findIndex, 1)
        const children = node.children
        if (children && children.length && !arbitrary) {
          this.handleIndeterminateChildrenOff(node)
          for (let i = 0; i < children.length; i++) {
            const item = children[i]
            const findKey = item[useProps.key]
            this.handleCheckOff(findKey)
          }
        }
      }
      if (parent && !arbitrary) {
        this.handleIndeterminateParentOff(parent)
      }
    },
    handleIndeterminateOn (node) {
      const { indeterminate, useProps, checkKey } = this
      const key = node[useProps.key]
      const findIndex = indeterminate.findIndex(item => item === key)
      if (findIndex === -1) {
        indeterminate.push(key)
      }
      const children = node[useProps.children]
      if (children && children.length && children.every(item => checkKey.indexOf(item[useProps.key]) > -1)) {
        if (checkKey.findIndex(item => item === key) === -1) {
          this.handleCheckOn(key, false)
        }
      }
    },
    handleIndeterminateChildrenOff (node) {
      const { indeterminate, useProps } = this
      const key = node[useProps.key]
      const indeterminateIndex = indeterminate.findIndex(item => item === key)
      if (indeterminateIndex > -1) {
        indeterminate.splice(indeterminateIndex, 1)
      }
    },
    handleIndeterminateParentOff (node) {
      const { indeterminate, checkKey, useProps } = this
      const key = node[useProps.key]
      const children = node[useProps.children]
      if (!(children.every(item => checkKey.indexOf(item[useProps.key]) > -1))) {
        const findIndex = checkKey.findIndex(item => item === key)
        if (findIndex > -1) {
          checkKey.splice(findIndex, 1)
        }
      }
      if (children.every(item => checkKey.indexOf(item[useProps.key]) === -1)) {
        const findIndex = indeterminate.findIndex(item => item === key)
        if (findIndex > -1) {
          indeterminate.splice(findIndex, 1)
        }
      }
    },
    handleClickEvent (row) {
      const { check, handleChangeCheckbox, handleChangeRadio, useProps, multiple } = this
      if (check) {
        if (multiple) {
          handleChangeCheckbox(row)
        } else {
          handleChangeRadio({ value: row[useProps.key] })
        }
      } else {
        this.handleIntoChildren(row)
      }
    },
    handleClickIcon (e, row) {
      this.$emit('click-icon', e.icon, row)
    },
    handleEvent () {
      const { tree, multiple, checkKey, checkIndex, useProps } = this
      if (multiple) {
        const checkRows = findNodesByKeys(tree, checkKey, useProps)
        this.$emit('input', checkKey)
        this.$emit('change', checkKey, checkRows)
      } else {
        const { node } = findTreeByKey(tree, checkIndex, null, useProps)
        this.$emit('input', checkIndex)
        this.$emit('change', checkIndex, node)
      }
    },
    // 自定义图标
    getPrefixIcon (row) {
      const { prefixIcon } = this
      let icon = ''
      if (typeof prefixIcon === 'function') {
        icon = prefixIcon(row)
      } else {
        icon = prefixIcon
      }
      if (typeof icon === 'object') {
        return icon
      } else if (typeof icon === 'string') {
        return {
          icon,
          color: ''
        }
      }
      return false
    },
    // 自定义图标
    getSuffixIcon (row) {
      const { suffixIcon } = this
      let icon = ''
      if (typeof suffixIcon === 'function') {
        icon = suffixIcon(row)
      } else {
        icon = suffixIcon
      }
      if (typeof icon === 'object') {
        return icon
      } else if (typeof icon === 'string') {
        return {
          icon,
          color: ''
        }
      }
      return false
    }
  }
}
</script>

<style lang="scss">
.ml-tree-box {
  background-color: #ffffff;
  .ml-tree-container {
    position: relative;
    overflow: hidden;
    .ml-tree-scroll {
      height: 100%;
      z-index: 2;
      .ml-tree-content {
        padding: 0 30rpx;

        .ml-tree-node {
          font-size: 34rpx;
          display: flex;
          align-items: center;
          border-bottom: 1rpx solid #efefef;

          .ml-tree-node__title {
            height: 116rpx;
            flex: 1;
            width: 0;
            display: flex;
            align-items: center;
            &.hasExplain {
              align-items: normal;
              justify-content: center;
              flex-direction: column;
            }
            .nowrap {
              white-space: nowrap;
              overflow: hidden;
              text-overflow: ellipsis;
            }

            &.back {
              color: #6e6e6e;
              font-size: 26rpx;
            }

            .ml-tree-node__text {
              margin: 0 10rpx;
            }
            .ml-tree-node__explain {
              color: #989898;
              font-size: 26rpx;
            }
          }

          .ml-tree-node__extra {
            height: 116rpx;
            display: flex;
            align-items: center;
            padding: 0 0 0 20rpx;
            .ml-tree-node__extra-item {
              display: flex;
              align-items: center;
              height: 100%;
            }
          }

          &:last-child {
            border-bottom: none;
          }

          .ml-tree-loading-tip {
            flex: 1;
            height: 116rpx;
            text-align: center;
            vertical-align: middle;
            display: flex;
            align-items: center;
            justify-content: center;
            .ml-tree-loading-text {
              font-size: 26rpx;
              color: #8a8686;
              margin-left: 15rpx;
            }
          }
        }
      }
    }
  }

  .ml-tree-tool {
    position: sticky;
    .ml-tree-breadcrumb-scroll {
      background-color: #f3f3f3;
    }
    .ml-tree-breadcrumb {
      display: flex;
      padding: 0 30rpx;
      align-items: center;

      .breadcrumb-item {
        position: relative;
        height: 50rpx;
        line-height: 50rpx;
        color: #afafaf;
        font-size: 26rpx;
        display: flex;
        align-items: center;
        .breadcrumb-text {
          max-width: 220rpx;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }

        &:last-child {
          color: #6e6e6e;
        }
      }
    }
  }
}
</style>
