<script lang="ts">
  import EmojiText from './EmojiText.svelte';
  import MessageRow from './MessageRow.svelte';
  import { formatDay } from './format';
  import { expand, motionActive, rowIn, rowOut, setRowMotion } from './motion';
  import type { PendingMessage } from './pending';
  import { buildGroups, type Group, lastOwnSent, lastSenderIs, pendingToEvent } from './timeline-groups';
  import type { ImagePack, LinkPreview, Person, QuickReaction, RecentHistoryState, ReplyReference, TimelineEvent, TimelineItem } from './types';

  export let items: TimelineItem[];
  export let pending: PendingMessage[] = [];
  export let joined: boolean;
  export let self: string | null;
  export let selfName: string | null = null;
  export let people: Record<string, Person> = {};
  export let senderAvatarFor: (userId: string) => string | null;
  export let reacting: boolean;
  export let quickReactions: QuickReaction[] = [];
  export let packs: ImagePack[] = [];
  export let linkPreviews: Record<string, LinkPreview | null> = {};
  export let loadPreview: ((url: string) => void) | null = null;
  export let editingEventId: string | null = null;
  export let recentState: RecentHistoryState | undefined = undefined;
  export let recentRetrying = false;
  export let paginating = false;
  export let fillingGap: string | null = null;
  export let following = true;
  export let onReact: (eventId: string, key: string, shortcode?: string) => void;
  export let onQuickChange: (list: QuickReaction[]) => void;
  export let onUser: (userId: string, x: number, y: number, menu: boolean) => void;
  export let onCloneSticker: (eventId: string, body: string, x: number, y: number) => void;
  export let onMore: (event: TimelineEvent, x: number, y: number) => void;
  export let onOpenThread: (rootEventId: string) => void;
  export let onReply: (target: ReplyReference) => void;
  export let onJump: (eventId: string) => void;
  export let onEdit: (eventId: string, body: string) => void;
  export let onPaginate: () => void;
  export let onGrew: () => void;
  export let onFillGap: (gapId: string) => void;
  export let onRetryRecent: () => void;
  export let onRetryPending: (id: string) => void;
  export let onDiscardPending: (id: string) => void;

  const INITIAL_GROUPS = 150;
  const READ_CAP = 400;
  const READ_CONTEXT = 20;
  const STEP = 30;
  const MAX_GROUPS = 300;
  const JUMP_HALF = 60;
  const LIVE_MAX = 8;

  let ready = false;
  let start = 0;
  let end = 0;
  let tail = true;
  let startKey: string | null = null;
  let endKey: string | null = null;
  let quiet = false;
  let mounted = false;
  let shownKeys: string[] = [];
  let shownPending = new Map<string, PendingMessage['status']>();
  let firstKey: string | undefined;

  function eventKey(list: Group[], from: number, step: 1 | -1): string | null {
    for (let i = from; i >= 0 && i < list.length; i += step) if (list[i].item.kind === 'event') return list[i].key;
    return null;
  }

  function settle(list: Group[]): void {
    startKey = eventKey(list, start, 1);
    endKey = eventKey(list, end - 1, -1);
    tail = end >= list.length;
  }

  function reconcile(list: Group[], follow: boolean): void {
    if (!ready) {
      if (list.length === 0) return;
      start = Math.max(0, list.length - INITIAL_GROUPS);
      const marker = list.findIndex((g) => g.item.kind === 'readMarker');
      if (marker >= 0 && marker < start) start = Math.max(0, marker - READ_CONTEXT, list.length - READ_CAP);
      end = list.length;
      ready = true;
      settle(list);
      return;
    }
    const oldest = start === 0;
    let s = startKey === null ? start : list.findIndex((g) => g.key === startKey);
    if (s < 0) s = start;
    if (oldest) s = 0;
    let e = list.length;
    if (!tail && !follow) {
      const at = endKey === null ? -1 : list.findIndex((g) => g.key === endKey);
      e = at >= 0 ? at + 1 : Math.min(end, list.length);
    }
    s = Math.min(s, e);
    if (e - s > MAX_GROUPS) {
      if (follow && e === list.length) s = e - MAX_GROUPS;
      else if (oldest) e = s + MAX_GROUPS;
      else s = e - MAX_GROUPS;
    }
    start = s;
    end = e;
    settle(list);
  }

  export function atTail(): boolean {
    return tail;
  }

  export function has(eventId: string): boolean {
    return grouped.some((g) => g.item.kind === 'event' && g.item.event.event_id === eventId);
  }

  export function reveal(eventId: string): boolean {
    const at = grouped.findIndex((g) => g.item.kind === 'event' && g.item.event.event_id === eventId);
    if (at < 0 || (at >= start && at < end)) return false;
    quiet = true;
    start = Math.max(0, at - JUMP_HALF);
    end = Math.min(grouped.length, at + JUMP_HALF);
    settle(grouped);
    return true;
  }

  export function resetToTail(): boolean {
    if (tail) return false;
    quiet = true;
    start = Math.max(0, grouped.length - INITIAL_GROUPS);
    end = grouped.length;
    settle(grouped);
    return true;
  }

  export function grow(direction: 'up' | 'down'): boolean {
    if (direction === 'up') {
      if (start === 0) return false;
      quiet = true;
      start = Math.max(0, start - STEP);
      if (end - start > MAX_GROUPS) end = start + MAX_GROUPS;
    } else {
      if (end >= grouped.length) return false;
      quiet = true;
      end = Math.min(grouped.length, end + STEP);
      if (end - start > MAX_GROUPS) start = end - MAX_GROUPS;
    }
    settle(grouped);
    return true;
  }

  function loadOlder(): void {
    if (grow('up')) onGrew();
    else onPaginate();
  }

  function trackMotion(vis: Group[], list: Group[], pend: PendingMessage[]): void {
    const keys = vis.map((g) => g.key);
    const pendingNow = new Map<string, PendingMessage['status']>(pend.map((p) => [`pending-${p.id}`, p.status]));
    const enter = new Set<string>();
    const leave = new Set<string>();
    const prepended = firstKey !== list[0]?.key;
    if (mounted && !quiet && !prepended && shownKeys.length > 0) {
      const old = new Set(shownKeys);
      const now = new Set(keys);
      const listKeys = new Set(list.map((g) => g.key));
      const added = keys.filter((k) => !old.has(k));
      const gone = shownKeys.filter((k) => !now.has(k) && !listKeys.has(k));
      if (added.length <= LIVE_MAX && gone.length <= LIVE_MAX) {
        const byKey = new Map(vis.map((g) => [g.key, g.item] as const));
        const echoed = shownPending.size > 0 && !motionActive();
        const lastOld = keys.lastIndexOf(shownKeys[shownKeys.length - 1]);
        for (const k of added) {
          const item = byKey.get(k);
          const appended = lastOld >= 0 && keys.indexOf(k) > lastOld;
          if ((appended || k === 'readMarker') && !(echoed && item?.kind === 'event' && item.event.own)) enter.add(k);
        }
        for (const k of gone) leave.add(k);
      }
      for (const id of pendingNow.keys()) if (!shownPending.has(id)) enter.add(id);
      for (const [id, status] of shownPending) if (!pendingNow.has(id) && status === 'failed') leave.add(id);
    }
    mounted = true;
    quiet = false;
    shownKeys = keys;
    shownPending = pendingNow;
    firstKey = list[0]?.key;
    setRowMotion(enter, leave);
  }

  $: hasStart = items.some((item) => item.kind === 'start');
  $: grouped = buildGroups(items);
  $: reconcile(grouped, following);
  $: visible = grouped.slice(start, end);
  $: trackMotion(visible, grouped, tail ? pending : []);
  $: lastSentId = lastOwnSent(items);
  $: pendingFollowsSelf = lastSenderIs(items, self);
