<script lang="ts" setup>
import {
  SelectMode,
  type Sort,
  SortValue,
  StatusType,
  type TableHeader,
  TableMode,
} from '@/models';
import _isEqual from 'lodash/isEqual';
import {
  type Component,
  computed,
  ModelRef,
  nextTick,
  onMounted,
  ref,
  watch,
} from 'vue';
import BmsLoader from '../feedback/BmsLoader.vue';
import { BmsTooltip } from '@/index';
import {
  ChevronDown,
  ChevronsUpDown,
  ChevronUp,
  HelpCircle,
} from 'lucide-vue-next';
import UiBmsInputCheckbox from '../form/UiBmsInputCheckbox.vue';
import BmsAlert from '../feedback/BmsAlert.vue';
import { enforceActionsColumnHeader, getHeaderClasses } from '@/helpers';
import UiBmsTableRow from './UiBmsTableRow.vue';

interface UiBmsTableProps {
  headers: TableHeader[];
  items: any[];
  mode?: TableMode;
  loading?: boolean;
  hasFilters?: boolean;
  sort?: Sort;
  selectable?: boolean;
  selectableDisabled?: boolean;
  selectedItems: unknown[];
  totalSize: number;
  maxSelectedSize?: number;
  selectMode?: SelectMode;
}

const props = withDefaults(defineProps<UiBmsTableProps>(), {
  mode: TableMode.NORMAL,
  hasFilters: false,
  sort: () => ({
    key: null,
    value: SortValue.default,
  }),
  selectable: false,
  selectableDisabled: false,
  maxSelectedSize: Infinity,
  selectMode: SelectMode.DEFAULT,
});

const selectedItems: ModelRef<unknown[]> = defineModel('selectedItems', {
  default: [] as unknown[],
});

const emits = defineEmits<{
  clickHeader: [header: TableHeader];
  selectAll: [];
  clearSelection: [];
}>();

// Pagination
const pagination = ref<HTMLInputElement | null>(null);
const isFocusOnPagination = () =>
  pagination.value?.contains(document.activeElement);

watch(
  () => props.items,
  async function keepFocusOnPaginationWhenUsed(newVal, oldVal) {
    if (oldVal.length > 0 && newVal.length > 0 && isFocusOnPagination()) {
      await nextTick();
      pagination.value?.focus();
    }
  },
  { deep: true },
);

// Headers style
const filteredHeaders = computed(() =>
  enforceActionsColumnHeader(props.headers),
);

const tableClass = computed(
  () => `bms-table__table bms-table__table--${props.mode}`,
);

const getSortComponent = (header: TableHeader): Component => {
  if (!props.sort.key || header.key !== props.sort.key) {
    return ChevronsUpDown;
  }
  switch (props.sort.value) {
    case SortValue.asc:
      return ChevronUp;
    case SortValue.desc:
      return ChevronDown;
    default:
      return ChevronsUpDown;
  }
};

// Selection
const isItemSelected = (item: unknown): boolean =>
  props.selectMode === SelectMode.ALL ||
  !!selectedItems.value.find((it) => _isEqual(item, it));

const onItemSelect = (item: unknown) => {
  if (props.selectMode === SelectMode.SINGLE) {
    selectedItems.value = [item];
  } else {
    if (isItemSelected(item)) {
      selectedItems.value = selectedItems.value.filter(
        (it) => !_isEqual(item, it),
      );
      areAllCurrentItemsSelected.value = false;
    } else {
      selectedItems.value.push(item);
    }
  }
};

const areAllCurrentItemsSelected = ref(props.selectMode === SelectMode.ALL);
const selectAll = () => {
  emits('selectAll');
  areAllCurrentItemsSelected.value = true;
};

const onToggleSelectAllCurrentItems = () => {
  // If we are in "all" mode, we untoggle all
  if (props.selectMode === SelectMode.ALL) {
    emits('clearSelection');
  }

  // If we have a selection spanning across more than the current page
  if (selectedItems.value.length > props.totalSize) {
    selectedItems.value = [] as unknown[];
  } else {
    // If only some are selected
    if (!areAllCurrentItemsSelected.value) {
      props.items.forEach((item) => {
        if (!isItemSelected(item)) {
          selectedItems.value.push(item);
        }
        if (item?.childElement && !isItemSelected(item?.childElement)) {
          selectedItems.value.push(item.childElement);
        }
      });
    } else {
      // If all the current page is selected
      selectedItems.value = [];
    }
  }
};

// Blob animation
const blob = ref<HTMLDivElement | null>(null);
const mainComponent = ref<HTMLDivElement | null>(null);

