<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, Endpoint } from '$lib/components/data-table/types';
  import { interpolateString as i } from '$lib/components/data-table/utils';

  /**
   * 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 = {
    callable: () => [],
    args: [],
  }
  /**
   * The function which fetches the total number of items. Used for pagination.
   */
  export let getItemCountEndpoint: Endpoint = {
    callable: () => 0,
    args: [],
  }

  type SortEvent = {
    detail: {
      order: null | 'asc' | 'desc',
      field: string,
    }
  }
  let currentSortField: string = columns[0].field;
  let currentSortOrder: 'asc' | 'desc' = 'asc';

  let currentState: 'finished' | 'loading' | 'error' = 'finished';
  let errorMessages: 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';
      totalItemCount = await getItemCountEndpoint.callable(
        ...getItemCountEndpoint.args,
      );
      currentItems = await getItemsEndpoint.callable(
        ...getItemsEndpoint.args,
        currentItemsOffset,
        currentItemsPerPage,
        currentSortField,
        currentSortOrder
      );
      currentState = 'finished';
    } catch (error: any) {
      if (!errorMessages.length) {
        // Make sure we have an error message to show to the user
        if (typeof error.message === 'string') {
          errorMessages.push(error.message);
        } else {
          errorMessages.push("An unknown error occurred.");
        }
      }

      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 () => {
    await updateTable();
  });
</script>

<table>
  {#if caption !== null}
    <caption>{caption}</caption>
  {/if}
  <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>
  <tbody>
    {#if currentState === 'finished'}
      {#each currentItems as item}
        <tr>
          {#each columns as column}
            <Td
              data={item[column.field]}
              type={column.type ?? 'string'}
              isLink={column.isLink ?? false}
              linkTarget={i(column.linkTarget, item) ?? ''}
            />
          {/each}
        </tr>
      {/each}
    {:else if currentState === 'loading'}
      <tr>
        <td colspan={columns.length}>Loading...</td>
      </tr>
    {:else if currentState === 'error'}
      {#each errorMessages as message}
        <tr>
          <td colspan={columns.length} class="text-center text-red-500">{message}</td>
        </tr>
      {/each}
    {/if}
  </tbody>
</table>
<button on:click={handleGotoFirstPage} disabled={gotoFirstPageDisabled}>
  <Icon src={ChevronDoubleLeft} mini class="w-6 h-6" />
</button>
<button on:click={handleGotoPrevPage} disabled={gotoPrevPageDisabled}>
  <Icon src={ChevronLeft} mini class="w-6 h-6" />
</button>
<input
  type=number bind:value={selectedCurrentPage} min=0 max={lastPage} disabled={lastPage === 0}
  on:change={handleCurrentPageInputChanged} on:focus={handleCurrentPageInputFocussed}
>
<span>of {lastPage + 1}</span>
<button on:click={handleGotoNextPage} disabled={gotoNextPageDisabled}>
  <Icon src={ChevronRight} mini class="w-6 h-6" />
</button>
<button on:click={handleGotoLastPage} disabled={gotoLastPageDisabled}>
  <Icon src={ChevronDoubleRight} mini class="w-6 h-6" />
</button>
<select bind:value={selectedItemsPerPage} on:change={handleItemsPerPageChange}>
  {#each itemsPerPageOptions as option}
    <option selected={option === currentItemsPerPage} value={option}>{option}</option>
  {/each}
</select>
