<template>
  <div class="mc-data-table" :class="getClasses" :style="styleObject">
    <slot />

    <div class="mc-datatable__container">
      <div class="mc-data-table__body" :style="$attrs.style">
        <table aria-describedby="data" :class="getTableClasses">
          <thead>
            <tr v-if="!hideHeader">
              <th
                v-for="(header, index) in getHeaders"
                :key="`header-${index}`"
                :class="header.cssClass"
                scope="col"
              >
                <div class="header">
                  <slot
                    :name="`header.${header.dataFieldExpr}`"
                    :header="header"
                  >
                    {{ header.caption }}
                  </slot>
                  <div
                    v-if="sorting.mode !== 'none' && header.allowSorting"
                    class="header__sort"
                    :class="header.sortOrder"
                    @click="!loading && onSortClick({ e: $event, header })"
                  >
                    <m-icon
                      :name="'ArrowArrowTop16'"
                      :class="{ active: header.sortOrder === 'asc' }"
                    />
                    <m-icon
                      :name="'ArrowArrowBottom16'"
                      :class="{ active: header.sortOrder === 'desc' }"
                    />
                  </div>
                </div>
              </th>
            </tr>
          </thead>
          <tbody>
            <tr
              v-for="(item, rowIndex) in getSource"
              :key="item[dataKeyExpr]"
              :class="rowClasses(item)"
            >
              <td
                v-for="(header, index) in getHeaders"
                :key="`${index}-${getItemValue(item, dataKeyExpr)}-${
                  header.dataFieldExpr
                }`"
                :class="header.cssClass"
                @click="
                  allowRowClick && onRowClick({ event: $event, item: item })
                "
              >
                <slot
                  :name="`item.${header.dataFieldExpr}`"
                  :item="item"
                  :row-index="rowIndex"
                >
                  {{ getItemValue(item, header.dataFieldExpr) }}
                </slot>
              </td>
            </tr>
            <tr v-if="getSource.length == 0">
              <td :colspan="getHeaders.length">
                <slot name="no-data" />
              </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div
        v-if="pagingOptions.enabled && total != null"
        class="mc-data-table__footer"
      >
        <div class="mc-data-table__footer__item-per-page">
          <m-select
            :id="'itemPerPage'"
            v-model="getPageValue"
            :disabled="loading"
            :options="getPageSizes"
            size="s"
            @update:model-value="onPageSizeChanged"
          >
            <template #text="{ option }">
              <slot name="pager.text" :pager="option">
                {{ option.text }}
              </slot>
            </template>
          </m-select>
        </div>
        <div
          v-if="pagingOptions.displayTotal"
          class="mc-data-table__footer__display-total"
        >
          <span class="strong">{{ getTotalStringCurrentCount }}</span> /
          <span class="strong">{{ total }}</span>
        </div>
        <div class="mc-data-table__footer__pagination">
          <m-pagination
            :disabled="loading"
            :length="getPagingSize"
            :page-label="pagingOptions.text"
            :value="pagingOptions.index"
            @on-update-page="onUpdatePage"
          >
            <template #text="{ option }">
              <slot name="paging.text" :paging="option">
                {{ option.text }}
              </slot>
            </template>
          </m-pagination>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import MIcon from '../icon/MIcon.vue';
import MPagination from '../pagination/MPagination.vue';
import MSelect from '../select/MSelect.vue';

import {
  getObjectValueByPath,
  isPromise,
  deepEqual,
  orderedArraySorted,
  parseClasses,
} from './helpers';

/** Map headers with default values. */
function headersMapped(headers) {
  return headers.map((header) => ({
    ...Column.defaultOptions,
    ...header,
    sortFieldExpr: header.sortFieldExpr
      ? header.sortFieldExpr
      : header.dataFieldExpr,
    sortOrder: header.sortOrder ? header.sortOrder : null,
  }));
}

/** Generate headers if there is no headers defined. */
function autoGenerateHeaders(data, headers) {
  if (headers.length === 0 && data.length > 0) {
    const cols = Object.keys(data[0]).map((key) => ({
      caption: key,
      dataFieldExpr: key,
    }));
    headersMapped(cols).forEach((col) => headers.push(col));
  }
}

/** Build options to manage request. */
function buildOptions(
  pagingEnabled,
  pagerValue,
  pagingIndex,
  pagingSize,
  sortedColmuns,
) {
  const columnSorters = sortedColmuns.reduce(
    (acc, header) => ({
      ...acc,
      [header.sortFieldExpr ?? header.dataFieldExpr]: header.sortOrder,
    }),
    {},
  );

  const index = pagingIndex < pagingSize ? pagingIndex : pagingSize;

  return {
    sort: columnSorters,
    skip: pagingEnabled ? pagerValue * (index - 1) : null,
    take: pagingEnabled ? pagerValue : null,
  };
}

/** Caution : I have used object but we must speak about it to know if we use it or normalize field directly in MDataTable. */
const Pager = {
  defaultOptions: {
    sizes: [10, 20, 30],
    text: 'Show',
    value: 20,
  },
};

const Paging = {
  defaultOptions: {
    enabled: false,
    text: 'sur',
    index: 1,
  },
};

const Column = {
  defaultOptions: {
    caption: null,
    dataFieldExpr: null,
    sortFieldExpr: null,
    allowSorting: false,
    sortOrder: null,
  },
};

const Sorting = {
  defaultOptions: {
    /** Get or set sorting mode. Possible value : ['none', 'single', 'multiple']. */
    mode: 'multiple',
  },
};

/**
 * > The DataTable is a complex and multi-usage component. It provides a structure to display many items and edit them easily through some features.
 *
 * The `MDataTable` component is the **Vue.js** implementation of the **DataTable** component of Mozaic Design System.<br/>
 * The full specification of this component is available in [the associated documentation](https://mozaic.adeo.cloud/components/dataTable/).
 */
export default {
  name: 'MDataTable',
  components: { MIcon, MSelect, MPagination },
  props: {
    /** Get or set the name of identity. */
    dataKeyExpr: {
      type: String,
      require: true,
      default: 'id',
    },
    /**
     * Get or set the headers informations.
     * @type {{ caption: string, dataFieldExpr: string, sortFieldExpr: string, allowSorting: boolean, sortOrder: 'asc' | 'desc' | null }[] }
     */
    headers: {
      type: Array,
      default: () => [],
    },
    /** Get or set the source of thedata table. _The source can be an `Array`, a `Promise` or a `Function`._
     * If the source is a `Function` or a `Promise`, it's must be return or resolve an object like:
     * `{ data: Array,  total: Number }`. Where `data` is an Array of items & `total` the total count of items.
     */
    source: {
      type: [Array, Function, Promise],
      default: () => [],
    },
    /**
     * Get or set pager informations.
     * @type {{ sizes: number[], text: string, value: number }}
     */
    pager: {
      type: Object,
      default: () => ({}),
    },
    /**
     * Get or set paging informations.
     * @type {{ enabled: boolean, text: string, index: number }}
     */
    paging: {
      type: Object,
      default: () => ({}),
    },
    /**
     * Make rows clickable.
     * Get or set if row can clickable.
     */
    allowRowClick: {
      type: Boolean,
      default: false,
    },
    /**
     * Make headers fixed.
     * Get or set if the headers are fixed.
     */
    fixedHeader: {
      type: Boolean,
      default: false,
    },
    /**
     * Get or set the sorting informations.
     * @type {{ mode:'none' | 'single' | 'multiple'}}
     */
    sorting: {
      type: Object,
      default: () => ({}),
    },
    /**
     * Allows you to hide the headers.
     * Get or set if the headers are hide.
     * @deprecated
     * @ignore
     * // TODO: To remove in @next
     */
    hideHeader: {
      type: Boolean,
      default: false,
    },
    /**
     * An object allowing to insert classes on all the rows of the component.
     * @example { foo: true, bar: false, 'custom-class': true, }
     */
    itemClasses: {
      type: [String, Function, Object],
      default: '',
    },
    /**
     * Allows to set the height of rows.
     * @values s, m, l
     */
    size: {
      type: String,
      default: null,
      validator: (value) => ['s', 'l'].includes(value),
    },
    /**
     * Allows you to pass the total number of pages to the component. _(Should only be used if the `source` value is a `Function` or a `Promise`)_.
     */
    totalElements: {
      type: Number,
      default: null,
    },
    /**
     * Allows you to pass the z-index for the static header.
     */
    zIndex: {
      type: Number,
    },
  },
  emits: [
    'headers-changed',
    'page-changed',
    'data-changed',
    'row-click',
    'sort-order-changed',
    'page-size-changed',
    'update:headers',
  ],
  data() {
    return {
      headersMapped: headersMapped(this.headers),
      sourceMapped: [],
      pagerOptions: null,
      pagingOptions: null,
      sortingOptions: null,
      total: null,
      data: null,
      loading: false,
      created: false,
    };
  },
  computed: {
    getSource() {
      return this.sourceMapped;
    },
    getHeaders() {
      return this.headersMapped;
    },
    getSortHeader() {
      return this.getHeaders.filter((header) => header.sortOrder != null);
    },
    getClasses() {
      return {
        'mc-data-table--fixed-header': this.fixedHeader,
        'mc-data-table--s': this.size === 's',
        'mc-data-table--l': this.size === 'l',
      };
    },
    getTableClasses() {
      return {
        'no-data': this.getSource.length === 0,
      };
    },
    getPageSizes() {
      return this.pagerOptions.sizes.map((size) => ({
        value: size,
        text: `${this.pagerOptions.text} ${size}`,
      }));
    },
    getPageValue() {
      const { sizes, value } = this.pagerOptions;

      if (sizes.includes(value)) {
        return value;
      } else {
        return sizes[0];
      }
    },
    getPagingSize() {
      let size = 1;

      if (this.total) {
        size = Math.ceil(this.total / this.pagerOptions.value);
      } else {
        size = parseInt(this.pagingOptions.index);
      }

      return size;
    },
    getPagingIndex() {
      return this.pagingOptions.index < this.getPagingSize
        ? this.pagingOptions.index
        : this.getPagingSize;
    },
    getTotalStringCurrentCount() {
      const { skip, take } = buildOptions(
        this.pagingOptions.enabled,
        this.getPageValue,
        this.getPagingIndex,
        this.getPagingSize,
        this.getSortHeader,
      );

      const on = skip + take;
      return `${skip + 1} - ${on >= this.total ? this.total : on}`;
    },
    styleObject() {
      return {
        '--datatable-head-z-index': this.zIndex,
      };
    },
  },
  watch: {
    source: {
      immediate: true,
      async handler(newValue, oldValue) {
        if (deepEqual(newValue, oldValue) && !(newValue instanceof Function)) {
          return;
        }

        if (this.created) {
          await this.load();
        }
      },
    },
    headers: {
      deep: true,
      async handler(newValue) {
        this.headersMapped = headersMapped(newValue);

        /**
         * Triggered at each headers change.
         * - @event **headers-changed**
         */
        this.$emit('headers-changed', newValue);

        if (this.created) {
          await this.load();
        }
      },
    },
    pager: {
      immediate: true,
      handler(newValue, oldValue) {
        if (deepEqual(newValue, oldValue)) {
          return;
        }
        this.pagerOptions = {
          ...Pager.defaultOptions,
          ...this.pager,
        };
      },
    },
    paging: {
      deep: true,
      immediate: true,
      async handler(newValue, oldValue) {
        if (
          deepEqual(newValue, oldValue) &&
          !(this.source instanceof Function)
        ) {
          return;
        }

        this.pagingOptions = {
          ...Paging.defaultOptions,
          ...this.paging,
        };

        if (this.created) {
          await this.load();
        }
      },
    },
    sorting: {
      immediate: true,
      handler(newValue, oldValue) {
        if (deepEqual(newValue, oldValue)) {
          return;
        }

        this.sortingOptions = {
          ...Sorting.defaultOptions,
          ...this.sorting,
        };

        // Reset
        this.headersMapped.forEach((header) => (header.sortOrder = null));
      },
    },
  },
  mounted() {
    this.load();
    this.created = true;
  },
  methods: {
    async addHeader(header) {
      this.headersMapped = headersMapped(this.headersMapped.concat(header));

      this.$emit('headers-changed', header);
      this.$emit('update:headers', this.headersMapped);

      if (this.created) {
        await this.load();
      }
    },
    removeHeader(index) {
      this.headersMapped.splice(index, 1);
    },
    getItemValue(item, key) {
      return getObjectValueByPath(item, key);
    },
    rowClasses(item) {
      const getClasses = parseClasses(this.itemClasses, item);

      return {
        'mc-data-table__body__row--clickable': this.allowRowClick,
        ...getClasses,
      };
    },
    /** Load data. */
    async load() {
      this.loading = true;

      if (this.source == null) {
        return;
      }

      try {
        const options = buildOptions(
          this.pagingOptions.enabled,
          this.getPageValue,
          this.getPagingIndex,
          this.getPagingSize,
          this.getSortHeader,
        );

        if (Array.isArray(this.source)) {
          let data = this.source.slice();

          const sortedKeys = Object.keys(options.sort);

          if (sortedKeys.length > 0) {
            data = data.sort(
              orderedArraySorted(
                sortedKeys.map((key) => ({
                  fieldExpr: key,
                  sortOrder: options.sort[key],
                })),
              ),
            );
          }

          if (options.skip != null && options.take != null) {
            data = data.splice(options.skip, options.take);
          }

          this.data = data;

          this.total = this.source.length;
        } else if (this.source instanceof Function) {
          const result = this.source(options);

          const { data, total } = isPromise(result) ? await result : result;

          this.data = data;
          this.total = total;
        }

        if (this.data) {
          autoGenerateHeaders(this.data, this.headersMapped);
          this.sourceMapped = this.data;
        }

        /**
         * Triggered each time the data is updated.
         * - @event **data-changed**
         * - @arg {array} - an array of updated data
         */
        this.$emit('data-changed', this.sourceMapped);
      } finally {
        this.loading = false;
      }
    },
    async onUpdatePage(index) {
      this.pagingOptions.index = +index;

      await this.load();

      /**
       * Triggered at each page change.
       * - @event **page-changed**
       * - @arg {number} - the value of the new page
       */
      this.$emit('page-changed', index);
    },
    async onSortClick(e) {
      if (
        this.sortingOptions.mode === 'single' &&
        !this.getSortHeader.includes(e.header)
      ) {
        // Reinitialize sortOrder because there is only one sortable header
        this.headersMapped.forEach((header) => {
          header.sortOrder = null;
        });
      }

      switch (e.header.sortOrder) {
        case 'asc':
          e.header.sortOrder = 'desc';
          break;
        case 'desc':
          e.header.sortOrder = null;
          break;
        default:
          e.header.sortOrder = 'asc';
          break;
      }

      await this.load();

      /**
       * Triggered each time a sort button is clicked.
       * - @event **sort-order-changed**
       * - @arg {object} - an object containing all the information of the header concerned by the sorting action
       * - @value - { allowSorting: boolean, caption: string, cssClass: string, dataFieldExpr: string, sortFieldExpr: string, sortOrder: string }
       */
      this.$emit('sort-order-changed', e.header);
    },
    async onPageSizeChanged(value) {
      this.pagerOptions.value = +value;

      await this.load();

      /**
       * Triggered each time the value of the rows per page selector _(Pager)_ is updated.
       * - @event **page-size-changed**
       * - @arg {number} - the new value for the number of rows per page
       */
      this.$emit('page-size-changed', value);
    },

    onRowClick(e) {
      /**
       * Triggered each time a row is clicked.
       * - @event **row-click**
       * - @arg {object} - An object containing all the data of the clicked line. _(Need the prop `allowRowClick` to be `true`)_
       */
      this.$emit('row-click', e);
    },
  },
};
</script>

<style lang="scss">
/* stylelint-disable */
@import 'settings-tools/all-settings';

.mc-data-table {
  @include set-font-family;

  $parent: get-parent-selector(&);

  background-color: $color-datatable-container-background;
  box-sizing: border-box;

  *,
  ::after,
  ::before {
    box-sizing: inherit;
  }

  &__body {
    @include set-datatable-scrollbar();

    overflow-x: auto;
    overflow-y: hidden;

    table {
      @include set-datatable-base();

      & > thead,
      & > tbody {
        background-color: $color-grey-000;
      }

      thead {
        th,
        td {
          @include set-datatable-head-label();

          height: map-get($datatabke-default-celle-size, 'height');

          .header {
            align-items: center;
            display: flex;
            height: 100%;
            font-family: inherit;
            justify-content: space-between;
            width: 100%;

            &__sort {
              align-items: center;
              display: flex;
              flex-direction: column;
              height: $mu150;
              justify-content: space-around;
              margin-left: $mu050;
              width: $mu150;
              cursor: pointer;

              &::after,
              &::before {
                background-color: $color-datatable-sort-arrow-default;
                content: '';
                flex-shrink: 0;
                height: $mu050;
                width: $mu075;
              }

              &::before {
                clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
              }

              &::after {
                clip-path: polygon(0 0, 100% 0, 50% 100%);
              }

              &.asc {
                &::before {
                  background-color: $color-datatable-sort-arrow-active;
                }
              }

              &.desc {
                &::after {
                  background-color: $color-datatable-sort-arrow-active;
                }
              }

              & svg {
                display: none;
                opacity: 0;
              }
            }
          }
        }
      }

      tbody {
        tr {
          &:hover {
            background-color: $color-datatable-cell-background-hover;
          }

          &.mc-data-table__body__row--clickable {
            cursor: pointer;

            &:active {
              background-color: $color-datatable-cell-background-selected;
            }
          }

          &.row-selected {
            background-color: $color-datatable-cell-background-selected;
          }
        }

        th,
        td {
          @include set-font-scale('04', 'm'); // 14px / 18px

          color: $color-datatable-cell-font;
        }
      }

      tr {
        height: map-get($datatabke-default-celle-size, 'height');
      }

      th,
      td {
        border-bottom: get-border(s) solid $color-grey-300;
        text-align: left;
        vertical-align: middle;
        padding: 0 $mu100;
      }
    }
  }

  &--s {
    #{$parent} {
      &__body {
        table {
          th,
          tr {
            height: $height-datatable-cell-size-s;
          }
        }
      }
    }
  }

  &--l {
    #{$parent} {
      &__body {
        table {
          th,
          tr {
            height: $height-datatable-cell-size-l;
          }
        }
      }
    }
  }

  // sticky
  &--fixed-header {
    #{$parent} {
      &__body {
        table {
          > thead {
            @include set-box-shadow('l');

            top: 0;
            z-index: var(--datatable-head-z-index, 2);
            position: sticky;
          }

          > tbody {
            tr:last-child {
              th,
              td {
                border-bottom: transparent;
              }
            }
          }
        }
      }
    }
  }

  // manage scroll
  &[style*='height'] {
    .mc-data-table__body {
      overflow-y: auto;
    }
  }

  // footer
  &__footer {
    @include set-box-shadow('s');

    align-items: center;
    background-color: $color-grey-000;
    display: flex;
    padding: $mu075 $mu100;
    gap: $mu100;

    @media screen and (max-width: ($screen-m - 1)) {
      flex-direction: column;
    }

    &__item-per-page {
      flex-shrink: 0;
    }

    &__display-total-item {
      width: auto;
      min-width: 150px;
      padding: 0.5rem 2.25rem;
      font-size: 0.875rem;

      .strong {
        font-weight: bold;
      }
    }

    &__pagination {
      margin-left: auto;
    }
  }
}

.mc-datatable {
  &__container {
    @include set-border-radius();
    @include set-box-shadow('s');

    background-color: $color-datatable-container-background;
    overflow: hidden;
  }
}
/* stylelint-enable */
</style>
