<template>
  <div class="table-responsive">
    <table>
      <!-- Table header -->
      <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>
      <!-- Table body -->
      <tbody>
        <tr v-for="(tr, index) in combineValues" :key="index">
          <!-- Row headers -->
          <th v-for="cell in tr.head" v-if="!cell.isRowspan" :key="cell.__index" :rowspan="cell.rowspan" :colspan="cell.colspan">
            <div :style="{ 'min-height': _getMinHeightByRowCount(cell.rowspan) }">
              {{ cell.value }}
            </div>
          </th>
          <!-- Values -->
          <td v-for="cell in tr.data" :key="cell.__index" :rowspan="cell.rowspan" :colspan="cell.colspan">
            <div :style="{ 'min-height': _getMinHeightByRowCount(cell.rowspan) }">
              {{ cell.value }}
            </div>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
import { mergeBaseInfo, convertPathToMap, getHeightByCount, SEPARATOR } from '@/utils/visual-chart'

export default {
  name: 'PivotTable',
  props: {
    data: {
      type: Array,
      default: () => []
    },
    rows: {
      type: Array,
      default: () => []
    },
    columns: {
      type: Array,
      default: () => []
    },
    values: {
      type: Array,
      default: () => []
    }
  },
  data: () => ({
    localRows: [],
    localColumns: [],
    localValues: [],
    localData: [],
    // 计算列的数据
    calcData: [],
    // Separator
    Separator: SEPARATOR,
    // 合并后的表头
    combineHeads: [],
    // 合并后的单元格
    combineValues: []
  }),
  computed: {
    watchAllProps() {
      const { rows, columns, values, data } = this
      return { rows, columns, values, data }
    },
    rowPaths() {
      const _paths = this._combineRowPaths(
        this.localData,
        ...this.localRows.map(({ key, values }) => { return { key, 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._combineColPaths(...keys)
      return _paths
    },
    // 列的表头
    colHeads() {
      // 共有多少行
      const _rows = this.localColumns.map(() => [])
      // 有几个值
      const valuesLen = this.localValues.length
      if (valuesLen) {
        _rows.push([])
      }
      // 计算合并单元格
      const colSpans = {}
      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
          if (!isLastRow) {
            // 计算合并行数
            let compareVal = valuesLen
            for (let i = rowIndex; i < this.localColumns.length - 1; i++) {
              compareVal *= this.localColumns[rowIndex + 1].values.length
            }
            const currColSpan = colSpans[rowIndex] || {}
            const currColSpanVal = (currColSpan[currPath.join(this.Separator)] || 0) + 1
            currColSpan[currPath.join(this.Separator)] = currColSpanVal
            colSpans[rowIndex] = currColSpan
            // 合并单元格：起始单元格加入
            if (currColSpanVal === 1) {
              row.push(
                Object.assign(
                  cellData,
                  mergeBaseInfo({
                    __index: `${baseX}-${baseY}`,
                    x: baseX,
                    y: baseY,
                    colspan: compareVal,
                    path: currPath.filter((item) => !!item),
                    value: currVal
                  })
                )
              )
            }
          } else {
            row.push(
              Object.assign(
                cellData,
                mergeBaseInfo({
                  __index: `${baseX}-${baseY}`,
                  x: baseX,
                  y: baseY,
                  path: currPath.filter((item) => !!item),
                  // 最后一行是 values，替换显示文本
                  value: this.localValues.find(({ key }) => key === currVal).label
                })
              )
            )
          }
        })
      })
      return _rows
    },
    // 行的表头
    rowHeads() {
      // 共有多少列
      const _columns = []
      // 左上角特殊处理, 有行维、列维时才有
      const columnsLen = this.localColumns.length
      const rowsLen = this.localRows.length
      if (rowsLen && columnsLen) {
        _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() {
      let _values = []
      const rowsLen = this.localRows.length
      if (rowsLen) {
        // 计算合并单元格
        const rowSpans = {}
        _values = this.rowPaths.map((path, pathIndex) => {
          const values = path.split(this.Separator)
          const currPath = []
          return this.localRows.map((item, rowIndex) => {
            const currVal = values[rowIndex] || ''
            const baseX = this.localColumns.length + +Boolean(this.localValues.length) + pathIndex
            const baseY = rowIndex
            currPath.push(currVal)
            // 是否为最后列
            const isLastCol = rowIndex === rowsLen - 1
            if (!isLastCol) {
              // 计算合并列数
              // 过滤条件
              const conditions = {}
              for (let i = 0; i < rowIndex + 1; i++) {
                conditions[i] = values[i] || ''
              }
              const filterData = this.rowPaths.filter((data) => {
                let status = true
                const splitValues = data.split(this.Separator)
                for (const key in conditions) {
                  if (conditions[key] !== splitValues[key]) {
                    status = false
                    return
                  }
                }
                return status
              }) || []
              const mergeSpans = filterData.length
              const currRowSpan = rowSpans[rowIndex] || {}
              const currRowSpanVal = (currRowSpan[currPath.join(this.Separator)] || 0) + 1
              currRowSpan[currPath.join(this.Separator)] = currRowSpanVal
              rowSpans[rowIndex] = currRowSpan
              if (currRowSpanVal === 1) {
                return mergeBaseInfo({
                  __index: `${baseX}-${baseY}`,
                  value: currVal,
                  x: baseX,
                  y: baseY,
                  rowspan: mergeSpans,
                  path: currPath.filter((item) => !!item)
                })
              } else {
                return mergeBaseInfo({
                  __index: `${baseX}-${baseY}`,
                  value: currVal,
                  x: baseX,
                  y: baseY,
                  path: currPath.filter((item) => !!item),
                  // 是否合并单元格，遍历时判断不显示
                  isRowspan: true
                })
              }
            } else {
              return mergeBaseInfo({
                __index: `${baseX}-${baseY}`,
                value: currVal,
                x: baseX,
                y: baseY,
                path: currPath.filter((item) => !!item)
              })
            }
          })
        })
      }
      return _values
    },
    // 计算所有对应条件的值
    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 中找出对应的值
            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
        }
      })
    }
  },
  watch: {
    watchAllProps() {
      this.init()
    }
  },
  created() {
    this.init()
  },
  methods: {
    init() {
      if (this.rows.length || this.columns.length || this.values.length) {
        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 = [...(_currValue.data || [])]
        combineValues.push(
          Object.assign({}, { head: [..._currRowHeadValue] }, { data: _row })
        )
      }
      this.combineValues = combineValues
    },
    // 初始计算值
    handleCalcData() {
      if (!this.localValues.length) return
      const _rowPaths = this._combineRowPaths(
        this.localData,
        ...this.localRows.map(({ key, values }) => { return { key, values } })
      )
      const _rowKeys = this.localRows.map(({ key }) => key)
      const _colPaths = this._combineColPaths(
        ...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.columns, 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 }) => {
                  _values[key] = isEmptyCell ? '' : this._reduceValue(filterData, key)
                })
                return Object.assign({}, conditions, _values)
              })
              .flat()
          )
          .filter((item) => item.length)
          .flat()
      )
    },
    _combineRowPaths(data, ...arrays) {
      const len = arrays.length
      let _result = []
      if (len) {
        const rowPaths = arrays.reduce((prev, curr) => {
          const arr = []
          prev.values.forEach(_prevEl => {
            const prevKey = prev.key.split(SEPARATOR)
            curr.values.forEach(_currEl => {
              const currKey = curr.key
              const conditions = {}
              prevKey.forEach((key, i) => {
                conditions[key] = _prevEl.split(SEPARATOR)[i]
              })
              conditions[currKey] = _currEl
              // 判断数据里是否有该项
              const filter = data.some((data) => {
                let status = true
                for (const key in conditions) {
                  if (conditions[key] !== data[key]) {
                    status = false
                    return
                  }
                }
                return status
              })
              if (filter) {
                arr.push(_prevEl + SEPARATOR + _currEl)
              }
            })
          })
          return { key: prev.key + SEPARATOR + curr.key, values: arr }
        }) || {}
        _result = rowPaths.values || []
      }
      return _result
    },
    _combineColPaths(...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 {
    background-color: rgba(255, 255, 255, 0.1);
  }
  tbody {
    background-color: rgba(255, 255, 255, 0.1);
  }
  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-color: rgba(255, 255, 255, 0.1);
      }
    }
  }
}
</style>
