<!-- @component `Table` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-text-elements-table--docs) -->
<script lang="ts">
  /** Import local helpers */
  import Block from '../Block/Block.svelte';
  import Pagination from './components/Pagination.svelte';
  import Select from './components/Select.svelte';
  import SortArrow from './components/SortArrow.svelte';
  import SearchInput from '../SearchInput/SearchInput.svelte';
  import { filterArray, paginateArray, getOptions } from './utils';

  // Types
  import type { TableData } from '../@types/global';

  interface Props {
    /** Data for the table as an array of objects. */
    data: TableData;

    /** A title that runs above the table. */
    title?: string;
    /** A block of text that runs above the table. */
    dek?: string;
    /** A footnote that runs below the table. */
    notes?: string;
    /** A source line that runs below the table. */
    source?: string;
    /** List of the fields to include in the table. By default everything goes. */
    includedFields?: string[];
    /** Whether or not the table is cutoff after a set number of rows. */
    truncated?: boolean;
    /** If the table is truncated, how many rows to allow before the cutoff. */
    truncateLength?: number;
    /** Whether or not the table is paginated after a set number of rows. */
    paginated?: boolean;
    /** The default page size. */
    pageSize?: number;
    /** Whether or not searches are allowed. */
    searchable?: boolean;
    /** The placeholder text that appears in the search box. */
    searchPlaceholder?: string;
    /**  A field to offer uses as an interactive filter. */
    filterField?: string;
    /** The label to place above the filter box. */
    filterLabel?: string;
    /** Whether or not sorts are allowed. */
    sortable?: boolean;
    /** The column to sort by. By default it's the first header. */
    sortField?: string;
    /** The columns that are allowed to sort. It's all of them by default. */
    sortableFields?: string[];
    /** The direction of the sort. By default it's ascending. */
    sortDirection?: 'ascending' | 'descending';
    /** Custom field formatting functions. Should be keyed to the name of the field. */
    fieldFormatters?: object;
    /** Width of the component within the text well. */
    width?: 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
    /** Add an ID to target with SCSS. */
    id?: string;
    /** Add a class to target with SCSS. */
    class?: string;
  }

  let {
    data,
    title,
    dek,
    notes,
    source,
    includedFields,
    truncated = false,
    truncateLength = 5,
    paginated = false,
    pageSize = 25,
    searchable = false,
    searchPlaceholder = 'Search in table',
    filterField,
    filterLabel,
    sortable = false,
    sortField,
    sortableFields,
    sortDirection = $bindable('ascending'),
    fieldFormatters = {},
    width = 'normal',
    id = '',
    class: cls = '',
  }: Props = $props();

  /** Derived variables */
  let includedFieldsDerived = $derived.by(() => {
    if (includedFields) return includedFields;
    if (data.length > 0)
      return Object.keys(data[0]).filter((f) => f !== 'searchStr');
    return [];
  });

  let sortableFieldsDerived = $derived.by(() => {
    if (sortableFields) return sortableFields;
    if (data.length > 0)
      return Object.keys(data[0]).filter((f) => f !== 'searchStr');
    return [];
  });

  let sortFieldDerived = $derived.by(() => {
    if (sortField) return sortField;
    if (data.length > 0) return Object.keys(data[0])[0];
    return '';
  });

  /** Set truncate, filtering and pagination configuration */
  let showAll = $state(false);
  let pageNumber = $state(1);
  let searchText = $state('');
  let filterList = $derived(
    filterField ? getOptions(data, filterField) : undefined
  );
  let filterValue = '';

  //* * Handle show all, search, filter, sort and pagination events */
  function toggleTruncate(_event) {
    showAll = !showAll;
  }

  function handleSearchInput(event) {
    searchText = event.detail.value;
    pageNumber = 1;
  }

  function handleFilterInput(event) {
    const value = event.detail.value;
    filterValue = value === 'All' ? '' : value;
    pageNumber = 1;
  }

  function handleSort(event) {
    if (!sortable) return;
    sortField = event.target.getAttribute('data-field');
    sortDirection = sortDirection === 'ascending' ? 'descending' : 'ascending';
  }

  function sortArray(array, column, direction) {
    if (!sortable) return array;
    return array.sort((a, b) => {
      if (a[column] < b[column]) {
        return direction === 'ascending' ? -1 : 1;
      } else if (a[column] > b[column]) {
        return direction === 'ascending' ? 1 : -1;
      } else {
        return 0;
      }
    });
  }

  function formatValue(item, field) {
    const value = item[field];
    if (field in fieldFormatters) {
      const func = fieldFormatters[field];
      return func(value);
    } else {
      return value;
    }
  }

  /** Set up the data pipeline */
  let filteredData = $derived.by(() =>
    filterArray(data, searchText, filterField, filterValue)
  );

  let sortedData = $derived.by(() =>
    sortArray(filteredData, sortField, sortDirection)
  );

  let currentPageData = $derived.by(() => {
    if (truncated) {
      return showAll ? sortedData : sortedData.slice(0, truncateLength + 1);
    }
    if (paginated) {
      return paginateArray(sortedData, pageSize, pageNumber);
    }
    return sortedData;
  });

  // $effect(() => {
  //   console.log('includedFieldsDerived', includedFieldsDerived);
  //   console.log('sortableFieldsDerived', sortableFieldsDerived);
  //   console.log('sortFieldDerived', sortFieldDerived);
  //   console.log('sortedData', sortedData);
  //   console.log('currentPageData', currentPageData);
  // });

  /** Add the `searchStr` field to data */
  let searchableData = $derived.by(() => {
    return data.map((d) => {
      return {
        ...d,
        searchStr: includedFieldsDerived
          .map((field) => d[field])
          .join(' ')
          .toLowerCase(),
      };
    });
  });
