<template>
  <div class="c-data-table__outer-container">
    <el-actions-failure-dialog
      :visible="batchActionFailureDialog.visible"
      :failures="batchActionFailureDialog.failures"
      :selectedRowsCount="selectedRows.length"
      :fields="batchActionFailureDialog.dialogFields"
      :onClose="batchActionFailureDialog.dialogOnClose"
      :getMessage="batchActionFailureDialog.dialogMessage" >
    </el-actions-failure-dialog>

    <el-data-filter
      v-if="hasDataWithoutQuery && !insideCollapsibleRow"
      :searchable="searchable"
      :searchQuery="searchQuery"
      @search-query-submitted="$emit('search-query-submitted', $event)"
      @search-query-changed="$emit('search-query-changed', $event)"
      @selected-filters-updated="$emit('selected-filters-updated', $event)"
      @badge-closed="$emit(`${$event.badge.type}-badge-closed`, $event.badge)"
      v-bind="filterBindings">
    </el-data-filter>

    <!-- mobile sort dropdown start -->
    <el-select
      v-if="sortableColumns.length"
      v-model="sortByColumn"
      placeholder="Sort By"
      @change="handleSortChange"
      class="u-mb3 u-hidden-md u-hidden-lg">
      <el-option
        v-for="(column, index) in sortableColumns"
        :key="`${column.dataKey}${index}`"
        :label="column.label"
        :value="column">
      </el-option>
    </el-select>
    <!-- mobile sort dropdown end -->


    <!-- mobile actions start -->
    <div
      v-if="isActionsVisible"
      class="c-data-table__mobile-actions u-hidden-md u-hidden-lg">

      <div v-if="selectable">
        {{ mobileSelectedRowsText }}
        <el-checkbox
          v-model="allRowsSelected"
          :indeterminate="!!someRowsSelected"
          :class="{
            'el-checkbox--hidden': singleSelect || !data || !data.length
          }"
          class="u-p0 u-mt0 u-ml1">
        </el-checkbox>
        <div v-if="!customPagination" class="u-mt1">
          <span
            v-if="selectedRows.length < rows.length"
            class="u-link"
            @click="selectAllRowsAcrossPages(true)">
            Select all {{ rows.length }} records
          </span>
          <span
            v-else-if="selectedRows.length === rows.length"
            class="u-link"
            @click="selectAllRowsAcrossPages(false)">
            Unselect all {{ rows.length }} records
          </span>
        </div>
      </div>

      <el-data-table-actions-dropdown
        v-if="actions.length"
        class="u-hidden-md u-hidden-lg u-ml-auto"
        button-text="Actions"
        :actions="selectedRows.length > 1 ? actionByType('batch') : actionByType('single')"
        :selectedRows="selectedRows"
        @command="handleActionInvoked($event)">
      </el-data-table-actions-dropdown>
    </div>
    <!-- mobile actions end -->

    <slot name="above-table"></slot>

    <div class="c-data-table__container" :class="{ 'c-data-table__container--no-border': useNestedCollapsible }" ref="container">
      <table
        class="c-data-table"
        ref="table"
        :class="{
          'c-data-table--loading':loading,
          'c-data-table--empty': !data || !data.length,
          'c-data-table--bordered': bordered,
          'c-data-table--collapsible-nested': useNestedCollapsible
        }">

        <!-- table head start -->
        <thead
          v-if="header"
          class="c-data-table__header u-hidden-xs"
          :style="{ 'top': headerPositionStickySpacing }"
          ref="tableHead">

          <tr class="c-data-table__row">

            <!-- shown when rows have radios/checkboxes and/or collapsible icons -->
            <th
              v-if="hasActionableElements"
              class="c-data-table__block c-data-table__block--left c-data-table__block--action c-data-table__block--header-action u-py0"
              :class="{ 'c-data-table__block--pinned': pinFirstColumn }">
              <el-checkbox
                v-if="selectable"
                :indeterminate="!!someRowsSelected"
                v-model="allRowsSelected"
                :class="{
                  'el-checkbox--hidden': singleSelect || !data || !data.length
                }">
              </el-checkbox>
            </th>

            <!-- headers (and sort icons) for user provided columns -->
            <template v-for="(column, index) in columns">
              <th
                v-if="column.visible"
                class="c-data-table__block c-data-table__block--header"
                :class="{
                  'c-data-table__block--pinned': pinFirstColumn && index === 0,
                  'c-data-table__block--header-sortable': column.sortable
                }"
                :key="column.dataKey + index"
                @click="handleSortClick($event, column)"
                :style="(index === 0 && pinFirstColumn && hasActionableElements) ? { left: pinnedColumnLeftOffset } : {}">

                <div
                  class="l-flex-grid--no-wrap l-flex-grid--v-center"
                  :class="{ [`l-flex-grid--h-${column.computedAlign}`]: !!column.computedAlign }">

                  <el-data-table-column-header :column="column" />

                  <el-tooltip v-if="column.tooltip" placement="top" transition="none" :open-delay="500">
                    <fa-icon icon="info-circle" size="xs" class="c-data-table__tooltip-icon"/>
                    <div class="u-pre-line" slot="content">{{ column.tooltip }}</div>
                  </el-tooltip>

                  <span
                    v-if="column.sortable"
                    class="c-data-table__sort-caret">
                    <fa-icon :icon="sortIconMap[column.sortOrder]" />
                  </span>

                </div>

                <div
                  v-if="bordered && index !== columns.length - 1"
                  class="c-data-table__resize-handle"
                  @click.stop
                  @mousedown="handleResizeStart($event, column, index)"></div>

              </th>
            </template>
          </tr>
        </thead>
        <!-- table head end -->

        <!-- table body start -->
        <component
          :is="tableBodyComponent.component"
          v-bind="tableBodyComponent.props"
          class="c-data-table__body"
          @sort="$emit('drag-order-updated', $event)"
          @start="toggleRowsHoverStyles"
          @end="toggleRowsHoverStyles"
          ref="draggable">

          <!-- user provided rows start -->
          <template v-if="data && data.length">
            <template v-for="row in paginatedRows">

              <!-- table content row start -->
              <el-data-table-row
                @row-selected="handleRowSelected"
                @row-clicked="$emit('row-clicked', row)"
                @toggle-collapsed-row="toggleCollapse"
                @toggle-child-rows="toggleChildRowsCollapse"
                @mouseenter="$emit('mouse-entered-row', row)"
                @mouseleave="$emit('mouse-left-row', row)"
                :key="`${row.$rowId}-row`"
                :searchQuery="searchQuery || fallbackSearchQuery"
                :row-data="row"
                :data-id="row.id || row.$rowId"
                :has-child-rows="!!(
                  childRowsComputed.show &&
                  (
                    (row[childRowsComputed.childrenKey] && row[childRowsComputed.childrenKey].length) ||
                    row[childRowsComputed.hasChildrenKey]
                  )
                )"
                v-bind="{
                  draggable,
                  striped,
                  hasCollapsibleRows,
                  columns,
                  rowClasses,
                  selectable,
                  singleSelect,
                  actionable,
                  hasActionableElements,
                  pinFirstColumn,
                  pinnedColumnLeftOffset
                }"
                ref="rows">
              </el-data-table-row>
              <!-- table content row end -->

              <!-- collapsible content row start -->
              <el-data-table-collapsible-slot
                v-if="hasCollapsibleRows"
                :key="`${row.$rowId}-collapsible`"
                :colspan="colspan">
                <el-collapse-transition>
                  <div v-show="!row.$collapsed" class="c-data-table__collapsible-content">
                    <!-- extra wrapping is needed for the collapse animation to work smoothly -->
                    <div v-if="useNestedCollapsible" class="c-data-table__collapsible-content__nested">
                      <div class="c-data-table__collapsible-content__handle" title="Collapse row" @click="toggleCollapse(row)" />
                      <slot name="collapsibleRow" :row="row"></slot>
                    </div>
                    <slot v-else name="collapsibleRow" :row="row"></slot>
                  </div>
                </el-collapse-transition>
              </el-data-table-collapsible-slot>
              <!-- collapsible content row end -->

              <!-- expandable rows -->
              <template v-if="childRowsComputed.show && !row.$childrenData.collapsed">
                <el-data-table-row
                  v-for="(childRow, idx) in row[childRowsComputed.childrenKey]"
                  @row-clicked="$emit('row-clicked', childRow)"
                  @mouseenter="$emit('mouse-entered-row', childRow)"
                  @mouseleave="$emit('mouse-left-row', childRow)"
                  is-child-row
                  :searchQuery="searchQuery || fallbackSearchQuery"
                  :key="`${row.$rowId}-${idx}-child-row`"
                  :row-data="childRow"
                  v-bind="{
                    columns,
                    rowClasses,
                    actionable,
                    pinFirstColumn,
                    pinnedColumnLeftOffset,
                    hasActionableElements
                  }">
                </el-data-table-row>
              </template>
              <!-- expandable rows -->

            </template>
          </template>
          <!-- user provided rows end -->

          <!-- no data row start -->
          <template v-else-if="!loading">
            <tr class="c-data-table__row c-data-table__row--loading">
              <td
                :colspan="colspan"
                class="c-data-table__block u-text--center">
                <slot name="no-data">
                  <span class="no-data-default">
                    <fa-icon icon="exclamation-triangle" />
                    No data
                    <span v-if="dataIsFiltered">for the selected filter criteria</span>
                  </span>
                </slot>
              </td>
            </tr>
          </template>
          <!-- no data row end -->

          <!-- summary row start -->
          <template v-if="paginatedRows.length && hasSummaryColumns">
            <el-data-table-row
              key="summary-row"
              :searchQuery="searchQuery || fallbackSearchQuery"
              :row-data="summaryRowData"
              is-summary
              v-bind="{
                columns,
                rowClasses,
                striped,
                hasActionableElements,
                pinFirstColumn,
                pinnedColumnLeftOffset
              }">
            </el-data-table-row>
          </template>
          <!-- summary row end -->

        </component>
        <!-- table body end -->

        <!-- table sizer start -->
        <tbody class="c-data-table__sizer-body">
          <tr>
            <td v-if="hasActionableElements" ref="sizerColumnActions" :style="{ width: '1rem' }"></td>
            <template v-for="(column, index) in columns">
              <td
                v-if="column.visible" :key="column.dataKey + index"
                :style="{ width: column.width, minWidth: column.minWidth }"
                ref="sizerColumns">
              </td>
            </template>
          </tr>
        </tbody>
        <!-- table sizer end -->

      </table>

      <!-- desktop table actions bar start -->
      <div
        v-if="isActionsVisible"
        class="c-data-table__actions-container u-hidden-xs">
        <div
          ref="dataTableActions"
          class="c-data-table__actions">
          <div
            class="c-data-table__actions-count"
            :class="{ 'c-data-table__actions-count--pinned': pinFirstColumn }">
            {{ selectedRowsText }}

            <template v-if="!customPagination">
              <span
                v-if="selectedRows.length < rows.length"
                class="u-link u-ml2"
                @click="selectAllRowsAcrossPages(true)">
                Select all {{ rows.length }} records
              </span>
              <span
                v-else-if="selectedRows.length === rows.length"
                class="u-link u-ml2"
                @click="selectAllRowsAcrossPages(false)">
                Unselect all {{ rows.length }} records
              </span>
            </template>
          </div>
          <el-data-table-action-buttons
            v-if="actions.length"
            :actions="selectedRows.length > 1 ? actionByType('batch') : actionByType('single')"
            :selected-rows-count="selectedRows.length"
            :selectedRows="selectedRows"
            @command="handleActionInvoked($event)">
          </el-data-table-action-buttons>
        </div>
      </div>
      <!-- desktop table actions bar end -->

    </div>

    <!-- pagination start -->
    <div
      class="c-data-table__pagination-container"
      v-if="showPagination">
      <div class="c-data-table__pagination-info">
        Showing {{ paginationAttrs.from }}-{{ paginationAttrs.to }} of {{ paginationAttrs.total }}
      </div>
      <el-pagination
        :key="paginationRenderKey"
        class="c-data-table__pagination"
        background
        layout="prev, pager, next"
        :total="paginationAttrs.total"
        :page-size="itemsPerPage"
        :current-page="paginationAttrs.currentPage"
        @current-change="updatePage"
        :disabled="loading"
        v-test-id="'table-pagination'">
      </el-pagination>
    </div>
    <!-- pagination end -->

    <slot></slot>
  </div>
