<script>
import { filter, get, isNil, orderBy, sortBy, debounce, pickBy, clone } from 'lodash'
import { stringify } from 'flatted'
import { downloadXLSX } from '../mixins/downloadXLSX.js'
class DataSheetRowMeta {
  constructor ({
    isSelected = false
  } = {}) {
    this.isSelected = isSelected
  }
}
class DataSheetColumn {
  constructor ({
    title = '',
    value = {
      property: '',
      iteratee: row => (row[this.key || 'value'] || '')
    },
    total = '',
    key = '',
    isVisible = true,
    format = {
      align: 'center',
      vAlign: 'middle',
      whitespace: 'initial'
    },
    orderBy = {
      iteratee: row => (this.value.iteratee(row) || row[this.key || 'value'] || ''),
      direction: '',
      sortOrder: 0
    }
  } = {}) {
    this.title = title
    this.value = value
    this.key = key
    this.isVisible = isVisible
    this.format = format
    this.orderBy = orderBy
    this.total = total
  }
}
export default {
  name: 'DataSheet',
  props: {
    model: {
      type: Array,
      required: true,
      default () {
        return []
      }
    },
    columns: {
      type: Array,
      required: true,
      default () {
        return []
      }
    },
    keyIteratee: {
      type: Function,
      required: false,
      default: (row, model, columns) => Object.keys(row).length ? Object.keys(row)[0] : row['id'] || row['key']
    },
    downloadIteratee: {
      type: Function,
      required: false,
      default: row => row
    },
    downloadFileName: {
      type: String,
      required: false,
      default: () => `data-sheet-${new Date().toISOString().split('T')[0]}`
    },
    defaultRowsPerPage: {
      type: Number,
      required: false,
      default: 50
    },
    unselectableRows: {
      type: Object,
      required: false,
      default () {
        return {}
      }
    }
  },
  mixins: [downloadXLSX],
  data () {
    return {
      state: {
        columns: this.columns,
        pagination: {
          rowsPerPage: this.defaultRowsPerPage,
          offset: 0
        },
        search: {
          matchText: '',
          matchProperty: '',
          matchType: 'indexOf'
        },
        settings: {
          isVisible: false
        }
      },
      filters: {},
      meta: {},
      isSelectAll: false
    }
  },
  watch: {
    'model' () {
      this.meta = {}
      this.clearFilters()
      this.state.search.matchText = ''
      this.state.search.matchProperty = ''
      this.state.search.matchType = 'indexOf'
    },
    'columns' () {
      this.state.columns = this.columns
    }
  },
  render () {
    return this.$scopedSlots.default({
      ...this.$props,
      keyIteratee: this.keyIteratee,
      displayModel: this.displayModel,
      displayModelSlice: this.displayModelSlice,
      displayColumns: this.displayColumns,
      columnar: {
        alignment: this.columnAlignment,
        whitespace: this.columnWhitespace,
        toggleSortDirection: this.toggleSortDirection,
        columns: this.columnsByValueProperty,
        columnTotals: this.columnTotals
      },
      filters: {
        active: this.filters,
        add: this.addFilter,
        clear: this.clearFilters,
        remove: this.removeFilter
      },
      select: {
        hasSelected: this.hasSelected,
        isSelected: this.isSelected,
        isUnselectable: this.isUnselectable,
        isSelectAll: this.isSelectAll,
        toggle: this.toggle,
        select: this.select,
        deselect: this.deselect,
        toggleAll: this.toggleAll,
        selectAll: this.selectAll,
        deselectAll: this.deselectAll
      },
      paginate: {
        pageForward: this.pageForward,
        pageTo: this.pageTo,
        pageBack: this.pageBack,
        setRowsPerPage: this.setRowsPerPage,
        offset: this.state.pagination.offset,
        rowsPerPage: this.state.pagination.rowsPerPage,
        lastPage: this.getPagination[this.getPagination.length - 1]
      },
      match: {
        text: this.state.search.matchText,
        property: this.state.search.matchProperty,
        updateMatchText: this.updateMatchText
      },
      download: this.download,
      settings: {
        show: this.showSettings,
        hide: this.hideSettings,
        toggle: this.toggleSettings
      }
    })
  },
  computed: {
    selectedProductIds () {
      return pickBy(this.meta, (value, key) => value.isSelected)
    },
    columnTotals () {
      const columnTotals = {}
      const totalableColumns = []
      const totaledColumns = []
      // Filter Columns to Totalable & Totaled
      for (let t = 0; t < this.displayColumns.length; t++) {
        const hasReducer = this.displayColumns[t].total.reducer && !(this.displayColumns[t].total.initial == null)
        if (this.displayColumns[t] && this.displayColumns[t].total) {
          if (hasReducer) {
            totalableColumns.push(this.displayColumns[t])
          } else if (typeof this.displayColumns[t].total === 'string' || typeof this.displayColumns[t].total === 'number') {
            totaledColumns.push(this.displayColumns[t])
          }
        }
      }
      // Totalable with Reducer (Reactive)
      if (totalableColumns.length) {
        for (let i = 0; i < this.displayModel.length; i++) {
          const row = this.displayModel[i]
          for (let t = 0; t < totalableColumns.length; t++) {
            const column = totalableColumns[t]
            // Instantiate with Initial Value
            if (i === 0 && !columnTotals[column.key]) {
              columnTotals[column.key] = this.getSafe(() => clone(column.total.initial), 0)
            }
            // Accumulate
            columnTotals[column.key] = column.total.reducer(columnTotals[column.key], row)
            // After All Rows Accumulated: Format
            if (i === (this.displayModel.length - 1) && this.getSafe(() => column.total.formatter, false)) {
              columnTotals[column.key] = column.total.formatter(columnTotals[column.key])
            }
          }
        }
      }
      // Totaled with Precompute (Not Reactive w/ e.g. Filter)
      if (totaledColumns.length) {
        for (let t = 0; t < totaledColumns.length; t++) {
          const column = totaledColumns[t]
          columnTotals[column.key] = column.total
        }
      }
      return columnTotals
    },
    hasSelected () {
      let hasSelected = false
      for (const key in this.meta) {
        hasSelected = this.meta[key] && this.meta[key].isSelected
        if (hasSelected) {
          break
        }
      }
      return hasSelected
    },
    columnAlignment () {
      let columnAlignment = {}
      for (let column of this.displayColumns) {
        const classes = {}
        classes[`text-align-${column.format.align}`] = true
        classes[`vertical-align-${column.format.vAlign}`] = true
        columnAlignment[column.key] = classes
      }
      return columnAlignment
    },
    columnWhitespace () {
      let columnWhitespace = {}
      for (let column of this.displayColumns) {
        const classes = {
          'ws-no-wrap': column.format.whitespace === 'nowrap',
          'ws-inherit': column.format.whitespace === 'inherit',
          'ws-initial': column.format.whitespace === 'initial',
          'ws-normal': column.format.whitespace === 'normal',
          'ws-pre': column.format.whitespace === 'pre',
          'ws-pre-line': column.format.whitespace === 'pre-line',
          'ws-pre-wrap': column.format.whitespace === 'pre-wrap',
          'ws-unset': column.format.whitespace === 'unset'
        }
        columnWhitespace[column.key] = classes
      }
      return columnWhitespace
    },
    displayModel () {
      let displayModel = filter([...this.model], row => {
        let predicateResult = true
        // Filter Rows
        for (const key in this.filters) {
          predicateResult = predicateResult && this.filters[key](row)
        }
        // Search Rows
        if (predicateResult && this.getSafe(() => this.state.search.matchText.length, false)) {
          const text = this.getSafe(() => this.state.search.matchProperty.length, false) ? stringify(get(row, this.state.search.matchProperty)) : stringify(row)
          if (this.state.search.matchType === 'RegExp') {
            predicateResult = predicateResult && text.match(new RegExp(this.state.search.matchText, 'i'))
          } else {
            predicateResult = predicateResult && text.toLowerCase().indexOf(this.state.search.matchText.toLowerCase()) >= 0
          }
        }
        return predicateResult
      })
      // Meta
      for (let i = 0; i < displayModel.length; i++) {
        if (isNil(this.getSafe(() => this.meta[this.keyIteratee(displayModel[i])]), null)) {
          this.$set(this.meta, this.keyIteratee(displayModel[i]), new DataSheetRowMeta())
        }
      }
      // Sort
      if (this.getSafe(() => this.sortColumns.directions.length, false)) {
        displayModel = orderBy(displayModel, this.sortColumns.iteratees, this.sortColumns.directions)
      }
      return displayModel
    },
    displayModelSlice () {
      const displayModelSlice = Array.from(this.displayModel)
      if (displayModelSlice.length <= this.state.pagination.rowsPerPage) {
        return displayModelSlice
      } else {
        return displayModelSlice.slice(this.state.pagination.offset * this.state.pagination.rowsPerPage, this.state.pagination.rowsPerPage + this.state.pagination.offset * this.state.pagination.rowsPerPage)
      }
    },
    displayColumns () {
      // Props Columns
      let columns = this.state.columns.map(column => new DataSheetColumn(column))
      // Default Columns from First Row Properties
      if (!columns.length && this.displayModel.length) {
        const firstRow = this.displayModel[0]
        columns = Object.keys(firstRow).map(key => {
          return new DataSheetColumn({
            title: key,
            value: {
              property: key
            },
            key: key
          })
        })
      }
      // Filter Visible Columns
      const displayColumns = filter([...columns], column => {
        return column.isVisible
      })
      return displayColumns
    },
    columnsByValueProperty () {
      const columns = {}
      for (let i = 0; i < this.displayColumns; i++) {
        columns[this.displayColumns[i].key] = this.displayColumns[i]
      }
      return columns
    },
    sortColumns () {
      let columns = Array.from(this.displayColumns)
      // Filter Columns by Active Direction
      columns = filter(columns, column => column.orderBy.direction.length)
      // Sort By Column Sort
      columns = sortBy(columns, column => column.orderBy.sortOrder)
      return {
        iteratees: columns.map(column => column.orderBy.iteratee),
        directions: columns.map(column => column.orderBy.direction)
      }
    },
    // Paginate
    getPagination () {
      const pagesShown = 10
      const totalPages = Math.ceil(this.displayModel.length / this.state.pagination.rowsPerPage)
      let returnData = []
      const min = (this.state.pagination.offset - 4 < 0 ? 0 : this.state.pagination.offset - 4)
      const max = min + pagesShown
      let i = 0
      while (i < totalPages) {
        if (i >= min && i <= max) {
          returnData.push(i + 1)
        }
        i++
      }
      if (min >= 1) {
        returnData[0] = 1
        returnData.splice(1, 1, '...')
      }
      if (max < totalPages) {
        returnData[returnData.length - 1] = totalPages - 1
        returnData[returnData.length] = totalPages
        returnData.splice(returnData.length - 3, 1, '...')
      }
      return returnData
    }
  },
  methods: {
    updateMatchText: debounce(function (event = {}) {
      this.pageTo(1)
      this.state.search.matchText = event.target.value
    }, 50),
    // Download
    download () {
      let download = [...this.displayModel]
      if (this.downloadIteratee) {
        download = download.map(row => {
          return this.downloadIteratee(row)
        })
      }
      this.downloadXLSX(download, this.downloadFileName)
    },
    // Utility
    getSafe (fn, fallback = null) {
      try {
        const result = fn()
        return result || fallback
      } catch (err) {
        const response = fallback === null ? undefined : fallback
        return response
      }
    },
    // Filters
    addFilter (filter) {
      this.pageTo(1)
      this.$set(this.filters, filter.name, filter.predicate)
    },
    clearFilters () {
      this.pageTo(1)
      this.filters = {}
    },
    removeFilter (name) {
      this.pageTo(1)
      this.$delete(this.filters, name)
    },
    // Selected
    isSelected (row) {
      return this.meta[this.keyIteratee(row)].isSelected
    },
    isUnselectable (row) {
      return this.unselectableRows[this.keyIteratee(row)]
    },
    toggle (row) {
      if (this.getSafe(() => this.meta[this.keyIteratee(row)].isSelected, false)) {
        this.deselect(row)
      } else if (!this.unselectableRows[this.keyIteratee(row)]) {
        this.select(row)
      }
    },
    announceSelect () {
      this.$emit('onSelect', this.selectedProductIds)
    },
    select (row) {
      this.$set(this.meta[this.keyIteratee(row)], 'isSelected', true)
      this.announceSelect()
    },
    deselect (row) {
      this.isSelectAll = false
      this.$set(this.meta[this.keyIteratee(row)], 'isSelected', false)
      this.announceSelect()
    },
    toggleAll () {
      if (this.isSelectAll || this.hasSelected) {
        this.deselectAll()
      } else {
        this.selectAll()
      }
    },
    selectAll () {
      this.isSelectAll = true
      for (const row of this.displayModel) {
        if (!this.unselectableRows[this.keyIteratee(row)]) {
          this.$set(this.meta[this.keyIteratee(row)], 'isSelected', true)
        }
      }
      this.announceSelect()
    },
    deselectAll () {
      this.isSelectAll = false
      for (const key in this.meta) {
        this.$set(this.meta[key], 'isSelected', false)
      }
      this.announceSelect()
    },
    // Sort
    toggleSortDirection (column) {
      if (column.orderBy.direction === 'desc') {
        column.orderBy.direction = ''
      } else if (column.orderBy.direction === 'asc') {
        column.orderBy.direction = 'desc'
      } else if (column.orderBy.direction === '') {
        column.orderBy.direction = 'asc'
      }
    },
    // Paginate
    setRowsPerPage (rowsPerPage) {
      this.state.pagination.rowsPerPage = isNaN(+rowsPerPage) ? this.defaultRowsPerPage : +rowsPerPage
    },
    pageTo: debounce(function (page) {
      const sanitizedPage = page - 1 < 0 ? 0 : page - 1 >= this.getPagination[this.getPagination.length - 1] ? this.getPagination[this.getPagination.length - 1] - 1 : page - 1
      this.state.pagination.offset = sanitizedPage
    }, 250),
    pageBack: debounce(function () {
      this.state.pagination.offset = this.state.pagination.offset - 1 >= 0 ? this.state.pagination.offset - 1 : 0
    }, 100),
    pageForward: debounce(function () {
      this.state.pagination.offset = this.state.pagination.offset + 1 < this.getPagination[this.getPagination.length - 1] ? this.state.pagination.offset + 1 : this.getPagination[this.getPagination.length - 1] - 1
    }, 100),
    // Settings
    showSettings () {
      this.state.settings.isVisible = true
    },
    hideSettings () {
      this.state.settings.isVisible = false
    },
    toggleSettings () {
      this.state.settings.isVisible = !this.state.settings.isVisible
    }
  }
}
</script>

<style>
/* Heading & Cell Alignment */
.table th.text-align-center, .text-align-center {
  text-align: center;
}
.table th.text-align-left, .text-align-left {
  text-align: left;
}
.table th.text-align-right, .text-align-right {
  text-align: right;
}
.table th.vertical-align-middle, .table td.vertical-align-middle {
  vertical-align: middle;
}
.table th.vertical-align-top, .table td.vertical-align-top {
  vertical-align: top;
}
.table th.vertical-align-bottom, .table td.vertical-align-bottom {
  vertical-align: bottom;
}
/*Whitespace Control*/
.table .ws-no-wrap {
  white-space: nowrap;
}
.table .ws-inherit {
  white-space: inherit;
}
.table .ws-initial {
  white-space: initial;
}
.table .ws-normal {
  white-space: normal;
}
.table .ws-pre {
  white-space: pre;
}
.table .ws-pre-line {
  white-space: pre-line;
}
.table .ws-pre-wrap {
  white-space: pre-wrap;
}
.table .ws-unset {
  white-space: unset;
}
</style>
