<template>
    <div
        class="good-table"
        :class="[
            {
                'good-table-overflow ' : allowOverflow,
                'pinned-actions' : allowOverflow && hasPinnedActions,
            },
            className
        ]">
        <div :class="{'is-loading': isLoading}">
            <slot
                v-if="paginate && paginateOnTop"
                name="pagination-top"
                :pageChanged="pageChanged"
                :perPageChanged="perPageChanged"
                :total="totalRows || totalRowCount">
                <vgt-pagination
                    ref="paginationTop"
                    :per-page="perPage"
                    :rtl="rtl"
                    :total="totalRows || totalRowCount"
                    :mode="paginationMode"
                    :allow-per-page-select="paginationSelect"
                    :next-text="nextText"
                    :prev-text="prevText"
                    :rows-per-page-text="rowsPerPageText"
                    :custom-rows-per-page-dropdown="customRowsPerPageDropdown"
                    :paginate-dropdown-allow-all="paginateDropdownAllowAll"
                    :of-text="ofText"
                    :page-text="pageText"
                    :all-text="allText"
                    :count-loading="countLoading"
                    @page-changed="pageChanged"
                    @per-page-changed="perPageChanged"></vgt-pagination>
            </slot>

            <vgt-global-search
                v-model="globalSearchTerm"
                :search-enabled="searchEnabled && externalSearchQuery == null"
                :global-search-placeholder="searchPlaceholder"
                :enable-search-button="searchTrigger === 'button'"
                @on-keyup="searchTableOnKeyUp"
                @on-enter="searchTableOnTrigger"
                @on-search-button-click="searchTableOnTrigger">
                <template slot="internal-table-actions">
                    <slot name="table-actions" />
                </template>
            </vgt-global-search>

            <div>
                <table
                    v-if="fixedHeader">
                    <thead
                        is="vgt-table-header"
                        ref="table-header-secondary"
                        :columns="columns"
                        :line-numbers="lineNumbers"
                        :selectable="selectable"
                        :all-selected="allSelected"
                        :all-selected-indeterminate="allSelectedIndeterminate"
                        :mode="mode"
                        :total-row-count="totalRowCount"
                        :sortable="sortable"
                        :typed-columns="typedColumns"
                        :get-classes="getClasses"
                        :search-enabled="searchEnabled"
                        :paginated="paginated"
                        :table-ref="$refs.table"
                        @on-toggle-select-all="toggleSelectAll"
                        @on-sort-change="changeSort">
                        <template
                            slot="table-column"
                            slot-scope="props">
                            <slot
                                name="table-column"
                                :column="props.column">
                                <span
                                    :class="{
                                        'invisible' : !props.column.label
                                    }"
                                    :aria-hidden="!props.column.label">
                                    {{ props.column.label || 'Hidden Placeholder' }}
                                </span>
                            </slot>
                        </template>
                    </thead>
                </table>
            </div>

            <!-- Active Filter Bar -->
            <keep-alive>
                <div v-if="hasFilterableColumns" class="good-table__filter-active-bar mb-3">
                    <div class="pr-1 good-table__filter-active-bar-endcap">
                        <i class="fas fa-filter text-primary mr-1" aria-hidden="true"/>
                        <span class="font-weight-bold">Filters</span>
                    </div>

                    <div class="px-1 d-flex flex-grow-1">
                        <div v-if="activeFilters.length > 0" class="d-flex flex-wrap">
                            <div
                                class="good-table__filter-active-item mr-1"
                                v-for="filter in activeFilters"
                                :key="filter.id">
                                <div class="d-flex align-items-center mr-1">{{ filter.value }}</div>

                                <button @click="removeFilter(filter.id)" class="btn btn-link p-0">
                                    <i class="fas fa-times-circle" aria-hidden="true"/>
                                </button>
                            </div>
                        </div>

                        <div v-else>
                            No filters currently active.
                        </div>
                    </div>

                    <div class="pl-1 good-table__filter-active-bar-endcap d-flex justify-content-end">
                        <button
                            @click="clearAllFilters"
                            class="btn btn-link p-0"
                            :disabled="!activeFilters.length">
                            <i class="fas fa-times-circle mr-1" aria-hidden="true"/>Clear All
                        </button>
                    </div>
                </div>
            </keep-alive>
            <!-- / Active Filter Bar -->

            <div class="d-flex justify-content-between align-center">
                <BulkActions
                    :all-selected-indeterminent="allSelectedIndeterminate"
                    :all-selected="allSelected"
                    @input="toggleSelectAll"
                    @select-all-on-page="selectAllInternal"
                    @unselect-all-on-page="unselectAllInternal"
                    @select-all-rows="selectAllInternal(false)"
                    @unselect-all-rows="unselectAllInternal(true)"
                    :selected-row-count="selectedRowCount"
                    :disable-select-info="disableSelectInfo"
                    :selection-info="selectionInfo"
                    :select-all-rows-enabled="enableSelectAllRows"
                    :select-all-by-page-enabled="enableSelectAllByPage"
                    v-if="selectable">
                    <slot name="selected-row-actions"/>
                </BulkActions>

                <div class="d-flex justify-content-end">
                    <slot name="table-info-top-right"/>
                </div>
            </div>

            <div class="good-table__container">
                <table
                    class="good-table__table"
                    ref="table">

                    <!-- Table header -->
                    <thead
                        is="vgt-table-header"
                        ref="table-header-primary"
                        :columns="columns"
                        :line-numbers="lineNumbers"
                        :selectable="selectable"
                        :total-row-count="totalRowCount"
                        :all-selected="allSelected"
                        :all-selected-indeterminate="allSelectedIndeterminate"
                        :mode="mode"
                        :sortable="sortable"
                        :typed-columns="typedColumns"
                        :get-classes="getClasses"
                        :search-enabled="searchEnabled"
                        @on-toggle-select-all="toggleSelectAll"
                        @on-sort-change="changeSort">

                        <template
                            slot="table-column"
                            slot-scope="props">

                            <div class="d-flex align-items-end position-relative">

                                <!-- filter icon -->
                                <template v-if="props.column.filterOptions
                                    && props.column.filterOptions.enabled
                                    && multiColumnFilterCheck(props.column)">
                                    <button
                                        title="filter"
                                        :disabled="
                                            (totalRowCount === 0)
                                                && props.column.filterOptions.type !== 'remote'"
                                        @click.stop.prevent="toggleFilter(props.column.field)"
                                        class="btn btn-filter mr-1"
                                        :class="{
                                            'good-table__filter-btn--active': columnFilterIsActive(props.column)
                                        }">
                                        <i class="fas fa-sm fa-filter" aria-hidden="true"/>
                                    </button>
                                </template>

                                <slot
                                    name="table-column"
                                    :column="props.column">

                                    <span
                                        :class="{
                                            'invisible' : !props.column.label
                                        }"
                                        :aria-hidden="!props.column.label">
                                        {{ props.column.label || 'Hidden Placeholder' }}
                                    </span>

                                    <template v-if="props.column.subFields">
                                        <span
                                            class="d-flex align-items-end"
                                            v-for="(subField, index) in props.column.subFields"
                                            :key="index">
                                            <span class="mx-1">|</span>

                                            <span>
                                                <template v-if="props.column.filterOptions
                                                    && props.column.filterOptions.enableSubFields
                                                    && props.column.filterOptions.enableSubFields
                                                        .includes(subField.field)">
                                                    <button
                                                        :disabled="
                                                            (totalRowCount === 0)
                                                                && subField.filterOptions
                                                                && subField.filterOptions.type !== 'remote'"
                                                        title="filter"
                                                        @click.stop.prevent="toggleFilter(subField.field)"
                                                        class="btn btn-filter mr-1"
                                                        :class="{
                                                            'good-table__filter-btn--active':
                                                                columnFilterIsActive(subField)
                                                        }">
                                                        <i
                                                            class="fas fa-sm fa-filter"
                                                            title="Filter"
                                                            aria-hidden="true"/>
                                                    </button>
                                                </template>
                                            </span>

                                            <span>{{ subField.label }} </span>

                                            <!-- filter dropdown -->
                                            <transition name="fade">
                                                <TaFilterDropdown
                                                    ref="filterOptions"
                                                    @close="closeFilter"
                                                    v-if="registeredFilters[subField.field]
                                                        && registeredFilters[subField.field].isOpen"
                                                    :column="subField"
                                                    :options="getFilterOptions(subField.field)"
                                                    @change-options="setTaFilterOptions">
                                                </TaFilterDropdown>
                                            </transition>
                                        </span>
                                    </template>
                                </slot>

                                <!-- sort icon -->
                                <template v-if="isSortableColumn(props.column)">
                                    <i
                                        :class="{
                                            'fa-arrow-up': getColumnSort(props.column) === 'asc',
                                            'fa-arrow-down': getColumnSort(props.column) === 'desc',
                                        }"
                                        class="fas ml-1 text-primary good-table__filter-icon"
                                        :title="{
                                            'Ascending Order': getColumnSort(props.column) === 'asc',
                                            'Descending Order': getColumnSort(props.column) === 'desc',
                                        }"
                                        aria-hidden="true" />
                                </template>

                                <!-- filter dropdown -->
                                <transition name="fade">
                                    <TaFilterDropdown
                                        ref="filterOptions"
                                        v-if="registeredFilters[props.column.field]
                                            && registeredFilters[props.column.field].isOpen"
                                        :column="props.column"
                                        @close="closeFilter"
                                        :options="getFilterOptions(props.column.field)"
                                        @change-options="setTaFilterOptions">
                                    </TaFilterDropdown>
                                </transition>
                            </div>

                        </template>
                    </thead>

                    <!-- Table body starts here -->
                    <template v-if="isLoading && loadingType === 'spinner'">
                        <tbody>
                            <tr>
                                <td :colspan="fullColspan">
                                    <LoadingSpinner></LoadingSpinner>
                                </td>
                            </tr>
                        </tbody>
                    </template>

                    <template v-else-if="isLoading && loadingType === 'lazy'">
                        <tbody>
                            <tr
                                v-for="(ele, index) in new Array(currentPerPage)"
                                :key="`tr-loading-${index}`">
                                <td v-if="selectable" class="good-table__checkbox-col">
                                    <Checkbox classes="default-input-height">
                                    </Checkbox>
                                </td>
                                <td
                                    v-for="(column, i) in columns"
                                    :key="i">
                                    <div
                                        class="d-flex lazy-load-skeleton"
                                        :class="`mr-${Math.floor(Math.random() * 10)}`">
                                        Loading
                                    </div>
                                </td>
                            </tr>
                        </tbody>
                    </template>

                    <template v-else>
                        <tbody
                            v-for="(headerRow, index) in paginated"
                            :key="`tbody-${index}`"
                            :is="enableRowTransitions ? 'transition-group' : 'tbody'"
                            :name="isTransitioning ? 'slide-toggle' : null">
                            <!-- if group row header is at the top -->
                            <vgt-header-row
                                v-if="groupHeaderOnTop"
                                :header-row="headerRow"
                                :columns="columns"
                                :line-numbers="lineNumbers"
                                :selectable="selectable"
                                :collect-formatted="collectFormatted"
                                :formatted-row="formattedRow"
                                :get-classes="getClasses"
                                :full-colspan="fullColspan">
                                <template
                                    v-if="hasHeaderRowTemplate"
                                    slot="table-header-row"
                                    slot-scope="props">
                                    <slot
                                        name="table-header-row"
                                        :column="props.column"
                                        :formattedRow="props.formattedRow"
                                        :row="props.row"/>
                                </template>
                            </vgt-header-row>

                            <!-- normal rows here. we loop over all rows -->
                            <tr
                                v-for="(row, index) in headerRow.children"
                                :key="row.originalIndex"
                                :class="getRowClassesObject(row)"
                                @mouseenter="onMouseenter(row, index)"
                                @mouseleave="onMouseleave(row, index)"
                                @dblclick="onRowDoubleClicked(row, index, $event)"
                                @click="onRowClicked(row, index, $event)"
                                @auxclick="onRowAuxClicked(row, index, $event)">
                                <slot
                                    name="table-full-row"
                                    :row="row">
                                    <th
                                        v-if="lineNumbers"
                                        class="line-numbers">
                                        {{ getCurrentIndex(index) }}
                                    </th>
                                    <th
                                        class="good-table__checkbox-col"
                                        v-if="selectable">

                                        <Checkbox
                                            classes="default-input-height"
                                            :value="row.vgtSelected"
                                            @input="onCheckboxClicked(row, index, $event)">
                                        </Checkbox>
                                    </th>

                                    <td
                                        v-for="(column, i) in columns"
                                        :key="i"
                                        :class="getClasses(i, 'td', row)"
                                        @click="onCellClicked(row, column, index, $event)">

                                        <slot
                                            name="table-row"
                                            :row="row"
                                            :column="column"
                                            :formattedRow="formattedRow(row)"
                                            :index="index">
                                            <template v-if="displayAsyncField(column)">
                                                <span v-if="getAsyncContent(row,column)">
                                                    {{getAsyncContent(row,column)}}
                                                    <template v-if="column.subFields">
                                                        <div
                                                            v-for="(subField, index) in column.subFields"
                                                            :key="`sub-field-${index}`"
                                                            class="sub-label text-truncate">
                                                            <template v-if="displayAsyncField(subField)">
                                                                <div v-if="getAsyncContent(row, subField)">
                                                                    {{ getAsyncContent(row, subField) }}
                                                                </div>

                                                                <div
                                                                    v-else
                                                                    class="d-flex lazy-load-skeleton"
                                                                    :class="`mr-${Math.floor(Math.random() * 5)}`">
                                                                    Loading
                                                                </div>
                                                            </template>

                                                            <template v-else>
                                                                {{ getSubFieldRowValue(row, subField) }}
                                                            </template>
                                                        </div>
                                                    </template>
                                                </span>
                                                <div
                                                    v-else
                                                    class="d-flex lazy-load-skeleton"
                                                    :class="`mr-${Math.floor(Math.random() * 5)}`">
                                                    Loading
                                                </div>
                                            </template>

                                            <span v-else-if="!column.html">
                                                {{ collectFormatted(row, column) || '--' }}
                                                <template v-if="column.subFields">
                                                    <div
                                                        v-for="(subField, index) in column.subFields"
                                                        :key="`sub-field-${index}`"
                                                        class="sub-label text-truncate">
                                                        <template v-if="displayAsyncField(subField)">
                                                            <div v-if="getAsyncContent(row, subField)">
                                                                {{ getAsyncContent(row, subField) }}
                                                            </div>

                                                            <div
                                                                v-else
                                                                class="d-flex lazy-load-skeleton"
                                                                :class="`mr-${Math.floor(Math.random() * 5)}`">
                                                                Loading
                                                            </div>
                                                        </template>

                                                        <template v-else>
                                                            {{ getSubFieldRowValue(row, subField) }}
                                                        </template>
                                                    </div>
                                                </template>
                                            </span>

                                            <span
                                                v-else-if="column.html"
                                                v-html="collect(row, column.field)"/>
                                        </slot>
                                    </td>
                                </slot>
                            </tr>

                            <!-- if group row header is at the bottom -->
                            <vgt-header-row
                                v-if="groupHeaderOnBottom"
                                :header-row="headerRow"
                                :columns="columns"
                                :line-numbers="lineNumbers"
                                :selectable="selectable"
                                :collect-formatted="collectFormatted"
                                :formatted-row="formattedRow"
                                :get-classes="getClasses"
                                :full-colspan="fullColspan">
                                <template
                                    v-if="hasHeaderRowTemplate"
                                    slot="table-header-row"
                                    slot-scope="props">
                                    <slot
                                        name="table-header-row"
                                        :column="props.column"
                                        :formattedRow="props.formattedRow"
                                        :row="props.row"/>
                                </template>
                            </vgt-header-row>
                        </tbody>

                        <tbody v-if="showEmptySlot">
                            <tr>
                                <td :colspan="fullColspan">
                                    <slot name="emptystate">
                                        <div
                                            class="my-4 text-center">
                                            <i
                                                class="far fa-folder-open text-disabled fa-3x mb-2"
                                                aria-hidden="true" />
                                            <div>{{ zeroStateMessage }}</div>
                                        </div>
                                    </slot>
                                </td>
                            </tr>
                        </tbody>
                    </template>

                    <tfoot v-if="hasFooterRowTemplate">
                        <slot name="table-footer-row"/>
                    </tfoot>
                </table>
            </div>

            <div class="d-flex justify-content-between align-items-center">
                <div>
                    <BulkActions
                        class="mt-3"
                        :all-selected-indeterminent="allSelectedIndeterminate"
                        :all-selected="allSelected"
                        @input="toggleSelectAll"
                        @select-all-on-page="selectAllInternal"
                        @unselect-all-on-page="unselectAllInternal"
                        @select-all-rows="selectAllInternal(false)"
                        @unselect-all-rows="unselectAllInternal(true)"
                        :selected-row-count="selectedRowCount"
                        :disable-select-info="disableSelectInfo"
                        :selection-info="selectionInfo"
                        :select-all-rows-enabled="enableSelectAllRows"
                        :select-all-by-page-enabled="enableSelectAllByPage"
                        v-if="selectable">
                        <slot name="selected-row-actions"/>
                    </BulkActions>

                </div>

                <div>
                    <span
                        v-if="isLoading || countLoading"
                        class="lazy-load-skeleton">
                        000
                    </span>
                    <span v-else>
                        {{ totalRows || totalRowCount }}
                    </span>
                    Records
                </div>
            </div>

            <div v-if="hasFooterSlot">
                <slot name="table-actions-bottom" />
            </div>

            <slot
                v-if="paginate && paginateOnBottom"
                name="pagination-bottom"
                :pageChanged="pageChanged"
                :perPageChanged="perPageChanged"
                :total="totalRows || totalRowCount">
                <vgt-pagination
                    ref="paginationBottom"
                    :per-page="perPage"
                    :rtl="rtl"
                    :total="totalRows || totalRowCount"
                    :mode="paginationMode"
                    :allow-per-page-select="paginationSelect"
                    :next-text="nextText"
                    :prev-text="prevText"
                    :rows-per-page-text="rowsPerPageText"
                    :custom-rows-per-page-dropdown="customRowsPerPageDropdown"
                    :paginate-dropdown-allow-all="paginateDropdownAllowAll"
                    :of-text="ofText"
                    :page-text="pageText"
                    :all-text="allText"
                    :meta="paginationMeta"
                    :count-loading="countLoading"
                    @page-changed="pageChanged"
                    @per-page-changed="perPageChanged">
                </vgt-pagination>
            </slot>
        </div>
    </div>
