<script lang="ts">
  import filesize from "filesize";
  import { 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 { ATTR_ADDED, ATTR_LABEL } from "upend/constants";
  const dispatch = createEventDispatcher();

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

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

  export let entries: UpEntry[];
  export let attributes: string[] | undefined = undefined;
  export let attributeOptions: string[] | undefined = undefined;

  // Display
  $: displayColumns = (columns || "entity, 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 adding = false;
  let addHover = false;
  let addFocus = false;
  let newAttrSelector: Selector;
  let newEntryAttribute = "";
  let newEntryValue: IValue | undefined;

  $: if (adding && newAttrSelector) newAttrSelector.focus();
  $: if (!addFocus && !addHover) adding = false;

  async function addEntry(attribute: string, value: IValue) {
    dispatch("change", {
      type: "create",
      attribute,
      value,
    } 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}) "${ATTR_LABEL}" ? )`,
    ).result;
  }

  // Sorting
  let sortedEntries = entries;

  let sortKeys: { [key: string]: string[] } = {};
  function addSortKeys(key: string, vals: string[], resort: boolean) {
    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(),
          false,
        );

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

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

  // Visibility
  let visible: Set<string> = new Set();
  let observer = new IntersectionObserver((intersections) => {
    intersections.forEach((intersection) => {
      const address = (intersection.target as HTMLElement).dataset["address"];
      if (!address) {
        console.warn("Intersected wrong element?");
        return;
      }

      if (intersection.isIntersecting) {
        visible.add(address);
      }
      visible = visible;
    });
  });

  function observe(node: HTMLElement) {
    observer.observe(node);

    return {
      destroy() {
        observer.unobserve(node);
      },
    };
  }

  // 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 ATTR_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);
    }
  }

  // Unused attributes
  let unusedAttributes = [];

  $: (async () => {
    unusedAttributes = await Promise.all(
      (attributes || []).filter(
        (attr) => !entries.some((entry) => entry.attribute === attr),
      ),
    );
  })();
</script>

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

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

    {#each sortedEntries as entry (entry.address)}
      <tr data-address={entry.address} use:observe>
        {#if visible.has(entry.address)}
          {#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 mark-entity">
                <UpObject
                  link
                  labels={$labelListing
                    ?.getObject(String(entry.entity))
                    ?.identify() || []}
                  address={entry.entity}
                  on:resolved={(event) => {
                    addSortKeys(entry.entity, event.detail, true);
                  }}
                />
              </td>
            {:else if column == ATTR_COL}
              <td
                class:formatted={Boolean(
                  Object.keys($attributeLabels).includes(entry.attribute),
                )}
                class="mark-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 mark-value">
                <Editable
                  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, true);
                      }}
                    />
                  {: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}
          <td class="attr-action">
            <IconButton
              subdued
              name="x-circle"
              on:click={() => removeEntry(entry.address)}
            />
          </td>
        {:else}
          <tr>
            <td colspan="99">
              <div class="skeleton" style="text-align: center">...</div>
            </td>
          </tr>
        {/if}
      </tr>
    {/each}

    {#each unusedAttributes as attribute}
      <tr>
        {#each displayColumns as column}
          {#if column == ATTR_COL}
            <td
              class:formatted={Boolean(
                Object.keys($attributeLabels).includes(attribute),
              )}
              class="mark-attribute"
            >
              <UpLink to={{ attribute }}>
                <Ellipsis
                  value={$attributeLabels[attribute] || attribute}
                  title={$attributeLabels[attribute]
                    ? `${$attributeLabels[attribute]} (${attribute})`
                    : attribute}
                />
              </UpLink>
            </td>
          {:else if column == VALUE_COL}
            <td>
              <Editable on:edit={(ev) => addEntry(attribute, ev.detail)}>
                <span class="unset">{$i18n.t("(unset)")}</span>
              </Editable>
            </td>
          {:else}
            <td></td>
          {/if}
        {/each}
        <td class="attr-action"></td>
      </tr>
    {/each}

    {#if !attributes?.length}
      {#if adding}
        <tr
          class="add-row"
          on:mouseenter={() => (addHover = true)}
          on:mouseleave={() => (addHover = false)}
        >
          {#each displayColumns as column}
            {#if column == ATTR_COL}
              <td>
                <Selector
                  type="attribute"
                  bind:attribute={newEntryAttribute}
                  {attributeOptions}
                  on:focus={(ev) => (addFocus = ev.detail)}
                  bind:this={newAttrSelector}
                />
              </td>
            {:else if column === VALUE_COL}
              <td>
                <Selector
                  type="value"
                  bind:value={newEntryValue}
                  on:focus={(ev) => (addFocus = ev.detail)}
                />
              </td>
            {:else}
              <td></td>
            {/if}
          {/each}
          <td class="attr-action">
            <IconButton
              name="save"
              on:click={() => addEntry(newEntryAttribute, newEntryValue)}
            />
          </td>
        </tr>
      {:else}
        <tr>
          <td class="fullwidth" colspan={displayColumns.length + 1}>
            <IconButton
              outline
              subdued
              name="plus-circle"
              on:click={() => (adding = true)}
            />
          </td>
        </tr>
      {/if}
    {/if}
  </table>
</div>

<style lang="scss" scoped>
  .container {
    overflow-x: clip;
  }

  table {
    table-layout: fixed;

    // gaps between columns, but hug the edges
    width: calc(100% + 1em);
    margin-left: -0.5em;
    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%;
    }

    :global(td.fullwidth > *) {
      width: 100%;
    }

    .unset {
      opacity: 0.66;
      pointer-events: none;
    }
  }
</style>