</script>

<Block {width} {id} class="fmy-6 {cls}">
  <div class="table-wrapper">
    {#if title || dek || searchable || filterList}
      <div class="table--header w-full">
        {#if title}
          <h3 class="table--header--title">{@html title}</h3>
        {/if}
        {#if dek}
          <p class="table--header--dek body-note">{@html dek}</p>
        {/if}
        {#if searchable || filterList}
          <nav class="input fmx-0 fmy-2">
            {#if filterList}
              <div class="table--header--filter">
                <Select
                  label={filterLabel || filterField}
                  options={filterList}
                  on:select={handleFilterInput}
                />
              </div>
            {/if}
            {#if searchable}
              <div class="table--header--search">
                <SearchInput
                  bind:searchPlaceholder
                  on:search={handleSearchInput}
                />
              </div>
            {/if}
          </nav>
        {/if}
      </div>
    {/if}
    <div class="table w-full">
      <table
        class="w-full"
        class:paginated
        class:truncated={truncated &&
          !showAll &&
          searchableData.length > truncateLength}
      >
        <thead class="table--thead">
          <tr>
            {#each includedFieldsDerived as field}
              <th
                scope="col"
                class="table--thead--th h4 pl-0 py-2 pr-2"
                class:sortable={sortable &&
                  sortableFieldsDerived.includes(field)}
                class:sort-ascending={sortable &&
                  sortField === field &&
                  sortDirection === 'ascending'}
                class:sort-descending={sortable &&
                  sortField === field &&
                  sortDirection === 'descending'}
                data-field={field}
                onclick={handleSort}
              >
                {field}
                {#if sortable && sortableFieldsDerived.includes(field)}
                  <div class="table--thead--sortarrow fml-1 avoid-clicks">
                    <SortArrow {sortDirection} active={sortField === field} />
                  </div>
                {/if}
              </th>
            {/each}
          </tr>
        </thead>
        <tbody class="table--tbody">
          {#each currentPageData as item, idx}
            <tr data-row-index={idx}>
              {#each includedFieldsDerived as field}
                <td
                  class="body-note pl-0 py-2 pr-2"
                  data-row-index={idx}
                  data-field={field}
                  data-value={item[field]}
                >
                  {@html formatValue(item, field)}
                </td>
              {/each}
            </tr>
          {/each}
          {#if searchable && searchText && currentPageData.length === 0}
            <tr>
              <td class="no-results" colspan={includedFieldsDerived.length}>
                No results found for "{searchText}"
              </td>
            </tr>
          {/if}
        </tbody>
        {#if notes || source}
          <tfoot class="table--tfoot">
            {#if notes}
              <tr>
                <td class="" colspan={includedFieldsDerived.length}>
                  <div class="fmt-2">
                    {@html notes}
                  </div>
                </td>
              </tr>
            {/if}
            {#if source}
              <tr>
                <td class="" colspan={includedFieldsDerived.length}>
                  <div class="fmt-1">
                    {@html source}
                  </div>
                </td>
              </tr>
            {/if}
          </tfoot>
        {/if}
      </table>
    </div>
    {#if truncated && searchableData.length > truncateLength}
      <nav
        aria-label="Show all button"
        class="show-all flex items-center justify-center fmt-2"
      >
        <button class="body-caption" onclick={toggleTruncate}>
          {#if showAll}
            Show fewer rows
          {:else}
            Show {searchableData.length - truncateLength}
            more rows
          {/if}
        </button>
      </nav>
    {/if}
    {#if paginated}
      <Pagination
        bind:pageNumber
        bind:pageSize
        bind:pageLength={currentPageData.length}
        bind:n={sortedData.length}
      />{/if}
  </div>
</Block>

<style lang="scss">
  @use '../../scss/mixins' as mixins;

  section.table {
    overflow-x: auto;
  }
  section.table table {
    background-color: transparent;
    border-collapse: separate;
    border-spacing: 0;
    thead {
      tr {
        th {
          border-bottom: 1px solid var(--theme-colour-text-primary);
          @include mixins.bg;
          text-align: inherit;
          &.sortable {
            cursor: pointer;
            white-space: nowrap;
          }
          .table--thead--sortarrow {
            display: inline-block;
            position: relative;
            top: 5px;
          }
        }
      }
    }
    tbody {
      td {
        @include mixins.text-sm;
        @include mixins.font-regular;
        vertical-align: top;
        border-bottom: 1px solid
          var(--theme-colour-brand-rules, var(--tr-muted-grey));
        &.no-results {
          @include mixins.text-secondary;
        }
      }
    }
    tfoot.table--tfoot {
      tr {
        border-bottom: 0;
      }
      td {
        @include mixins.body-caption;
      }
    }
    &.truncated {
      tbody tr:last-child:not(:first-child) {
        border-bottom: none;
        mask-image: linear-gradient(
          to bottom,
          var(--theme-colour-text-primary) 0%,
          transparent 100%
        );
        -webkit-mask-image: linear-gradient(
          to bottom,
          var(--theme-colour-text-primary) 0%,
          transparent 100%
        );
      }
    }
  }

  .avoid-clicks {
    pointer-events: none;
  }

  nav.input {
    padding: 0;
    width: 100%;
    display: flex;
    justify-content: flex-start;
    align-items: flex-end;
    flex-direction: row;
    flex-wrap: wrap;
    gap: clamp(0.31rem, calc(0.29rem + 0.1vw), 0.38rem);
  }

  nav.show-all {
    button {
      min-width: 13rem;
      height: 2.15rem;
      border: 1px solid var(--theme-colour-brand-rules, var(--tr-muted-grey));
      border-radius: 0.25rem;
      @include mixins.bg;
      cursor: pointer;
    }
  }
</style>