</template>

<script>
import _ from 'lodash';
import draggable from 'vuedraggable';
import ElDataTableCollapsibleSlot from './DataTableCollapsibleSlot';
import ElDataTableActionsDropdown from './DataTableActionsDropdown';
import { hashCode, delay } from 'element-ui/src/utils/util';

const invalidPaginatedDataLengthMessage = '[Design System Warning] The Data Table is using custom pagination and the itemsPerPage prop value is less than the amount of records passed to it\'s data prop';

export default {
  name: 'ElDataTable',
  props: {
    tableId: { type: String },
    data: { type: Array, required: true },
    dataIsFiltered: { type: Boolean, default: false },
    bordered: { type: Boolean, default: false },
    striped: { type: Boolean, default: false },
    pinFirstColumn: { type: Boolean, default: false },
    loading: { type: Boolean, default: false },
    header: { type: Boolean, default: true },
    headerPositionStickySpacing: { type: String, default: '-2rem' }, // This prop for correctly position the table header's sticky position
    compact: { type: Boolean, default: false },
    selectable: { type: Boolean, default: false },
    singleSelect: { type: Boolean, default: false },
    actionable: { type: Boolean, default: false },
    searchable: { type: Boolean, default: true },
    searchQuery: { type: String, default: '' },
    fallbackSearchQuery: { type: String, default: '' },
    draggable: { type: Boolean, default: false },
    customPagination: { type: Object, required: false }, // { totalItemCount: Number, currentPage: Number, disableSelectionCheck: Boolean (default: false) }
    itemsPerPage: { type: Number, default: 20 },
    hasCollapsibleRows: { type: Boolean, default: false },
    insideCollapsibleRow: { type: Boolean, default: false },
    useNestedCollapsible: { type: Boolean, default: false },
    useCustomSort: { type: Boolean, default: false },
    childRows: {
      type: Object,
      default: () => ({ show: false }),
      validator: ({ show, load, childrenKey, hasChildrenKey }) => {
        return _.every([
          _.isBoolean(show),
          _.isNil(load) || _.isFunction(load),
          _.isNil(childrenKey) || (_.isString(childrenKey) && !!childrenKey),
          _.isNil(hasChildrenKey) || (_.isString(hasChildrenKey) && !!hasChildrenKey)
        ]);
      }
    },
    filterBindings: {
      type: Object,
      validator: (bindings) => {
        return _.every([
          _.isArray(bindings.availableFilterCriteria),
          _.isObject(bindings.selectedFilterCriteria),
          _.isArray(bindings.badges),
          _.every(bindings.badges, badge => _.isObject(badge))
        ]);
      },
      default: () => {
        return {
          availableFilterCriteria: [],
          selectedFilterCriteria: {},
          badges: [],
          _isEmpty: true // used in template to determine whether filters have been passed by the user
        };
      }
    },
    rowClasses: { type: Function, required: false },
    actions: {
      type: Array,
      default: () => [],
      validator: actions => {
        return _.every(_.map(actions, (action) => {
          let valid = !_.some([
            !action.label || (action.label.constructor !== String && action.label.constructor !== Function),
            !action.action || action.action.constructor !== Function,
            action.executeRowByRow && typeof action.executeRowByRow !== 'boolean',
            (action.type && action.type.constructor !== String) || (action.type && ['batch', 'single'].indexOf(action.type) === -1)
          ]);
          if (valid && action.executeRowByRow) {
            valid = !_.some([
              typeof action.callbacks !== 'object',
              !action.callbacks.onSuccess || action.callbacks.onSuccess.constructor !== Function,
              typeof action.callbacks.onFailure !== 'object',
              !action.callbacks.onFailure.fields || action.callbacks.onFailure.fields.constructor !== Array
            ]);
          }
          return valid;
        }));
      }
    }
  },
  data: function() {
    return {
      columnSlots: [],
      sortByColumn: null,
      selectColumnsModalVisible: false,
      currentSortColumn: null,
      hasDataWithoutQuery: this.searchQuery === '' && this.data.length,
      isDragging: false,
      paginationRenderKey: 1, // used for forcing re-render of el-pagination
      currentPage: 1,
      batchActionFailureDialog: {
        visible: false,
        failures: [],
        dialogFields: [],
        dialogOnClose: () => {
          this.batchActionFailureDialog.visible = false;
        }
      },
      sortIconMap: {
        null: 'sort',
        asc: 'sort-up',
        desc: 'sort-down'
      },
      pinnedColumnLeftOffset: '0px',
      isMouseDownForResize: false,
      isColumnWidthsLocked: false
    };
  },
  components: {
    draggable,
    ElDataTableActionsDropdown,
    ElDataTableCollapsibleSlot
  },
  async mounted() {
    this.columnSlots = this.$slots.default || [];

    // Make sure totalItemsCount is set if we are using custom pagination
    if (this.customPagination) {
      const { totalItemCount, currentPage } = this.customPagination;
      if ((!totalItemCount && totalItemCount !== 0) || !currentPage) {
        console.error('[Design System Error] The Data Table\'s customPagination object requires `totalItemCount` and `currentPage`');
      }
      if (this.itemsPerPage < this.data.length) {
        console.warn(invalidPaginatedDataLengthMessage);
      }
    }

    await this.$nextTick(); // wait for columns to render
    if (this.pinFirstColumn) this.setPinnedColumnLeftOffset();
    if (this.bordered) this.applyPersistedColumnWidths();
  },
  beforeUpdate() {
    if (process.env.NODE_ENV === 'production') return;

    const { header, hasCollapsibleRows, draggable, actions } = this;
    if (!header && actions.length) console.warn('[Design System Warning] Headerless tables are not compatible with actions. Set header: true to suppress this warning.');
    if (draggable && hasCollapsibleRows) console.warn('[Design System Warning] Draggable rows are not compatible with tables that have collapsible content. Unset either prop to suppress this warning.');
  },
  computed: {
    summaryRowData() {
      const ret = {};
      this.columns.forEach(({ summaryValue, dataKey }) => {
        ret[dataKey] = _.isFunction(summaryValue) ? summaryValue(this.paginatedRows, dataKey) : '';
      });
      return ret;
    },
    computedTableId() {
      if (this.tableId) return this.tableId;

      const { host, pathname, hash } = location;
      const isUsingHashRouting = hash.startsWith('#/');

      const urlIdentifier = host + pathname + (isUsingHashRouting ? hash.split('#')[1] : '');
      const dataKeysConcatenated = this.columns.map(column => column.dataKey).join('|');
      const labelsConcatenated = this.columns.map(column => column.label).join('|');

      return hashCode(urlIdentifier + '-' + dataKeysConcatenated + '-' + labelsConcatenated).toString();
    },
    persistence() {
      const localStorageKey = `bc-ds-data-table:${this.computedTableId}`;

      return {
        get: (key, defaultVal = null) => {
          let ret = defaultVal;
          try {
            const obj = JSON.parse(localStorage.getItem(localStorageKey)) || {};
            if (obj.hasOwnProperty(key)) ret = obj[key];
          } catch (err) {}
          return ret;
        },
        set: (key, value) => {
          let objToWrite = {};
          try {
            objToWrite = JSON.parse(localStorage.getItem(localStorageKey)) || {};
          } catch (err) {}
          objToWrite[key] = value;
          localStorage.setItem(localStorageKey, JSON.stringify(objToWrite));
        }
      };
    },
    hasActionableElements() {
      return this.selectable || this.singleSelect || this.hasCollapsibleRows || this.draggable || this.childRowsComputed.show;
    },
    tableBodyComponent() {
      if (this.loading) {
        return {
          component: 'el-loading-wrapper',
          props: { tag: 'tbody', colspan: this.colspan, loading: true, loadingText: 'Loading...' }
        };
      }
      if (this.draggable) {
        return {
          component: 'draggable',
          props: { tag: 'tbody', draggable: '.drag-handle__row', handle: '.drag-handle' }
        };
      }
      return { component: 'tbody', props: {} };
    },
    colspan() {
      let columnCount = this.columns.map(column => column.visible).length;
      if (this.hasActionableElements) ++columnCount;
      return columnCount;
    },
    columns: function() {
      let columns = this.columnSlots.filter(slot => slot.componentInstance).map(slot => slot.componentInstance);
      if (this.$refs.actionsColumn) {
        columns.push(this.$refs.actionsColumn);
      }
      return columns;
    },
    hasSummaryColumns() {
      return !!this.columns.find(column => column.summaryValue);
    },
    sortableColumns: function() {
      let sortableColumns = [];
      this.columns.filter(column => column.sortable).map(column => {
        sortableColumns.push({
          label: `${column.label}: Desc`,
          value: { dataKey: column.dataKey, sortOrder: 'desc' }
        });
        sortableColumns.push({
          label: `${column.label}: Asc`,
          value: { dataKey: column.dataKey, sortOrder: 'asc' }
        });
      });
      return sortableColumns;
    },
    selectedRows: function() {
      return this.rows.filter(row => row.$selected);
    },
    selectedRowsText: function() {
      const pluralizedRow = this.selectedRows.length === 1 ? 'row' : 'rows';
      return `${this.selectedRows.length} ${pluralizedRow} selected`;
    },
    mobileSelectedRowsText: function() {
      return this.selectedRows.length > 0 ? this.selectedRowsText : 'Select All';
    },
    isActionsVisible() {
      return this.actions.length && this.selectable && this.selectedRows.length > 0 && !this.singleSelect;
    },
    rows: function() {
      return this.data.map((rowData, index) => {
        // we are checking if these parameters are already existent because it is causing issues
        // when assigning them multiple times.
        if (!rowData['$rowId']) {
          this.$set(rowData, '$rowId', _.uniqueId('data-table-row-id_'));
        }
        if (!rowData['$loading']) {
          this.$set(rowData, '$loading', false); // used when loading data in a collapsible row.
        }
        if (!rowData['$selected']) {
          this.$set(rowData, '$selected', false);
        }
        // Test if this is explicitly null because we do not want to set it if it is already set to either false/true
        if (rowData['$collapsed'] == null) {
          this.$set(rowData, '$collapsed', true);
        }
        if (!rowData.hasOwnProperty('$childrenData')) {
          this.$set(rowData, '$childrenData', { collapsed: true, loading: false });
        }
        return rowData;
      });
    },
    sortedRows() {
      const { useCustomSort, currentSortColumn, rows } = this;
      if (useCustomSort || !currentSortColumn || !rows.length) return rows;
      const { sortOrder, dataKey, sortMethod } = currentSortColumn;

      // handle custom sort function
      if (_.isFunction(sortMethod)) {
        let ret = sortMethod(rows, dataKey, sortOrder);
        if (!_.isArray(ret)) {
          console.error(`"Error: "sortMethod" for "${dataKey}" column returned a non-array value.`);
          return [];
        }
        return ret;
      }

      // use inbuilt sort
      // use case-insensitive (natural) sort for strings
      if (typeof _.get(rows[0], dataKey) === 'string') {
        const ret = [...rows].sort(
          (rowA, rowB) => {
            const [valA, valB] = sortOrder === 'asc'
              ? [_.get(rowA, dataKey), _.get(rowB, dataKey)]
              : [_.get(rowB, dataKey), _.get(rowA, dataKey)];

            // push falsy values to the bottom irrespective of the sort order
            if (!valA) return sortOrder === 'asc' ? 1 : -1;
            if (!valB) return sortOrder === 'asc' ? -1 : 1;

            // normal sort
            return _.toString(valA).localeCompare(_.toString(valB), undefined, { caseFirst: 'upper' });
          }
        );
        return ret;
      }
      // and lodash's sort for other types
      return _.orderBy(rows, [
        row => {
          const val = _.get(row, dataKey);
          if (!_.isNil(val)) return val; // normal sort for non nil values
          return sortOrder === 'asc' ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY; // push nil values to the bottom irrespective of the sort order
        }
      ], [sortOrder]);
    },
    paginatedRows: function() {
      const { sortedRows, currentPage, itemsPerPage } = this;
      if (this.customPagination) return sortedRows;
      const startingRowIndex = itemsPerPage * (currentPage - 1);
      const endingIndex = startingRowIndex + itemsPerPage;
      return sortedRows.slice(startingRowIndex, endingIndex);
    },
    showPagination() {
      const { customPagination, data, itemsPerPage } = this;
      const moreRecordsThanShouldShow = !customPagination && data.length > itemsPerPage;

      if (customPagination) {
        const { totalItemCount, currentPage } = customPagination;
        const pageCount = Math.ceil(totalItemCount / itemsPerPage);

        if (data.length >= totalItemCount) return false; // if all rows are showing, we don't need to show pagination
        // Don't show pagination if there are not enough records or are on the last page
        const lessDataThanPageLimit = data.length < itemsPerPage;
        if ((lessDataThanPageLimit && currentPage === 1) || (lessDataThanPageLimit && currentPage !== pageCount)) {
          return false;
        }
      }

      return !!customPagination || moreRecordsThanShouldShow;
    },
    paginationAttrs() {
      const { customPagination, itemsPerPage } = this;
      const [total, currentPage] = customPagination
        ? [customPagination.totalItemCount, customPagination.currentPage]
        : [this.data.length, this.currentPage];
      const zeroBasedFrom = ((currentPage - 1) * itemsPerPage);
      return {
        total,
        currentPage,
        from: zeroBasedFrom + 1,
        to: zeroBasedFrom + this.paginatedRows.length
      };
    },
    // `allRowsSelected` and `someRowsSelected` are only computed using the current page's rows and the checks below achieve that
    allRowsSelected: {
      get: function() {
        return (
          this.selectedRows.length &&
          this.selectedRows.length >= this.paginatedRows.length &&
          this.paginatedRows.every(row => this.selectedRows.includes(row))
        );
      },
      set: function(val) {
        this.paginatedRows.forEach(row => {
          if (row.$selected === val) return;
          row.$selected = val;
          this.handleRowSelected(row);
        });
      }
    },
    someRowsSelected() {
      return !this.allRowsSelected && this.paginatedRows.some(row => this.selectedRows.includes(row));
    },
    childRowsComputed() {
      const { show, load, childrenKey = 'children', hasChildrenKey = 'hasChildren' } = this.childRows;
      return { show, load, childrenKey, hasChildrenKey };
    }
  },
  methods: {
    lockColumnWidths() {
      const { $refs, columns } = this;

      // lock all the columns except the last one if they don't have an explicit width
      columns.forEach(({ width }, colIdx) => {
        const isLast = colIdx === columns.length - 1;
        const ref = $refs.sizerColumns[colIdx];

        if ((width && width !== 'auto') || isLast || (ref.style.width && ref.style.width !== 'auto')) return;

        ref.style.width = `${ref.offsetWidth}px`;
      });
      if (!this.isColumnWidthsLocked) this.isColumnWidthsLocked = true;
    },
    handleResizeStart(event, column, idx) {
      const { $refs } = this;
      document.body.classList.add('is-resizing');

      this.lockColumnWidths();

      // mousePosition and columnWidth are updated throughout the mousemove
      let [mousePosition, columnWidth] = [event.x, $refs.sizerColumns[idx].offsetWidth];

      const onResize = event => {
        // prevent text selection while resizing
        event.preventDefault();
        event.stopPropagation();

        const dx = event.x - mousePosition;
        if (dx === 0) return;

        const [computedWidth, proposedWidth] = [$refs.sizerColumns[idx].offsetWidth, columnWidth + dx];
        // if the difference between the two is more than 2px, that means width is not applying
        // 2 is being used to get around sub-pixel calculation issues that arise on chrome when resizing while zoomed out
        if (Math.abs(columnWidth - computedWidth) > 2) {
          // increase the table's width if the default of 100% is not enough
          if (dx > 0 && proposedWidth > computedWidth) {
            this.$refs.table.style.width = `${this.$refs.table.offsetWidth + (proposedWidth - computedWidth)}px`;
          }
          // return if user is decreasing the width but browser is not respecting width decrease
          if (dx < 0 && proposedWidth < computedWidth) {
            return onStop(null, true);
          }
        }

        // as the user decreases any column's width, reduce the table width as well if it's greater than 100%
        if (this.$refs.container.offsetWidth < this.$refs.table.offsetWidth) {
          this.$refs.table.style.width = `${this.$refs.table.offsetWidth - (computedWidth - proposedWidth)}px`;
        } else { // remove the custom width from table if 100% is enough
          this.$refs.table.style.width = '';
        }

        // update internal state and column width
        mousePosition = event.x;
        $refs.sizerColumns[idx].style.width = `${proposedWidth}px`;
        columnWidth += dx;
        return false;
      };

      const onStop = (__, manual = false) => {
        document.body.classList.remove('is-resizing');
        document.removeEventListener('mousemove', onResize);
        document.removeEventListener('mouseup', onStop);

        if (manual) { // we're force stopping the resize because user is trying to shrink past the width that column allows
          this.isMouseDownForResize = true; // disable sorting

          // one time handler that enables sorting once the user has let go of the mouse button
          const mouseupHandler = async() => {
            document.removeEventListener('mouseup', mouseupHandler, true);
            await delay(100); // wait a little to make sure we don't set the flag to false before the sort function is called
            this.isMouseDownForResize = false;
          };
          document.addEventListener('mouseup', mouseupHandler, true);
        }

        this.storePersistedColumnWidths();
      };

      document.addEventListener('mousemove', onResize);
      document.addEventListener('mouseup', onStop);
    },
    storePersistedColumnWidths() {
      const { columns, $refs } = this;
      const tableWidth = $refs.table.style.width;
      const columnWidths = {};
      columns.forEach(({ dataKey }, idx) => {
        if (idx === columns.length - 1) return;
        columnWidths[dataKey] = $refs.sizerColumns[idx].offsetWidth;
      });
      this.persistence.set('resizeData', { tableWidth, columnWidths });
    },
    applyPersistedColumnWidths() {
      const { columns, $refs } = this;
      let resizeData = this.persistence.get('resizeData');
      if (!resizeData) return;

      const { tableWidth, columnWidths } = resizeData;
      if (tableWidth) $refs.table.style.width = tableWidth;
      columns.forEach(({ dataKey }, idx) => {
        const [isLast, persistedWidth] = [idx === columns.length - 1, columnWidths[dataKey]];
        if (isLast || !persistedWidth) return;
        $refs.sizerColumns[idx].style.width = `${persistedWidth}px`;
      });
    },
    async setPinnedColumnLeftOffset() {
      if (!this.pinFirstColumn) return;
      await this.$nextTick();
      const el = this.$refs.sizerColumnActions;
      this.pinnedColumnLeftOffset = `${el ? el.offsetWidth : 0}px`;
    },

    getNextSortingOrder: function(currentSortOrder) {
      switch (currentSortOrder) {
        case 'asc':
          return 'desc';
        case 'desc':
          return null;
        default:
          return 'asc';
      }
    },
    handleActionInvoked: async function(action, affectedRows) {
      const { prompt, executeRowByRow, callbacks } = action;
      const rows = affectedRows || this.selectedRows;
      if (prompt) {
        try {
          await this.$confirm(prompt.message(rows.length), prompt.config);
        } catch (e) {
          return;
        }
      }
      if (!executeRowByRow) {
        action.action({
          selectedRows: rows
        });
      } else {
        this.batchActionFailureDialog.failures = [];

        for (const row of rows) {
          try {
            await action.action(row);
          } catch (err) {
            this.batchActionFailureDialog.failures.push({...row, errorMessage: err.message});
          }
        }
        const allActionsPassed = this.batchActionFailureDialog.failures.length === 0;
        if (allActionsPassed) {
          callbacks.onSuccess(rows.length);
        } else {
          this.batchActionFailureDialog.dialogFields = callbacks.onFailure.fields;
          this.batchActionFailureDialog.dialogMessage = callbacks.onFailure.message;
          this.batchActionFailureDialog.dialogOnClose = () => {
            this.batchActionFailureDialog.visible = false;
            if (callbacks.onFailure.onDialogClose) callbacks.onFailure.onDialogClose();
          };
          this.batchActionFailureDialog.visible = true;
        }
      }
    },
    async setTableActionsDimensions() {
      await this.$nextTick();
      const { dataTableActions, tableHead, table } = this.$refs;
      if (!dataTableActions || !tableHead || !table) return;
      Object.assign(dataTableActions.style, {
        width: `calc(${table.clientWidth}px - .75em - 3em)`, // account for right padding & margin left
        height: `calc(${tableHead.clientHeight}px - 2px)`,
        opacity: 1
      });
    },
    handleRowSelected: function(row) {
      if (this.singleSelect) {
        this.paginatedRows.forEach(paginatedRow => {
          if (row.$rowId !== paginatedRow.$rowId) {
            paginatedRow.$selected = false;
          }
        });
      }
      this.$emit('row-selected', row);
    },
    toggleCollapse: function(row) {
      this.$emit('toggle-collapsed-row', row);
      this.paginatedRows.forEach(paginatedRow => {
        if (row.$rowId === paginatedRow.$rowId) {
          this.$set(paginatedRow, '$collapsed', !paginatedRow.$collapsed);
        }
      });
    },
    toggleChildRowsCollapse(row) {
      const { load, childrenKey, hasChildrenKey, isColumnWidthsLocked } = this.childRowsComputed;

      // lock column widths so columns don't adjust their widths to expanded rows' contents
      if (!isColumnWidthsLocked) this.lockColumnWidths();

      this.paginatedRows.forEach(async paginatedRow => {
        if (row.$rowId !== paginatedRow.$rowId) return;

        const { $childrenData } = paginatedRow;
        const [children, hasChildren] = [paginatedRow[childrenKey], paginatedRow[hasChildrenKey]];

        if ($childrenData.loading) return;

        if (!$childrenData.collapsed) { // collapse if expanded
          $childrenData.collapsed = true;
          return;
        };

        // row expansion logic from here onwards
        if (
          (children && children.length) || // children already exist or have been loaded before
          (!hasChildren || !load) // no children and can't be lazily loaded either
        ) {
          $childrenData.collapsed = false;
          return;
        }

        // lazy row expansion
        $childrenData.loading = true;
        try {
          const children = await load(row);
          this.$set(paginatedRow, childrenKey, children);
          $childrenData.collapsed = false;
        } catch (err) {}
        $childrenData.loading = false;
      });
      this.$emit('toggle-child-rows', row);
    },
    handleSortChange: function() {
      if (this.currentSortColumn) { this.currentSortColumn.sortOrder = null; }
      this.currentSortColumn = this.columns.find(column => column.dataKey === this.sortByColumn.value.dataKey);
      this.currentSortColumn.sortOrder = this.sortByColumn.value.sortOrder;
      this.$emit('sort', [{ column: this.currentSortColumn, order: this.currentSortColumn.sortOrder }]);
    },
    handleSortClick: function(event, column, sortOrder) {
      if (this.isMouseDownForResize || !column.sortable) {
        return;
      }
      if (this.currentSortColumn && this.currentSortColumn !== column) {
        this.currentSortColumn.sortOrder = null;
        this.sortByColumn = null;
      }
      if (!sortOrder) {
        sortOrder = this.getNextSortingOrder(this.currentSortColumn ? this.currentSortColumn.sortOrder : null);
      }
      column.sortOrder = sortOrder;
      if (sortOrder) {
        this.sortByColumn = this.sortableColumns.find(sortableColumn => {
          return sortableColumn.value.dataKey === column.dataKey && sortableColumn.value.sortOrder === sortOrder;
        });
        this.currentSortColumn = column;
      } else {
        this.currentSortColumn = null;
        this.sortByColumn = null;
      }
      this.$emit('sort', [{ column: this.currentSortColumn, order: sortOrder }]);
    },
    toggleRowsHoverStyles() {
      const backgroundClass = 'c-data-table--no-background';
      const existingClasses = this.$refs.draggable.$el.classList;
      const method = existingClasses.contains(backgroundClass) ? 'remove' : 'add';
      existingClasses[method](backgroundClass);
    },
    actionByType(type) {
      return this.actions.filter(action => !action.type || action.type === type);
    },
    async updatePage(newPage) {
      if (this.selectedRows.length && this.customPagination && !this.customPagination.disableSelectionCheck) {
        try {
          await this.$confirm(
            'Selected rows will be unselected when navigating away from the page. <br> Are you sure you want to proceed?',
            'Confirm', { confirmButtonText: 'Yes', cancelButtonText: 'No', type: 'warning', dangerouslyUseHTMLString: true }
          );
        } catch (err) {
          ++this.paginationRenderKey;
          return;
        }
      }
      this.currentPage = newPage;
      this.$emit('current-page-updated', newPage);
    },
    resetPage() {
      if (!this.customPagination) {
        this.currentPage = 1;
      }
    },
    selectAllRowsAcrossPages(val) {
      this.rows.forEach(row => {
        row.$selected = val;
      });
    }
  },
  watch: {
    // Need to watch for fetched data
    // If there is ever data the search bar needs to always be present
    data() {
      const { searchQuery, data, customPagination, itemsPerPage } = this;
      if (searchQuery === '' && data.length) {
        this.hasDataWithoutQuery = true;
      }
      if (customPagination && itemsPerPage < data.length) {
        console.warn(invalidPaginatedDataLengthMessage);
      }
    },
    searchQuery() {
      this.resetPage();
    },
    'filterBindings.selectedFilterCriteria': {
      deep: true,
      handler() {
        this.resetPage();
      }
    },
    isActionsVisible(val) {
      if (val) this.setTableActionsDimensions();
    }
  }
};
</script>
