<script lang="ts">
  import EntryView, { type Widget } from "./EntryView.svelte";
  import { useEntity } from "../lib/entity";
  import UpObject from "./display/UpObject.svelte";
  import { createEventDispatcher, setContext } from "svelte";
  import { derived, writable, type Readable } from "svelte/store";
  import type { UpEntry } from "upend";
  import Spinner from "./utils/Spinner.svelte";
  import NotesEditor from "./utils/NotesEditor.svelte";
  import type { AttributeChange } from "../types/base";
  import type { EntityInfo } from "upend/types";
  import IconButton from "./utils/IconButton.svelte";
  import type { BrowseContext } from "../util/browse";
  import { Link, useParams } from "svelte-navigator";
  import Icon from "./utils/Icon.svelte";
  import BlobViewer from "./display/BlobViewer.svelte";
  import { i18n } from "../i18n";
  import EntryList from "./widgets/EntryList.svelte";
  import api from "../lib/api";
  import Gallery from "./widgets/Gallery.svelte";
  import { ATTR_IN, ATTR_LABEL, ATTR_KEY, ATTR_OF } from "upend/constants";
  import InspectGroups from "./InspectGroups.svelte";
  import InspectTypeEditor from "./InspectTypeEditor.svelte";

  const dispatch = createEventDispatcher();
  const params = useParams();

  export let address: string;
  export let index: number | undefined;
  export let detail: boolean;
  export let editable = false;
  let showAsEntries = false;
  let highlightedType: string | undefined;

  let blobHandled = false;

  let indexStore = writable(index);
  $: $indexStore = index;

  let addressesStore = writable([]);
  $: $addressesStore = $params.addresses?.split(",") || [];

  setContext("browse", {
    index: indexStore,
    addresses: addressesStore,
  } as BrowseContext);

  $: ({ entity, entityInfo, error, revalidate } = useEntity(address));

  $: allTypes = derived(
    entityInfo,
    ($entityInfo, set) => {
      getAllTypes($entityInfo).then((allTypes) => {
        set(allTypes);
      });
    },
    {},
  ) as Readable<{
    [key: string]: {
      labels: string[];
      attributes: string[];
    };
  }>;

  async function getAllTypes(entityInfo: EntityInfo) {
    const allTypes = {};

    if (!entityInfo) {
      return {};
    }

    const typeAddresses: string[] = [
      await api.getAddress(entityInfo.t),
      ...($entity?.attr[ATTR_IN] || []).map((e) => e.value.c as string),
    ];
    const typeAddressesIn = typeAddresses.map((addr) => `@${addr}`).join(" ");

    const labelsQuery = await api.query(
      `(matches (in ${typeAddressesIn}) "${ATTR_LABEL}" ?)`,
    );

    typeAddresses.forEach((address) => {
      let labels = labelsQuery.getObject(address).identify();

      let typeLabel: string | undefined;
      if (typeLabel) {
        labels.unshift(typeLabel);
      }

      allTypes[address] = {
        labels,
        attributes: [],
      };
    });

    const attributes = await api.query(
      `(matches ? "${ATTR_OF}" (in ${typeAddressesIn}))`,
    );
    await Promise.all(
      typeAddresses.map(async (address) => {
        allTypes[address].attributes = (
          await Promise.all(
            (attributes.getObject(address).attr[`~${ATTR_OF}`] || []).map(
              async (e) => {
                try {
                  const { t, c } = await api.addressToComponents(e.entity);
                  if (t == "Attribute") {
                    return c;
                  }
                } catch (err) {
                  console.error(err);
                  return false;
                }
              },
            ),
          )
        ).filter(Boolean);
      }),
    );

    const result = {};
    Object.keys(allTypes).forEach((addr) => {
      if (allTypes[addr].attributes.length > 0) {
        result[addr] = allTypes[addr];
      }
    });

    return result;
  }

  let untypedAttributes = [] as UpEntry[];
  let untypedLinks = [] as UpEntry[];

  $: {
    untypedAttributes = [];
    untypedLinks = [];

    ($entity?.attributes || []).forEach((entry) => {
      const entryTypes = Object.entries($allTypes || {}).filter(([_, t]) =>
        t.attributes.includes(entry.attribute),
      );
      if (entryTypes.length === 0) {
        if (entry.value.t === "Address") {
          untypedLinks.push(entry);
        } else {
          untypedAttributes.push(entry);
        }
      }
    });

    untypedAttributes = untypedAttributes;
    untypedLinks = untypedLinks;
  }

  $: filteredUntypedAttributes = untypedAttributes.filter(
    (entry) =>
      ![
        ATTR_LABEL,
        ATTR_IN,
        ATTR_KEY,
        "NOTE",
        "LAST_VISITED",
        "NUM_VISITED",
        "LAST_ATTRIBUTE_WIDGET",
      ].includes(entry.attribute),
  );

  $: currentUntypedAttributes = editable
    ? untypedAttributes
    : filteredUntypedAttributes;

  $: filteredUntypedLinks = untypedLinks.filter(
    (entry) => ![ATTR_IN, ATTR_OF].includes(entry.attribute),
  );

  $: currentUntypedLinks = editable ? untypedLinks : filteredUntypedLinks;

  $: currentBacklinks =
    (editable
      ? $entity?.backlinks
      : $entity?.backlinks.filter(
          (entry) => ![ATTR_IN, ATTR_OF].includes(entry.attribute),
        )) || [];

  $: tagged = $entity?.attr[`~${ATTR_IN}`] || [];

  let attributesUsed: UpEntry[] = [];
  $: {
    if ($entityInfo?.t === "Attribute") {
      api
        .query(`(matches ? "${$entityInfo.c}" ?)`)
        .then((result) => (attributesUsed = result.entries));
    }
  }

  async function onChange(ev: CustomEvent<AttributeChange>) {
    const change = ev.detail;
    switch (change.type) {
      case "create":
        await api.putEntry({
          entity: address,
          attribute: change.attribute,
          value: change.value,
        });
        break;
      case "delete":
        await api.deleteEntry(change.address);
        break;
      case "update":
        await api.putEntityAttribute(address, change.attribute, change.value);
        break;
      default:
        console.error("Unimplemented AttributeChange", change);
        return;
    }
    revalidate();
  }

  let identities = [address];

  function onResolved(ev: CustomEvent<string[]>) {
    identities = ev.detail;
    dispatch("resolved", ev.detail);
  }

  async function deleteObject() {
    if (confirm(`${$i18n.t("Really delete")} "${identities.join(" | ")}"?`)) {
      await api.deleteEntry(address);
      dispatch("close");
    }
  }

  const attributeWidgets: Widget[] = [
    {
      name: "List",
      icon: "list-check",
      components: (entries) => [
        {
          component: EntryList,
          props: {
            entries,
            columns: "attribute, value",
          },
        },
      ],
    },
  ];

  const linkWidgets: Widget[] = [
    {
      name: "List",
      icon: "list-check",
      components: (entries) => [
        {
          component: EntryList,
          props: {
            entries,
            columns: "attribute, value",
          },
        },
      ],
    },
    {
      name: "Gallery",
      icon: "image",
      components: (entries) => [
        {
          component: Gallery,
          props: {
            entities: entries
              .filter((e) => e.value.t == "Address")
              .map((e) => e.value.c),
            thumbnails: true,
          },
        },
      ],
    },
  ];

  const taggedWidgets: Widget[] = [
    {
      name: "List",
      icon: "list-check",
      components: (entries) => [
        {
          component: Gallery,
          props: {
            entities: entries.map((e) => e.entity),
            thumbnails: false,
          },
        },
      ],
    },
    {
      name: "Gallery",
      icon: "image",
      components: (entries) => [
        {
          component: Gallery,
          props: {
            entities: entries.map((e) => e.entity),
            thumbnails: true,
          },
        },
      ],
    },
  ];

  $: entity.subscribe(async (object) => {
    if (object && object.listing.entries.length) {
      await api.putEntityAttribute(
        object.address,
        "LAST_VISITED",
        {
          t: "Number",
          c: new Date().getTime() / 1000,
        },
        "IMPLICIT",
      );

      await api.putEntityAttribute(
        object.address,
        "NUM_VISITED",
        {
          t: "Number",
          c: (parseInt(String(object.get("NUM_VISITED"))) || 0) + 1,
        },
        "IMPLICIT",
      );
    }
  });