</script>

<div class="timeline-top">
  {#if recentState === 'Loading'}
    <p class="side-note" transition:expand>Loading recent history…</p>
  {:else if recentState && typeof recentState === 'object'}
    <div class="history-gap" transition:expand>
      <span>Recent history could not be loaded</span>
      <button type="button" class="ghost-btn" disabled={recentRetrying} on:click={onRetryRecent}>
        {recentRetrying ? 'Loading…' : 'Retry'}
      </button>
    </div>
  {/if}
  {#if hasStart && start === 0}
    <p class="start-note" transition:expand>Beginning of history</p>
  {:else}
    <button type="button" class="ghost-btn" transition:expand disabled={paginating || recentState === 'Loading'} on:click={loadOlder}>
      {paginating ? 'Loading…' : 'Load older messages'}
    </button>
  {/if}
</div>
{#if grouped.length === 0}
  <p class="side-note timeline-empty" transition:expand>No messages yet</p>
{:else}
  {#each visible as g (g.key)}
    {#if g.item.kind === 'day'}
      <div class="day-row" data-row={g.key} in:rowIn|global out:rowOut|global><span>{formatDay(g.item.timestamp_ms)}</span></div>
    {:else if g.item.kind === 'readMarker'}
      <div class="new-row" data-row={g.key} in:rowIn|global out:rowOut|global><span>New</span></div>
    {:else if g.item.kind === 'start'}
      <p class="start-note" data-row={g.key} in:rowIn|global out:rowOut|global>Beginning of history</p>
    {:else if g.item.kind === 'historyGap'}
      {@const gap = g.item.gap}
      <div class="history-gap" data-row={g.key} in:rowIn|global out:rowOut|global>
        <span>{gap.failure ?? 'Some history is not loaded'}</span>
        <button type="button" class="ghost-btn" disabled={!!fillingGap || gap.lifecycle === 'Unavailable'} on:click={() => onFillGap(gap.id)}>
          {fillingGap === gap.id ? 'Loading…' : gap.lifecycle === 'Unavailable' ? 'History unavailable' : 'Fetch older history'}
        </button>
      </div>
    {:else if g.item.kind === 'unknown'}
      <p class="unknown-note" data-row={g.key} in:rowIn|global out:rowOut|global>Unsupported event ({g.item.label})</p>
    {:else}
      <MessageRow
        itemId={g.key}
        event={g.item.event}
        showSender={g.showSender}
        senderAvatar={senderAvatarFor(g.item.event.sender.user_id) ?? g.item.event.sender.avatar}
        {reacting}
        {onReact}
        {quickReactions}
        {packs}
        {linkPreviews}
        {loadPreview}
        {onQuickChange}
        {people}
        {self}
        {onUser}
        {onCloneSticker}
        {onMore}
        onOpenThread={joined || g.item.event.thread_summary ? onOpenThread : null}
        onReply={joined ? onReply : null}
        {onJump}
        onEdit={joined ? onEdit : null}
        editing={editingEventId !== null && editingEventId === g.item.event.event_id}
        showSent={g.item.event.item_id === lastSentId && pending.length === 0}
      />
    {/if}
  {/each}
{/if}
{#each tail ? pending : [] as p, i (p.id)}
  <MessageRow
    itemId={`pending-${p.id}`}
    event={pendingToEvent(p, self, selfName)}
    {onJump}
    showSender={i === 0 && !pendingFollowsSelf}
    senderAvatar={senderAvatarFor(self ?? '')}
    {reacting}
    {onReact}
    {quickReactions}
    {packs}
    {linkPreviews}
    {loadPreview}
    {onQuickChange}
  />
  {#if p.status === 'failed'}
    <div class="pending-actions" transition:expand>
      <span class="pending-error" role="alert"><EmojiText text={p.error ?? ''} /></span>
      <button type="button" class="ghost-btn" on:click={() => onRetryPending(p.id)}>Retry</button>
      <button type="button" class="ghost-btn danger" on:click={() => onDiscardPending(p.id)}>Discard</button>
    </div>
  {/if}
{/each}
