<script lang='ts'>
  import { onMount } from 'svelte';
  import { Icon, ChevronRight, ChevronLeft, ChevronDoubleRight, ChevronDoubleLeft } from 'svelte-hero-icons';
  import Th from '$lib/components/data-table/Th.svelte';
  import Td from '$lib/components/data-table/Td.svelte';
  import type { Column } from '$lib/components/data-table/types';
  import { interpolateString as i } from '$lib/components/data-table/utils';
  import type { Endpoint, ItemCount } from '$lib/api/types';

  /**
   * The caption for the data table.
   */
  export let caption: string | null = null;
  /**
   * The columns for the data table.
   */
  export let columns: Column[];

  /**
   * The function which fetches rows for the data table.
   * Must support pagination and sorting.
   */
  export let getItemsEndpoint: Endpoint<any>;

  /**
   * The function which fetches the total number of items. Used for pagination.
   */
  export let getItemCountEndpoint: Endpoint<ItemCount>;

  /**
   * The name of the field by which table entries are sorted by default.
   */
  export let defaultSortField: string = columns[0].field;

  /**
   * The sort order by which table entries are sorted by default.
   */
  export let defaultSortOrder: 'asc' | 'desc' = 'asc';

  let viewportWidth: number = 0;
  let table: Element;
  let tableMaxWidth: number = 0;
  let compactView = false;
  function checkForTableResize() {
    if (!table) return;

    if (table.scrollWidth > tableMaxWidth) {
      tableMaxWidth = table.scrollWidth;
    }

    compactView = tableMaxWidth >= viewportWidth;
  }

  type SortEvent = {
    detail: {
      order: null | 'asc' | 'desc',
      field: string,
    }
  }
  let currentSortField: string = defaultSortField;
  let currentSortOrder: 'asc' | 'desc' = defaultSortOrder;

  let currentState: 'finished' | 'loading' | 'error' = 'finished';
  let errorMessage: string;

  let currentItems: any[] = []; // TODO apply generics here
  let totalItemCount: number | null = null;

  const itemsPerPageOptions = [10, 25, 50, 100];
  let currentItemsPerPage: number = itemsPerPageOptions[0];

  let currentPage: number = 0;
  let lastPage: number;
  $: lastPage = totalItemCount === null ? 0 : Math.floor(totalItemCount / currentItemsPerPage);
  $: currentItemsOffset = currentPage * currentItemsPerPage;

  // The <select> field value is not bound directly to 'currentItemsPerPage', because we need to know
  // its previous value to stay on the right page.
  let selectedItemsPerPage: number;
  async function handleItemsPerPageChange() {
    currentPage = Math.floor(currentItemsOffset / selectedItemsPerPage);
    currentItemsPerPage = selectedItemsPerPage;
    await updateTable();
  }

  function handleCurrentPageInputFocussed(this: HTMLInputElement) {
    this.select();
  }

  $: selectedCurrentPage = typeof currentPage === 'number' ? currentPage + 1 : 1;
  async function handleCurrentPageInputChanged() {
    if (selectedCurrentPage === currentPage + 1) {
      return;
    } else if (selectedCurrentPage < 1) {
      // TODO show error message
      console.warn(`Selected page number ${selectedCurrentPage} is smaller than 1.`);
      selectedCurrentPage = currentPage + 1;
      return;
    } else if (selectedCurrentPage > lastPage + 1) {
      // TODO show error message
      console.warn(`Selected page number ${selectedCurrentPage} is greater than ${lastPage + 1}.`);
      selectedCurrentPage = currentPage + 1;
      return;
    }

    currentPage = selectedCurrentPage - 1;
    await updateTable();
  }

  $: gotoPrevPageDisabled = currentState !== 'finished' || currentPage === 0 || lastPage === 0;
  async function handleGotoPrevPage() {
    if (gotoPrevPageDisabled) return;

    currentPage -= 1;
    await updateTable();
  }

  $: gotoNextPageDisabled = currentState !== 'finished' || currentPage === lastPage || lastPage === 0;
  async function handleGotoNextPage() {
    if (gotoNextPageDisabled) return;

    currentPage += 1;
    await updateTable();
  }

  $: gotoFirstPageDisabled = currentState !== 'finished' || currentPage === 0 || lastPage === 0;
  async function handleGotoFirstPage() {
    if (gotoFirstPageDisabled) return;

    currentPage = 0;
    await updateTable();
  }

  $: gotoLastPageDisabled = currentState !== 'finished' || currentPage === lastPage || lastPage === 0;
  async function handleGotoLastPage() {
    if (gotoLastPageDisabled) return;

    currentPage = lastPage;
    await updateTable();
  }

  async function updateTable() {
    try {
      currentState = 'loading';

      let itemCountResponse = await getItemCountEndpoint.call();
      totalItemCount = itemCountResponse.total;

      let itemsResponse = await getItemsEndpoint.call({
        queryParameters: {
          skip: `${currentItemsOffset}`,
          limit: `${currentItemsPerPage}`,
          sortby: `${currentSortField}`,
          sortorder: `${currentSortOrder}`,
        }
      });
      currentItems = itemsResponse;

      currentState = 'finished';
    } catch (error: any) {
      if (typeof error.body.message === 'string') {
        errorMessage = error.body.message;
      } else {
        errorMessage = "An unknown error occurred.";
      }
      console.log(error)
      currentState = 'error';
    }
  }

  async function handleSortEvent(sortEvent: SortEvent) {
    currentSortField = sortEvent.detail.field;
    currentSortOrder = sortEvent.detail.order === null ? 'asc' : sortEvent.detail.order;
    currentPage = 0;
    await updateTable();
  }

  onMount(async () => {
    checkForTableResize();
    await updateTable();
    checkForTableResize();
  });
