<script lang="ts">
  import {onMount} from "svelte"
  import {page} from "$app/stores"
  import {goto} from "$app/navigation"
  import type {Readable} from "svelte/store"
  import {readable} from "svelte/store"
  import {debounce} from "throttle-debounce"
  import {now, int, ifLet, formatTimestampAsDate, MINUTE, ago} from "@welshman/lib"
  import type {TrustedEvent, EventContent} from "@welshman/util"
  import {makeEvent, MESSAGE, RELAY_ADD_MEMBER} from "@welshman/util"
  import {pubkey, publishThunk} from "@welshman/app"
  import {fade, fly} from "@lib/transition"
  import ChatRound from "@assets/icons/chat-round.svg?dataurl"
  import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
  import Icon from "@lib/components/Icon.svelte"
  import Button from "@lib/components/Button.svelte"
  import Spinner from "@lib/components/Spinner.svelte"
  import PageContent from "@lib/components/PageContent.svelte"
  import Divider from "@lib/components/Divider.svelte"
  import ThunkToast from "@app/components/ThunkToast.svelte"
  import SpaceBar from "@app/components/SpaceBar.svelte"
  import SpaceSearch from "@app/components/SpaceSearch.svelte"
  import RoomItem from "@app/components/RoomItem.svelte"
  import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"

  import RoomCompose from "@app/components/RoomCompose.svelte"
  import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte"
  import RoomComposeParent from "@app/components/RoomComposeParent.svelte"
  import {userSettingsValues, decodeRelay, PROTECTED} from "@app/core/state"
  import {prependParent, canEnforceNip70, publishDelete} from "@app/core/commands"
  import {checked} from "@app/util/notifications"
  import {pushToast} from "@app/util/toast"
  import {makeFeed} from "@app/core/requests"
  import {popKey} from "@lib/implicit"

  const mounted = now()
  const lastChecked = $checked[$page.url.pathname]
  const url = decodeRelay($page.params.relay!)
  const shouldProtect = canEnforceNip70(url)
  const at = $derived(parseInt($page.url.searchParams.get("at")!))

  const replyTo = (event: TrustedEvent) => {
    parent = event
    compose?.focus()
  }

  const clearParent = () => {
    parent = undefined
  }

  const clearEventToEdit = () => {
    eventToEdit = undefined
  }

  const clearShare = () => {
    share = undefined
  }

  const onSubmit = async ({content, tags}: EventContent) => {
    if (!content && !share) {
      return
    }

    try {
      let template: EventContent & {created_at?: number} = {content, tags}

      if (eventToEdit) {
        // Don't do anything if message hasn't changed
        if (eventToEdit.content === content) {
          return
        }

        // Delete previous message, to be republished with same timestamp
        template.created_at = eventToEdit.created_at
        publishDelete({relays: [url], event: eventToEdit, protect: await shouldProtect})
      }

      if (await shouldProtect) {
        tags.push(PROTECTED)
      }

      if (share) {
        template = prependParent(share, template, url)
      }

      if (parent) {
        template = prependParent(parent, template, url)
      }

      const thunk = publishThunk({
        relays: [url],
        event: makeEvent(MESSAGE, template),
        delay: $userSettingsValues.send_delay,
      })

      if ($userSettingsValues.send_delay) {
        pushToast({
          timeout: 30_000,
          children: {
            component: ThunkToast,
            props: {thunk},
          },
        })
      }
    } finally {
      clearParent()
      clearShare()
      clearEventToEdit()
    }
  }

  const manageScrollPosition = () => {
    showScrollButton = !isNaN(at) || Math.abs(element?.scrollTop || 0) > 1500

    const newMessages = document.getElementById("new-messages")

    if (newMessagesSeen) {
      showFixedNewMessages = false
    } else if (newMessages) {
      const {y} = newMessages.getBoundingClientRect()

      if (y > 0 && y < 300) {
        newMessagesSeen = true
        showFixedNewMessages = false
      } else {
        showFixedNewMessages = y < 0
      }
    }

    if (!userHasScrolled && !isNaN(at)) {
      const targetEvent = $events.find(event => event.created_at >= at)

      if (targetEvent) {
        const target = element?.querySelector(`[data-event="${targetEvent.id}"]`)

        if (target instanceof HTMLElement) {
          isProgrammaticScroll = true
          target.scrollIntoView({block: "center"})
        }
      }
    }
  }

  const onScroll = () => {
    if (!isProgrammaticScroll) {
      userHasScrolled = true
      isUserScrolling = true
      clearIsUserScrolling()
      manageScrollPosition()
    }

    isProgrammaticScroll = false
  }

  const scrollToNewMessages = () =>
    document.getElementById("new-messages")?.scrollIntoView({behavior: "smooth", block: "center"})

  const scrollToBottom = () => {
    if (!isNaN(at)) {
      goto($page.url.pathname, {replaceState: true})
    } else {
      element?.scrollTo({top: 0, behavior: "smooth"})
    }
  }

  let loadingBackward = $state(true)
  let loadingForward = $state(true)
  let userHasScrolled = $state(false)
  let isProgrammaticScroll = $state(false)
  let isUserScrolling = $state(false)
  let share = $state(popKey<TrustedEvent | undefined>("share"))
  let parent: TrustedEvent | undefined = $state()
  let element: HTMLElement | undefined = $state()
  let newMessagesSeen = false
  let showFixedNewMessages = $state(false)
  let showScrollButton = $state(false)
  let cleanup: () => void
  let events: Readable<TrustedEvent[]> = $state(readable([]))
  let compose: RoomCompose | undefined = $state()
  let eventToEdit: TrustedEvent | undefined = $state()

  const clearIsUserScrolling = debounce(150, () => {
    isUserScrolling = false
  })

  const elements = $derived.by(() => {
    const elements = []
    const seen = new Set()

    let previousDate
    let previousKind
    let previousPubkey
    let previousCreatedAt = 0
    let newMessagesSeen = false

    if (events) {
      const lastUserEvent = $events.find(e => e.pubkey === $pubkey)

      // Adjust last checked to account for messages that came from a different device
      const adjustedLastChecked =
        lastChecked && lastUserEvent ? Math.max(lastUserEvent.created_at, lastChecked) : lastChecked

      for (const event of $events) {
        if (seen.has(event.id)) {
          continue
        }

        const date = formatTimestampAsDate(event.created_at)

        if (
          !newMessagesSeen &&
          adjustedLastChecked &&
          event.pubkey !== $pubkey &&
          event.created_at > adjustedLastChecked &&
          event.created_at < mounted
        ) {
          elements.push({type: "new-messages", id: "new-messages"})
          newMessagesSeen = true
        }

        if (date !== previousDate) {
          elements.push({type: "date", value: date, id: date, showPubkey: false})
        }

        const showPubkey =
          previousPubkey !== event.pubkey ||
          event.created_at - previousCreatedAt > int(3, MINUTE) ||
          previousKind === RELAY_ADD_MEMBER

        if (showPubkey && elements.length > 0) {
          elements[elements.length - 1].addSpaceBelow = true
        }

        elements.push({
          id: event.id,
          type: "note",
          value: event,
          showPubkey,
          addSpaceBelow: false,
        })

        previousDate = date
        previousKind = event.kind
        previousPubkey = event.pubkey
        previousCreatedAt = event.created_at
        seen.add(event.id)
      }
      if (elements.length > 0) {
        elements[elements.length - 1].addSpaceBelow = true
      }
    }

    elements.reverse()

    return elements
  })

  $effect(() => {
    if (elements.length > 0 && !isUserScrolling) {
      requestAnimationFrame(manageScrollPosition)
    }
  })

  const start = () => {
    cleanup?.()

    const feed = makeFeed({
      url,
      at: at || now(),
      element: element!,
      filters: [{kinds: [MESSAGE, RELAY_ADD_MEMBER]}],
      onBackwardExhausted: () => {
        loadingBackward = false
      },
      onForwardExhausted: () => {
        loadingForward = false
      },
    })

    events = feed.events
    cleanup = feed.cleanup
  }

  const onEscape = () => {
    clearParent()
    clearShare()
    eventToEdit = undefined
  }

  const canEditEvent = (event: TrustedEvent) =>
    event.pubkey === $pubkey && event.created_at >= ago(5, MINUTE)

  const onEditEvent = (event: TrustedEvent) => {
    clearParent()
    clearShare()
    eventToEdit = event
  }

  const onEditPrevious = () => ifLet($events.toReversed().find(canEditEvent), onEditEvent)

  onMount(() => {
    start()

    // Wrap in a closure to avoid calling a stale cleanup function
    return () => cleanup?.()
  })
