<script lang="ts">
import { browser } from "$app/environment";
import { hydrateMediaListCache } from "$lib/Data/AniList/cacheHydration";
import type { AniListAuthorisation } from "$lib/Data/AniList/identity";
import { type Media, mediaListCollection, Type } from "$lib/Data/AniList/media";
import type {
	AiringEntry,
	AiringSchedule,
	AirType,
} from "$lib/Media/Anime/Airing/animeSchedule";
import { findClosestMedia } from "$lib/Media/Anime/Airing/match";
import { outboundLink } from "$lib/Media/links";
import { parseOrDefault } from "$lib/Utility/parameters";
import settings from "$stores/settings";
import CoverBypass from "./CoverBypass.svelte";
import "$lib/List/covers.css";
import { onDestroy, onMount } from "svelte";
import RateLimitedError from "$lib/Error/RateLimited.svelte";
import ParallaxImage from "$lib/Image/ParallaxImage.svelte";
import Message from "$lib/Loading/Message.svelte";
import Skeleton from "$lib/Loading/Skeleton.svelte";
import LinkedTooltip from "$lib/Tooltip/LinkedTooltip.svelte";
import anime from "$stores/anime";
import identity from "$stores/identity";
import lastPruneTimes from "$stores/lastPruneTimes";
import locale from "$stores/locale";

export let schedule: AiringSchedule;
export let scheduledMedia: Partial<Media[]>;
export let forceListMode = false;
export let user: AniListAuthorisation | undefined;

const urlParameters = browser
	? new URLSearchParams(window.location.search)
	: null;
let day: string | null = parseOrDefault(urlParameters, "day", null);

let mediaListPromise: Promise<Media[]>;

const trackParameter: string | null = parseOrDefault(
	urlParameters,
	"type",
	null,
);

// The view track defaults to the countdown source but is overridable via the
// in-page toggle and a ?type= query param, independent of the global setting.
let selectedTrack: AirType =
	trackParameter === "sub" || trackParameter === "dub"
		? trackParameter
		: $settings.countdownSource === "dub"
			? "dub"
			: "sub";

$: source = selectedTrack;

const selectTrack = (track: AirType) => {
	selectedTrack = track;

	if (browser) {
		const url = new URL(window.location.href);

		url.searchParams.set("type", track);
		history.replaceState(history.state, "", url);
	}
};

// Two masonry columns above the $bp-md (800px) breakpoint, one below.
let columnCount = 1;
let columnQuery: MediaQueryList | undefined;

const updateColumnCount = () => {
	columnCount = columnQuery?.matches ? 1 : 2;
};

onMount(async () => {
	columnQuery = window.matchMedia("(max-width: 800px)");

	updateColumnCount();
	columnQuery.addEventListener("change", updateColumnCount);

	if (user === undefined || $identity.id === -2)
		mediaListPromise = Promise.resolve([]);
	else {
		await hydrateMediaListCache("anime");

		mediaListPromise = mediaListCollection(
			user,
			$identity,
			Type.Anime,
			$anime,
			$lastPruneTimes.anime,
			{
				all: true,
			},
		);
	}
});

onDestroy(() => {
	columnQuery?.removeEventListener("change", updateColumnCount);
});

const WEEKDAYS = [
	"Sunday",
	"Monday",
	"Tuesday",
	"Wednesday",
	"Thursday",
	"Friday",
	"Saturday",
];

const formatTime = (airingAt: number) =>
	new Date(airingAt * 1000).toLocaleTimeString([], {
		hour12: !$settings.display24HourTime,
		hour: "numeric",
		minute: "2-digit",
	});

// Bucket releases by the viewer's local weekday, ordered from today onward.
const scheduleByDay = (entries: AiringEntry[]) => {
	const buckets: { [key: string]: AiringEntry[] } = {};

	for (const entry of entries) {
		if (!entry.airingAt) continue;

		const weekday = new Date(entry.airingAt * 1000).toLocaleString("en-us", {
			weekday: "long",
		});

		if (!buckets[weekday]) buckets[weekday] = [];

		buckets[weekday].push(entry);
	}

	for (const weekday in buckets)
		buckets[weekday].sort((a, b) => a.airingAt - b.airingAt);

	if (day && buckets[day]) return { [day]: buckets[day] };

	const todayIndex = new Date().getDay();
	const ordered: { [key: string]: AiringEntry[] } = {};

	for (let offset = 0; offset < 7; offset += 1) {
		const weekday = WEEKDAYS[(todayIndex + offset) % 7];

		if (buckets[weekday]?.length) ordered[weekday] = buckets[weekday];
	}

	return ordered;
};

// Round-robin the ordered days across columns so reading stays left-to-right
// (e.g. Friday, then Saturday to its right), while each column still packs and
// reflows independently when a day is collapsed.
const distributeDays = (
	daysByWeekday: { [key: string]: AiringEntry[] },
	columns: number,
) => {
	const distributed: {
		day: string;
		scheduleEntry: AiringEntry[];
		dayIndex: number;
	}[][] = Array.from({ length: columns }, () => []);

	Object.entries(daysByWeekday).forEach(([day, scheduleEntry], dayIndex) => {
		distributed[dayIndex % columns].push({ day, scheduleEntry, dayIndex });
	});

	return distributed;
};

const associateMedia = (
	media: (Media | undefined)[],
	title: string,
	mediaList: Media[],
) => {
	const closestMedia = findClosestMedia(media as Media[], title);

	if ($settings.displayScheduleFilterList && closestMedia && mediaList)
		return mediaList.find((m) => m.id === closestMedia?.id) || null;

	return closestMedia;
};

