<script lang="ts">
  import filesize from "filesize";
  import { format, formatRelative, fromUnixTime, parseISO } from "date-fns";
  import Ellipsis from "../utils/Ellipsis.svelte";
  import UpObject from "../display/UpObject.svelte";
  import { createEventDispatcher } from "svelte";
  import type { AttributeChange, AttributeUpdate } from "../../types/base";
  import type { UpEntry, UpListing } from "upend";
  import IconButton from "../utils/IconButton.svelte";
  import Selector from "../utils/Selector.svelte";
  import type { IValue } from "upend/types";
  import Editable from "../utils/Editable.svelte";
  import { query } from "../../lib/entity";
  import { type Readable, readable } from "svelte/store";
  import { defaultEntitySort, entityValueSort } from "../../util/sort";
  import { attributeLabels } from "../../util/labels";
  import { formatDuration } from "../../util/fragments/time";
  import { i18n } from "../../i18n";
  import UpLink from "../display/UpLink.svelte";
  import type { UpType } from "src/lib/types";
  const dispatch = createEventDispatcher();

  export let columns: string | undefined = undefined;
  export let header = true;

  export let orderByValue = false;
  export let columnWidths: string[] = [];

  export let entries: UpEntry[];
  export let type: UpType | undefined = undefined;
  export let editable = false;

  // Display
  $: displayColumns = (columns || "attribute, value")
    .split(",")
    .map((c) => c.trim());

  const TIMESTAMP_COL = "timestamp";
  const PROVENANCE_COL = "provenance";
  const ENTITY_COL = "entity";
  const ATTR_COL = "attribute";
  const VALUE_COL = "value";

  // Editing
  let newEntryAttribute = "";
  let newEntryValue: IValue | undefined;

  async function addEntry() {
    dispatch("change", {
      type: "create",
      attribute: newEntryAttribute,
      value: newEntryValue,
    } as AttributeChange);
    newEntryAttribute = "";
    newEntryValue = undefined;
  }
  async function removeEntry(address: string) {
    if (confirm($i18n.t("Are you sure you want to remove the attribute?"))) {
      dispatch("change", { type: "delete", address } as AttributeChange);
    }
  }
  async function updateEntry(
    address: string,
    attribute: string,
    value: IValue
  ) {
    dispatch("change", {
      type: "update",
      address,
      attribute,
      value,
    } as AttributeUpdate);
  }

  // Labelling
  let labelListing: Readable<UpListing> = readable(undefined);
  $: {
    const addresses = [];
    entries
      .flatMap((e) =>
        e.value.t === "Address" ? [e.entity, e.value.c] : [e.entity]
      )
      .forEach((addr) => {
        if (!addresses.includes(addr)) {
          addresses.push(addr);
        }
      });

    const addressesString = addresses.map((addr) => `@${addr}`).join(" ");

    labelListing = query(`(matches (in ${addressesString}) "LBL" ? )`).result;
  }

  // Sorting
  let sortedEntries = entries;
  let resort = false;

  let sortKeys: { [key: string]: string[] } = {};
  function addSortKeys(key: string, vals: string[]) {
    if (!sortKeys[key]) {
      sortKeys[key] = [];
    }
    let changed = false;
    vals.forEach((val) => {
      if (!sortKeys[key].includes(val)) {
        changed = true;
        sortKeys[key].push(val);
      }
    });

    if (resort && changed) sortEntries();
  }

  function sortEntries() {
    sortedEntries = orderByValue
      ? entityValueSort(entries, Object.assign(sortKeys, $attributeLabels))
      : defaultEntitySort(entries, Object.assign(sortKeys, $attributeLabels));
  }

  $: {
    if ($labelListing) {
      entries.forEach((entry) => {
        addSortKeys(
          entry.entity,
          $labelListing.getObject(entry.entity).identify()
        );

        if (entry.value.t === "Address") {
          addSortKeys(
            entry.value.c,
            $labelListing.getObject(String(entry.value.c)).identify()
          );
        }
      });
      sortEntries();
      resort = true;
    }
  }

  entries.forEach((entry) => {
    addSortKeys(entry.entity, entry.listing.getObject(entry.entity).identify());
    if (entry.value.t === "Address") {
      addSortKeys(
        entry.value.c,
        entry.listing.getObject(String(entry.value.c)).identify()
      );
    }
  });

  // Formatting & Display
  const COLUMN_LABELS: { [key: string]: string } = {
    timestamp: $i18n.t("Added at"),
    provenance: $i18n.t("Provenance"),
    entity: $i18n.t("Entity"),
    attribute: $i18n.t("Attribute"),
    value: $i18n.t("Value"),
  };

  function formatValue(value: string | number, attribute: string): string {
    switch (attribute) {
      case "FILE_SIZE":
        return filesize(parseInt(String(value), 10), { base: 2 });
      case "ADDED":
      case "LAST_VISITED":
        return formatRelative(
          fromUnixTime(parseInt(String(value), 10)),
          new Date()
        );
      case "NUM_VISITED":
        return `${value} times`;
      case "MEDIA_DURATION":
        return formatDuration(parseInt(String(value), 10));
      default:
        return String(value);
    }
  }