</script>

<div class="inspect" class:detail class:blob={blobHandled}>
  <header>
    <h2>
      {#if $entity}
        <UpObject banner {address} on:resolved={onResolved} />
      {:else}
        <Spinner centered />
      {/if}
    </h2>
  </header>
  {#if !showAsEntries}
    <div class="main-content">
      <div class="detail-col">
        <div class="blob-viewer">
          <BlobViewer
            {address}
            {editable}
            {detail}
            on:handled={(ev) => (blobHandled = ev.detail)}
          />
        </div>
        <NotesEditor {address} {editable} on:change={onChange} />
        {#if !$error}
          <InspectGroups
            {entity}
            {editable}
            on:highlighted={(ev) => (highlightedType = ev.detail)}
            on:change={() => revalidate()}
          />
          <div class="attributes">
            <InspectTypeEditor
              {entity}
              {editable}
              on:change={() => revalidate()}
            />
            {#each Object.entries($allTypes) as [typeAddr, { labels, attributes }]}
              <EntryView
                entries={($entity?.attributes || []).filter((e) =>
                  attributes.includes(e.attribute),
                )}
                {editable}
                widgets={linkWidgets}
                on:change={onChange}
                highlighted={highlightedType == typeAddr}
                title={labels.join(" | ")}
                group={typeAddr}
              />
            {/each}

            {#if currentUntypedAttributes.length > 0 || editable}
              <EntryView
                title={$i18n.t("Other Attributes")}
                {editable}
                widgets={attributeWidgets}
                entries={currentUntypedAttributes}
                on:change={onChange}
              />
            {/if}

            {#if currentUntypedLinks.length > 0 || editable}
              <EntryView
                title={$i18n.t("Links")}
                {editable}
                widgets={linkWidgets}
                entries={currentUntypedLinks}
                on:change={onChange}
              />
            {/if}

            {#if tagged.length > 0}
              <EntryView
                title={`${$i18n.t("Members")}`}
                widgets={taggedWidgets}
                entries={tagged}
                on:change={onChange}
              />
            {/if}

            {#if currentBacklinks.length > 0}
              <EntryView
                title={`${$i18n.t("Referred to")} (${currentBacklinks.length})`}
                entries={currentBacklinks}
                on:change={onChange}
              />
            {/if}

            {#if $entityInfo?.t === "Attribute"}
              <div class="buttons">
                <div class="button">
                  <Link to="/surface?x={$entityInfo.c}">
                    {$i18n.t("Surface view")}
                  </Link>
                </div>
              </div>

              <section class="labelborder">
                <header>
                  <h3>{$i18n.t("Used")} ({attributesUsed.length})</h3>
                </header>
                <div class="content">
                  <EntryList
                    columns="entity,value"
                    columnWidths={["auto", "33%"]}
                    entries={attributesUsed}
                    orderByValue
                  />
                </div>
              </section>
            {/if}
          </div>

          {#if editable}
            <!-- svelte-ignore a11y-click-events-have-key-events -->
            <div class="button" on:click={deleteObject}>
              <Icon name="trash" />
            </div>
          {/if}
        {:else}
          <div class="error">
            {$error}
          </div>
        {/if}
      </div>
    </div>
  {:else}
    <div class="entries">
      <h2>{$i18n.t("Attributes")}</h2>
      <EntryList
        {editable}
        entries={$entity.attributes}
        columns={detail
          ? "timestamp, provenance, attribute, value"
          : "attribute, value"}
      />
      <h2>{$i18n.t("Backlinks")}</h2>
      <EntryList
        entries={$entity.backlinks}
        columns={detail
          ? "timestamp, provenance, entity, attribute"
          : "entity, attribute"}
      />
    </div>
  {/if}
  <div class="footer">
    <IconButton
      name="detail"
      title="Show as entries"
      active={showAsEntries}
      on:click={() => (showAsEntries = !showAsEntries)}
    />
  </div>
</div>

<style scoped lang="scss">
  @use "./util";

  header h2 {
    margin-bottom: 0;
  }

  .inspect,
  .main-content {
    flex: auto;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    min-height: 0;
  }

  .attributes {
    flex: auto;
    height: 0; // https://stackoverflow.com/a/14964944
    min-height: 12em;
    overflow-y: auto;
  }

  @media screen and (max-height: 1080px) {
    .main-content {
      overflow-y: auto;
      // min-height: 0;
    }

    .attributes {
      height: unset;
      min-height: unset;
      overflow-y: unset;
    }
  }

  @media screen and (min-width: 1600px) {
    .inspect.detail {
      .main-content {
        position: relative;
        flex-direction: row;
        justify-content: end;
      }

      &.blob {
        .detail-col {
          width: 25%;
          flex-grow: 0;
        }

        .blob-viewer {
          width: 73%;
          height: 100%;
          position: absolute;
          left: 1%;
          top: 0;
        }
      }
    }
  }

  .main-content .detail-col {
    display: flex;
    flex-direction: column;
    flex-grow: 1;
  }

  .entries {
    flex-grow: 1;
  }

  .footer {
    display: flex;
    justify-content: end;
  }

  .buttons {
    display: flex;
  }

  .error {
    color: red;
  }
</style>
