---
import type { Lang } from "../i18n";
import { IconStar, IconRetweet } from "./icons";

type Props = {
  lang: Lang;
  link: string;
  instance: string;
  user: string;
  postId: string;
};

const { link, instance, user, postId } = Astro.props;
---

<section
  id="comments-section"
  class="px-2 font-serif"
  aria-describedby="title-comments-section"
  data-link={link}
  data-instance={instance}
  data-user={user}
  data-post-id={postId}
>
  <h2 id="title-comments-section" class="py-2 font-serif text-xl font-semibold text-stone-800 dark:text-stone-100">
    Comments
  </h2>
  <p id="comments-description" class="my-1 text-stone-800 dark:text-stone-100">
    <span data-noscript
      ><a class="u-syndication text-link underline" href={link} target="_blank">View comments on Mastodon</a>.</span
    >
    <span style={{ display: "none" }} data-no-comments
      >No comments yet. <a class="text-link underline" href={link} target="_blank"
        >Be the first to join the conversation on Mastodon</a
      >.</span
    >
    <span style={{ display: "none" }} data-comments
      >Join the conversation <a class="text-link underline" href={link} target="_blank">by replying on Mastodon</a
      >.</span
    >
    <span style={{ display: "none" }} data-error>Unable to load comments. Please try again later.</span>
  </p>
  <button
    style={{ display: "none" }}
    class="group mx-auto w-64 rounded-lg bg-bm-300 px-4 py-1 text-stone-800 disabled:bg-bm-400 dark:bg-green-800 dark:text-stone-100 dark:disabled:bg-green-900"
    id="load-comments-button"
  >
    <span class="block underline group-disabled:hidden">Click to load comments</span>
    <span class="hidden group-disabled:block">
      <svg
        style={{ width: "1.25rem", height: "1.25rem", display: "inline" }}
        class="-mt-1 mr-1 animate-spin"
        fill="none"
        viewBox="0 0 24 24"
        aria-hidden="true"
      >
        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
        <path
          class="opacity-100"
          fill="currentColor"
          d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
        ></path>
      </svg>
      Loading...
    </span>
  </button>
  <div id="comments" style={{ display: "none" }}></div>
</section>

<template id="template-comment-emoji">
  <picture>
    <source media="(prefers-reduced-motion: reduce)" />
    <img width={20} class="mx-[1px] inline" />
  </picture>
</template>

<template id="template-comment-box">
  <div
    role="article"
    class="p-comment h-entry my-2 rounded-md border-2 border-stone-400 bg-stone-100 p-2 text-stone-800 dark:border-stone-600 dark:bg-stone-800 dark:text-stone-100"
  >
    <div class="ml-1">
      <a
        data-author
        class="p-author h-card u-url text-link flex items-center text-lg hover:underline focus:underline"
        rel="nofollow"
        target="_blank"
      >
        <picture>
          <source data-avatar-static media="(prefers-reduced-motion: reduce)" />
          <img data-avatar width={40} class="u-photo mr-2 rounded-full border border-stone-400 dark:border-stone-600" />
        </picture>
        <span data-display-name class="p-nickname" aria-label="Display name"></span>
      </a>
      <a
        data-post-link
        class="u-url text-link my-1 flex items-center text-sm font-light hover:underline focus:underline"
        rel="nofollow"
        target="_blank"
      >
        <time class="dt-published mr-1" data-published-date aria-label="Publish date"></time>
        <time style={{ display: "none" }} class="italic" data-edited-date>(edited)</time>
      </a>
    </div>
    <div
      data-content
      class="e-content prose-a:text-link prose-story prose my-1 dark:prose-invert prose-img:my-0"
      aria-label="Comment body"
    >
    </div>
    <div class="ml-1 flex flex-row pb-2 pt-1">
      <div class="flex" aria-label="Favorites">
        <span data-favorites></span>
        <IconStar width="1.25rem" height="1.25rem" class="ml-2" />
      </div>
      <div class="ml-4 flex" aria-label="Reblogs">
        <span data-reblogs></span>
        <IconRetweet width="1.25rem" height="1.25rem" class="ml-2" />
      </div>
    </div>
    <div data-comment-thread class="-mb-2" aria-hidden="true" aria-label="Replies"></div>
  </div>
