<template>
  <div
    ref="tableWrapperContainer"
    v-spinner="realLoading"
    class="mm-table-wrapper">
    <div
      v-if="Boolean($slots.header)"
      class="mm-table__header">
      <slot name="header"></slot>
    </div>
    <div
      ref="tableWrapper"
      :class="wrapperClassList"
      :style="wrapperStyle">
      <mm-thead
        v-if="!!realHeight"
        ref="thead"
        v-bind="theadProps"
        :columns="localColumns"></mm-thead>
      <mm-tbody
        ref="tbody"
        v-bind="tbodyProps"
        :columns="localColumns"
        :draggable="draggable"
        :drag-handle="dragHandle"
        :set-table-layout="setTableLayout"
        @drop="handleDrop">
        <template #empty>
          <slot name="empty"></slot>
        </template>
      </mm-tbody>
      <mm-tfixed
        v-if="leftColumns.length && !sticky"
        :store="store"
        position="left">
        <mm-thead
          v-if="!!realHeight"
          :columns="leftColumns"
          v-bind="theadProps"
          fixed="left"></mm-thead>
        <mm-tbody
          v-bind="tbodyProps"
          :columns="leftColumns"
          fixed="left"></mm-tbody>
      </mm-tfixed>
      <mm-tfixed
        v-if="rightColumns.length && !sticky"
        :store="store"
        position="right">
        <mm-thead
          v-if="!!realHeight"
          :columns="rightColumns"
          v-bind="theadProps"
          fixed="right"></mm-thead>
        <mm-tbody
          v-bind="tbodyProps"
          :columns="rightColumns"
          fixed="right"></mm-tbody>
      </mm-tfixed>
      <mm-tfooter
        v-if="store.state.filteredRows.length && summary"
        ref="tfooter"
        :tfooter-props="tfooterProps"
        :left-columns="leftColumns"
        :right-columns="rightColumns"
        :columns="localColumns"
        :store="store"></mm-tfooter>
    </div>
    <div
      v-if="Boolean($slots.footer)"
      class="mm-table__footer">
      <slot name="footer"></slot>
    </div>
    <mm-pagination
      v-if="!!pagination"
      ref="pagination"
      v-bind.sync="pagination"></mm-pagination>
  </div>
</template>

<script>
import isEqual from "lodash/isEqual"
import get from "lodash/get"
import { addListener, removeListener } from "resize-detector/dist"

import spinner from "../../directives/spinner"

import locale from "../../mixins/locale"
import { t } from "../../locale"
import Pagination from "../pagination"

import Thead from "./thead"
import Tbody from "./tbody.vue"
import Tfixed from "./tfixed"
import Tfooter from "./tfooter"
import Store from "./store"
import { getFlattenColumns, getScrollBarWidth, cssSupport, isPositiveNumber, createSetCachedColWidth } from "./utils"

import config from "../../managers/config"

config.defaults({
  "Table.size": "medium",
  "Table.draggable": false,
  "Table.theme": "striped",
  "Table.minColWidth": 80,
  "Table.minDragWidth": 20,
  "Table.checkboxColWidth": 60,
  "Table.columnDraggable": true,
  // "Table.useVirtual": false,
  "Table.useSticky": true,
  // 自定义渲染类型映射
  "Table.TypeRenderMap": {},
})

