<template>
  <div class="table-responsive">
    <table>
      <thead>
        <tr v-for="(tr, index) in combineHeads" :key="index">
          <th
            v-for="cell in tr"
            :key="cell.__index"
            :rowspan="cell.rowspan"
            :colspan="cell.colspan"
          >
            <div
              :class="{ 'col-corner-bg': cell.isCorner }"
              :style="{ 'min-height': _getMinHeightByRowCount(cell.rowspan) }"
            >
              {{ cell.isCorner ? (rowPaths.length + ' x ' + colPaths.length) : cell.value }}
            </div>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(tr, index) in combineValues" :key="tr.__index">
          <td
            v-for="cell in tr.data"
            :key="cell.__index"
            :rowspan="cell.rowspan"
            :colspan="cell.colspan"
          >
            <div
              :class="{ 'col-corner-bg': cell.isCorner }"
              :style="{ 'min-height': _getMinHeightByRowCount(cell.rowspan) }"
            >
              {{ cell.value }}
            </div>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
import {
  mergeBaseInfo,
  convertPathToMap,
  getHeightByCount
} from './utils'
import { SEPARATOR } from './utils/constants'

export default {
  name: 'PivotTable',
  props: {
    data: {
      type: Array,
      default: () => []
    },
    rows: {
      type: Array,
      default: () => []
    },
    columns: {
      type: Array,
      default: () => []
    },
    values: {
      type: Array,
      default: () => []
    },
    summary: {
      type: Array,
      default: () => []
    }
  },
  data: () => ({
    localRows: [],
    localColumns: [],
    localValues: [],
    localData: [],
    // 计算列的数据
    calcData: [],
    // Separator
    Separator: SEPARATOR,
    // 合并后的表头
    combineHeads: [],
    // 合并后的单元格
    combineValues: []
  }),
  computed: {
    rowPaths() {
      const _paths = this._combinePaths(
        ...this.localRows.map(({ values }) => values)
      )
      return _paths
    },
    colPaths() {
      const keys = this.localColumns.map(({ values }) => values)
      if (this.localValues.length) {
        keys.push(this.localValues.map(({ key }) => key))
      }
      const _paths = this._combinePaths(...keys)
      return _paths
    },
    // 列的表头
    colHeads() {
      // 共有多少行
      const _rows = this.localColumns.map(() => [])
      // 有几个值
      const valuesLen = this.localValues.length
      if (valuesLen) {
        _rows.push([])
      }
      // todo: 仅有 values 的时候会有一个空的path
      this.colPaths.forEach((path, pathIndex) => {
        // 条件值
        const pathValues = path.split(this.Separator)
        // 存储路径
        const currPath = []
        _rows.forEach((row, rowIndex) => {
          const cellData = {}
          const currVal = pathValues[rowIndex] || ''
          // 是否为 values 行
          const isLastRow = rowIndex === _rows.length - 1
          // 存储路径
          currPath.push(currVal)
          const baseX = rowIndex
          const baseY = this.localRows.length + pathIndex
          row.push(
            Object.assign(
              cellData,
              mergeBaseInfo({
                __index: `${baseX}-${baseY}`,
                x: baseX,
                y: baseY,
                path: currPath.filter((item) => !!item),
                // 最后一行是 values，替换显示文本
                value: valuesLen && isLastRow && currVal ? this.localValues.find(({ key }) => key === currVal).label : currVal
              })
            )
          )
        })
      })
      return _rows
    },
    // 行的表头
    rowHeads() {
      // 共有多少列
      const _columns = []
      // 左上角特殊处理
      _columns.push(mergeBaseInfo({
        __index: `0-0`,
        colspan: this.localRows.length,
        rowspan: this.localColumns.length,
        // 左上角标记
        isCorner: true
      }))
      this.localRows.forEach(({ label }, index) => {
        _columns.push(mergeBaseInfo({
          __index: `${this.localColumns.length}-${index}`,
          value: label,
          x: this.localColumns.length,
          y: index
        }))
      })
      return _columns
    },
    // 行对应的值
    rowHeadValues() {
      return this.localRows.length
        ? this.rowPaths.map((path, index) => {
          const values = path.split(this.Separator)
          const currPath = []
          return this.localRows.map((item, rowIndex) => {
            const _index = rowIndex
            const _currVal = values[_index] || ''
            const baseX = this.localColumns.length + +Boolean(this.localValues.length) + index
            const baseY = _index
            currPath.push(_currVal)
            return mergeBaseInfo({
              __index: `${baseX}-${baseY}`,
              value: _currVal,
              x: baseX,
              y: baseY,
              path: currPath.filter((item) => !!item)
            })
          })
        })
        : []
    },
    // 计算所有对应条件的值
    dataValues() {
      // 列对应的条件
      const colConditions = convertPathToMap(
        this.colPaths,
        this.localColumns.map(({ key }) => key).concat(this.localValues.length ? ['value'] : [])
      )
      // 行对应的条件
      const rowConditions = convertPathToMap(
        this.rowPaths,
        this.localRows.map(({ key }) => key)
      )
      // console.log('colConditions', colConditions)
      // console.log('rowConditions', rowConditions)
      // 针对没传入行或列的处理
      // !colConditions.length && colConditions.push({});
      // !rowConditions.length && rowConditions.push({});
      // 过滤数据, 遍历行以及遍历行对应的列
      return rowConditions.map((rowCondition, rowConditionIndex) => {
        const _data = colConditions.map((colCondition, colConditionIndex) => {
          // 存储当前单元对应的数据
          const cellData = {}
          // 当前单元对应的条件
          const conditions = Object.assign({}, rowCondition, colCondition)
          const _filterConditions = Object.fromEntries(
            Object.entries(conditions).filter(
              (item) => item[0] !== 'value'
            )
          )
          // 通过当前单元对应的条件，过滤数据
          const filterData = this._filterData(_filterConditions, this.localData)
          // 对应表格的坐标位置
          const baseX = this.localColumns.length + +Boolean(this.localValues.length) + rowConditionIndex
          const baseY = this.localRows.length + colConditionIndex
          Object.assign(
            cellData,
            mergeBaseInfo({
              conditions,
              x: baseX,
              y: baseY,
              __index: `${baseX}-${baseY}`
            })
          )
          // 针对为指定值 props.values 的空处理(绘制空表格)
          const isEmptyValues = this.localColumns.length && this.localRows.length && !this.localValues.length
          if (isEmptyValues) {
            Object.assign(cellData, { value: '' })
          } else {
            // 从 props.values 中找出对应的值 handle
            // 注意：this.localValues 通过 JSON.xxx 序列化后，handle 会被忽略
            const _value = this.values.find(({ key }) => key === conditions.value)
            Object.assign(cellData, { value: _value && _value.key ? this._reduceValue(filterData, _value.key) : '' })
          }
          return cellData
        }
        )
        return {
          __index: _data[0].x,
          data: _data
        }
      })
    }
  },
  created: function() {
    this.init()
  },
  methods: {
    init() {
      if (this.rows.length || this.columns.length || this.values) {
        this.handleDataClone()
        this.setValuesToColAndRow()
        this.handleCalcData()
        this.handleCombineHeads()
        this.handleCombineValues()
      } else {
        console.warn(
          '[Warn]: props.rows, props.columns, props.values at least one is not empty.'
        )
      }
    },
    // clone data
    handleDataClone() {
      this.localRows = JSON.parse(JSON.stringify(this.rows))
      this.localColumns = JSON.parse(JSON.stringify(this.columns))
      this.localValues = JSON.parse(JSON.stringify(this.values))
      this.localData = Object.freeze(this.data)
    },
    // set the `values` attribute to rows and columns
    setValuesToColAndRow() {
      const rowKeys = this.localRows.map(({ key }) => key)
      const columnKeys = this.localColumns.map(({ key }) => key)
      const rowValues = this._findCategory(rowKeys, this.localData)
      const columnValues = this._findCategory(columnKeys, this.localData)
      this.localRows.forEach((row) => {
        const { key, values } = row
        this.$set(row, 'values', values || rowValues[key] || [])
      })
      this.localColumns.forEach((column) => {
        const { key, values } = column
        this.$set(column, 'values', values || columnValues[key] || [])
      })
    },
    // 合并表头
    handleCombineHeads() {
      let combineColHeads = JSON.parse(JSON.stringify(this.colHeads))
      combineColHeads[0] = combineColHeads[0] || []
      combineColHeads[0].unshift(...this.rowHeads.filter((item) => item.isCorner))
      combineColHeads[combineColHeads.length - 1].unshift(...this.rowHeads.filter((item) => !item.isCorner))
      combineColHeads = combineColHeads.filter((item) => item.length)
      this.combineHeads = combineColHeads
    },
    // 合并值
    handleCombineValues() {
      // values
      const combineValues = []
      const valueRowCount = this.dataValues.length || this.rowHeadValues.length
      for (let i = 0; i < valueRowCount; i++) {
        const _currRowHeadValue = this.rowHeadValues[i] || []
        const _currValue = this.dataValues[i] || {}
        const _row = [..._currRowHeadValue, ...(_currValue.data || [])]
        combineValues.push(
          Object.assign({}, _currValue, { data: _row })
        )
      }
      this.combineValues = combineValues
    },
    // 初始计算值
    handleCalcData() {
      if (!this.localValues.length) return
      const _rowPaths = this._combinePaths(
        ...this.localRows.map(({ values }) => values)
      )
      const _rowKeys = this.localRows.map(({ key }) => key)
      const _colPaths = this._combinePaths(
        ...this.localColumns.map(({ values }) => values)
      )
      const _colKeys = this.localColumns.map(({ key }) => key)
      // conditions of col head
      const colConditions = convertPathToMap(_colPaths, _colKeys)
      // conditions of row-head
      const rowConditions = convertPathToMap(_rowPaths, _rowKeys)
      // Note: if there are no props.rows or props.column, push an empty object
      !colConditions.length && colConditions.push({})
      !rowConditions.length && rowConditions.push({})
      // draw data
      this.calcData = Object.freeze(
        rowConditions
          .map((rowCondition, rowConditionIndex) =>
            colConditions
              .map((colCondition, colConditionIndex) => {
                // the condition of current cell
                const conditions = Object.assign({}, rowCondition, colCondition)
                // filter the data
                const filterData = this._filterData(conditions, this.localData)
                // empty cell
                const isEmptyCell = this.localRows.length && this.localColumns.length && !this.localValues.length
                const _values = {}
                // 多个值，多条数据
                this.values.forEach(({ key, handle }) => {
                  _values[key] = isEmptyCell ? '' : this._reduceValue(filterData, key)
                })
                return Object.assign({}, conditions, _values)
              })
              .flat()
          )
          .filter((item) => item.length)
          .flat()
      )
    },
    _combinePaths(...arrays) {
      return arrays.length ? arrays.reduce((prev, curr) => {
        const arr = []
        prev.forEach(_prevEl => {
          curr.forEach(_currEl => {
            arr.push(_prevEl + SEPARATOR + _currEl)
          })
        })
        return arr
      }) : arrays
    },
    _findCategory(keys = [], data = []) {
      const _result = {}
      data.forEach(item => {
        keys.forEach(key => {
          // Remove duplicates
          _result[key] = _result[key] || []
          _result[key].push(item[key])
          _result[key] = [...new Set(_result[key])]
        })
      })
      return _result
    },
    _reduceValue(data, key) {
      if (!data.length) return ''
      return data.reduce((sum, item) => { return sum + Number(item[key]) }, 0)
    },
    _filterData(conditions, data) {
      return data.filter((data) => {
        let status = true
        for (const key in conditions) {
          if (conditions[key] !== data[key]) {
            status = false
            return
          }
        }
        return status
      })
    },
    // get min height by rowspan
    _getMinHeightByRowCount(count) {
      return getHeightByCount(count)
    }
  }
}
</script>

<style lang="scss" scoped>
table {
  border-collapse: collapse;
  border-spacing: 0;
  border: none;
  thead th {
    background-color: #CCE8EB;
  }
  tbody {
    tr:nth-child(odd) {
      background: #fff;
    }
    tr:nth-child(even) {
      background: #F5FAFA;
    }
  }
  td, th {
    border: 1px solid #ccc;
    padding: 0;
    vertical-align: middle;
    box-sizing: border-box;
    > div {
      display: flex;
      align-items: center;
      justify-content: center;
      box-sizing: border-box;
      padding: 5px;
      text-align: center;
      white-space: nowrap;
      width: 100%;
      height: 100%;
      min-height: 36px;
      cursor: default;
      &.col-corner-bg {
        background: #f1f7fd;
      }
    }
  }
}
</style>
