<template>
  <div class="vc-table__container">
    <div class="vc-table__header">
      <slot name="header">
        <div
          v-if="withPagination && perPageOptions.length"
          class="vc-table__perpage"
        >
          <VcDropdown
            v-model="pagination.perPage"
            :options="perPageOptions"
            readonly
          >
            <template #suffix-content>
              <span class="text-sm pr-1">{{ perPageMessage }}</span>
            </template>
          </VcDropdown>
        </div>
        <div
          v-if="$slots['header-actions']"
          class="vc-table__header-actions"
        >
          <slot name="header-actions" />
        </div>
      </slot>
    </div>
    <div
      :class="[{ 'shadow-table rounded-md': hasFooter }, wrapperClass]"
    >
      <div class="vc-table-wrapper">
        <table
          class="vc-table"
          :class="[
            hasFooter ? 'vc-table--with-footer' : 'vc-table--without-footer',
            { 'w-full': withPagination },
            { 'vc-table--small': compact },
            { 'vc-table--striped': stripe },
            tableClass
          ]"
        >
          <thead class="vc-table-header">
            <tr>
              <th
                v-if="showSelection"
                ref="headerChecked"
                class="selection-col relative"
              >
                <div class="selection-col-content">
                  <VcCheckbox
                    :checked="headerChecked"
                    :indeterminate="isIndeterminate"
                    hand-update
                    @changed="onChangeAll"
                  />
                  <div
                    v-if="!!$slots['checked-tooltip'] && localOptions.some((el) => el.selected)"
                    class="checked-tooltip"
                    :style="setPositionForTooltip"
                  >
                    <slot name="checked-tooltip" />
                  </div>
                </div>
              </th>
              <th
                v-if="expandableRow"
                class="expandable-td"
              />
              <th
                v-for="(column, index) in columns"
                :key="index"
                :style="setColumnStyles(column)"
              >
                <div
                  :class="`justify-${gettingFlexAlignment(column.columnAlignment)}`"
                  class="vc-table-slot"
                >
                  <slot
                    :name="column.columnItemKey"
                    :column="column"
                  >
                    <div
                      class="vc-table-column-header select-none"
                      @click.stop="handleSortClick(column)"
                    >
                      <i
                        :class="column.columnItemIcon"
                        class="vc-table-column-icon"
                      />
                      <span :class="column.columnClasses">
                        {{ gettingColumnItemData(column) }}
                      </span>
                      <span
                        v-if="isSortableColumn(column)"
                        class="header-sort-wrapper"
                      >
                        <i
                          :class="isActiveSortColumn(column, 'asc')"
                          class="sort-caret ascending"
                          @click.stop="handleSortClick(column, 'asc')"
                        />
                        <i
                          :class="isActiveSortColumn(column, 'desc')"
                          class="sort-caret descending"
                          @click.stop="handleSortClick(column, 'desc')"
                        />
                      </span>
                    </div>
                  </slot>
                </div>
              </th>
              <th
                v-if="showActionsColumn"
                :class="actionsColumnConfig.classes"
                :style="actionsColumnStyles"
              >
                <slot name="actions-column-header">
                  <div class="vc-table-slot justify-center">
                    <div class="vc-table-column-header">
                      <i
                        :class="actionsColumnConfig.icon"
                        class="vc-table-column-icon"
                      />
                      <span>
                        {{ actionsColumnConfig.columnName }}
                      </span>
                    </div>
                  </div>
                </slot>
              </th>
            </tr>
          </thead>
          <tbody class="vc-table-body">
            <template v-if="$slots['prepend-row']">
              <slot
                name="prepend-row"
                :columns="columns"
              />
            </template>
            <template
              v-for="(option, index) in visibleOptions"
              :key="`tr-${option}`"
            >
              <tr
                :class="[{
                  'selected': rowSelectable && (selectedRow === index)
                }, getRowClass(option, index)]"
                @click="onRowClick(option, index)"
              >
                <td
                  v-if="showSelection"
                  class="selection-col"
                >
                  <div class="selection-col-content">
                    <VcCheckbox
                      :checked="option.selected"
                      hand-update
                      @changed="onChangeRow(option)"
                    />
                  </div>
                </td>
                <td
                  v-if="expandableRow"
                  class="flex items-center justify-center expandable-td"
                >
                  <i
                    class="vc-icon-down icon-expandable p-1"
                    :class="{ 'icon-expandable-active': opened.includes(index) }"
                    @click="expandableAction(index)"
                  />
                </td>
                <td
                  v-for="(rowColumn, rowIndex) in columns"
                  :key="`column-${index}-${rowIndex}`"
                  :style="setColumnStyles(rowColumn)"
                >
                  <div
                    :class="`justify-${gettingFlexAlignment(rowColumn.rowItemAlignment)}`"
                    class="vc-table-slot"
                  >
                    <slot
                      :name="rowColumn.rowItemKeyKey"
                      :row="option"
                      :index="index"
                    >
                      {{ gettingRowItemData(option, rowColumn) }}
                    </slot>
                  </div>
                </td>
                <td
                  v-if="showActionsColumn"
                  :style="actionsColumnStyles"
                >
                  <slot
                    name="actions-column"
                    :row="option"
                    :index="index"
                  />
                </td>
              </tr>
              <tr
                v-if="expandableRow && opened.includes(index)"
              >
                <td :colspan="colspanNumber">
                  <slot
                    name="expanded-content"
                    :row="option"
                  />
                </td>
              </tr>
            </template>
            <slot
              v-if="!tableOptions.length"
              name="empty-data"
            >
              <tr class="empty-row">
                <td :colspan="columns.length + (+showActionsColumn)">
                  <div class="vc-table__empty-block">
                    {{ emptyDataText }}
                  </div>
                </td>
              </tr>
            </slot>
            <tr
              v-if="lazyLoadingData && !withPagination"
              v-show="!loading"
              ref="loadMoreTrigger"
              class="vc-table__load-trigger"
            >
              <td :colspan="columns.length" />
            </tr>
          </tbody>
        </table>
      </div>
      <div
        v-if="hasFooter"
        class="vc-table-footer"
      >
        <slot name="table-footer">
          <div
            v-if="isPaginationView"
            class="vc-table-pagination"
          >
            <VcPagination
              v-model="pagination.currentPage"
              :per-page="pagination.perPage"
              :total="pagination.total"
              :label="pagesLabelText"
            />
          </div>
        </slot>
      </div>
    </div>
    <VcLoading
      :active="loading"
      :loader="loaderType"
    />
  </div>