export default {
  name: "mm-table",
  components: {
    "mm-pagination": Pagination,
    "mm-thead": Thead,
    "mm-tbody": Tbody,
    "mm-tfixed": Tfixed,
    "mm-tfooter": Tfooter,
  },
  directives: { spinner },
  mixins: [locale],
  provide() {
    return {
      "mm-table": this,
    }
  },
  props: {
    columns: {
      type: Array,
      default: () => [],
    },
    dataSource: {
      type: Array,
      default: () => [],
    },
    selectedRows: {
      type: Array,
      default: () => [],
    },
    height: {
      type: Number,
      default: 0,
    },
    maxHeight: {
      type: Number,
    },
    width: {
      type: Number,
      default: 0,
    },
    size: {
      type: String,
      default: () => config.get("Table.size"),
    },
    rowSelection: {
      type: Object,
      default: null,
    },
    draggable: {
      type: Boolean,
      default: () => config.get("Table.draggable"),
    },
    dragHandle: String,
    spanMethod: Function,
    onClick: {
      type: Function,
      default: () => {},
    },
    rowKey: String,
    theme: {
      type: String,
      default: () => config.get("Table.theme"),
      validator() {
        return ["horizontal-bordered", "vertical-bordered", "bordered", "striped", "striped-bordered", "line"]
      },
    },
    minColWidth: {
      type: Number,
      default: () => config.get("Table.minColWidth"),
    },
    minDragWidth: {
      type: Number,
      default: () => config.get("Table.minDragWidth"),
    },
    checkboxColWidth: {
      type: Number,
      default: () => config.get("Table.checkboxColWidth"),
    },
    headerRowClassName: [String, Function],
    rowClass: {
      type: [String, Object, Array, Function],
    },
    loading: {
      type: [Boolean, Object],
      default: false,
    },
    pagination: {
      type: [Object, Boolean],
      validator(val) {
        return val === false || typeof val === "object"
      },
    },
    columnDraggable: {
      type: Boolean,
      default: () => config.get("Table.columnDraggable"),
    },
    onSort: Function,
    onFilter: Function,
    summary: Boolean,
    summaryText: {
      type: String,
      default() {
        return t("ui.table.sumText")
      },
    },
    summaryRender: Function,
    rowHeight: Number,
    remainRows: {
      type: Number,
      default: 2,
    },
    // useVirtual: {
    //   type: Boolean,
    //   default: () => config.get("Table.useVirtual"),
    // },
    useSticky: {
      type: Boolean,
      default: () => config.get("Table.useSticky"),
    },
    rowEvents: {
      type: Object,
      default: () => ({}),
    },
    observerDomResize: {
      type: Boolean,
      default: false,
    },
    cacheWidthKey: String,
  },
  data() {
    return {
      leftColumns: [],
      rightColumns: [],
      localColumns: [],
      store: new Store(),
    }
  },
  computed: {
    showSelectCol() {
      return Boolean(this.rowSelection && this.rowSelection.onChange)
    },
    wrapperClassList() {
      const { size, theme } = this
      return ["mm-table", `mm-table__${size}`, `mm-table__${theme}`]
    },
    wrapperStyle() {
      return this.width
        ? {
          width: `${this.width}px`,
        }
        : {}
    },
    theadProps() {
      return {
        rowSelection: this.rowSelection,
        store: this.store,
        scrollHandler: this.scrollHandler,
        sticky: this.sticky,
        headerRowClassName: this.headerRowClassName,
      }
    },
    tbodyProps() {
      return {
        height: this.height,
        maxHeight: this.maxHeight,
        loading: this.loading,
        rowSelection: this.rowSelection,
        store: this.store,
        scrollHandler: this.scrollHandler,
        spanMethod: this.spanMethod,
        handleClick: this.handleClick,
        rowKey: this.rowKey,
        rowClass: this.rowClass,
        summary: this.summary,
        rowHeight: this.rowHeight,
        remainRows: this.remainRows,
        // useVirtual: this.useVirtual,
        sticky: this.sticky,
        rowEvents: this.rowEvents,
      }
    },
    tfooterProps() {
      return {
        rowSelection: this.rowSelection,
        store: this.store,
        scrollHandler: this.scrollHandler,
        spanMethod: this.spanMethod,
        summaryText: this.summaryText,
        summaryRender: this.summaryRender,
        rowClass: this.rowClass,
      }
    },
    realHeight() {
      return this.height || this.maxHeight
    },
    realLoading() {
      if (typeof this.loading === "boolean") {
        return {
          visible: this.loading,
        }
      }

      return this.loading
    },
    sticky() {
      if (this.$isServer) {
        return false
      }
      return this.useSticky && cssSupport("position", "sticky")
    },
  },
  watch: {
    rowKey: {
      immediate: true,
      handler(value) {
        this.store.setState("rowKey", value)
      },
    },
    rowSelection: {
      immediate: true,
      handler(value) {
        this.store.setRowSelection(value)
      },
    },
    columns: {
      immediate: true,
      handler() {
        this.setLocalColumns()
      },
    },
    cacheWidthKey: {
      handler() {
        this.setLocalColumns()
      },
    },
    dataSource: {
      immediate: true,
      handler(value) {
        this.store.setState("rows", value)
        this.store.setState("dataSource", value)
      },
    },
    selectedRows: {
      immediate: true,
      handler(value = []) {
        const rows = this.rowKey ? value.map(item => get(item, this.rowKey)) : value
        if (!isEqual(rows, this.store.state.selectedRows)) {
          this.store.state.selectedRows = rows
        }
      },
    },
    "store.state.selectedRows": {
      immediate: true,
      handler(value = []) {
        if (!this.showSelectCol) {
          return
        }

        const { onChange } = this.rowSelection
        const { rowKey } = this
        const { enabledRows } = this.store.state
        const length = value.length
        let update = value
        if (rowKey) {
          update = this.dataSource.filter(item => value.includes(get(item, rowKey)))
        }

        this.$emit("update:selectedRows", update)
        onChange && onChange(update)

        this.store.state.selectAll = Boolean(
          length && length === enabledRows.length
        )
        this.store.state.indeterminate = Boolean(
          length && length < enabledRows.length
        )
      },
    },
    "store.state.theadHover"(value) {
      const classList = this.$refs.tableWrapper && this.$refs.tableWrapper.classList

      if (!classList) {
        return
      }

      if (value) {
        classList.add("mm-table-thead-hover")
        return
      }
      classList.remove("mm-table-thead-hover")
    },
    pagination: {
      immediate: true,
      handler() {
        const pageHeight = this.$refs.pagination ? this.$refs.pagination.$el.offsetHeight : 0
        const borderTop = ["bordered", "striped-bordered"].includes(this.theme) ? 1 : 0 // borderTop 是放在容器上的，会影响表格整体的高度
        this.store.setLayout("pageHeight", pageHeight + borderTop)
      },
    },
  },
  mounted() {
    this.initLayoutData()
    this.initResizeListener()
  },
  methods: {
    initLayoutData() {
      // TODO line 版本的表格稳定后给所有主题表格加上相关样，可移除此代码
      this.store.setLayout("scrollBarWidth", this.theme === "line" ? 0 : getScrollBarWidth())
    },
    initResizeListener() {
      if (this.observerDomResize) {
        addListener(this.$refs.tableWrapperContainer, this.domResizeHandler)
        this.$once("hook:beforeDestroy", () => {
          removeListener(this.$refs.tableWrapperContainer, this.domResizeHandler)
        })
      }
    },
    domResizeHandler() {
      this.$refs.tbody.debounceResizeHandler()
      const { theadInner, tfooterInner } = this.store.refs
      theadInner && theadInner.debounceResizeHandler()
      tfooterInner && tfooterInner.debounceResizeHandler()
    },
    getColFixedOffset(columns, direction) {
      const selectColWidth = this.showSelectCol ? this.checkboxColWidth : 0
      const initWidth = direction === "left" && selectColWidth ? selectColWidth : 0
      return !columns.length ? initWidth : columns[columns.length - 1].offset + columns[columns.length - 1].width
    },
    setLocalColumns() {
      let columns = this.columns
      let left = []
      let right = []
      let normal = []
      columns = this.groupedColumns(columns)
      columns.forEach(col => {
        switch (col.fixed) {
          case "left":
            left.push(col)
            break
          case "right":
            right.push(col)
            break
          default:
            normal.push(col)
        }
      })
      if (left.length) {
        left[left.length - 1].isLast = true
      }
      if (right.length) {
        right[0].isLast = true
      }

      this.patchCachedColumnsWidth(columns)
      this.leftColumns = left
      this.rightColumns = right
      this.localColumns = [...left, ...normal, ...right]
      this.store.setState("columns", columns)
    },
    groupedColumns(columns, currentRow = 0, parentColumn = {}, rows = []) {
      rows[currentRow] = rows[currentRow] || []
      const grouped = []
      const setRowSpan = column => {
        const rowSpan = rows.length - currentRow
        if (
          column &&
          !column.children &&
          rowSpan > 1 &&
          (!column.rowSpan || column.rowSpan < rowSpan)
        ) {
          column.rowSpan = rowSpan
        }
      }

      columns.forEach((column, index) => {
        const newColumn = { ...column }
        rows[currentRow].push(newColumn)
        parentColumn.colSpan = parentColumn.colSpan || 0
        parentColumn.width = parentColumn.width || 0
        if (newColumn.children && newColumn.children.length > 0) {
          newColumn.children = this.groupedColumns(
            newColumn.children,
            currentRow + 1,
            newColumn,
            rows
          )
          parentColumn.colSpan += newColumn.colSpan
          parentColumn.width += newColumn.width
        } else {
          parentColumn.colSpan++
          parentColumn.width += newColumn.width
        }
        // update rowspan to all same row columns
        for (let i = 0; i < rows[currentRow].length - 1; ++i) {
          setRowSpan(rows[currentRow][i])
        }
        // last column, update rowspan immediately
        if (index + 1 === columns.length) {
          setRowSpan(newColumn)
        }
        grouped.push(newColumn)
      })

      return grouped
    },
    setTableLayout() {
      if (this.$isServer || !this.$el) {
        return
      }
      const columns = getFlattenColumns(this.localColumns)
      const { tbody, thead, pagination, tfooter } = this.$refs
      const headHeight = thead ? thead.$el.clientHeight : 0
      const pageHeight = pagination ? pagination.$el.clientHeight : 0
      const footHeight = tfooter ? tfooter.$el.clientHeight : 0
      const containerWidth = this.width || this.$refs.tableWrapper.clientWidth
      const maxHeight = this.realHeight ? (this.realHeight - headHeight - footHeight - pageHeight) : 0
      const scrollY = Boolean(
        maxHeight && tbody.$refs.tbody.clientHeight > maxHeight
      )
      const scrollBarWidth = scrollY
        ? tbody.$el.offsetWidth - tbody.$el.clientWidth
        : 0
      const selectColWidth = this.showSelectCol ? this.checkboxColWidth : 0
      const minColWidth = this.minColWidth

      const fixedWidth = columns
        .reduce((prev, next) => {
          let configMinWidth = 0
          if (!next.flex && isPositiveNumber(next.width)) {
            configMinWidth = Math.max(next.width, configMinWidth)
          } else if (isPositiveNumber(next.minWidth)) {
            configMinWidth = Math.max(next.minWidth, configMinWidth)
          }
          return prev + (configMinWidth || minColWidth)
        }, 0)
      const flexColumns = columns.filter(
        col => !isPositiveNumber(col.width) || col.flex
      )
      const flexCount = flexColumns.length

      if (flexCount) {
        const restWidth =
          containerWidth - fixedWidth - selectColWidth - scrollBarWidth
        let addonWidth = Math.floor(restWidth / flexCount)
        const remainder = addonWidth < 0 ? 0 : restWidth % flexCount
        flexColumns.forEach((col, index) => {
          let realWidth = isPositiveNumber(col.minWidth) ? col.minWidth : minColWidth
          if (addonWidth > 0) {
            realWidth += addonWidth
          }
          this.$set(col, "flex", true)
          this.$set(col, "width", index === 0 ? realWidth + remainder : realWidth)
        })
      }

      const leftColumns = columns.filter(col => col.fixed === "left")
      const rightColumns = columns.filter(col => col.fixed === "right")

      if (leftColumns.length && this.sticky) {
        leftColumns.forEach((col, index) => {
          const offset =
            index === 0
              ? selectColWidth
              : leftColumns[index - 1].offset + leftColumns[index - 1].width
          this.$set(col, "offset", offset)
        })
      }

      if (rightColumns.length && this.sticky) {
        rightColumns.reverse().forEach((col, index) => {
          const offset =
            index === 0
              ? 0
              : rightColumns[index - 1].offset + rightColumns[index - 1].width
          this.$set(col, "offset", offset)
        })
      }

      const tableWidth = columns.reduce(
        (prev, next) => prev + next.width,
        selectColWidth
      )
      const scrollX = tableWidth > containerWidth - scrollBarWidth

      this.store.setState("flattenColumns", columns)
      this.store.setScrollStatus(scrollX, scrollY)
      this.store.setLayout("containerWidth", containerWidth)
      this.store.setLayout("tableWidth", tableWidth)
      this.store.setLayout("selectColWidth", selectColWidth)
    },
    scrollHandler(e) {
      this.store.setState("hoverRowIndex", -1)

      const { scrollTop, scrollLeft } = e.target
      const { scrollX, scrollY } = this.store.layout
      const {
        thead,
        tbody,
        tfooter,
        fixedTbody,
        rightFixedTbody,
      } = this.store.refs

      if (e.target === thead && scrollLeft !== scrollX) {
        tbody.scrollLeft = scrollLeft
        tfooter && (tfooter.scrollLeft = scrollLeft)
        this.store.setScroll(scrollLeft, "x")
      }

      if (e.target === tbody && scrollLeft !== scrollX) {
        thead && (thead.scrollLeft = scrollLeft)
        tfooter && (tfooter.scrollLeft = scrollLeft)
        this.store.setScroll(scrollLeft, "x")
      }

      if (e.target === tfooter && scrollLeft !== scrollX) {
        thead && (thead.scrollLeft = scrollLeft)
        tbody.scrollLeft = scrollLeft
        this.store.setScroll(scrollLeft, "x")
      }

      if (!this.sticky && e.target !== thead && e.target !== tfooter && scrollTop !== scrollY) {
        tbody.scrollTop = scrollTop
        fixedTbody && (fixedTbody.scrollTop = scrollTop)
        rightFixedTbody && (rightFixedTbody.scrollTop = scrollTop)
        this.store.setScroll(scrollTop, "y")
      }
    },
    scrollTo(value) {
      const { tbody } = this.store.refs
      tbody.scrollTop = value
    },
    patchCachedColumnsWidth(columns) {
      if (!this.cacheWidthKey) {
        return
      }
      const setCachedColWidth = createSetCachedColWidth(this.cacheWidthKey)
      const flattenColumns = getFlattenColumns(columns)
      flattenColumns.forEach(col => setCachedColWidth(col, this.minDragWidth))
    },
    handleDrop(...args) {
      this.$emit("drop", ...args)
    },
    handleClick(...args) {
      this.$emit("click", ...args)
      this.onClick && this.onClick(...args)
    },
  },
}
</script>