</script>

<SpaceBar>
  {#snippet title()}
    <Icon icon={ChatRound} />
    <strong>Chat</strong>
  {/snippet}
  {#snippet action()}
    <SpaceSearch {url} />
  {/snippet}
</SpaceBar>

<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4 pb-0!">
  {#if loadingForward}
    <p class="py-20 flex justify-center">
      <Spinner loading={loadingForward}>Looking for messages...</Spinner>
    </p>
  {/if}
  {#each elements as { type, id, value, showPubkey, addSpaceBelow }, i (id)}
    {#if type === "new-messages"}
      <div
        {id}
        class="flex items-center py-2 text-xs transition-colors"
        class:opacity-0={showFixedNewMessages}>
        <div class="h-px grow bg-primary"></div>
        <p class="rounded-full bg-primary px-2 py-1 text-primary-content">New Messages</p>
        <div class="h-px grow bg-primary"></div>
      </div>
    {:else if type === "date"}
      <Divider>{value}</Divider>
    {:else}
      {@const event = value as TrustedEvent}
      {#if event.kind === RELAY_ADD_MEMBER}
        <RoomItemAddMember {url} {event} />
      {:else}
        <RoomItem
          {url}
          {event}
          {replyTo}
          {showPubkey}
          canEdit={canEditEvent}
          onEdit={onEditEvent}
          {addSpaceBelow} />
      {/if}
    {/if}
  {/each}
  <p class="flex h-10 items-center justify-center py-20">
    {#if loadingBackward}
      <Spinner loading={loadingBackward}>Looking for messages...</Spinner>
    {:else}
      <Spinner>End of message history</Spinner>
    {/if}
  </p>
  <div class="h-screen"></div>
</PageContent>

<div class="chat__compose bg-base-200">
  <div>
    {#if parent}
      <RoomComposeParent event={parent} clear={clearParent} verb="Replying to" />
    {/if}
    {#if share}
      <RoomComposeParent event={share} clear={clearShare} verb="Sharing" />
    {/if}
    {#if eventToEdit}
      <RoomComposeEdit clear={clearEventToEdit} />
    {/if}
  </div>
  {#key eventToEdit}
    <RoomCompose
      {url}
      {onSubmit}
      {onEscape}
      {onEditPrevious}
      initialValues={eventToEdit}
      bind:this={compose} />
  {/key}
</div>

{#if showScrollButton}
  <div in:fade class="chat__scroll-down">
    <Button class="btn btn-circle btn-neutral" onclick={scrollToBottom}>
      <Icon icon={AltArrowDown} />
    </Button>
  </div>
{/if}

{#if showFixedNewMessages}
  <div class="relative z-popover flex justify-center">
    <div transition:fly={{duration: 200}} class="fixed top-12 mt-sai">
      <Button class="btn btn-primary btn-xs rounded-full" onclick={scrollToNewMessages}>
        New Messages
      </Button>
    </div>
  </div>
{/if}