</template>

<script lang="ts">
/* eslint-disable @typescript-eslint/no-explicit-any */
import { defineComponent, PropType } from 'vue'
import {
  OptionType,
  Alignment,
  ColumnTypeItem,
  OptionObjType,
  ConfigType,
  PaginationProp,
  ActionsColumnType
} from './VcTable.types'
import VcCheckbox from '@/components/VcCheckbox/VcCheckbox.vue'
import cloneDeep from 'lodash/cloneDeep'
import { convertToUnit, resolveFieldData } from '@/utils/helpers'
import tablePropsMixin from '@/mixins/table/tableProps.mixin'
import VcLoading from '@/components/VcLoading/VcLoading.vue'
import { VcLoadingType } from '@/components/VcLoading/VcLoading.types'
import {
  DEFAULT_PAGINATION,
  DEFAULT_PER_PAGE_OPTIONS,
  SORT_TYPE
} from './tableOptions'

export default defineComponent({
  name: 'VcTable',
  components: {
    VcLoading,
    VcCheckbox
  },
  mixins: [tablePropsMixin],
  inheritAttrs: false,
  props: {
    withPagination: {
      type: Boolean,
      default: false
    },
    externalPaginate: {
      type: Object as PropType<PaginationProp>,
      default: undefined
    },
    perPageOptions: {
      type: Array as PropType<number[]>,
      default: DEFAULT_PER_PAGE_OPTIONS
    },
    perPageText: {
      type: String,
      default: ''
    },
    isServerData: {
      type: Boolean,
      default: false
    },
    pagesLabel: {
      type: String,
      default: ''
    },
    loading: {
      type: Boolean,
      default: false
    },
    loaderType: {
      type: String as PropType<VcLoadingType>,
      default: 'lds-spinner'
    },
    lazyLoadingData: {
      type: Boolean,
      default: false
    },
    compact: {
      type: Boolean,
      default: false
    },
    stripe: {
      type: Boolean,
      default: false
    },
    showActionsColumn: {
      type: Boolean,
      default: false
    },
    actionsColumn: {
      type: Object as PropType<ActionsColumnType>,
      default: () => ({
        width: '120px',
        columnName: 'Actions',
        icon: 'vc-icon-edit-pencil',
        minWidth: '100px',
        maxWidth: 'auto'
      })
    },
    rowClassName: {
      type: [String, Function],
      default: undefined
    }
  },
  emits: [
    'on-row-click',
    'handle-selection-change',
    'handle-sort',
    'update:externalPaginate',
    'update:currentPage',
    'load-more'
  ],
  data () {
    return {
      localOptions: [] as OptionType,
      observer: null as null | MutationObserver,
      dir: 'ltr' as string,
      rowOpened: false as boolean,
      opened: [] as number[],
      selectedRow: null as null | number,
      sortBy: '',
      sortOrder: '',
      customSortFunction: undefined as undefined | any,
      localPagination: { ...DEFAULT_PAGINATION },
      viewObserver: null as null | IntersectionObserver
    }
  },
  computed: {
    actionsColumnConfig () {
      return { ...this.actionsColumn }
    },
    actionsColumnStyles () {
      if (this.showActionsColumn) {
        const { width, minWidth, maxWidth } = this.actionsColumn
        return this.setColumnStyles({ width, minWidth, maxWidth })
      }
      return {}
    },
    hasFooter () {
      return !!(this.withPagination || this.$slots['table-footer'])
    },
    isPaginationView () {
      return !!(this.withPagination && this.tableOptions.length)
    },
    visibleOptions () {
      if (!this.withPagination || this.isServerData) {
        return this.tableOptions
      }
      return this.tableOptions.slice(this.start, this.end)
    },
    pagination () {
      if (this.externalPaginate) {
        return this.externalPaginate
      } else {
        return this.localPagination
      }
    },
    start () {
      return Math.max(0, (this.pagination.currentPage * this.pagination.perPage) - this.pagination.perPage)
    },
    end () {
      return this.start + this.pagination.perPage
    },
    setPositionForTooltip () {
      const element = this.$refs.headerChecked as HTMLElement
      const height = element.offsetHeight
      const align = this.dir === 'ltr' ? 'left' : 'right'
      const _checkBox = element.querySelector('.vc-form-check') as HTMLElement
      return {
        [align]: `${_checkBox.offsetLeft + _checkBox.offsetWidth + 4}px`,
        top: '50%',
        transform: 'translateY(-50%)',
        height: `${height}px`
      }
    },
    colspanNumber () {
      let numberOfColumnSpan = this.columns?.length
      if (this.expandableRow && this.showSelection) {
        numberOfColumnSpan++
      }
      if (this.expandableRow) {
        numberOfColumnSpan++
      }

      return numberOfColumnSpan
    },
    headerChecked: {
      get () {
        return this.localOptions.every(el => el.selected)
      },
      set (val: boolean) {
        return val
      }
    },
    isIndeterminate () {
      return this.localOptions.some(el => !el.selected) && this.localOptions.some(el => el.selected)
    },
    selectedOptions () {
      if (!this.showSelection) {
        return []
      }
      return this.localOptions.filter(i => i.selected)
    },
    tableOptions () {
      if (!this.sortBy || !this.sortOrder) {
        return this.localOptions
      } else {
        const _options = [...this.localOptions]
        if (!this.customSortFunction) {
          return _options.sort(this.defaultSortMethod)
        } else {
          return _options.sort((a, b) => {
            const val1 = this.sortOrder === 'desc' ? b : a
            const val2 = this.sortOrder === 'desc' ? a : b
            return this.customSortFunction(val1, val2)
          })
        }
      }
    },
    perPageMessage () {
      return this.perPageText ? this.perPageText : this.$tVU.t('table.pagination.per.page')
    },
    pagesLabelText () {
      return this.pagesLabel ? this.pagesLabel : this.$tVU.t('table.pagination.pages')
    }
  },
  watch: {
    options: {
      handler (val) {
        this.localOptions = cloneDeep(val)
        this.localPagination.total = this.externalPaginate ? this.externalPaginate.total : val.length
        this.opened = []
      },
      immediate: true
    },
    'pagination.currentPage' () {
      this.$emit('update:currentPage', { ...this.pagination })
    },
    'pagination.perPage' () {
      this.pagination.currentPage = 1
    }
  },
  mounted () {
    if (this.sortingOrderDefault) {
      this.sortBy = this.sortingOrderDefault.columnName
      this.sortOrder = this.sortingOrderDefault.order
    }
    if (this.showSelection) {
      this.localOptions = this.localOptions.map(el => {
        el.selected = false
        return el
      })
      const dir = document.documentElement.getAttribute('dir')
      if (dir) {
        this.dir = dir
      }

      this.initObserver()
    }
    if (this.lazyLoadingData && !this.withPagination) {
      if (this.$refs.loadMoreTrigger) {
        this.viewObserver = new IntersectionObserver(() => {
          this.$emit('load-more', true)
        })
        this.viewObserver.observe(this.$refs.loadMoreTrigger as Element)
      }
    }
  },
  beforeUnmount () {
    if (this.observer) {
      this.observer.disconnect()
    }
    if (this.viewObserver) {
      this.viewObserver.disconnect()
    }
  },
  methods: {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    defaultSortMethod (data1: any, data2: any) {
      const v1 = resolveFieldData(data1, this.sortBy)
      const v2 = resolveFieldData(data2, this.sortBy)
      let result = null as null | number
      if (v1 == null && v2 != null) {
        result = -1
      } else if (v1 != null && v2 == null) {
        result = 1
      } else if (v1 == null && v2 == null) {
        result = 0
      } else if (typeof v1 === 'string' && typeof v2 === 'string') {
        result = v1.localeCompare(v2, undefined, { numeric: true })
      } else {
        result = (v1 < v2) ? -1 : (v1 > v2) ? 1 : 0
      }
      return result * SORT_TYPE[this.sortOrder]
    },
    isActiveSortColumn (column: ColumnTypeItem, order: string): string | undefined {
      if (!this.isSortableColumn(column) || order !== this.sortOrder) {
        return ''
      }
      if (column.sortBy === this.sortBy) {
        return 'text-active-elements'
      }
    },
    isSortableColumn (column: ColumnTypeItem): boolean {
      return !!(column.sortBy || column.sortableFunction)
    },
    handleSortClick (column: ColumnTypeItem, order = '') {
      if (this.isSortableColumn(column)) {
        const oldSort = this.sortBy
        this.sortBy = column.sortBy || column.columnItemKey
        this.customSortFunction = column.sortableFunction
        if (order) {
          this.sortOrder = order
        } else {
          if (oldSort !== this.sortBy) {
            this.sortOrder = ''
          }
          this.toggleOrder()
        }
        this.$emit('handle-sort', {
          sortBy: this.sortBy,
          order: this.sortOrder
        })
      }
    },
    toggleOrder (): void {
      if (this.sortOrder === 'asc') {
        this.sortOrder = 'desc'
      } else if (this.sortOrder === 'desc') {
        this.sortOrder = ''
      } else {
        this.sortOrder = 'asc'
      }
    },
    onRowClick (option: OptionObjType, index: number): void {
      this.selectedRow = index
      if (this.rowUnselect) {
        this.selectedRow = this.selectedRow == null ? index : null
      }
      this.$emit('on-row-click', option, index)
    },
    gettingFlexAlignment (columnAlignment: Alignment): string {
      const alignments = {
        left: 'start',
        center: 'center',
        right: 'end'
      }

      return columnAlignment ? alignments[columnAlignment] : 'center'
    },
    setColumnStyles (column: ColumnTypeItem | ActionsColumnType): { [key: string]: string | undefined } {
      return {
        minWidth: convertToUnit(column.minWidth),
        maxWidth: convertToUnit(column.maxWidth),
        width: convertToUnit(column.width)
      }
    },
    gettingColumnItemData (column: ColumnTypeItem): string {
      return column.columnItemLabel
    },
    gettingRowItemData (option: OptionObjType, rowColumn: ColumnTypeItem): string {
      const labelKey = rowColumn.rowItemLabelKey ? rowColumn.rowItemLabelKey : rowColumn.columnItemLabel
      return option[labelKey] as string
    },
    setOptionChecked (option: OptionObjType, actionValue: boolean) {
      const optionUniqueKey = (this.config as ConfigType).optionUniqueKey as string
      const equalItem = this.localOptions.find((localOption: OptionObjType) => localOption[optionUniqueKey] === option[optionUniqueKey])
      if (equalItem) {
        equalItem.selected = actionValue
      }
    },
    setBulkOptionChecked (optionArray: OptionType, actionValue: boolean) {
      optionArray.forEach((option: OptionObjType) => {
        this.setOptionChecked(option, actionValue)
      })
    },
    onChangeAll (value: boolean) {
      this.localOptions.map(el => {
        el.selected = !value
        return el
      })
      this.onSelectionChange()
    },
    onChangeRow (option: any) {
      option.selected = !option.selected
      this.onSelectionChange()
    },
    onSelectionChange () {
      this.$emit('handle-selection-change', this.selectedOptions)
    },
    initObserver () {
      this.observer = new MutationObserver((mutationList) => {
        if (!mutationList || !Object.keys(mutationList).length) {
          return
        }
        if (mutationList[0].attributeName === 'dir') {
          const dir = document.documentElement.getAttribute('dir')
          if (dir) {
            this.dir = dir
          }
        }
      })

      const elementNode = document.documentElement

      this.observer.observe(elementNode,
        {
          attributes: true,
          childList: false,
          subtree: false
        }
      )
    },
    expandableAction (rowIndex: number) {
      const index = this.opened.indexOf(rowIndex)
      if (index > -1) {
        this.opened.splice(index, 1)
      } else {
        this.opened.push(rowIndex)
      }
    },
    getRowClass (row: any, index: number) {
      if (typeof this.rowClassName === 'string') {
        return this.rowClassName
      } else if (typeof this.rowClassName === 'function') {
        return this.rowClassName.call(null, {
          row,
          index
        })
      }
    }
  }
})
</script>