</template>

<script>
  interface MastodonPost {
    link: string;
    instance: string;
    user: string;
    postId: string;
  }

  interface CustomEmoji {
    shortcode: string;
    url: string;
    static_url: string;
  }

  interface Status {
    id: string;
    in_reply_to_id: string;
    url: string;
    favourites_count: number;
    reblogs_count: number;
    created_at: string;
    edited_at: string | null;
    account: {
      username: string;
      acct: string;
      display_name: string;
      url: string;
      avatar: string;
      avatar_static: string;
      emojis: CustomEmoji[];
    };
    content: string;
    emojis: CustomEmoji[];
  }

  interface StatusContext {
    ancestors: Status[];
    descendants: Status[];
  }

  async function renderComments(section: Element, post: MastodonPost) {
    const commentsDescription = section.querySelector<HTMLElementTagNameMap["p"]>("p#comments-description")!;
    const loadCommentsButton = section.querySelector<HTMLElementTagNameMap["button"]>("button#load-comments-button")!;
    try {
      const response = await fetch(`https://${post.instance}/api/v1/statuses/${post.postId}/context`);
      if (!response.ok) {
        throw new Error(`Received error status ${response.status} - ${response.statusText}!`);
      }
      const data: StatusContext = await response.json();

      const emojiTemplate = document.querySelector<HTMLElementTagNameMap["template"]>(
        "template#template-comment-emoji",
      )!;
      const emojiMap: Record<string, string> = {};
      const replaceEmojis = (text: string, emojis: CustomEmoji[]) =>
        emojis.reduce((acc, emoji) => {
          let emojiHTML = emojiMap[emoji.url];
          if (!emojiHTML) {
            const emojiPicture = emojiTemplate.content.cloneNode(true) as DocumentFragment;
            const emojiStatic = emojiPicture.querySelector("source")!;
            emojiStatic.srcset = emoji.static_url;
            const emojiImg = emojiPicture.querySelector("img")!;
            emojiImg.src = emoji.url;
            emojiImg.alt = `:${emoji.shortcode}: emoji`;
            emojiHTML = emojiPicture.firstElementChild!.outerHTML;
            emojiMap[emoji.url] = emojiHTML;
          }
          return acc.replaceAll(`:${emoji.shortcode}:`, emojiHTML);
        }, text);
      const commentsList: DocumentFragment[] = [];
      const commentMap: Record<string, number> = {};
      const commentTemplate = document.querySelector<HTMLElementTagNameMap["template"]>(
        "template#template-comment-box",
      )!;

      data.descendants.forEach((comment) => {
        const commentBox = commentTemplate.content.cloneNode(true) as DocumentFragment;
        commentBox.firstElementChild!.id = `comment-${comment.id}`;

        const commentBoxAuthor = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-author]")!;
        commentBoxAuthor.href = comment.account.url;
        commentBoxAuthor.title = comment.account.acct;
        commentBoxAuthor.setAttribute("aria-label", comment.account.acct);
        const avatar = commentBoxAuthor.querySelector<HTMLElementTagNameMap["img"]>("img[data-avatar]")!;
        avatar.src = comment.account.avatar;
        avatar.alt = `Avatar of @${comment.account.acct}`;
        const avatarStatic =
          commentBoxAuthor.querySelector<HTMLElementTagNameMap["source"]>("source[data-avatar-static]")!;
        avatarStatic.srcset = comment.account.avatar_static;
        const displayName = commentBoxAuthor.querySelector<HTMLElementTagNameMap["span"]>("span[data-display-name]")!;
        displayName.insertAdjacentHTML(
          "afterbegin",
          replaceEmojis(comment.account.display_name, comment.account.emojis),
        );

        const commentBoxPostLink = commentBox.querySelector<HTMLElementTagNameMap["a"]>("a[data-post-link]")!;
        commentBoxPostLink.href = comment.url;
        const publishDate =
          commentBoxPostLink.querySelector<HTMLElementTagNameMap["time"]>("time[data-published-date]")!;
        publishDate.dateTime = comment.created_at;
        publishDate.insertAdjacentText(
          "afterbegin",
          new Date(Date.parse(comment.created_at)).toLocaleString("en-US", {
            month: "short",
            day: "numeric",
            year: "numeric",
            hour: "2-digit",
            minute: "2-digit",
          }),
        );

        if (comment.edited_at) {
          const edited = commentBoxPostLink.querySelector<HTMLElementTagNameMap["time"]>("time[data-edited-date]")!;
          edited.dateTime = comment.edited_at;
          edited.title = comment.edited_at;
          edited.classList.remove("hidden");
          edited.classList.add("dt-updated");
          edited.style.removeProperty("display");
        }

        const commentBoxContent = commentBox.querySelector<HTMLElementTagNameMap["div"]>("div[data-content]")!;
        commentBoxContent.insertAdjacentHTML("afterbegin", replaceEmojis(comment.content, comment.emojis));

        const commentBoxFavorites = commentBox.querySelector<HTMLElementTagNameMap["span"]>("span[data-favorites]")!;
        commentBoxFavorites.insertAdjacentText("afterbegin", comment.favourites_count.toString());

        const commentBoxReblogs = commentBox.querySelector<HTMLElementTagNameMap["span"]>("span[data-reblogs]")!;
        commentBoxReblogs.insertAdjacentText("afterbegin", comment.reblogs_count.toString());

        if (comment.in_reply_to_id === post.postId || !(comment.in_reply_to_id in commentMap)) {
          commentMap[comment.id] = commentsList.length;
          commentsList.push(commentBox);
        } else {
          const commentsIndex = commentMap[comment.in_reply_to_id];
          commentMap[comment.id] = commentsIndex;
          const parentThreadDiv =
            commentsList[commentsIndex].querySelector<HTMLElementTagNameMap["div"]>("div[data-comment-thread]")!;
          parentThreadDiv.setAttribute("aria-hidden", "false");
          parentThreadDiv.appendChild(commentBox);
        }
      });
      if (commentsList.length) {
        const fragment = document.createDocumentFragment();
        commentsList.forEach((comment) => fragment.appendChild(comment));
        commentsDescription
          .querySelector<HTMLElementTagNameMap["span"]>("span[data-comments]")!
          .style.removeProperty("display");
        const commentsDiv = section.querySelector<HTMLElementTagNameMap["div"]>("div#comments")!;
        commentsDiv.appendChild(fragment);
        commentsDiv.style.removeProperty("display");
      } else {
        commentsDescription
          .querySelector<HTMLElementTagNameMap["span"]>("span[data-no-comments]")!
          .style.removeProperty("display");
      }
    } catch (e) {
      console.error("Fetch Mastodon comments error", e);
      commentsDescription
        .querySelector<HTMLElementTagNameMap["span"]>("span[data-error]")!
        .style.removeProperty("display");
    } finally {
      loadCommentsButton.style.display = "none";
      loadCommentsButton.blur();
      commentsDescription.style.removeProperty("display");
    }
  }

  function initCommentSection() {
    const commentSection = document.querySelector<HTMLElementTagNameMap["section"]>("section#comments-section");
    if (!commentSection) {
      return;
    }
    const post = {
      link: commentSection.dataset.link,
      instance: commentSection.dataset.instance,
      user: commentSection.dataset.user,
      postId: commentSection.dataset.postId,
    };
    if (!post.link || !post.instance || !post.user || !post.postId) {
      return;
    }
    const loadCommentsButton =
      commentSection.querySelector<HTMLElementTagNameMap["button"]>("button#load-comments-button")!;
    loadCommentsButton.addEventListener(
      "click",
      (e) => {
        e.preventDefault();
        loadCommentsButton.disabled = true;
        renderComments(commentSection, post as MastodonPost);
      },
      { once: true },
    );
    const commentsDescription = commentSection.querySelector<HTMLElementTagNameMap["p"]>("p#comments-description")!;
    commentsDescription.style.display = "none";
    commentsDescription.querySelector<HTMLElementTagNameMap["span"]>("span[data-noscript]")!.style.display = "none";
    loadCommentsButton.style.removeProperty("display");
  }

  initCommentSection();
</script>