const onMouseMove = (e: MouseEvent) => {
  const rec = mainComponent.value?.getBoundingClientRect();
  const blobRec = blob.value?.getBoundingClientRect();

  if (rec && blobRec && blob.value) {
    if (
      e.clientX < rec.left ||
      e.clientX > rec.width + rec.left ||
      e.clientY < rec.top ||
      e.clientY > rec.height + rec.top
    ) {
      blob.value.style.visibility = 'hidden';
    } else {
      blob.value.style.visibility = 'visible';

      const minX = 15;
      const maxX = rec.width - blobRec.width * 1.1;
      const minY = 30;
      const maxY = rec.height - blobRec.height * 1.4;

      const targetY = e.clientY - rec.top - blobRec.height / 2;
      const y = Math.max(minY, Math.min(targetY, maxY));

      const targetX = e.clientX - rec.left - blobRec.width / 2;
      const x = Math.max(minX, Math.min(maxX, targetX));

      blob.value.style.transform = `translate(${x}px,${y}px)`;
    }
  }
};

window.addEventListener('mousemove', onMouseMove);

watch(
  () => props.items.length,
  () => {
    if (blob.value) blob.value.style.visibility = 'hidden';
  },
);
onMounted(() => {
  if (mainComponent.value && blob.value) {
    mainComponent.value.addEventListener('mouseleave', () => {
      if (blob.value) blob.value.style.visibility = 'hidden';
    });
    mainComponent.value.addEventListener('mouseenter', () => {
      if (blob.value) blob.value.style.visibility = 'visible';
    });
  }
});
</script>