</script>

<table>
  <colgroup>
    {#if editable}
      <col class="action-col" />
    {/if}
    {#each displayColumns as column, idx}
      {#if columnWidths.length}
        <col
          class="{column}-col"
          style="width: {columnWidths[idx] || 'unset'}"
        />
      {:else}
        <col class="{column}-col" />
      {/if}
    {/each}
  </colgroup>

  {#if header}
    <tr>
      {#if editable}
        <th />
      {/if}
      {#each displayColumns as column}
        <th>{COLUMN_LABELS[column] || $attributeLabels[column] || column}</th>
      {/each}
    </tr>
  {/if}

  {#each sortedEntries as entry (entry.address)}
    <tr>
      {#if editable}
        <td class="attr-action">
          <IconButton
            name="x-circle"
            on:click={() => removeEntry(entry.address)}
          />
        </td>
      {/if}
      {#each displayColumns as column}
        {#if column == TIMESTAMP_COL}
          <td title={entry.timestamp}
            >{formatRelative(parseISO(entry.timestamp), new Date())}</td
          >
        {:else if column == PROVENANCE_COL}
          <td>{entry.provenance}</td>
        {:else if column == ENTITY_COL}
          <td class="entity">
            <UpObject
              link
              labels={$labelListing
                ?.getObject(String(entry.entity))
                ?.identify() || []}
              address={entry.entity}
              on:resolved={(event) => {
                addSortKeys(entry.entity, event.detail);
              }}
            />
          </td>
        {:else if column == ATTR_COL}
          <td
            class:formatted={Boolean(
              Object.keys($attributeLabels).includes(entry.attribute)
            )}
          >
            <UpLink to={{ attribute: entry.attribute }}>
              <Ellipsis
                value={$attributeLabels[entry.attribute] || entry.attribute}
                title={$attributeLabels[entry.attribute]
                  ? `${$attributeLabels[entry.attribute]} (${entry.attribute})`
                  : entry.attribute}
              />
            </UpLink>
          </td>
        {:else if column == VALUE_COL}
          <td class="value">
            <Editable
              {editable}
              attribute={entry.attribute}
              value={entry.value}
              on:edit={(ev) =>
                updateEntry(entry.address, entry.attribute, ev.detail)}
            >
              {#if entry.value.t === "Address"}
                <UpObject
                  link
                  address={String(entry.value.c)}
                  labels={$labelListing
                    ?.getObject(String(entry.value.c))
                    ?.identify() || []}
                  on:resolved={(event) => {
                    addSortKeys(String(entry.value.c), event.detail);
                  }}
                />
              {:else}
                <div
                  class:formatted={Boolean(
                    formatValue(entry.value.c, entry.attribute)
                  )}
                >
                  <Ellipsis
                    value={formatValue(entry.value.c, entry.attribute) ||
                      String(entry.value.c)}
                  />
                </div>
              {/if}
            </Editable>
          </td>
        {:else}
          <td>?</td>
        {/if}
      {/each}
    </tr>
  {/each}

  {#if editable}
    <tr class="add-row">
      <td class="attr-action">
        <IconButton name="plus-circle" on:click={addEntry} />
      </td>
      {#if displayColumns.includes(ATTR_COL)}
        <td>
          <Selector
            type="attribute"
            bind:attribute={newEntryAttribute}
            attributeOptions={type?.attributes}
          />
        </td>
      {/if}
      {#if displayColumns.includes(VALUE_COL)}
        <td>
          <Selector type="value" bind:value={newEntryValue} />
        </td>
      {/if}
    </tr>
  {/if}
</table>

<style lang="scss" scoped>
  table {
    width: 100%;
    table-layout: fixed;

    border-spacing: 0.5em 0;

    th {
      text-align: left;
    }

    td {
      font-family: var(--monospace-font);
      line-break: anywhere;

      border-radius: 4px;
      padding: 2px;

      &.attr-action {
        max-width: 1em;
      }

      &.formatted,
      .formatted {
        font-family: var(--default-font);
      }
    }

    .action-col {
      width: 1.5em;
    }

    .attribute-col {
      width: 33%;
    }
  }
</style>
