<script lang="ts">
  import { onMount } from 'svelte';
  import type { PageData } from './$types';
  import type { Post } from '$lib/mastodon/response';
  import {
    PUBLIC_REFRESH_INTERVAL,
    PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME
  } from '$env/static/public';
  import PostComponent from '$lib/components/PostComponent.svelte';
  import LoadMoreComponent from '$lib/components/LoadMoreComponent.svelte';
  import { fly, type FlyParams } from 'svelte/transition';
  import { cubicInOut } from 'svelte/easing';
  import { errorToast } from '$lib/errorToast';

  interface Props {
    data: PageData;
  }

  let { data = $bindable() }: Props = $props();
  let posts: Post[] = $state(data.posts);

  interface FetchOptions {
    since?: string;
    before?: string;
    count?: number;
  }

  interface EdgeFlyParams extends FlyParams {
    created_at: string;
  }

  const refreshInterval = parseInt(PUBLIC_REFRESH_INTERVAL);
  let interval: ReturnType<typeof setTimeout> | null = null;
  let moreOlderPostsAvailable = $state(true);
  let loadingOlderPosts = $state(false);

  // Needed, so that edgeFly() can do its thing:
  // To determine whether a newly loaded post is older than the existing ones, is required to know what the oldest
  // post was, before the fetch happened.
  let oldestBeforeLastFetch: number | null = null;

  /**
   * Animate either from the top, or the bottom of the window, depending if the post is
   * newer than the existing ones or older.
   */
  function edgeFly(node: Element, opts: EdgeFlyParams) {
    const createdAt = new Date(opts.created_at).getTime();
    const diffNewest = Math.abs(new Date(posts[0].created_at).getTime() - createdAt);
    const oldest =
      oldestBeforeLastFetch !== null
        ? oldestBeforeLastFetch
        : new Date(posts[posts.length - 1].created_at).getTime();
    const diffOldest = Math.abs(oldest - createdAt);
    const fromTop = diffNewest <= diffOldest;

    const rect = node.getBoundingClientRect();
    const paramY = +`${opts.y}`;
    let offset = isNaN(paramY) ? 0 : paramY + rect.height;
    opts.y = fromTop ? -offset : window.innerHeight + offset;
    return fly(node, opts);
  }

  async function fetchPosts(options: FetchOptions): Promise<Post[]> {
    const params = new URLSearchParams();
    if (options?.since !== undefined) {
      params.set('since', options.since);
    }
    if (options?.before !== undefined) {
      params.set('before', options.before);
    }
    if (options?.count !== undefined) {
      params.set('count', options.count.toFixed(0));
    }

    const response = await fetch(`/api/posts?${params}`);
    return await response.json();
  }

  function filterDuplicates(posts: Post[]): Post[] {
    return posts.filter((obj, index, arr) => {
      return arr.map((mapObj) => mapObj.url).indexOf(obj.url) === index;
    });
  }

  function refresh() {
    let filter: FetchOptions = {};
    if (posts.length > 0) {
      filter = { since: posts[0].created_at };
    }
    fetchPosts(filter)
      .then((resp) => {
        if (resp.length > 0) {
          // Prepend new posts, filter dupes
          // There shouldn't be any duplicates, but better be safe than sorry
          posts = filterDuplicates(resp.concat(posts));
        }
      })
      .catch((e: Error) => {
        errorToast('Error loading newest posts: ' + e.message);
      });
  }

  onMount(async () => {
    posts = data.posts;
    if (posts.length > 0) {
      oldestBeforeLastFetch = new Date(posts[posts.length - 1].created_at).getTime();
    }
    interval = setInterval(refresh, refreshInterval);

    // - If the page is hidden, slow down refresh rate
    // - If the page is shown, bump up refresh rate
    document.addEventListener('visibilitychange', () => {
      const delay = document.hidden ? refreshInterval * 10 : refreshInterval;
      if (interval) {
        clearInterval(interval);
      }
      interval = setInterval(refresh, delay);
    });

    return () => {
      if (interval !== null) {
        clearInterval(interval);
      }
    };
  });

  function loadOlderPosts() {
    loadingOlderPosts = true;
    const filter: FetchOptions = { count: 20 };
    if (posts.length > 0) {
      const before = posts[posts.length - 1].created_at;
      filter.before = before;
      oldestBeforeLastFetch = new Date(before).getTime();
    }

    fetchPosts(filter)
      .then((resp) => {
        if (resp.length > 0) {
          // Append old posts, filter dupes
          // There shouldn't be any duplicates, but better be safe than sorry
          posts = filterDuplicates(posts.concat(resp));
          // If we got less than we expected, there are no older posts available
          moreOlderPostsAvailable = resp.length >= (filter.count ?? 20);
        } else {
          moreOlderPostsAvailable = false;
        }
        loadingOlderPosts = false;
      })
      .catch((e) => {
        loadingOlderPosts = false;
        errorToast('Error loading older posts: ' + e.message);
      });
  }
</script>

<svelte:head>
  <title>{PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME} music list</title>
</svelte:head>
<h2>{PUBLIC_MASTODON_INSTANCE_DISPLAY_NAME} music list</h2>
<div class="wrapper">
  <div></div>
  <div class="posts">
    {#if posts.length === 0}
      Sorry, no posts recommending music have been found yet
    {/if}
    {#each posts as post (post.url)}
      <div
        class="post"
        transition:edgeFly|global={{
          y: 10,
          created_at: post.created_at,
          duration: 300,
          easing: cubicInOut
        }}
      >
        <PostComponent {post} />
      </div>
    {/each}
    <LoadMoreComponent
      {loadOlderPosts}
      moreAvailable={moreOlderPostsAvailable}
      isLoading={loadingOlderPosts}
    />
  </div>
  <div></div>
</div>

<style>
  .posts {
    display: flex;
    flex-direction: column;
    align-items: center;
  }
  .post {
    width: 100%;
    max-width: min(800px, 80vw);
    margin-bottom: 1em;
    border-bottom: 1px solid var(--color-border);
    padding: 1em;
  }
  .wrapper {
    display: flex;
    justify-content: space-between;
  }

  h2 {
    text-align: center;
    z-index: 100;
  }

  @media only screen and (max-width: 650px) {
    .post {
      max-width: calc(100vw - 16px);
      padding: 1em 0;
    }
  }
</style>
