<template>
  <div
    :class="[
      'el-cascader-panel',
      border && 'is-bordered'
    ]"
    @keydown="handleKeyDown">
    <cascader-menu
      ref="menu"
      v-for="(menu, index) in menus"
      :index="index"
      :key="index"
      :nodes="menu"></cascader-menu>
  </div>
</template>

<script>
import CascaderMenu from './cascader-menu';
import Store from './store';
import merge from 'element-ui/src/utils/merge';
import AriaUtils from 'element-ui/src/utils/aria-utils';
import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
import {
  noop,
  coerceTruthyValueToArray,
  isEqual,
  isEmpty,
  valueEquals
} from 'element-ui/src/utils/util';

const { keys: KeyCode } = AriaUtils;
const DefaultProps = {
  expandTrigger: 'click', // or hover
  multiple: false,
  checkStrictly: false, // whether all nodes can be selected
  emitPath: true, // wether to emit an array of all levels value in which node is located
  lazy: false,
  lazyLoad: noop,
  value: 'value',
  label: 'label',
  children: 'children',
  leaf: 'leaf',
  disabled: 'disabled',
  hoverThreshold: 500
};

const isLeaf = el => !el.getAttribute('aria-owns');

const getSibling = (el, distance) => {
  const { parentNode } = el;
  if (parentNode) {
    const siblings = parentNode.querySelectorAll('.el-cascader-node[tabindex="-1"]');
    const index = Array.prototype.indexOf.call(siblings, el);
    return siblings[index + distance] || null;
  }
  return null;
};

const getMenuIndex = (el, distance) => {
  if (!el) return;
  const pieces = el.id.split('-');
  return Number(pieces[pieces.length - 2]);
};

const focusNode = el => {
  if (!el) return;
  el.focus();
  !isLeaf(el) && el.click();
};

const checkNode = el => {
  if (!el) return;

  const input = el.querySelector('input');
  if (input) {
    input.click();
  } else if (isLeaf(el)) {
    el.click();
  }
};