</template>

<script>
import _ from 'lodash'
import Checkbox from './Checkbox.vue'
import defaultType from './types/default'
import TaFilterDropdown from './TaFilterDropdown.vue'
import BulkActions from './BulkActions.vue'
import VgtPagination from './VgtPagination.vue'
import VgtGlobalSearch from './VgtGlobalSearch.vue'
import VgtTableHeader from './VgtTableHeader.vue'
import VgtHeaderRow from './VgtHeaderRow.vue'
import LoadingSpinner from './LoadingSpinner.vue'

// here we load each data type module.
import * as CoreDataTypes from './types/index'

const dataTypes = {}
const coreDataTypes = CoreDataTypes.default
_.forEach(Object.keys(coreDataTypes), key => {
    const compName = key.replace(/^\.\//, '').replace(/\.js/, '')
    dataTypes[compName] = coreDataTypes[key].default
})

export default {
    name: 'VueGoodTable',
    components: {
        BulkActions,
        Checkbox,
        LoadingSpinner,
        TaFilterDropdown,
        'vgt-pagination': VgtPagination,
        'vgt-global-search': VgtGlobalSearch,
        'vgt-header-row': VgtHeaderRow,
        'vgt-table-header': VgtTableHeader,
    },
    props: {
        isLoading: { default: null, type: Boolean },
        loadingType: {
            default: 'lazy',
            validator(value) {
                return [ 'lazy', 'spinner' ].includes(value);
            },
            type: String,
        },
        maxHeight: { default: null, type: String },
        fixedHeader: { default: false, type: Boolean },
        theme: { default: '' },
        mode: { default: 'local' }, // could be remote
        totalRows: {}, // required if mode = 'remote'
        cols: {},
        rows: {},
        requests: {
            type: Array,
            default: () => ([]),
        },
        lineNumbers: { default: false },
        responsive: { default: true },
        rtl: { default: false },
        className: {
            type: String,
            default: '',
        },
        zeroStateMessage: { type: String, default: 'No results found' },
        rowStyleClass: { default: null, type: [ Function, String ] },
        clickableRowOptions: { // rowProperty has to be root level (for now)
            type: Object, // { url: '/url', 'rowProperty': 'uuid', useVueRouter: bool }
            default: null,
        },
        isTransitioning: { default: false, type: Boolean },
        enableRowTransitions: { default: false, type: Boolean },
        enableSelectAllRows: { default: false, type: Boolean },
        enableSelectAllByPage: { default: true, type: Boolean },

        groupOptions: {
            default() {
                return {
                    enabled: false,
                }
            },
        },

        multiColumnFilter: {
            type: Object,
            default: () => ({
                enabled: false,
                columnSets: [], // [ ['column1', 'column2'], ] // the first field in each set will have
                // the filter icon in the header
            }),
        },

        selectOptions: {
            default() {
                return {
                    enabled: false,
                    selectionInfoClass: '',
                    selectionText: 'rows selected',
                    clearSelectionText: 'clear',
                    disableSelectInfo: false,
                    selectionIdentifier: '',
                    returnSelectedRowsRemoteData: false,
                    getAllSelectionItems: () => ([]),
                }
            },
        },

        selectedRowsRemote: {
            type: Array,
            default: () => ([]),
        },

        selectedRowsRemoteData: {
            type: Array,
            default: () => ([]),
        },

        // sort
        sortOptions: {
            default() {
                return {
                    enabled: true,
                    initialSortBy: {},
                }
            },
        },

        // pagination
        paginationOptions: {
            default() {
                return {
                    enabled: true,
                    mode: 'records',
                    perPage: 10,
                    position: 'bottom',
                    perPageDropdown: [ 10, 20, 50 ],
                    dropdownAllowAll: false,
                    setCurrentPage: 1,
                }
            },
        },

        searchOptions: {
            default() {
                return {
                    enabled: false,
                    trigger: null,
                    externalQuery: null,
                    searchFn: null,
                    placeholder: 'Search Table',
                }
            },
        },

        allowOverflow: {
            type: Boolean,
            default: false,
        },

        /**
         * This prop takes an object to set an initial filter for the table
         * Formatting looks like:
         * {
         *  field: $nameOfColumnField,
         *  value: $valueToFilterBy,
         * }
         */
        initialFilter: {
            type: Array,
            default: () => ([]),
        },

        /**
         * Indicates if the total count is still loading (for lazy-loaded counts)
         */
        countLoading: {
            type: Boolean,
            default: false,
        },
    },

    data: () => ({
        // loading state for remote mode
        tableLoading: false,

        // text options
        nextText: 'Next',
        prevText: 'Prev',
        rowsPerPageText: 'Rows per page',
        ofText: 'of',
        allText: 'All',
        pageText: 'page',

        // internal select options
        selectable: false,
        selectOnCheckboxOnly: false,
        selectAllByPage: true,
        disableSelectInfo: false,
        selectionInfoClass: '',
        selectionText: 'rows selected',
        clearSelectionText: 'clear',
        selectionIdentifier: '',
        returnSelectedRowsRemoteData: false,

        // internal sort options
        sortable: true,
        defaultSortBy: null,

        // internal search options
        searchEnabled: false,
        searchTrigger: null,
        externalSearchQuery: null,
        searchFn: null,
        searchPlaceholder: 'Search Table',
        searchSkipDiacritics: false,

        // internal pagination options
        perPage: null,
        paginate: false,
        paginateOnTop: false,
        paginateOnBottom: true,
        customRowsPerPageDropdown: [],
        paginateDropdownAllowAll: true,
        paginationMode: 'records',
        paginationMeta: {},
        paginationSelect: false,
        paginationParams: {},

        currentPage: 1,
        currentPerPage: 10,
        sorts: [],
        globalSearchTerm: '',
        filteredRows: [],
        columnFilters: {},
        registeredFilters: {},
        forceSearch: false,
        sortChanged: false,
        dataTypes: dataTypes || {},

        // ta filters
        taFilterOptions: [],
        filterIdCounter: 0,

        // lazy load requests
        requestResponses: {},
        requestIsLoading: {},
        requestIsErrored: {},

        // remote parameter options
        activeRemoteFilters: {},
        remoteSearchTerm: null,
        remotePage: null,
    }),

    computed: {
        hasPinnedActions() {
            return !!this.columns.find(col => col.pinned && col.field === 'actions')
        },
        hasFilterableColumns() {
            return !!this.columns.find(col => col.filterOptions && col.filterOptions.enabled)
        },
        columns() {
            return this.cols.map(item => {
                item.tdClass = this.formatColumnElement(item, 'td')
                item.thClass = this.formatColumnElement(item, 'th')

                return item
            }).filter(col => !col.hidden && col.field)
        },
        // formatted for the active filter bar in the UI
        activeFilters() {
            const activeFilters = []

            this.taFilterOptions.forEach(item => {
                item.options
                    .filter(opt => opt.active)
                    .forEach(opt => {
                        activeFilters.push(opt)
                    })
            })

            return activeFilters
        },
        // formats the active filters for the "filterRows" method
        formattedActiveFilters() {
            const activeFilters = {}

            this.taFilterOptions.forEach(item => {
                const opts = item.options
                    .filter(opt => opt.active)
                    .map(opt => opt.value)

                if (opts.length > 0) activeFilters[item.field] = opts
            })

            return activeFilters
        },
        hasFooterSlot() {
            return !!this.$slots['table-actions-bottom']
        },
        wrapperStyles() {
            return {
                overflow: 'scroll-y',
                maxHeight: this.maxHeight ? this.maxHeight : 'auto',
            }
        },

        hasHeaderRowTemplate() {
            return (
                !!this.$slots['table-header-row']
                || !!this.$scopedSlots['table-header-row']
            )
        },

        hasFooterRowTemplate() {
            return (
                !!this.$slots['table-footer-row']
                || !!this.$scopedSlots['table-footer-row']
            )
        },

        showEmptySlot() {
            if (!this.paginated.length) return true

            if (
                this.paginated[0].label === 'no groups'
                && !this.paginated[0].children.length
            ) {
                return true
            }

            return false
        },

        allSelected() {
            return (
                this.selectedRowCount > 0
                && ((this.selectAllByPage
                    && this.selectedPageRowsCount === this.totalPageRowCount)
                    || (!this.selectAllByPage
                        && this.selectedRowCount === this.totalRows))
            )
        },

        allSelectedIndeterminate() {
            return (
                !this.allSelected
                && ((this.selectAllByPage && this.selectedPageRowsCount > 0)
                    || (!this.selectAllByPage && this.selectedRowCount > 0))
            )
        },

        selectionInfo() {
            return `${this.selectedRowCount} ${this.selectionText}`
        },

        selectedRowCount() {
            return this.selectedRows.length
        },

        selectedPageRowsCount() {
            return this.selectedPageRows.length
        },

        selectedPageRows() {
            const selectedRows = []
            _.forEach(this.paginated, headerRow => {
                _.forEach(headerRow.children, row => {
                    if (row.vgtSelected) {
                        selectedRows.push(row)
                    }
                })
            })
            return selectedRows
        },

        selectedRows() {
            if (this.isRemoteMode && !this.selectAllByPage) {
                return _.cloneDeep(this.selectedRowsRemote)
            }

            const selectedRows = []
            _.forEach(this.processedRows, headerRow => {
                _.forEach(headerRow.children, row => {
                    if (row.vgtSelected) {
                        selectedRows.push(row)
                    }
                })
            })
            return selectedRows.sort((r1, r2) => r1.originalIndex - r2.originalIndex)
        },

        fullColspan() {
            let fullColspan = 0
            for (let i = 0; i < this.columns.length; i += 1) {
                if (!this.columns[i].hidden) {
                    fullColspan += 1
                }
            }
            if (this.lineNumbers) fullColspan++
            if (this.selectable) fullColspan++
            return fullColspan
        },
        groupHeaderOnTop() {
            if (
                this.groupOptions
                && this.groupOptions.enabled
                && this.groupOptions.headerPosition
                && this.groupOptions.headerPosition === 'bottom'
            ) {
                return false
            }
            if (this.groupOptions && this.groupOptions.enabled) return true

            // will only get here if groupOptions is false
            return false
        },
        groupHeaderOnBottom() {
            if (
                this.groupOptions
                && this.groupOptions.enabled
                && this.groupOptions.headerPosition
                && this.groupOptions.headerPosition === 'bottom'
            ) {
                return true
            }
            return false
        },
        totalRowCount() {
            let total = 0
            _.forEach(this.processedRows, headerRow => {
                total += headerRow.children ? headerRow.children.length : 0
            })
            return total
        },
        totalPageRowCount() {
            let total = 0
            _.forEach(this.paginated, headerRow => {
                total += headerRow.children ? headerRow.children.length : 0
            })
            return total
        },

        searchTerm() {
            return this.externalSearchQuery != null
                ? this.externalSearchQuery
                : this.globalSearchTerm
        },

        // this is done everytime sortColumn
        // or sort type changes
        //----------------------------------------
        processedRows() {
            // we only process rows when mode is local
            let computedRows = this.filteredRows

            if (this.isRemoteMode) {
                return computedRows
            }

            // take care of the global filter here also
            if (this.globalSearchAllowed()) {
                // here also we need to de-construct and then
                // re-construct the rows.
                const allRows = []
                _.forEach(this.filteredRows, headerRow => {
                    allRows.push(...headerRow.children)
                })
                const filteredRows = []
                _.forEach(allRows, row => {
                    // grab all subfields from the column objects
                    const subFields = _.flatten(this.columns
                        .filter(col => col.subFields)
                        .map(col => col.subFields))

                    // merge subfields and columns to create fields to search on
                    const filterableFields = this.columns.concat(subFields)

                    _.forEach(filterableFields, col => { //eslint-disable-line
                        // if col does not have search disabled,
                        if (!col.globalSearchDisabled) {
                            // if a search function is provided,
                            // use that for searching, otherwise,
                            // use the default search behavior
                            if (this.searchFn) {
                                const foundMatch = this.searchFn(
                                    row,
                                    col,
                                    this.collectFormatted(row, col),
                                    this.searchTerm,
                                )
                                if (foundMatch) {
                                    filteredRows.push(row)
                                    return false // break the loop
                                }
                            } else {
                                // comparison
                                const matched = defaultType.filterPredicate(
                                    this.collectFormatted(row, col),
                                    this.searchTerm,
                                    this.searchSkipDiacritics,
                                )
                                if (matched) {
                                    filteredRows.push(row)
                                    return false // break loop
                                }
                            }
                        }
                    })
                })

                // this is where we emit on search
                this.$emit('on-search', {
                    searchTerm: this.searchTerm,
                    rowCount: filteredRows.length,
                })

                // here we need to reconstruct the nested structure
                // of rows
                computedRows = []
                _.forEach(this.filteredRows, headerRow => {
                    const i = headerRow.vgt_header_id
                    const children = _.filter(filteredRows, [ 'vgt_id', i ])
                    if (children.length) {
                        const newHeaderRow = _.cloneDeep(headerRow)
                        newHeaderRow.children = children
                        computedRows.push(newHeaderRow)
                    }
                })
            }
            if (this.sorts.length) {
                //* we need to sort
                computedRows.forEach(cRows => {
                    cRows.children.sort((xRow, yRow) => {
                        //* we need to get column for each sort
                        let sortValue
                        for (let i = 0; i < this.sorts.length; i += 1) {
                            const column = this.getColumnForField(this.sorts[i].field)
                            const isNonRemoteAsync = column.async && column.async.enabled && this.mode !== 'remote'

                            const xvalue = isNonRemoteAsync
                                ? this.getAsyncContent(xRow, column)
                                : this.collect(xRow, this.sorts[i].field)

                            const yvalue = isNonRemoteAsync
                                ? this.getAsyncContent(yRow, column)
                                : this.collect(yRow, this.sorts[i].field)

                            //* if a custom sort function has been provided we use that
                            const { sortFn } = column
                            if (sortFn && typeof sortFn === 'function') {
                                sortValue = sortValue
                                    || sortFn(xvalue, yvalue, column, xRow, yRow)
                                        * (this.sorts[i].type === 'desc' ? -1 : 1)
                            } else {
                                //* else we use our own sort
                                sortValue = sortValue
                                    || column.typeDef.compare(xvalue, yvalue, column)
                                        * (this.sorts[i].type === 'desc' ? -1 : 1)
                            }
                        }
                        return sortValue
                    })
                })
            }

            // if the filtering is event based, we need to maintain filter
            // rows
            if (this.searchTrigger != null) {
                this.filteredRows = computedRows //eslint-disable-line
            }

            return computedRows
        },

        paginated() {
            if (!this.processedRows.length) return []

            if (this.isRemoteMode) {
                return this.processedRows
            }

            // for every group, extract the child rows
            // to cater to paging
            let paginatedRows = []
            _.forEach(this.processedRows, childRows => {
                paginatedRows.push(...childRows.children)
            })

            if (this.paginate) {
                let pageStart = (this.currentPage - 1) * this.currentPerPage

                // in case of filtering we might be on a page that is
                // not relevant anymore
                // also, if setting to all, current page will not be valid
                if (pageStart >= paginatedRows.length || this.currentPerPage === -1) {
                    this.currentPage = 1 //eslint-disable-line
                    pageStart = 0
                }

                // calculate page end now
                let pageEnd = paginatedRows.length + 1

                // if the setting is set to 'all'
                if (this.currentPerPage !== -1) {
                    pageEnd = this.currentPage * this.currentPerPage
                }

                paginatedRows = paginatedRows.slice(pageStart, pageEnd)
            }
            // reconstruct paginated rows here
            const reconstructedRows = []
            _.forEach(this.processedRows, headerRow => {
                const i = headerRow.vgt_header_id
                const children = _.filter(paginatedRows, [ 'vgt_id', i ])
                if (children.length) {
                    const newHeaderRow = _.cloneDeep(headerRow)
                    newHeaderRow.children = children
                    reconstructedRows.push(newHeaderRow)
                }
            })
            return reconstructedRows
        },

        originalRows() {
            const rows = _.cloneDeep(this.rows)
            let nestedRows = []
            if (!this.groupOptions.enabled) {
                nestedRows = this.handleGrouped([
                    {
                        label: 'no groups',
                        children: rows,
                    },
                ])
            } else {
                nestedRows = this.handleGrouped(rows)
            }
            // we need to preserve the original index of
            // rows so lets do that
            let index = 0
            _.forEach(nestedRows, headerRow => {
                _.forEach(headerRow.children, row => {
                    row.originalIndex = index++
                })
            })

            return nestedRows
        },

        typedColumns() {
            const columns = _.assign(this.columns, [])
            for (let i = 0; i < this.columns.length; i++) {
                const column = columns[i]
                column.typeDef = this.dataTypes[column.type] || defaultType
            }
            return columns
        },

        hasRowClickListener() {
            return this.$listeners && this.$listeners['on-row-click']
        },

        remoteParams() {
            return this.mode !== 'remote'
                ? {}
                : {
                    ...this.activeRemoteFilters,
                    ...this.paginationParams,
                    search: this.remoteSearchTerm,
                }
        },
        asyncRequest() {
            return this.columns
                .reduce((acc, itr) => [
                    ...acc,
                    itr,
                    ...(itr.subFields ? itr.subFields : []),
                ], [])
                .filter(col => col.async && col.async.enabled)
                .map(col => {
                    const requestName = col.async.request
                    return { requestName, ...this.getAsyncRequest(requestName) }
                })
        },
        isRemoteMode() {
            return this.mode === 'remote'
        },
        isRemoteModeWithoutSelectedRowsData() {
            return this.isRemoteMode && !this.returnSelectedRowsRemoteData
        },
        isRemoteModeWithSelectedRowsData() {
            return this.isRemoteMode && this.returnSelectedRowsRemoteData
        },
    },

    watch: {
        asyncRequest: {
            handler() {
                this.handleAsyncFields()
            },
            deep: true,
        },

        activeFilters: {
            handler(newVal, oldVal) {
                if (_.isEqual(oldVal, newVal)) return
                this.$emit('update:isLoading', true)
                this.filterRows(this.formattedActiveFilters, false)
            },
            deep: true,
            immediate: true,
        },

        rows: {
            handler() {
                this.$emit('update:isLoading', false)
                this.filterRows(this.formattedActiveFilters, false, true)

                if (this.rows.length > 0) {
                    this.cols.forEach(col => {
                        this.setFilterOptions(col.field)

                        if (col.subFields) {
                            col.subFields.forEach(subField => {
                                this.setFilterOptions(subField.field, true)
                            })
                        }
                    })

                    this.getAsyncRequests()
                }
            },
            deep: true,
            immediate: true,
        },

        globalSearchTerm: _.debounce(function(newSearch) { // eslint-disable-line
            // Only update the search if we're triggering on `keyup` instead instead of on `enter` or `button`
            // Also update the search if we've cleared out our search term
            if (this.searchTrigger == null || newSearch.length === 0) {
                this.remoteSearchTerm = newSearch
            }
        }, 350),

        remoteParams: {
            deep: true,
            handler(newParams, oldParams) {
                // Ensure the new params are not the same as the old params
                if (JSON.stringify(newParams) !== JSON.stringify(oldParams)) {
                    this.$emit('page-change', _.cloneDeep(newParams))

                    if (this.isRemoteMode && !this.selectAllByPage) {
                        this.filterSelectedRowsRemote()
                    }
                }
            },
        },

        selectOptions: {
            handler() {
                this.initializeSelect()
            },
            deep: true,
            immediate: true,
        },

        paginationOptions: {
            handler() {
                this.initializePagination()
            },
            deep: true,
            immediate: true,
        },

        searchOptions: {
            handler() {
                if (
                    this.searchOptions.externalQuery !== undefined
                    && this.searchOptions.externalQuery !== this.searchTerm
                ) {
                    //* we need to set searchTerm to externalQuery first.
                    this.externalSearchQuery = this.searchOptions.externalQuery
                    this.handleSearch()
                }
                this.initializeSearch()
            },
            deep: true,
            immediate: true,
        },

        sortOptions: {
            handler() {
                if (!this.isLoading) this.initializeSort()
            },
            deep: true,
        },

        selectedRows(newValue, oldValue) {
            if (!_.isEqual(newValue, oldValue)) {
                if (this.isRemoteModeWithSelectedRowsData) {
                    this.$emit(
                        'on-selected-rows-changed',
                        this.selectAllByPage ? this.selectedRows : this.selectedRowsRemoteData,
                    )
                } else {
                    this.$emit('on-selected-rows-change', {
                        selectedRows: this.selectedRows,
                    })
                }
            }
        },

        isLoading(newValue) {
            if (!newValue) this.initializeSort()
        },
    },

    mounted() {
        if (this.perPage) {
            this.currentPerPage = this.perPage
        }

        this.registerFilters()

        if (!this.isLoading) this.initializeSort()
    },

    methods: {
        // registers filters so we can track whether the filter dropdown is open or not
        registerFilters() {
            this.$set(this, 'registeredFilters', this.columns.reduce((acc, col) => {
                // add root level filters
                if (col.filterOptions && col.filterOptions.enabled) acc[col.field] = { isOpen: false }
                // add subField filters
                if (col.subFields) {
                    col.subFields.forEach(subField => {
                        if (subField.filterOptions
                            && subField.filterOptions.enabled) acc[subField.field] = { isOpen: false }
                    })
                }

                return acc
            }, {}))
        },
        setRequestOnRow(col, row) {
            if (!col.async || !col.async.enabled) return
            const requestName = col.async.request

            const result = this.getAsyncRequest(requestName)

            // If request is loading/errored lets continue to the next col
            if (!result.data || result.loading || result.errored) return

            // Get the response intersect for this col/row
            const responseIntersect = _.cloneDeep(this.getAsyncIntersect(
                result, requestName, row, col.async.multipleMatches,
            ))

            // Allows request response to be used when referencing the row
            // We only want to set this if its not already been set on this request
            if (!row[`request:${requestName}`]) {
                this.$set(
                    row,
                    `request:${requestName}`,
                    responseIntersect,
                )
            }
        },
        handleAsyncFields() {
            // Find each loaded/not errored request
            const requestsWithChanges = this.asyncRequest
                .filter(req => !req.errored && !req.loading)
                .map(req => req.requestName)

            // Nothing to do if we have no changes
            if (!requestsWithChanges.length) return

            this.columns
                // We also want to find the sub fields async content, so lets combine these
                .reduce((acc, itr) => [
                    ...acc,
                    itr,
                    ...(itr.subFields ? itr.subFields : []),
                ], [])
                // We only want columns that have async content
                .filter(col => col.async && col.async.enabled)
                .forEach(col => {
                    this.filteredRows.forEach(headerRow => {
                        headerRow.children.forEach(row => {
                            // The request we are getting the data for
                            const requestName = col.async.request

                            // If this request has no changes we dont care about it
                            if (!requestsWithChanges.includes(requestName)) return

                            this.setRequestOnRow(col, row)
                        })
                    })

                    this.setFilterOptions(col.field)
                })
        },
        displayAsyncField(field) {
            return field.async && field.async.enabled && field.async.fn
        },

        getAsyncIntersect(result, requestName, row, multiple = false) {
            const requestObject = this.requests.find(req => req.name === requestName)

            // If the user specified there could be multiple matches for this intersect
            if (multiple) {
                return result.data.filter(res => requestObject.params.reduce((acc, itr) => {
                    // If the row has an array of params we need to find all matches for each index
                    if (Array.isArray(row[itr.field])) {
                        return acc && (row[itr.field].includes(res[itr.responseIdentifier]))
                    }

                    // Otherwise we can just find one match here
                    // since this is a filter, there can still be multiple - but not multiple row params
                    return acc && (res[itr.responseIdentifier] === row[itr.field])
                }, true))
            }

            // The user didnt mention there would be multiple, so lets assume theres only 1 intersect
            return result.data.find(res => requestObject.params.reduce(
                (acc, itr) => acc && (res[itr.responseIdentifier] === row[itr.field]), true,
            ))
        },
        getAsyncRequest(requestName) {
            return {
                loading: this.requestIsLoading[requestName],
                errored: this.requestIsErrored[requestName],
                data: this.requestResponses[requestName],
            }
        },
        getAsyncContent(row, col) {
            const requestName = col.async.request
            const responseIntersect = row[`request:${requestName}`]

            if (!responseIntersect) return null
            const formattedResponse = col.async.fn(responseIntersect, row, col)

            if (![ 'string', 'number' ].includes(typeof (formattedResponse))) {
                return null
            }

            return String(formattedResponse)
        },
        getAsyncRequests() {
            this.requests.forEach(request => {
                // Set loading/error states
                this.$set(this.requestIsLoading, request.name, true)
                this.$set(this.requestIsErrored, request.name, false)

                let params = {}

                // Extend dynamic params
                request.params.forEach(param => {
                    params[param.identifier] = this.rows
                        .map(row => row[param.field])
                        .filter(rowParam => rowParam !== null)
                    // Reduce array of arrays
                    params[param.identifier] = params[param.identifier].reduce((acc, itr) => {
                        let changed = []

                        if (typeof itr === 'string') {
                            // Allows us to handle rows with idententifiers that are strings
                            changed = [ ...acc, itr ]
                        } else if (Array.isArray(itr)) {
                            // Allows us to handle rows with idententifiers that are arrays (presumably of strings)
                            changed = [ ...acc, ...itr ]
                        }

                        return changed
                    }, [])

                    // Remove all duplicates in this array (since it will be sent to an API)
                    params[param.identifier] = params[param.identifier].filter((v, i, a) => a.indexOf(v) === i);
                })

                // Extend static params
                if (request.staticParams) params = { ...params, ...request.staticParams }

                // Dispatch request with desired params
                request.request(params)
                    .then(({ data }) => { this.$set(this.requestResponses, request.name, data.data) })
                    .catch(() => { this.$set(this.requestIsErrored, request.name, true) })
                    .finally(() => { this.$set(this.requestIsLoading, request.name, false) })
            })
        },
        columnFilterIsActive(columnData) {
            if (this.taFilterOptions.length) {
                const column = this.taFilterOptions.find(col => col.field === columnData.field)
                return column && column.options.reduce((acc, cur) => (acc || cur.active), false)
            }

            return false
        },
        getSubFieldRowValue(row, subField) {
            const column = {
                label: subField.label,
                field: subField.field,
                type: subField.type,
                filterFn: subField.filterFn,
                formatFn: subField.formatFn,
            }

            return this.collectFormatted(row, column)
        },
        closeSubFieldFilters() {
            this.columns.forEach(item => {
                if (item.subFields) {
                    item.subFields.forEach(field => {
                        field.filterActive = false
                    })
                }
            })
        },
        multiColumnFilterCheck(column) {
            if (!this.multiColumnFilter.enabled
                || !this.multiColumnFilter.columnSets.find(colSet => colSet.includes(column.field))) return true
            return !!this.findMultiColumnFilterSet(column.field)
        },
        // only needs to look for the first item in the set because that is the header that has the filter icon
        findMultiColumnFilterSet(field) {
            return this.multiColumnFilter.columnSets.find(item => item[0] === field)
        },

        globalSearchAllowed() {
            if (
                this.searchEnabled
                && !!this.globalSearchTerm
                && this.searchTrigger == null
            ) {
                return true
            }

            if (this.externalSearchQuery != null && this.searchTrigger == null) {
                return true
            }

            if (this.forceSearch) {
                this.forceSearch = false
                return true
            }

            return false
        },
        // if field is flat nested ("obj.property"), will return the final property ("property")
        getFieldName(string) {
            if (typeof string !== 'string') {
                console.error('column property "field" is required and must be a string')
                return ''
            }
            if (string.indexOf('.') > -1) {
                const strings = string.split('.')
                return strings[strings.length - 1]
            }

            return string
        },

        // allows for ability to pass in a custom tdClass/thClass for a column
        formatColumnElement(item, type) {
            const suffix = type === 'td' ? 'cell' : 'header'
            const typeClass = `${type}Class`

            const classString = item[typeClass] || this.getFieldName(item.field)

            return `${this.className}__${classString} ${this.className}__${classString}--${suffix}`
        },

        getColumnSort(column) {
            for (let i = 0; i < this.sorts.length; i += 1) {
                if (this.sorts[i].field === column.field) {
                    return this.sorts[i].type || 'asc'
                }
            }
            return null
        },

        clearAllFilters() {
            this.taFilterOptions.forEach(filt => {
                filt.options.forEach(option => {
                    option.active = false
                })
            })
        },
        removeFilter(id) {
            this.taFilterOptions.forEach((filt, index) => {
                const foundIndex = filt.options.findIndex(opt => opt.id === id)

                if (foundIndex > -1) {
                    this.taFilterOptions[index].options[foundIndex].active = false
                }
            })
        },
        toggleFilter(field) {
            if (this.registeredFilters[field].isOpen === false) this.closeFilter()
            this.registeredFilters[field].isOpen = !this.registeredFilters[field].isOpen
        },
        closeFilter() {
            Object.keys(this.registeredFilters).forEach(field => {
                this.registeredFilters[field].isOpen = false
            })
        },
        getFilterOptionsByField(field) {
            const column = this.columns.find(col => col.field === field)

            // if column has defined custom filter options, use those
            if (column
                && column.filterOptions
                && column.filterOptions.customFilterOptions
                && column.filterOptions.customFilterOptions.length) {
                return column.filterOptions.customFilterOptions
            }

            if (this.mode !== 'remote'
                && column
                && column.async
                && column.async.enabled
                && this.requestResponses
                && this.requestResponses[column.async.request]) {
                const requestOptions = this.requestResponses[column.async.request]
                    .map(opt => column.async.fn(opt))

                return requestOptions
            }

            // otherwise pull them from the fields in the rows
            // also check if a row has an array of items, we can push each item separately to filter individually
            const options = []
            this.rows.map(item => _.get(item, field))
                .forEach(item => (Array.isArray(item)
                    ? item.forEach(subItem => options.push(subItem))
                    : options.push(item)))

            return options
        },
        getFilterOptions(field) {
            const filterOptions = this.taFilterOptions.find(opt => opt.field === field)

            return filterOptions
                ? filterOptions.options
                    .map(opt => opt.value)
                    .sort()
                    .map(opt => filterOptions.options.find(option => opt === option.value))
                : []
        },
        // ta filter options are all of the options by field. whatever is set here is what shows in the dropdown
        setTaFilterOptions({ field, options }) {
            const filterOptions = this.taFilterOptions.find(opt => opt.field === field)

            if (filterOptions) filterOptions.options = options
            else this.taFilterOptions.push({ field, options })
        },
        // grabs the options by field and pushes them to taFilterOptions
        setFilterOptions(field, isSubField = false) {
            const column = this.getColumnForField(field, isSubField)

            const type = column && column.filterOptions && column.filterOptions.type
                ? column.filterOptions.type
                : 'records'

            if (type === 'remote') {
                return
            }

            let multiColumnFilterSet = null
            let options = null

            if (this.multiColumnFilter.enabled) multiColumnFilterSet = this.findMultiColumnFilterSet(field)

            const filterListIndex = this.taFilterOptions.findIndex(option => option.field === field)

            options = multiColumnFilterSet
                ? multiColumnFilterSet.reduce((acc, cur) => acc.concat(this.getFilterOptionsByField(cur)), [])
                : this.getFilterOptionsByField(field)

            const optionObject = {
                field,
                options: _.uniq(options)
                    .map(item => {
                        const isActive = !!this.initialFilter.find(obj => obj.field === field && obj.value === item)

                        this.filterIdCounter++

                        return {
                            active: isActive,
                            value: item,
                            id: this.filterIdCounter,
                        }
                    })
                    .filter(item => item.value),
            }

            if (filterListIndex < 0) {
                this.taFilterOptions.push(optionObject)
            } else if (!this.taFilterOptions[filterListIndex].options.length) {
                this.taFilterOptions.splice(filterListIndex, 1, optionObject)
            }
        },
        getColumnForField(field, isSubField = false) {
            return isSubField
                ? this.typedColumns.reduce((acc, cur) => {
                    if (acc || !cur.subFields) return acc
                    return cur.subFields.find(sub => sub.field === field)
                }, null)
                : this.typedColumns.find(col => col.field === field)
        },

        handleSearch() {
            this.resetTable()

            this.$emit('on-search', {
                search: this.searchTerm,
            })
        },

        reset() {
            this.initializeSort()
            this.changePage(1)
            this.$refs['table-header-primary'].reset(true)
            if (this.$refs['table-header-secondary']) {
                this.$refs['table-header-secondary'].reset(true)
            }
        },

        emitSelectedRows(selected) {
            if (this.isRemoteMode && !this.selectAllByPage) {
                if (selected) {
                    this.selectOptions.getAllSelectionItems(this.remoteParams)
                        .then(({ data }) => {
                            this.$emit('on-selected-rows-changed', data.data)
                        })
                } else {
                    this.$emit('on-selected-rows-changed', [])
                }
            } else {
                this.$emit('on-select-all', {
                    selected: this.selectedRowCount === this.totalRowCount,
                    selectedRows: this.selectedRows,
                })
            }
        },

        unselectAllInternal(forceAll) {
            const rows = this.selectAllByPage && !forceAll ? this.paginated : this.filteredRows
            _.forEach(rows, headerRow => {
                _.forEach(headerRow.children, row => {
                    this.$set(row, 'vgtSelected', false)
                })
            })
            this.emitSelectedRows(false)
        },

        selectAllInternal(selectAllByPage = true) {
            this.$set(this, 'selectAllByPage', this.shouldSelectAllByPage(selectAllByPage))

            const rows = selectAllByPage ? this.paginated : this.filteredRows
            _.forEach(rows, headerRow => {
                _.forEach(headerRow.children, row => {
                    this.$set(row, 'vgtSelected', true)
                })
            })
            this.emitSelectedRows(true)
        },

        shouldSelectAllByPage(selectAllByPage) {
            if (this.isRemoteModeWithoutSelectedRowsData) {
                return false
            }

            return selectAllByPage
        },

        toggleSelectAll() {
            if (this.allSelected) {
                this.unselectAllInternal()
                return
            }

            this.selectAllInternal()
        },

        filterSelectedRowsRemote() {
            this.selectOptions.getAllSelectionItems(this.remoteParams)
                .then(({ data }) => {
                    const filteredSelection = this.selectedRowsRemote.filter(uuid => data.data.includes(uuid))

                    if (!_.isEqual(filteredSelection, this.selectedRowsRemote)) {
                        this.$emit('on-selected-rows-changed', filteredSelection)
                    }
                })
        },

        changePage(value) {
            if (this.paginationOptions.enabled) {
                let paginationWidget = this.$refs.paginationBottom
                if (this.paginationOptions.position === 'top') {
                    paginationWidget = this.$refs.paginationTop
                }
                if (paginationWidget) {
                    paginationWidget.currentPage = value
                    // we also need to set the currentPage
                    // for table.
                    this.currentPage = value
                }
            }
        },

        pageChangedEvent() {
            return {
                currentPage: this.currentPage,
                currentPerPage: this.currentPerPage,
                total: Math.floor(this.totalRowCount / this.currentPerPage),
            }
        },

        pageChanged(pagination = this.remotePage) {
            if (this.paginationMode === 'records') {
                this.currentPage = pagination.currentPage
                const pageChangedEvent = this.pageChangedEvent()
                pageChangedEvent.prevPage = pagination.prevPage
                this.$emit('on-page-change', pageChangedEvent)
                if (this.isRemoteMode) {
                    this.$emit('update:isLoading', true)
                }
            } else if (this.paginationMode === 'offset') {
                this.remotePage = pagination
                this.$emit('page-change', { ...this.remoteParams, page: pagination })
            } else if (this.paginationMode === 'cursor') {
                if (pagination !== 1) {
                    this.remotePage = pagination
                    this.$emit('page-change', { ...this.remoteParams, cursor: pagination })
                }
            }
        },

        perPageChanged(pagination) {
            this.currentPerPage = pagination.currentPerPage
            //* update perPage also
            const perPageChangedEvent = this.pageChangedEvent()
            this.$emit('on-per-page-change', perPageChangedEvent)

            if (this.paginationMode === 'offset') {
                this.$set(this.paginationParams, 'per_page', pagination.currentPerPage)
            }

            if (this.isRemoteMode) {
                this.$emit('update:isLoading', true)
            }
        },

        changeSort(sorts) {
            this.sorts = sorts
            this.$emit('on-sort-change', sorts)

            // every time we change sort we need to reset to page 1
            this.changePage(1)

            // if the mode is remote, we don't need to do anything
            // after this. just set table loading to true
            if (this.isRemoteMode) {
                this.$emit('update:isLoading', true)
                return
            }
            this.sortChanged = true
        },

        emitSelectedRowsRemote(row) {
            const index = this.selectedRowsRemote.indexOf(row[this.selectionIdentifier])
            const newSelection = _.cloneDeep(
                this.returnSelectedRowsRemoteData
                    ? this.selectedRowsRemoteData
                    : this.selectedRowsRemote,
            )

            // Add selected row
            if (index === -1 && !!row.vgtSelected) {
                const selection = this.returnSelectedRowsRemoteData ? row : row[this.selectionIdentifier]
                newSelection.push(selection)
            }

            // Remove unselected row
            if (index > -1 && !row.vgtSelected) {
                newSelection.splice(index, 1)
            }

            // Emit event only on changes
            if (newSelection.length !== this.selectedRowsRemote) {
                this.$emit('on-selected-rows-changed', newSelection)
            }
        },

        // checkbox click should always do the following
        onCheckboxClicked(row, index, event) {
            this.$set(row, 'vgtSelected', !row.vgtSelected)
            this.$emit('on-row-click', {
                row,
                pageIndex: index,
                selected: !!row.vgtSelected,
                event,
            })

            if (this.isRemoteMode && !this.selectAllByPage) {
                this.emitSelectedRowsRemote(row)
            }
        },

        onRowDoubleClicked(row, index, event) {
            this.$emit('on-row-dblclick', {
                row,
                pageIndex: index,
                selected: !!row.vgtSelected,
                event,
            })
        },

        onRowClicked(row, index, event) {
            if (this.selectable && !this.selectOnCheckboxOnly) {
                this.$set(row, 'vgtSelected', !row.vgtSelected)

                if (this.isRemoteMode && !this.selectAllByPage) {
                    this.emitSelectedRowsRemote(row)
                }
            }

            // clickable row routing
            if (this.clickableRowOptions) {
                const path = `${this.clickableRowOptions.url}${row[this.clickableRowOptions.rowProperty]}`

                if (this.clickableRowOptions.useVueRouter) this.$router.push({ path })
                else window.location = path
            }

            this.$emit('on-row-click', {
                row,
                pageIndex: index,
                selected: !!row.vgtSelected,
                event,
            })
        },

        onRowAuxClicked(row, index, event) {
            this.$emit('on-row-aux-click', {
                row,
                pageIndex: index,
                selected: !!row.vgtSelected,
                event,
            })
        },

        onCellClicked(row, column, rowIndex, event) {
            this.$emit('on-cell-click', {
                row,
                column,
                rowIndex,
                event,
            })
        },

        onMouseenter(row, index) {
            this.$emit('on-row-mouseenter', {
                row,
                pageIndex: index,
            })
        },

        onMouseleave(row, index) {
            this.$emit('on-row-mouseleave', {
                row,
                pageIndex: index,
            })
        },

        searchTableOnTrigger(details) {
            if (this.searchTrigger != null && details.trigger === this.searchTrigger) {
                this.handleSearch()
                this.remoteSearchTerm = this.searchTerm

                if (this.paginationMode === 'records') {
                    // we reset the filteredRows here because
                    // we want to search across everything.
                    this.filteredRows = _.cloneDeep(this.originalRows)
                    this.forceSearch = true
                    this.sortChanged = true
                }
            }
        },

        searchTableOnKeyUp() {
            if (this.searchTrigger == null) {
                this.handleSearch()
            }
        },

        resetTable() {
            this.unselectAllInternal(true)
            // every time we searchTable
            this.changePage(1)
        },

        // field can be:
        // 1. regular property - ex: 'prop'
        // 2. nested property path - ex: 'nested.prop'
        collect(obj, field) {
            if (typeof field === 'function') return field(obj)
            if (typeof field === 'string') return _.get(obj, field)
            return undefined
        },

        collectFormatted(obj, column, headerRow = false) {
            let value
            if (this.mode !== 'remote' && column.async && column.async.enabled) {
                value = this.getAsyncContent(obj, column)
            } else if (headerRow && column.headerField) {
                value = this.collect(obj, column.headerField)
            } else {
                value = this.collect(obj, column.field)
            }

            if (value === undefined) return ''

            // if user has supplied custom formatter,
            // use that here
            if (column.formatFn && typeof column.formatFn === 'function') {
                return column.formatFn(value)
            }

            // lets format the resultant data
            let type = column.typeDef
            // this will only happen if we try to collect formatted
            // before types have been initialized. for example: on
            // load when external query is specified.
            if (!type) {
                type = this.dataTypes[column.type] || defaultType
            }
            return type.format(value, column)
        },

        formattedRow(row, isHeaderRow = false) {
            const formattedRow = {}
            for (let i = 0; i < this.typedColumns.length; i++) {
                const col = this.typedColumns[i]
                // what happens if field is
                if (col.field) {
                    formattedRow[col.field] = this.collectFormatted(
                        row,
                        col,
                        isHeaderRow,
                    )
                }
            }
            return formattedRow
        },

        // Check if a column is sortable.
        isSortableColumn(column) {
            const { sortable } = column
            const isSortable = typeof sortable === 'boolean' ? sortable : this.sortable
            return isSortable
        },

        // Get classes for the given column index & element.
        getClasses(index, element, row) {
            const { [`${element}Class`]: custom } = this.typedColumns[index]

            const classes = {}

            // for td we need to check if value is
            // a function.
            if (typeof custom === 'function') {
                classes[custom(row)] = true
            } else if (typeof custom === 'string') {
                classes[custom] = true
            }
            return classes
        },
        formatSubFieldsForFilter() {
            return this.columns.reduce((acc, cur) => {
                if (cur.filterOptions && cur.filterOptions.enableSubFields) {
                    return acc.concat(cur.filterOptions.enableSubFields.map(field => {
                        const fieldData = cur.subFields.find(item => item.field === field)
                        if (!fieldData) return null

                        return {
                            ...fieldData,
                            field: fieldData.field,
                            label: fieldData.label,
                            filterOptions: {
                                ...fieldData.filterOptions || {},
                                enabled: true,
                            },
                            typeDef: this.dataTypes[fieldData.type] || defaultType,
                        }
                    }))
                }
                return acc
            }, []).filter(col => col)
        },

        // method to filter rows
        // NEW: custom filter methods need to accept an array as second argument
        filterRows(columnFilters, fromFilter = true, ignoreRemoteFilters = false) {
            // this is invoked either as a result of changing filters
            // or as a result of modifying rows.
            this.columnFilters = columnFilters
            let computedRows = _.cloneDeep(this.originalRows)

            // find any sub fields that are filterable and merge with column fields
            const filterableFields = this.typedColumns.concat(this.formatSubFieldsForFilter())
            const filteredColumns = Object.keys(this.columnFilters)

            if (this.mode !== 'remote' || fromFilter) this.changePage(1)

            if (this.columnFilters) {
                const remoteFilteredFields = this.getRemoteFilteredFields(filterableFields, filteredColumns)
                const localFilteredFields = this.getLocalFilteredFields(filterableFields, filteredColumns)

                if (!ignoreRemoteFilters) {
                    const activeRemoteFilters = this.getActiveRemoteFilters(remoteFilteredFields)

                    this.$emit('remote-filter-change', activeRemoteFilters)

                    this.activeRemoteFilters = activeRemoteFilters
                }

                if (localFilteredFields.length) {
                    // every time we filter rows, we need to set current page
                    // to 1
                    // if the mode is remote, we only need to reset, if this is
                    // being called from filter, not when rows are changing
                    for (let i = 0; i < filterableFields.length; i++) {
                        const col = filterableFields[i]

                        if (this.columnFilters[col.field]) {
                            computedRows = _.forEach(computedRows, headerRow => {
                                const newChildren = headerRow.children.filter(row => {
                                    // set the async requests on the row so we have the values to filter
                                    if (this.mode !== 'remote') this.setRequestOnRow(col, row)

                                    // if has a multicolumn set, fieldsToFilter will have more than one item
                                    const fieldsToFilter = this.multiColumnFilter.enabled
                                        && this.multiColumnFilter.columnSets.find(colSet => colSet.includes(col.field))
                                        ? this.findMultiColumnFilterSet(col.field)
                                        : [ col.field ]

                                    // If column has a custom filter, use that.
                                    if (col.filterOptions && typeof col.filterOptions.filterFn === 'function') {
                                        return fieldsToFilter.reduce((acc, cur) => {
                                            const filterBool = col.filterOptions.filterFn(
                                                this.collect(row, cur),
                                                this.columnFilters[col.field],
                                            )
                                            return filterBool || acc
                                        }, false)
                                    }

                                    return fieldsToFilter.reduce((acc, cur) => {
                                        const fieldData = col.async && col.async.enabled && this.mode !== 'remote'
                                            ? this.getAsyncContent(row, col)
                                            : this.collect(row, cur)

                                        const filterBool = this.defaultFilter(
                                            fieldData,
                                            this.columnFilters[col.field],
                                        )
                                        return filterBool || acc
                                    }, false)
                                })
                                // should we remove the header?
                                headerRow.children = newChildren
                            })
                        }
                    }
                }
            }

            // When using remote mode and allowing selection across pages
            // we need to select rows on when the rows change
            if (this.isRemoteMode && this.selectable && !this.selectAllByPage) {
                _.forEach(computedRows, headerRow => {
                    _.forEach(headerRow.children, row => {
                        if (this.selectedRows.includes(row[this.selectionIdentifier])) {
                            this.$set(row, 'vgtSelected', true)
                        }
                    })
                })
            }

            this.filteredRows = computedRows

            // Leave calling of handleAsyncFields to asyncRequest in remote mode
            // to prevent setRequestOnRow when request is not ready
            if (this.mode !== 'remote') {
                this.handleAsyncFields()
            }

            // we need to emit an event and that's that.
            // but this only needs to be invoked if filter is changing
            // not when row object is modified.
            if (fromFilter) {
                this.$emit('on-column-filter', {
                    columnFilters: this.columnFilters,
                    filteredRowCount: this.filteredRows[0].children.length,
                    totalRowCount: this.rows.length,
                })
            }
        },

        getRemoteFilteredFields(filterableFields, filteredColumns) {
            return filterableFields.filter(field => filteredColumns.includes(field.field)
                && (field.filterOptions.type && field.filterOptions.type === 'remote'))
        },

        getLocalFilteredFields(filterableFields, filteredColumns) {
            return filterableFields.filter(field => filteredColumns.includes(field.field)
                && (!field.filterOptions.type || field.filterOptions.type !== 'remote'))
        },

        getActiveRemoteFilters(remoteFilteredFields) {
            const activeRemoteFilters = {}

            remoteFilteredFields.forEach(field => {
                const filterOptions = this.getFilterOptions(field.field)
                const activeFilters = this.columnFilters[field.field]
                    .map(filter => filterOptions.find(opt => opt.value === filter))
                    .filter(filter => filter.active)
                    .map(filter => filter.id)

                activeRemoteFilters[field.filterOptions.outputFilterIdentifier] = activeFilters
            })

            return activeRemoteFilters
        },

        defaultFilter(data, filterStringArray) {
            const lowerCaseFilters = filterStringArray.map(item => item.toString().toLowerCase())
            const lowerCaseData = data ? data.toString().toLowerCase() : null

            return data && lowerCaseFilters.includes(lowerCaseData)
        },

        getCurrentIndex(index) {
            return ((this.currentPage - 1) * this.currentPerPage) + index + 1
        },

        getRowStyleClass(row) {
            let classes = ''
            if (this.hasRowClickListener) classes += 'clickable'
            let rowStyleClasses
            if (typeof this.rowStyleClass === 'function') {
                rowStyleClasses = this.rowStyleClass(row)
            } else {
                rowStyleClasses = this.rowStyleClass
            }
            if (rowStyleClasses) {
                classes += ` ${rowStyleClasses}`
            }
            return classes
        },

        getRowClassesObject(row) {
            const classes = {
                'good-table__row-clickable': this.clickableRowOptions,
                'good-table__row-selected': row.vgtSelected,
            }
            if (this.hasRowClickListener) {
                classes['good-table__row-clickable'] = true
                classes.clickable = true
            }

            if (typeof this.rowStyleClass === 'function') {
                classes[this.rowStyleClass(row)] = true
            } else {
                classes[this.rowStyleClass] = true
            }
            return classes
        },

        handleGrouped(originalRows) {
            _.forEach(originalRows, (headerRow, i) => {
                headerRow.vgt_header_id = i
                _.forEach(headerRow.children, childRow => {
                    childRow.vgt_id = i
                })
            })
            return originalRows
        },

        initializePagination() {
            const {
                enabled,
                perPage,
                position,
                perPageDropdown,
                dropdownAllowAll,
                nextLabel,
                prevLabel,
                rowsPerPageLabel,
                ofLabel,
                pageLabel,
                allLabel,
                setCurrentPage,
                mode,
                paginationMeta,
                allowPaginationSelect,
            } = this.paginationOptions

            this.paginationSelect = allowPaginationSelect

            if (typeof enabled === 'boolean') {
                this.paginate = enabled
            }

            if (typeof perPage === 'number') {
                this.perPage = perPage
            }

            if (position === 'top') {
                this.paginateOnTop = true // default is false
                this.paginateOnBottom = false // default is true
            } else if (position === 'both') {
                this.paginateOnTop = true
                this.paginateOnBottom = true
            }

            if (Array.isArray(perPageDropdown) && perPageDropdown.length) {
                this.customRowsPerPageDropdown = perPageDropdown
                if (!this.perPage) {
                    [ this.perPage ] = perPageDropdown
                }
            }

            if (typeof dropdownAllowAll === 'boolean') {
                this.paginateDropdownAllowAll = dropdownAllowAll
            }

            if (typeof mode === 'string') {
                this.paginationMode = mode
            }

            if (typeof paginationMeta === 'object') {
                this.paginationMeta = paginationMeta
            }

            if (typeof nextLabel === 'string') {
                this.nextText = nextLabel
            }

            if (typeof prevLabel === 'string') {
                this.prevText = prevLabel
            }

            if (typeof rowsPerPageLabel === 'string') {
                this.rowsPerPageText = rowsPerPageLabel
            }

            if (typeof ofLabel === 'string') {
                this.ofText = ofLabel
            }

            if (typeof pageLabel === 'string') {
                this.pageText = pageLabel
            }

            if (typeof allLabel === 'string') {
                this.allText = allLabel
            }

            if (typeof setCurrentPage === 'number') {
                setTimeout(() => {
                    this.changePage(setCurrentPage)
                }, 500)
            }
        },

        initializeSearch() {
            const {
                enabled,
                trigger,
                externalQuery,
                searchFn,
                placeholder,
                skipDiacritics,
            } = this.searchOptions

            if (typeof enabled === 'boolean') {
                this.searchEnabled = enabled
            }

            if (trigger != null) {
                this.searchTrigger = trigger
            }

            if (typeof externalQuery === 'string') {
                this.externalSearchQuery = externalQuery
            }

            if (typeof searchFn === 'function') {
                this.searchFn = searchFn
            }

            if (typeof placeholder === 'string') {
                this.searchPlaceholder = placeholder
            }

            if (typeof skipDiacritics === 'boolean') {
                this.searchSkipDiacritics = skipDiacritics
            }
        },

        initializeSort() {
            const { enabled, initialSortBy } = this.sortOptions

            if (typeof enabled === 'boolean') {
                this.sortable = enabled
            }

            //* initialSortBy can be an array or an object
            if (typeof initialSortBy === 'object') {
                const ref = this.fixedHeader
                    ? this.$refs['table-header-secondary']
                    : this.$refs['table-header-primary']
                if (Array.isArray(initialSortBy)) {
                    ref.setInitialSort(initialSortBy)
                } else {
                    const hasField = Object.prototype.hasOwnProperty.call(
                        initialSortBy,
                        'field',
                    )
                    if (hasField) ref.setInitialSort([ initialSortBy ])
                }
            }
        },

        initializeSelect() {
            const {
                enabled,
                selectionInfoClass,
                selectionText,
                clearSelectionText,
                selectOnCheckboxOnly,
                selectAllByPage,
                disableSelectInfo,
                selectionIdentifier,
                returnSelectedRowsRemoteData,
            } = this.selectOptions

            if (typeof enabled === 'boolean') {
                this.selectable = enabled
            }

            if (typeof selectOnCheckboxOnly === 'boolean') {
                this.selectOnCheckboxOnly = selectOnCheckboxOnly
            }

            if (typeof selectAllByPage === 'boolean') {
                this.selectAllByPage = selectAllByPage
            }

            if (typeof disableSelectInfo === 'boolean') {
                this.disableSelectInfo = disableSelectInfo
            }

            if (typeof selectionInfoClass === 'string') {
                this.selectionInfoClass = selectionInfoClass
            }

            if (typeof selectionText === 'string') {
                this.selectionText = selectionText
            }

            if (typeof clearSelectionText === 'string') {
                this.clearSelectionText = clearSelectionText
            }

            if (typeof selectionIdentifier === 'string') {
                this.selectionIdentifier = selectionIdentifier
            }

            if (typeof returnSelectedRowsRemoteData === 'boolean') {
                this.returnSelectedRowsRemoteData = returnSelectedRowsRemoteData
            }
        },
    },
}
</script>