<style lang="scss" scoped>
.vc-table {
  @apply w-full;
  &--with-footer {
    @apply rounded-t-md;
    thead, tr {
      @apply rounded-t-md overflow-hidden;
    }
    th:first-child {
      @apply rounded-tl-md;
    }
    th:last-child {
      @apply rounded-tr-md;
    }
  }

  &--without-footer {
    @apply shadow-table rounded-md overflow-hidden;
  }

  &-slot {
    @apply flex items-center;
  }

  &-column-header {
    @apply text-table-headings flex items-center;
  }

  &__header {
    &:not(:empty) {
      @apply mb-3 w-full flex items-center;
    }
  }

  &__header-actions {
    @apply flex-shrink-0 flex-grow;
  }

  &__perpage {
    @apply flex-grow flex-shrink-0;
    max-width: 160px;
  }

  &-pagination {
    @apply py-3 px-4 border-t border-ui-lines bg-white;
  }
  &-footer {
    @apply rounded-b-md overflow-hidden;
  }

  .selection-col {
    @apply w-14;
    .selection-col-content {
      @apply flex justify-center items-center h-full w-full;
    }
    .vc-form-check {
      @apply py-0;
      min-height: 1rem;
    }
  }

  tr.empty-row {
    @apply bg-default-bg rounded-b-md;
    td {
      @apply rounded-b-md;
    }
  }

  &__empty-block {
    @apply w-full flex justify-center items-center text-default-text text-sm font-semibold;
  }

  &-column-icon {
    @apply text-lg;
  }

  &-column-icon, [dir='ltr'] &-column-icon {
    @apply mr-1 ml-0;
  }

  [dir='rtl'] &-column-icon {
    @apply mr-0 ml-1;
  }

  td:not(:last-child), th:not(:last-child) {
    @apply border-r border-ui-lines;
  }

  .header-sort-wrapper {
    @apply inline-flex flex-col items-center cursor-pointer relative align-middle text-inactive-elements w-6 h-4;
    overflow: initial;

    .sort-caret {
      @apply w-0 h-0 absolute;
      border: solid 5px transparent;
      left: 7px;

      &.ascending {
        border-bottom-color: currentColor;
        top: -5px;
      }

      &.descending {
        border-top-color: currentColor;
        bottom: -3px;
      }
    }
  }

  &__load-trigger {
    td {
      padding: 0 !important;
    }
  }

  th, td {
    @apply p-2;
  }

  .expandable-td {
    @apply h-full;
  }

  thead tr {
    @apply border-b border-ui-lines;
  }

  tr:not(:first-child) {
    @apply border-t border-ui-lines;
  }

  tr {
    @apply bg-white;
  }

  &.vc-table--small {
    @apply text-sm;
    .vc-table-column-icon {
      @apply text-base;
    }

    th, td {
      @apply py-1.5 px-1;
    }
  }

  &.vc-table--striped {
    tbody tr:nth-child(even) {
      background-color: #fafafa;
    }
  }

  tbody tr:hover {
    @apply relative shadow-table-row transition duration-300 ease-in-out;
  }

  .checked-tooltip {
    @apply absolute flex items-center px-2;
    background: linear-gradient(90deg, #FFFFFF 93.42%, rgba(255, 255, 255, 0) 100%);
  }

  .icon-expandable {
    @apply transition duration-300 ease-in-out;
  }

  .icon-expandable-active {
    @apply text-primary;
    transform: rotate(180deg);
  }

  .icon-expandable:hover {
    @apply transition duration-300 ease-in-out text-primary;
  }
}
</style>
<style lang="scss">
[dir="rtl"] {
  .vc-table {
    td:not(:last-child), th:not(:last-child) {
      @apply border-l border-r-0;
    }
  }
}
</style>
