<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, getContext } from "svelte";
  import type { AttributeChange } from "../../types/base";
  import { useParams } from "svelte-navigator";
  import type { Writable } from "svelte/store";
  import type { UpEntry } from "upend";
  const dispatch = createEventDispatcher();
  const params = useParams();

  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 = "";

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

  // 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 (
          !Boolean(sortKeys[aEntry.value.c]?.length) ||
          !Boolean(sortKeys[bEntry.value.c]?.length)
        ) {
          if (
            Boolean(sortKeys[aEntry.value.c]?.length) &&
            !Boolean(sortKeys[bEntry.value.c]?.length)
          ) {
            return -1;
          } else if (
            !Boolean(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 (
          !Boolean(sortKeys[aEntry.entity]?.length) ||
          !Boolean(sortKeys[bEntry.entity]?.length)
        ) {
          if (
            Boolean(sortKeys[aEntry.entity]?.length) &&
            !Boolean(sortKeys[bEntry.entity]?.length)
          ) {
            return -1;
          } else if (
            !Boolean(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(
        String(entry.value.c),
        entry.listing.getObject(String(entry.value.c)).identify(),
        false
      );
    }
  });

  sortAttributes();

  // Navigation highlights
  const { index } = getContext("browse") as { index: Writable<number> };
  $: addresses = $params.addresses.split(",");

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

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

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

  function formatValue(
    value: string | number,
    attribute: string
  ): string | undefined {
    const handler = VALUE_FORMATTERS[attribute];
    if (handler) {
      return handler(value);
    }
  }

  // Optimizations
  let resolve = [];
</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
      class:left-active={entry.entity == addresses[$index - 1] ||
        entry.value.c == addresses[$index - 1]}
      class:right-active={entry.value.c == addresses[$index + 1] ||
        entry.entity == addresses[$index + 1]}
    >
      {#if editable}
        <td class="attr-action">
          <sl-icon-button
            name="x-circle"
            on:click={removeEntry(entry.address)}
          />
        </td>
      {/if}

      {#if showEntity}
        <td class="entity">
          <UpObject
            link
            labels={entry.listing.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">
          <text-input
            {editable}
            value={entry.value.c}
            on:edit={(val) => updateEntry(entry.address, entry.attribute, val)}
          >
            {#if entry.value.t === "Address"}
              <UpObject
                link
                address={String(entry.value.c)}
                labels={entry.listing
                  .getObject(String(entry.value.c))
                  .identify()}
                resolve={Boolean(resolve[entry.address]) || true}
                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}
          </text-input>
        </td>
      {/if}
    </tr>
  {/each}

  {#if editable}
    <tr>
      <td class="attr-action">
        <sl-icon-button name="plus-circle" on:click={addEntry} />
      </td>
      {#if showAttribute}
        <td>
          <sl-input
            on:sl-input={(ev) => (newEntryAttribute = ev.target.value)}
            size="small"
          />
        </td>
      {/if}
      {#if showValue}
        <td>
          <sl-input
            on:sl-input={(ev) => (newEntryValue = ev.target.value)}
            size="small"
          />
        </td>
      {/if}
    </tr>
  {/if}
</table>

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

    border-spacing: 0.5em 0;

    tr {
      $active-color: hsl(22, 70%, 40%);
      &.left-active {
        td:first-child {
          background: linear-gradient(
            90deg,
            $active-color 0%,
            transparent 100%
          );
        }
      }

      &.right-active {
        td:last-child {
          background: linear-gradient(
            90deg,
            transparent 0%,
            $active-color 100%
          );
        }
      }
    }

    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%;
    }

    sl-icon-button {
      &::part(base) {
        padding: 2px;
      }
    }
  }
</style>