</script>

<svelte:window bind:outerWidth={viewportWidth} on:resize={checkForTableResize} />

<div id="table-container"
  class="flex flex-col items-center gap-2"
  class:compact-view={compactView}
  class:extended-view={!compactView}
>
  <table
    bind:this={table}
  >
    {#if caption !== null}
      <caption>{caption}</caption>
    {/if}
  {#if compactView}
  <div class="table-controls m-2">
    <span>Sort by:</span>
    <select bind:value={currentSortField} on:change={updateTable} class="mr-2">
      {#each columns as column}
        <option selected={column.field === currentSortField} value={column.field}>{column.heading}</option>
      {/each}
    </select>
    <span>Order:</span>
    <select bind:value={currentSortOrder} on:change={updateTable}>
      <option selected={currentSortOrder === 'asc'} value={'asc'}>Ascending</option>
      <option selected={currentSortOrder === 'desc'} value={'desc'}>Descending</option>
    </select>
  </div>
  {/if}
    {#if !compactView}
      <thead>
        <tr>
          {#each columns as column}
            <Th
              on:sortEvent={handleSortEvent}
              field={column.field}
              heading={column.heading}
              order={currentSortField === column.field ? currentSortOrder : null}
              sortable={column.sortable ?? true}
            />
          {/each}
        </tr>
      </thead>
    {/if}
    <tbody
      class:compact-view={compactView}
      class:extended-view={!compactView}
    >
      {#if currentState === 'finished'}
        {#each currentItems as item}
          <tr>
            {#each columns as column}
              <Td
                fieldName={column.heading}
                data={item[column.field]}
                type={column.type ?? 'string'}
                isLink={column.isLink ?? false}
                linkTarget={i(column.linkTarget, item) ?? ''}
                compactView={compactView}
              />
            {/each}
          </tr>
        {/each}
        {:else if currentState === 'loading'}
        <tr>
          <td colspan={columns.length}>Loading...</td>
        </tr>
        {:else if currentState === 'error'}
          <tr>
            <td colspan={columns.length} class="text-center text-failure font-bold">{errorMessage}</td>
          </tr>
      {/if}
    </tbody>
  </table>
  <div class="table-controls">
    <label for="itemsPerPageSelector" class="text-lg">Items per page</label>
    <select id="itemsPerPageSelector" bind:value={selectedItemsPerPage} on:change={handleItemsPerPageChange} class="mr-2">
      {#each itemsPerPageOptions as option}
        <option selected={option === currentItemsPerPage} value={option}>{option}</option>
      {/each}
    </select>
    <span>Total: <b>{totalItemCount}</b></span>
  </div>
  <div class="table-controls">
    <button on:click={handleGotoFirstPage} disabled={gotoFirstPageDisabled} class="outlineButton">
      <Icon src={ChevronDoubleLeft} mini class="w-6 h-6" />
    </button>
    <button on:click={handleGotoPrevPage} disabled={gotoPrevPageDisabled} class="outlineButton">
      <Icon src={ChevronLeft} mini class="w-6 h-6" />
    </button>
    <label for="currentPageNumberInput" class="text-lg">Page</label>
    <input id="currentPageNumberInput"
      type=number bind:value={selectedCurrentPage} min=0 max={lastPage} disabled={lastPage === 0}
      on:change={handleCurrentPageInputChanged} on:focus={handleCurrentPageInputFocussed}
      class="text-center"
    >
    <span>of {lastPage + 1}</span>
    <button on:click={handleGotoNextPage} disabled={gotoNextPageDisabled} class="outlineButton">
      <Icon src={ChevronRight} mini class="w-6 h-6" />
    </button>
    <button on:click={handleGotoLastPage} disabled={gotoLastPageDisabled} class="outlineButton">
      <Icon src={ChevronDoubleRight} mini class="w-6 h-6" />
    </button>
  </div>
</div>

<style>
  option {
    @apply text-center;
  }

  #table-container.compact-view {
    @apply max-w-fit;
    @apply p-4;
  }
  #table-container.extended-view {
    @apply max-w-max;
  }
  .compact-view caption {
    @apply text-center;
  }
  .compact-view tr {
    @apply border border-primary;
  }

  div.table-controls {
    @apply flex gap-1 items-center justify-center;
  }
  .table-controls span {
    @apply text-lg text-secondary font-light;
  }
</style>