<template>
  <div class="bms-table">
    <div
      class="bms-table__actions"
      :class="{ 'bms-table__actions--small': mode === TableMode.SMALL }"
    >
      <div class="bms-table__actions__saved-filters">
        <div class="bms-table__actions__custom">
          <slot name="custom-actions"></slot>
        </div>
        <slot name="saved-filters"></slot>
      </div>
      <div class="bms-table__actions__search">
        <slot name="search"></slot>
      </div>
    </div>

    <div v-if="hasFilters" class="bms-table__filters">
      <slot name="filters"></slot>
    </div>

    <BmsAlert
      v-if="
        selectMode === SelectMode.ALL ||
        (selectedItems.length > 0 && selectMode !== SelectMode.SINGLE)
      "
      :type="StatusType.Information"
      :dismissable="false"
      class="bms-table__selected"
    >
      <template #message>
        <template
          v-if="
            selectMode === SelectMode.ALL || selectedItems.length === totalSize
          "
        >
          Vous avez sélectionné la totalité des {{ totalSize }} éléments.
        </template>
        <span v-else>
          <template v-if="selectedItems.length === 1">
            <strong>1</strong> élément du tableau est sélectionné.
          </template>
          <template v-else>
            <strong>
              {{ selectedItems.length }} éléments du tableau sont sélectionnés.
            </strong>
          </template>
          <span
            class="select-mode-all"
            @click="selectAll"
            v-if="totalSize < maxSelectedSize"
          >
            Sélectionner la totalité des {{ totalSize }} éléments
          </span>
        </span>
      </template>
      <template #action>
        <slot name="alert-action"></slot>
      </template>
    </BmsAlert>
    <div class="selected-wrapper"></div>
    <div class="bms-table__table-wrapper">
      <Transition name="fade">
        <div v-if="loading" class="bms-table__loader">
          <BmsLoader />
        </div>
      </Transition>
      <div :class="`blob ${mode}`" ref="blob" v-if="items.length"></div>
      <table
        v-if="filteredHeaders.length"
        :class="tableClass"
        ref="mainComponent"
      >
        <thead ref="thead" class="bms-table__header">
          <tr class="bms-table__headers bms-table__row">
            <th v-if="selectable">
              <UiBmsInputCheckbox
                v-if="selectMode !== SelectMode.SINGLE"
                name="select-all"
                :disabled="items.length === 0 || selectableDisabled"
                @update:model-value="onToggleSelectAllCurrentItems"
                v-model="areAllCurrentItemsSelected"
              />
            </th>
            <th
              v-for="header in filteredHeaders"
              :style="{
                '--table-cell-width': header?.width || undefined,
              }"
              :class="getHeaderClasses(header, sort)"
              :key="header.label"
              @click="emits('clickHeader', header)"
            >
              <span class="header-content">
                {{ header.label }}
                <BmsTooltip
                  v-if="header.helperText"
                  :tooltipText="header.helperText"
                >
                  <span
                    class="header-content-helper"
                    @click.stop
                    @pointerdown.stop
                  >
                    <HelpCircle :size="18" />
                  </span>
                </BmsTooltip>
                <component
                  v-if="header.sortable"
                  :is="getSortComponent(header)"
                  :size="18"
                  class="header-content-sort"
                />
              </span>
            </th>
          </tr>
        </thead>
        <tbody class="bms-table__body">
          <template v-if="items.length">
            <template v-for="item in items" :key="item">
              <UiBmsTableRow
                :item="item"
                :selected-items="selectedItems"
                :selectable="selectable"
                :headers="filteredHeaders"
                :select-mode="selectMode"
                :selectable-disabled="selectableDisabled"
                :dense="mode === TableMode.DENSE || mode === TableMode.SMALL"
                @select="onItemSelect"
              >
                <template v-for="cell in headers" v-slot:[cell.key]="slotData">
                  <slot :name="cell.key" v-bind="slotData" />
                </template>
                <template #default="{ row }">
                  <slot name="default" :row="row"></slot>
                </template>
              </UiBmsTableRow>
              <template v-if="item?.childElement">
                <slot name="child-element">
                  <UiBmsTableRow
                    is-child-element
                    :item="item"
                    :selected-items="selectedItems"
                    :selectable="selectable"
                    :headers="filteredHeaders"
                    :select-mode="selectMode"
                    :selectable-disabled="selectableDisabled"
                    @select="onItemSelect"
                  >
                    <template
                      v-for="cell in headers"
                      v-slot:[cell.key]="slotData"
                    >
                      <slot :name="cell.key" v-bind="slotData" />
                    </template>
                  </UiBmsTableRow>
                </slot>
              </template>
            </template>
          </template>
          <template v-else>
            <tr class="bms-table__row">
              <td
                :colspan="
                  selectable
                    ? filteredHeaders.length + 1
                    : filteredHeaders.length
                "
                class="bms-table__cell bms-table__cell--empty"
              >
                <slot v-if="!props.loading" name="empty">Aucune donnée</slot>
              </td>
            </tr>
          </template>
        </tbody>
      </table>

      <div ref="pagination" class="bms-table__footer" tabindex="0">
        <slot name="pagination"></slot>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.bms-table {
  --table-cell-padding: 1em;
  --table-cell-radius: var(--bms-border-radius-large);
  --dynamic-border-height: 2px;
  --table-border: 1px solid var(--bms-border-color);

  &__loader {
    position: absolute;
    background: color-mix(in srgb, var(--bms-white) 72%, transparent);
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 3em;
    z-index: 10;
  }

  &__actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1em;

    &--small {
      margin-bottom: 0.5em;
    }

    &__saved-filters {
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 1em;
    }

    &__search {
      display: flex;
      align-items: center;
      gap: 1em;
    }

    &__custom {
      display: flex;
      gap: 1em;

      &:empty {
        display: none;
      }
    }
  }

  &__selected {
    margin-bottom: 1em;

    .select-mode-all {
      cursor: pointer;
      color: var(--bms-main-100);
    }
  }

  &__cell {
    &__checkbox {
      width: 4em;
    }
    &--action {
      display: flex;
      justify-content: end;
    }

    &--empty {
      height: 360px;
    }
  }

  &__table {
    width: 100%;
    border-spacing: 0 var(--dynamic-border-height);
    border-radius: var(--bms-border-radius-large);
    border: var(--table-border);
    position: relative;

    &-wrapper {
      position: relative;
    }

    thead {
      position: sticky;
      z-index: 2;
      top: var(--header-height, 0);
    }

    &--dense {
      --table-cell-padding: 0.5em 1em;
    }

    :deep(tr) {
      // Modification des border à cause d'un bug causé par le position:sticky
      // https://stackoverflow.com/questions/50361698/border-style-do-not-work-with-sticky-position-element
      // https://jira-sipa.ouest-france.fr/browse/SINBMS-1715

      td {
        padding: var(--table-cell-padding);
      }
    }

    th {
      --header-content-sort-icon-color: var(--bms-grey-25);
      --header-content-justify: start;

      width: var(--table-cell-width, auto);
      padding: var(--table-cell-padding);

      &.sortable:hover {
        --header-content-sort-icon-color: light-dark(
          var(--bms-grey-100),
          var(--bms-font-color)
        );
        cursor: pointer;
      }

      &.u-text-align-start {
        --header-content-justify: start;
      }

      &.u-text-align-center {
        --header-content-justify: center;
      }

      &.u-text-align-end {
        --header-content-justify: end;
      }

      &.sorted {
        --header-content-sort-icon-color: light-dark(
          var(--bms-grey-100),
          var(--bms-font-color)
        );
      }

      .header-content {
        display: flex;
        align-items: center;
        justify-content: var(--header-content-justify);
        gap: 0.5em;

        &-sort {
          flex-shrink: 0;
          color: var(--header-content-sort-icon-color);
        }

        &-helper {
          display: flex;
          align-items: center;
          flex-shrink: 0;
          color: light-dark(var(--bms-grey-100), var(--bms-font-color));
        }
      }
    }
  }

  &__footer {
    padding-top: 16px;
  }

  .blob {
    visibility: hidden;
    --table-blob-height: 80px;

    &.small,
    &.dense {
      --table-blob-height: 50px;
    }

    width: 200px;
    height: var(--table-blob-height);
    background-color: var(--bms-main-50);
    position: absolute;
    border-radius: 20%;
    z-index: -5;
    filter: blur(20px);
  }
}

:deep(table) {
  padding: 0 var(--dynamic-border-height);

  // Define border-radius for each corner
  th:first-child {
    border-top-left-radius: var(--table-cell-radius);
  }

  th:last-child {
    border-top-right-radius: var(--table-cell-radius);
  }

  tr:last-child td:first-child {
    border-bottom-left-radius: var(--table-cell-radius);
  }

  tr:last-child td:last-child {
    border-bottom-right-radius: var(--table-cell-radius);
  }

  th {
    background-color: var(--bms-white);
  }

  tbody {
    overflow: hidden;
    background: var(--bms-white);

    tr {
      position: relative;
    }
  }
}
</style>
