text: import React from 'react'
  import _ from './utils'
  
  export default Base =>
    class extends Base {
      getResolvedState (props, state) {
        const resolvedState = {
          ..._.compactObject(this.state),
          ..._.compactObject(this.props),
          ..._.compactObject(state),
          ..._.compactObject(props),
        }
        return resolvedState
      }
  
      getDataModel (newState, dataChanged) {
        const {
          columns,
          pivotBy = [],
          data,
          resolveData,
          pivotIDKey,
          pivotValKey,
          subRowsKey,
          aggregatedKey,
          nestingLevelKey,
          originalKey,
          indexKey,
          groupedByPivotKey,
          SubComponent,
        } = newState
  
        // Determine Header Groups
        let hasHeaderGroups = false
        columns.forEach(column => {
          if (column.columns) {
            hasHeaderGroups = true
          }
        })
  
        let columnsWithExpander = [...columns]
  
        let expanderColumn = columns.find(
          col => col.expander || (col.columns && col.columns.some(col2 => col2.expander))
        )
        // The actual expander might be in the columns field of a group column
        if (expanderColumn && !expanderColumn.expander) {
          expanderColumn = expanderColumn.columns.find(col => col.expander)
        }
  
        // If we have SubComponent's we need to make sure we have an expander column
        if (SubComponent && !expanderColumn) {
          expanderColumn = { expander: true }
          columnsWithExpander = [expanderColumn, ...columnsWithExpander]
        }
  
        const makeDecoratedColumn = (column, parentColumn) => {
          let dcol
          if (column.expander) {
            dcol = {
              ...this.props.column,
              ...this.props.expanderDefaults,
              ...column,
            }
          } else {
            dcol = {
              ...this.props.column,
              ...column,
            }
          }
  
          // Ensure minWidth is not greater than maxWidth if set
          if (dcol.maxWidth < dcol.minWidth) {
            dcol.minWidth = dcol.maxWidth
          }
  
          if (parentColumn) {
            dcol.parentColumn = parentColumn
          }
  
          // First check for string accessor
          if (typeof dcol.accessor === 'string') {
            dcol.id = dcol.id || dcol.accessor
            const accessorString = dcol.accessor
            dcol.accessor = row => _.get(row, accessorString)
            return dcol
          }
  
          // Fall back to functional accessor (but require an ID)
          if (dcol.accessor && !dcol.id) {
            console.warn(dcol)
            throw new Error(
              'A column id is required if using a non-string accessor for column above.'
            )
          }
  
          // Fall back to an undefined accessor
          if (!dcol.accessor) {
            dcol.accessor = () => undefined
          }
  
          return dcol
        }
  
        const allDecoratedColumns = []
  
        // Decorate the columns
        const decorateAndAddToAll = (column, parentColumn) => {
          const decoratedColumn = makeDecoratedColumn(column, parentColumn)
          allDecoratedColumns.push(decoratedColumn)
          return decoratedColumn
        }
  
        const decorateColumn = (column, parent) => {
          if (column.columns) {
            return {
              ...column,
              columns: column.columns.map(d => decorateColumn(d, column)),
            }
          }
          return decorateAndAddToAll(column, parent)
        }
  
        const decoratedColumns = columnsWithExpander.map(decorateColumn)
  
        // Build the visible columns, headers and flat column list
        let visibleColumns = decoratedColumns.slice()
        const allVisibleColumns = []
  
        const visibleReducer = (visible, column) => {
          if (column.columns) {
            const visibleSubColumns = column.columns.reduce(visibleReducer, [])
            return visibleSubColumns.length ? visible.concat({
              ...column,
              columns: visibleSubColumns,
            }) : visible
          } else if (
            pivotBy.indexOf(column.id) > -1
              ? false
              : _.getFirstDefined(column.show, true)
          ) {
            return visible.concat(column)
          }
          return visible
        }
  
        visibleColumns = visibleColumns.reduce(visibleReducer, [])
  
        // Find any custom pivot location
        const pivotIndex = visibleColumns.findIndex(col => col.pivot)
  
        // Handle Pivot Columns
        if (pivotBy.length) {
          // Retrieve the pivot columns in the correct pivot order
          const pivotColumns = []
          pivotBy.forEach(pivotID => {
            const found = allDecoratedColumns.find(d => d.id === pivotID)
            if (found) {
              pivotColumns.push(found)
            }
          })
  
          const PivotParentColumn = pivotColumns.reduce(
            (prev, current) => prev && prev === current.parentColumn && current.parentColumn,
            pivotColumns[0].parentColumn
          )
  
          let PivotGroupHeader = hasHeaderGroups && PivotParentColumn.Header
          PivotGroupHeader = PivotGroupHeader || (() => <strong>Pivoted</strong>)
  
          let pivotColumnGroup = {
            Header: PivotGroupHeader,
            columns: pivotColumns.map(col => ({
              ...this.props.pivotDefaults,
              ...col,
              pivoted: true,
            })),
          }
  
          // Place the pivotColumns back into the visibleColumns
          if (pivotIndex >= 0) {
            pivotColumnGroup = {
              ...visibleColumns[pivotIndex],
              ...pivotColumnGroup,
            }
            visibleColumns.splice(pivotIndex, 1, pivotColumnGroup)
          } else {
            visibleColumns.unshift(pivotColumnGroup)
          }
        }
  
        // Build Header Groups
        let headerGroupLayers = []
  
        const addToLayer = (columns, layer, totalSpan, column) => {
          if (!headerGroupLayers[layer]) {
            headerGroupLayers[layer] = {
              span: totalSpan.length,
              groups: (totalSpan.length ? [{
                ...this.props.column,
                columns: totalSpan,
              }] : []),
            }
          }
          headerGroupLayers[layer].span += columns.length
          headerGroupLayers[layer].groups = headerGroupLayers[layer].groups.concat({
            ...this.props.column,
            ...column,
            columns,
          })
        }
  
        // Build flast list of allVisibleColumns and HeaderGroups
        let layer = 0
        const getAllVisibleColumns = ({
          currentSpan = [],
          add,
          totalSpan = [],
        }, column) => {
          if (column.columns) {
            if (add) {
              addToLayer(add, layer, totalSpan)
              totalSpan = totalSpan.concat(add)
            }
  
            layer += 1
            const {
              currentSpan: mySpan,
            } = column.columns.reduce(getAllVisibleColumns, {
              totalSpan,
            })
            layer -= 1
  
            addToLayer(mySpan, layer, totalSpan, column)
            return {
              add: false,
              totalSpan: totalSpan.concat(mySpan),
              currentSpan: currentSpan.concat(mySpan),
            }
          }
          allVisibleColumns.push(column)
          return {
            add: (add || []).concat(column),
            totalSpan,
            currentSpan: currentSpan.concat(column),
          }
        }
        const { currentSpan } = visibleColumns.reduce(getAllVisibleColumns, {})
        if (hasHeaderGroups) {
          headerGroupLayers = headerGroupLayers.map(layer => {
            if (layer.span !== currentSpan.length) {
              layer.groups = layer.groups.concat({
                ...this.props.column,
                columns: currentSpan.slice(layer.span),
              })
            }
            return layer.groups
          })
        }
  
        // Access the data
        const accessRow = (d, i, level = 0) => {
          const row = {
            [originalKey]: d,
            [indexKey]: i,
            [subRowsKey]: d[subRowsKey],
            [nestingLevelKey]: level,
          }
          allDecoratedColumns.forEach(column => {
            if (column.expander) return
            row[column.id] = column.accessor(d)
          })
          if (row[subRowsKey]) {
            row[subRowsKey] = row[subRowsKey].map((d, i) => accessRow(d, i, level + 1))
          }
          return row
        }
  
        // // If the data hasn't changed, just use the cached data
        let resolvedData = this.resolvedData
        // If the data has changed, run the data resolver and cache the result
        if (!this.resolvedData || dataChanged) {
          resolvedData = resolveData(data)
          this.resolvedData = resolvedData
        }
        // Use the resolved data
        resolvedData = resolvedData.map((d, i) => accessRow(d, i))
  
        // TODO: Make it possible to fabricate nested rows without pivoting
        const aggregatingColumns = allVisibleColumns.filter(d => !d.expander && d.aggregate)
  
        // If pivoting, recursively group the data
        const aggregate = rows => {
          const aggregationValues = {}
          aggregatingColumns.forEach(column => {
            const values = rows.map(d => d[column.id])
            aggregationValues[column.id] = column.aggregate(values, rows)
          })
          return aggregationValues
        }
        if (pivotBy.length) {
          const groupRecursively = (rows, keys, i = 0) => {
            // This is the last level, just return the rows
            if (i === keys.length) {
              return rows
            }
            // Group the rows together for this level
            let groupedRows = Object.entries(_.groupBy(rows, keys[i])).map(([key, value]) => ({
              [pivotIDKey]: keys[i],
              [pivotValKey]: key,
              [keys[i]]: key,
              [subRowsKey]: value,
              [nestingLevelKey]: i,
              [groupedByPivotKey]: true,
            }))
            // Recurse into the subRows
            groupedRows = groupedRows.map(rowGroup => {
              const subRows = groupRecursively(rowGroup[subRowsKey], keys, i + 1)
              return {
                ...rowGroup,
                [subRowsKey]: subRows,
                [aggregatedKey]: true,
                ...aggregate(subRows),
              }
            })
            return groupedRows
          }
          resolvedData = groupRecursively(resolvedData, pivotBy)
        }
  
        return {
          ...newState,
          resolvedData,
          allVisibleColumns,
          headerGroupLayers,
          allDecoratedColumns,
          hasHeaderGroups,
        }
      }
  
      getSortedData (resolvedState) {
        const {
          manual,
          sorted,
          filtered,
          defaultFilterMethod,
          resolvedData,
          allVisibleColumns,
          allDecoratedColumns,
        } = resolvedState
  
        const sortMethodsByColumnID = {}
  
        allDecoratedColumns.filter(col => col.sortMethod).forEach(col => {
          sortMethodsByColumnID[col.id] = col.sortMethod
        })
  
        // Resolve the data from either manual data or sorted data
        return {
          sortedData: manual
            ? resolvedData
            : this.sortData(
              this.filterData(resolvedData, filtered, defaultFilterMethod, allVisibleColumns),
              sorted,
              sortMethodsByColumnID
            ),
        }
      }
  
      fireFetchData () {
        this.props.onFetchData(this.getResolvedState(), this)
      }
  
      getPropOrState (key) {
        return _.getFirstDefined(this.props[key], this.state[key])
      }
  
      getStateOrProp (key) {
        return _.getFirstDefined(this.state[key], this.props[key])
      }
  
      filterData (data, filtered, defaultFilterMethod, allVisibleColumns) {
        let filteredData = data
  
        if (filtered.length) {
          filteredData = filtered.reduce((filteredSoFar, nextFilter) => {
            const column = allVisibleColumns.find(x => x.id === nextFilter.id)
  
            // Don't filter hidden columns or columns that have had their filters disabled
            if (!column || column.filterable === false) {
              return filteredSoFar
            }
  
            const filterMethod = column.filterMethod || defaultFilterMethod
  
            // If 'filterAll' is set to true, pass the entire dataset to the filter method
            if (column.filterAll) {
              return filterMethod(nextFilter, filteredSoFar, column)
            }
            return filteredSoFar.filter(row => filterMethod(nextFilter, row, column))
          }, filteredData)
  
          // Apply the filter to the subrows if we are pivoting, and then
          // filter any rows without subcolumns because it would be strange to show
          filteredData = filteredData
            .map(row => {
              if (!row[this.props.subRowsKey]) {
                return row
              }
              return {
                ...row,
                [this.props.subRowsKey]: this.filterData(
                  row[this.props.subRowsKey],
                  filtered,
                  defaultFilterMethod,
                  allVisibleColumns
                ),
              }
            })
            .filter(row => {
              if (!row[this.props.subRowsKey]) {
                return true
              }
              return row[this.props.subRowsKey].length > 0
            })
        }
  
        return filteredData
      }
  
      sortData (data, sorted, sortMethodsByColumnID = {}) {
        if (!sorted.length) {
          return data
        }
  
        const sortedData = (this.props.orderByMethod || _.orderBy)(
          data,
          sorted.map(sort => {
            // Support custom sorting methods for each column
            if (sortMethodsByColumnID[sort.id]) {
              return (a, b) => sortMethodsByColumnID[sort.id](a[sort.id], b[sort.id], sort.desc)
            }
            return (a, b) => this.props.defaultSortMethod(a[sort.id], b[sort.id], sort.desc)
          }),
          sorted.map(d => !d.desc),
          this.props.indexKey
        )
  
        sortedData.forEach(row => {
          if (!row[this.props.subRowsKey]) {
            return
          }
          row[this.props.subRowsKey] = this.sortData(
            row[this.props.subRowsKey],
            sorted,
            sortMethodsByColumnID
          )
        })
  
        return sortedData
      }
  
      getMinRows () {
        return _.getFirstDefined(this.props.minRows, this.getStateOrProp('pageSize'))
      }
  
      // User actions
      onPageChange (page) {
        const { onPageChange, collapseOnPageChange } = this.props
  
        const newState = { page }
        if (collapseOnPageChange) {
          newState.expanded = {}
        }
        this.setStateWithData(newState, () => onPageChange && onPageChange(page))
      }
  
      onPageSizeChange (newPageSize) {
        const { onPageSizeChange } = this.props
        const { pageSize, page } = this.getResolvedState()
  
        // Normalize the page to display
        const currentRow = pageSize * page
        const newPage = Math.floor(currentRow / newPageSize)
  
        this.setStateWithData(
          {
            pageSize: newPageSize,
            page: newPage,
          },
          () => onPageSizeChange && onPageSizeChange(newPageSize, newPage)
        )
      }
  
      sortColumn (column, additive) {
        const { sorted, skipNextSort, defaultSortDesc } = this.getResolvedState()
  
        const firstSortDirection = Object.prototype.hasOwnProperty.call(column, 'defaultSortDesc')
          ? column.defaultSortDesc
          : defaultSortDesc
        const secondSortDirection = !firstSortDirection
  
        // we can't stop event propagation from the column resize move handlers
        // attached to the document because of react's synthetic events
        // so we have to prevent the sort function from actually sorting
        // if we click on the column resize element within a header.
        if (skipNextSort) {
          this.setStateWithData({
            skipNextSort: false,
          })
          return
        }
  
        const { onSortedChange } = this.props
  
        let newSorted = _.clone(sorted || []).map(d => {
          d.desc = _.isSortingDesc(d)
          return d
        })
        if (!_.isArray(column)) {
          // Single-Sort
          const existingIndex = newSorted.findIndex(d => d.id === column.id)
          if (existingIndex > -1) {
            const existing = newSorted[existingIndex]
            if (existing.desc === secondSortDirection) {
              if (additive) {
                newSorted.splice(existingIndex, 1)
              } else {
                existing.desc = firstSortDirection
                newSorted = [existing]
              }
            } else {
              existing.desc = secondSortDirection
              if (!additive) {
                newSorted = [existing]
              }
            }
          } else if (additive) {
            newSorted.push({
              id: column.id,
              desc: firstSortDirection,
            })
          } else {
            newSorted = [
              {
                id: column.id,
                desc: firstSortDirection,
              },
            ]
          }
        } else {
          // Multi-Sort
          const existingIndex = newSorted.findIndex(d => d.id === column[0].id)
          // Existing Sorted Column
          if (existingIndex > -1) {
            const existing = newSorted[existingIndex]
            if (existing.desc === secondSortDirection) {
              if (additive) {
                newSorted.splice(existingIndex, column.length)
              } else {
                column.forEach((d, i) => {
                  newSorted[existingIndex + i].desc = firstSortDirection
                })
              }
            } else {
              column.forEach((d, i) => {
                newSorted[existingIndex + i].desc = secondSortDirection
              })
            }
            if (!additive) {
              newSorted = newSorted.slice(existingIndex, column.length)
            }
            // New Sort Column
          } else if (additive) {
            newSorted = newSorted.concat(
              column.map(d => ({
                id: d.id,
                desc: firstSortDirection,
              }))
            )
          } else {
            newSorted = column.map(d => ({
              id: d.id,
              desc: firstSortDirection,
            }))
          }
        }
  
        this.setStateWithData(
          {
            page: (!sorted.length && newSorted.length) || !additive ? 0 : this.state.page,
            sorted: newSorted,
          },
          () => onSortedChange && onSortedChange(newSorted, column, additive)
        )
      }
  
      filterColumn (column, value) {
        const { filtered } = this.getResolvedState()
        const { onFilteredChange } = this.props
  
        // Remove old filter first if it exists
        const newFiltering = (filtered || []).filter(x => x.id !== column.id)
  
        if (value !== '') {
          newFiltering.push({
            id: column.id,
            value,
          })
        }
  
        this.setStateWithData(
          {
            filtered: newFiltering,
          },
          () => onFilteredChange && onFilteredChange(newFiltering, column, value)
        )
      }
  
      resizeColumnStart (event, column, isTouch) {
        event.stopPropagation()
        const parentWidth = event.target.parentElement.getBoundingClientRect().width
  
        let pageX
        if (isTouch) {
          pageX = event.changedTouches[0].pageX
        } else {
          pageX = event.pageX
        }
  
        this.trapEvents = true
        this.setStateWithData(
          {
            currentlyResizing: {
              id: column.id,
              startX: pageX,
              parentWidth,
            },
          },
          () => {
            if (isTouch) {
              document.addEventListener('touchmove', this.resizeColumnMoving)
              document.addEventListener('touchcancel', this.resizeColumnEnd)
              document.addEventListener('touchend', this.resizeColumnEnd)
            } else {
              document.addEventListener('mousemove', this.resizeColumnMoving)
              document.addEventListener('mouseup', this.resizeColumnEnd)
              document.addEventListener('mouseleave', this.resizeColumnEnd)
            }
          }
        )
      }
  
      resizeColumnMoving (event) {
        event.stopPropagation()
        const { onResizedChange } = this.props
        const { resized, currentlyResizing } = this.getResolvedState()
  
        // Delete old value
        const newResized = resized.filter(x => x.id !== currentlyResizing.id)
  
        let pageX
  
        if (event.type === 'touchmove') {
          pageX = event.changedTouches[0].pageX
        } else if (event.type === 'mousemove') {
          pageX = event.pageX
        }
  
        // Set the min size to 10 to account for margin and border or else the
        // group headers don't line up correctly
        const newWidth = Math.max(
          currentlyResizing.parentWidth + pageX - currentlyResizing.startX,
          11
        )
  
        newResized.push({
          id: currentlyResizing.id,
          value: newWidth,
        })
  
        this.setStateWithData(
          {
            resized: newResized,
          },
          () => onResizedChange && onResizedChange(newResized, event)
        )
      }
  
      resizeColumnEnd (event) {
        event.stopPropagation()
        const isTouch = event.type === 'touchend' || event.type === 'touchcancel'
  
        if (isTouch) {
          document.removeEventListener('touchmove', this.resizeColumnMoving)
          document.removeEventListener('touchcancel', this.resizeColumnEnd)
          document.removeEventListener('touchend', this.resizeColumnEnd)
        }
  
        // If its a touch event clear the mouse one's as well because sometimes
        // the mouseDown event gets called as well, but the mouseUp event doesn't
        document.removeEventListener('mousemove', this.resizeColumnMoving)
        document.removeEventListener('mouseup', this.resizeColumnEnd)
        document.removeEventListener('mouseleave', this.resizeColumnEnd)
  
        // The touch events don't propagate up to the sorting's onMouseDown event so
        // no need to prevent it from happening or else the first click after a touch
        // event resize will not sort the column.
        if (!isTouch) {
          this.setStateWithData({
            skipNextSort: true,
            currentlyResizing: false,
          })
        }
      }
    }
  