const episode = (media: Media, weekday: string) => {
	if (media.nextAiringEpisode?.episode === 1) return 1;

	if (
		media.nextAiringEpisode?.airingAt &&
		weekday === new Date().toLocaleString("en-us", { weekday: "long" }) &&
		new Date(media.nextAiringEpisode.airingAt * 1000).getTime() - Date.now() >
			24 * 60 * 60 * 1000
	)
		return media.nextAiringEpisode?.episode - 1;

	return media.nextAiringEpisode?.episode || 1;
};
</script>

<div class="track-toggle" role="group" aria-label="Schedule track">
  <button
    type="button"
    aria-pressed={source === 'sub'}
    class:button-action={source === 'sub'}
    onclick={() => selectTrack('sub')}
  >
    {$locale().schedule?.tracks?.sub ?? 'Sub'}
  </button>
  <button
    type="button"
    aria-pressed={source === 'dub'}
    class:button-action={source === 'dub'}
    onclick={() => selectTrack('dub')}
  >
    {$locale().schedule?.tracks?.dub ?? 'Dub'}
  </button>
</div>

{#await mediaListPromise}
  <Message message="Loading media lists ..." />

  <Skeleton grid={true} count={7} height="15em" width="49.5%" />
{:then mediaList}
  <div class="masonry">
    {#each distributeDays(scheduleByDay(schedule[source]), columnCount) as column}
      <div class="masonry-column">
        {#each column as { day, scheduleEntry, dayIndex } (dayIndex)}
          <details
            open
            class="list"
            class:today={day === new Date().toLocaleString('en-us', { weekday: 'long' })}
          >
      <summary>{day}</summary>

      {#if !$settings.displayScheduleListMode && !forceListMode}
        <div
          class="covers"
          style={`grid-template-columns: repeat(auto-fill, minmax(${$settings.displayCoverWidth}px, 1fr))`}
        >
          {#each scheduleEntry as entry, entryIndex}
            {@const media = associateMedia(scheduledMedia, entry.title, mediaList)}

            {#if ($settings.displayScheduleFilterList && media) || !$settings.displayScheduleFilterList}
              <div class="cover-card" id={`cover-${dayIndex}-${entryIndex}`}>
                <LinkedTooltip
                  content={media ? media.title.english || media.title.romaji : entry.title}
                  pin={`cover-${dayIndex}-${entryIndex}`}
                >
                  <a
                    href={outboundLink(
                      media ? media : null,
                      'anime',
                      $settings.displayOutboundLinksTo,
                      media === null,
                      entry.title
                    )}
                    target="_blank"
                  >
                    <div class="cover-card-image">
                      <ParallaxImage
                        source={media
                          ? $settings.displayDataSaver
                            ? media.coverImage.medium
                            : media.coverImage.extraLarge
                          : entry.imageUrl}
                        limit={12.5}
                        alternativeText="Cover"
                      />
                    </div>
                  </a>

                  <div class="cover-title">
                    <div class="cover-title-element">
                      <CoverBypass {media} {entry} cover={false} showTooltip={false} />
                    </div>

                    <span class:countdown={$settings.displayCountdownRightAligned}>
                      {#if media && media.nextAiringEpisode}
                        <span class="opaque">
                          {episode(media, day)}{media.episodes ? `/${media.episodes}` : ''} at
                        </span>
                      {/if}
                      {formatTime(entry.airingAt)}
                    </span>
                  </div>
                </LinkedTooltip>
              </div>
            {/if}
          {/each}
        </div>
      {:else}
        <ol>
          {#each scheduleEntry as entry}
            {@const media = associateMedia(scheduledMedia, entry.title, mediaList)}

            {#if ($settings.displayScheduleFilterList && media) || !$settings.displayScheduleFilterList}
              <li class="entry">
                <CoverBypass {media} {entry} />
                {#if !$settings.displayCountdownRightAligned}
                  <span class="opaque">|</span>
                {/if}
                <span class:countdown={$settings.displayCountdownRightAligned}>
                  {#if media && media.nextAiringEpisode}
                    <span class="opaque">
                      {episode(media, day)}{media.episodes ? `/${media.episodes}` : ''} at
                    </span>
                  {/if}
                  {formatTime(entry.airingAt)}
                </span>
              </li>
            {/if}
          {/each}
        </ol>
      {/if}
    </details>
        {/each}
      </div>
    {/each}
  </div>
{:catch}
  <RateLimitedError type="Media lists" loginSessionError={false} card list={false} />
{/await}

<style lang="scss">
  @use "breakpoints" as *;

  .track-toggle {
    display: flex;
    gap: 0.5em;
    margin-bottom: 0.5em;
  }

  .masonry {
    display: flex;
    gap: 0.5em;
    align-items: flex-start;
  }

  .masonry-column {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5em;
  }

  .countdown {
    white-space: nowrap;
    float: right;
  }

  .today {
    box-shadow:
      0 2.5px 10px var(--base01),
      0 0 0 5px var(--base0E),
      0 4px 30px var(--base01);
  }

  .list {
    overflow-y: auto;
  }

  @media (max-width: $bp-md) {
    .cover-title-element {
      display: none;
    }

    .cover-card-image {
      margin-bottom: 0em;
    }
  }

  @media (max-width: $bp-2xl) {
    .cover-title-element {
      display: none;
    }
  }
</style>