export default {
  name: 'ElCascaderPanel',

  components: {
    CascaderMenu
  },

  props: {
    value: {},
    options: Array,
    props: Object,
    border: {
      type: Boolean,
      default: true
    },
    renderLabel: Function
  },

  provide() {
    return {
      panel: this
    };
  },

  data() {
    return {
      checkedValue: null,
      checkedNodePaths: [],
      store: [],
      menus: [],
      activePath: [],
      loadCount: 0
    };
  },

  computed: {
    config() {
      return merge({ ...DefaultProps }, this.props || {});
    },
    multiple() {
      return this.config.multiple;
    },
    checkStrictly() {
      return this.config.checkStrictly;
    },
    leafOnly() {
      return !this.checkStrictly;
    },
    isHoverMenu() {
      return this.config.expandTrigger === 'hover';
    },
    renderLabelFn() {
      return this.renderLabel || this.$scopedSlots.default;
    }
  },

  watch: {
    options: {
      handler: function() {
        this.initStore();
      },
      immediate: true,
      deep: true
    },
    value() {
      this.syncCheckedValue();
      this.checkStrictly && this.calculateCheckedNodePaths();
    },
    checkedValue(val) {
      if (!isEqual(val, this.value)) {
        this.checkStrictly && this.calculateCheckedNodePaths();
        this.$emit('input', val);
        this.$emit('change', val);
      }
    }
  },

  mounted() {
    if (!isEmpty(this.value)) {
      this.syncCheckedValue();
    }
  },

  methods: {
    initStore() {
      const { config, options } = this;
      if (config.lazy && isEmpty(options)) {
        this.lazyLoad();
      } else {
        this.store = new Store(options, config);
        this.menus = [this.store.getNodes()];
        this.syncMenuState();
      }
    },
    syncCheckedValue() {
      const { value, checkedValue } = this;
      if (!isEqual(value, checkedValue)) {
        this.checkedValue = value;
        this.syncMenuState();
      }
    },
    syncMenuState() {
      const { multiple, checkStrictly } = this;
      this.syncActivePath();
      multiple && this.syncMultiCheckState();
      checkStrictly && this.calculateCheckedNodePaths();
      this.$nextTick(this.scrollIntoView);
    },
    syncMultiCheckState() {
      const nodes = this.getFlattedNodes(this.leafOnly);

      nodes.forEach(node => {
        node.syncCheckState(this.checkedValue);
      });
    },
    syncActivePath() {
      const { store, multiple, activePath, checkedValue } = this;

      if (!isEmpty(activePath)) {
        const nodes = activePath.map(node => this.getNodeByValue(node.getValue()));
        this.expandNodes(nodes);
      } else if (!isEmpty(checkedValue)) {
        const value = multiple ? checkedValue[0] : checkedValue;
        const checkedNode = this.getNodeByValue(value) || {};
        const nodes = (checkedNode.pathNodes || []).slice(0, -1);
        this.expandNodes(nodes);
      } else {
        this.activePath = [];
        this.menus = [store.getNodes()];
      }
    },
    expandNodes(nodes) {
      nodes.forEach(node => this.handleExpand(node, true /* silent */));
    },
    calculateCheckedNodePaths() {
      const { checkedValue, multiple } = this;
      const checkedValues = multiple
        ? coerceTruthyValueToArray(checkedValue)
        : [ checkedValue ];
      this.checkedNodePaths = checkedValues.map(v => {
        const checkedNode = this.getNodeByValue(v);
        return checkedNode ? checkedNode.pathNodes : [];
      });
    },
    handleKeyDown(e) {
      const { target, keyCode } = e;

      switch (keyCode) {
        case KeyCode.up:
          const prev = getSibling(target, -1);
          focusNode(prev);
          break;
        case KeyCode.down:
          const next = getSibling(target, 1);
          focusNode(next);
          break;
        case KeyCode.left:
          const preMenu = this.$refs.menu[getMenuIndex(target) - 1];
          if (preMenu) {
            const expandedNode = preMenu.$el.querySelector('.el-cascader-node[aria-expanded="true"]');
            focusNode(expandedNode);
          }
          break;
        case KeyCode.right:
          const nextMenu = this.$refs.menu[getMenuIndex(target) + 1];
          if (nextMenu) {
            const firstNode = nextMenu.$el.querySelector('.el-cascader-node[tabindex="-1"]');
            focusNode(firstNode);
          }
          break;
        case KeyCode.enter:
          checkNode(target);
          break;
        case KeyCode.esc:
        case KeyCode.tab:
          this.$emit('close');
          break;
        default:
          return;
      }
    },
    handleExpand(node, silent) {
      const { activePath } = this;
      const { level } = node;
      const path = activePath.slice(0, level - 1);
      const menus = this.menus.slice(0, level);

      if (!node.isLeaf) {
        path.push(node);
        menus.push(node.children);
      }

      this.activePath = path;
      this.menus = menus;

      if (!silent) {
        const pathValues = path.map(node => node.getValue());
        const activePathValues = activePath.map(node => node.getValue());
        if (!valueEquals(pathValues, activePathValues)) {
          this.$emit('active-item-change', pathValues); // Deprecated
          this.$emit('expand-change', pathValues);
        }
      }
    },
    handleCheckChange(value) {
      this.checkedValue = value;
    },
    lazyLoad(node, onFullfiled) {
      const { config } = this;
      if (!node) {
        node = node || { root: true, level: 0 };
        this.store = new Store([], config);
        this.menus = [this.store.getNodes()];
      }
      node.loading = true;
      const resolve = dataList => {
        const parent = node.root ? null : node;
        dataList && dataList.length && this.store.appendNodes(dataList, parent);
        node.loading = false;
        node.loaded = true;

        // dispose default value on lazy load mode
        if (Array.isArray(this.checkedValue)) {
          const nodeValue = this.checkedValue[this.loadCount++];
          const valueKey = this.config.value;
          const leafKey = this.config.leaf;

          if (Array.isArray(dataList) && dataList.filter(item => item[valueKey] === nodeValue).length > 0) {
            const checkedNode = this.store.getNodeByValue(nodeValue);

            if (!checkedNode.data[leafKey]) {
              this.lazyLoad(checkedNode, () => {
                this.handleExpand(checkedNode);
              });
            }

            if (this.loadCount === this.checkedValue.length) {
              this.$parent.computePresentText();
            }
          }
        }

        onFullfiled && onFullfiled(dataList);
      };
      config.lazyLoad(node, resolve);
    },

    /**
     * public methods
    */
    calculateMultiCheckedValue() {
      this.checkedValue = this.getCheckedNodes(this.leafOnly)
        .map(node => node.getValueByOption());
    },
    scrollIntoView() {
      if (this.$isServer) return;

      const menus = this.$refs.menu || [];
      menus.forEach(menu => {
        const menuElement = menu.$el;
        if (menuElement) {
          const container = menuElement.querySelector('.el-scrollbar__wrap');
          const activeNode = menuElement.querySelector('.el-cascader-node.is-active') ||
            menuElement.querySelector('.el-cascader-node.in-active-path');
          scrollIntoView(container, activeNode);
        }
      });
    },
    getNodeByValue(val) {
      return this.store.getNodeByValue(val);
    },
    getFlattedNodes(leafOnly) {
      const cached = !this.config.lazy;
      return this.store.getFlattedNodes(leafOnly, cached);
    },
    getCheckedNodes(leafOnly) {
      const { checkedValue, multiple } = this;
      if (multiple) {
        const nodes = this.getFlattedNodes(leafOnly);
        return nodes.filter(node => node.checked);
      } else {
        return isEmpty(checkedValue)
          ? []
          : [this.getNodeByValue(checkedValue)];
      }
    },
    clearCheckedNodes() {
      const { config, leafOnly } = this;
      const { multiple, emitPath } = config;
      if (multiple) {
        this.getCheckedNodes(leafOnly)
          .filter(node => !node.isDisabled)
          .forEach(node => node.doCheck(false));
        this.calculateMultiCheckedValue();
      } else {
        this.checkedValue = emitPath ? [] : null;
      }
    }
  }
};
</script>
