<script lang="ts">
  import filesize from "filesize";
  import { format, fromUnixTime } 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 } 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 { readable } from "svelte/store";
  const dispatch = createEventDispatcher();

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

  export let entries: UpEntry[];
  export let editable = false;

  // Display
  $: columns = columns || "attribute, value";
  $: showEntity = columns.includes("entity");
  $: showAttribute = columns.includes("attribute");
  $: showValue = columns.includes("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("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(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 sortedAttributes = entries;

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

  function sortAttributes() {
    sortedAttributes = entries
      .concat()
      .sort((aEntry, bEntry) => {
        if (
          !sortKeys[aEntry.value.c]?.length ||
          !sortKeys[bEntry.value.c]?.length
        ) {
          if (
            Boolean(sortKeys[aEntry.value.c]?.length) &&
            !sortKeys[bEntry.value.c]?.length
          ) {
            return -1;
          } else if (
            !sortKeys[aEntry.value.c]?.length &&
            Boolean(sortKeys[bEntry.value.c]?.length)
          ) {
            return 1;
          } else {
            return String(aEntry.value.c).localeCompare(String(bEntry.value.c));
          }
        } else {
          return sortKeys[aEntry.value.c][0].localeCompare(
            sortKeys[bEntry.value.c][0],
            undefined,
            { numeric: true, sensitivity: "base" }
          );
        }
      })
      .sort((aEntry, bEntry) => {
        return String(aEntry.value.c).length - String(bEntry.value.c).length;
      })
      .sort((aEntry, bEntry) => {
        return aEntry.attribute.localeCompare(bEntry.attribute);
      })
      .sort((aEntry, bEntry) => {
        if (
          !sortKeys[aEntry.entity]?.length ||
          !sortKeys[bEntry.entity]?.length
        ) {
          if (
            Boolean(sortKeys[aEntry.entity]?.length) &&
            !sortKeys[bEntry.entity]?.length
          ) {
            return -1;
          } else if (
            !sortKeys[aEntry.entity]?.length &&
            Boolean(sortKeys[bEntry.entity]?.length)
          ) {
            return 1;
          } else {
            return aEntry.entity.localeCompare(bEntry.entity);
          }
        } else {
          return sortKeys[aEntry.entity][0].localeCompare(
            sortKeys[bEntry.entity][0]
          );
        }
      });
  }

  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(),
        false
      );
    }
  });

  sortAttributes();

  // Formatting & Display
  const ATTRIBUTE_LABELS: { [key: string]: string } = {
    FILE_MIME: "MIME type",
    FILE_MTIME: "Last modified",
    FILE_SIZE: "File size",
    ADDED: "Added at",
  };

  const VALUE_FORMATTERS: { [key: string]: (val: string | number) => string } =
    {
      FILE_MTIME: (val) =>
        format(fromUnixTime(parseInt(String(val), 10)), "PPpp"),
      FILE_SIZE: (val) => filesize(parseInt(String(val), 10), { base: 2 }),
      ADDED: (val) => format(fromUnixTime(parseInt(String(val), 10)), "PPpp"),
    };

  function formatAttribute(attribute: string) {
    return ATTRIBUTE_LABELS[attribute];
  }

  function formatValue(value: string | number, attribute: string): string {
    const handler = VALUE_FORMATTERS[attribute];
    if (handler) {
      try {
        return handler(value);
      } catch (error) {
        console.warn(`Error while formatting "${value}": ${error}`);
      }
    }
    return String(value);
  }
</script>

<table>
  <colgroup>
    {#if editable}
      <col class="attr-action-col" />
    {/if}
    {#if showEntity}
      <col class="entity-col" />
    {/if}
    {#if showAttribute}
      <col class="attr-col" />
    {/if}
    {#if showValue}
      <col class="val-col" />
    {/if}
  </colgroup>

  {#if header}
    <tr>
      {#if editable}
        <th />
      {/if}
      {#if showEntity}
        <th>Entity</th>
      {/if}
      {#if showAttribute}
        <th>Attribute</th>
      {/if}
      {#if showValue}
        <th>Value</th>
      {/if}
    </tr>
  {/if}

  {#each sortedAttributes as entry (entry.address)}
    <tr>
      {#if editable}
        <td class="attr-action">
          <IconButton
            name="x-circle"
            on:click={() => removeEntry(entry.address)}
          />
        </td>
      {/if}

      {#if showEntity}
        <td class="entity">
          <UpObject
            link
            labels={$labelListing
              ?.getObject(String(entry.entity))
              ?.identify() || []}
            address={entry.entity}
            on:resolved={(event) => {
              addSortKeys(entry.entity, event.detail);
            }}
          />
        </td>
      {/if}

      {#if showAttribute}
        <td class:formatted={Boolean(formatAttribute(entry.attribute))}>
          <Ellipsis
            value={formatAttribute(entry.attribute) || entry.attribute}
          />
        </td>
      {/if}

      {#if showValue}
        <td class="value">
          <Editable
            {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);
                }}
              />
            {: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>
      {/if}
    </tr>
  {/each}

  {#if editable}
    <tr class="add-row">
      <td class="attr-action">
        <IconButton name="plus-circle" on:click={addEntry} />
      </td>
      {#if showAttribute}
        <td>
          <Selector type="attribute" bind:attribute={newEntryAttribute} />
        </td>
      {/if}
      {#if showValue}
        <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);
      }
    }

    .attr-action-col {
      width: 1.5em;
    }

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