import {
  startTransition,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react'
import {useWindowDimensions, View} from 'react-native'
import Animated, {useAnimatedStyle} from 'react-native-reanimated'
import {moderatePost} from '@atproto/api'
import {Trans} from '@lingui/react/macro'

import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
import {usePostViewAuthorShadowFilter} from '#/state/cache/profile-shadow'
import {useFeedFeedback} from '#/state/feed-feedback'
import {useAlsoLikedCollapseByDefault} from '#/state/preferences/also-liked-collapse-by-default'
import {useAlsoLikedFeedEnabled} from '#/state/preferences/also-liked-feed-enabled'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {
  ALSO_LIKED_PAGE_SIZE,
  usePostAlsoLikedQuery,
} from '#/state/queries/post-also-liked'
import {type ThreadViewOption} from '#/state/queries/preferences/useThreadPreferences'
import {
  PostThreadContextProvider,
  type ThreadItem,
  usePostThread,
} from '#/state/queries/usePostThread'
import {useSession} from '#/state/session'
import {type OnPostSuccessData} from '#/state/shell/composer'
import {useShellLayout} from '#/state/shell/shell-layout'
import {useUnstablePostSource} from '#/state/unstable-post-source'
import {List, type ListMethods} from '#/view/com/util/List'
import {HeaderDropdown} from '#/screens/PostThread/components/HeaderDropdown'
import {ThreadAlsoLiked} from '#/screens/PostThread/components/ThreadAlsoLiked'
import {ThreadComposePrompt} from '#/screens/PostThread/components/ThreadComposePrompt'
import {ThreadError} from '#/screens/PostThread/components/ThreadError'
import {
  ThreadItemAnchor,
  ThreadItemAnchorSkeleton,
} from '#/screens/PostThread/components/ThreadItemAnchor'
import {ThreadItemAnchorNoUnauthenticated} from '#/screens/PostThread/components/ThreadItemAnchorNoUnauthenticated'
import {
  ThreadItemPost,
  ThreadItemPostSkeleton,
} from '#/screens/PostThread/components/ThreadItemPost'
import {ThreadItemPostNoUnauthenticated} from '#/screens/PostThread/components/ThreadItemPostNoUnauthenticated'
import {ThreadItemPostTombstone} from '#/screens/PostThread/components/ThreadItemPostTombstone'
import {ThreadItemReadMore} from '#/screens/PostThread/components/ThreadItemReadMore'
import {ThreadItemReadMoreUp} from '#/screens/PostThread/components/ThreadItemReadMoreUp'
import {ThreadItemReplyComposerSkeleton} from '#/screens/PostThread/components/ThreadItemReplyComposer'
import {ThreadItemShowOtherReplies} from '#/screens/PostThread/components/ThreadItemShowOtherReplies'
import {
  ThreadItemTreePost,
  ThreadItemTreePostSkeleton,
} from '#/screens/PostThread/components/ThreadItemTreePost'
import {atoms as a, native, platform, useBreakpoints, web} from '#/alf'
import * as Layout from '#/components/Layout'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'

const PARENT_CHUNK_SIZE = IS_NATIVE ? 5 : 20
const CHILDREN_CHUNK_SIZE = 50

export function PostThread({uri}: {uri: string}) {
  const ax = useAnalytics()
  const {gtMobile} = useBreakpoints()
  const {hasSession} = useSession()
  const initialNumToRender = useInitialNumToRender()
  const {height: windowHeight} = useWindowDimensions()
  const moderationOpts = useModerationOpts()
  const anchorPostSource = useUnstablePostSource(uri)
  const feedFeedback = useFeedFeedback(
    anchorPostSource?.feedSourceInfo,
    hasSession,
  )

  /*
   * One query to rule them all
   */
  const thread = usePostThread({anchor: uri})
  // eslint-disable-next-line react-hooks/preserve-manual-memoization -- restored memoization
  const {anchor, hasParents} = useMemo(() => {
    let hasParents = false
    for (const item of thread.data.items) {
      if (item.type === 'threadPost' && item.depth === 0) {
        return {anchor: item, hasParents}
      }
      hasParents = true
    }
    return {hasParents}
  }, [thread.data.items])

  // Track post:view event when anchor post is viewed
  const seenPostUriRef = useRef<string | null>(null)
  useEffect(() => {
    if (
      anchor?.type === 'threadPost' &&
      anchor.value.post.uri !== seenPostUriRef.current
    ) {
      const post = anchor.value.post
      seenPostUriRef.current = post.uri

      ax.metric('post:view', {
        uri: post.uri,
        authorDid: post.author.did,
        logContext: 'Post',
        feedDescriptor: feedFeedback.feedDescriptor,
      })
    }
  }, [ax, anchor, feedFeedback.feedDescriptor])

  // Track post:view events for parent posts and replies (non-anchor posts)
  const trackThreadItemView = usePostViewTracking('PostThreadItem')

  const {openComposer} = useOpenComposer()
  const optimisticOnPostReply = useNonReactiveCallback(
    (payload: OnPostSuccessData) => {
      if (payload) {
        const {replyToUri, posts} = payload
        if (replyToUri && posts.length) {
          thread.actions.insertReplies(replyToUri, posts)
        }
      }
    },
  )
  const onReplyToAnchor = useNonReactiveCallback(() => {
    if (anchor?.type !== 'threadPost') {
      return
    }
    const post = anchor.value.post
    openComposer({
      replyTo: {
        uri: anchor.uri,
        cid: post.cid,
        text: post.record.text,
        author: post.author,
        embed: post.embed,
        moderation: anchor.moderation,
        langs: post.record.langs,
      },
      onPostSuccess: optimisticOnPostReply,
      logContext: 'PostReply',
    })

    if (anchorPostSource) {
      feedFeedback.sendInteraction({
        item: post.uri,
        event: 'app.bsky.feed.defs#interactionReply',
        feedContext: anchorPostSource.post.feedContext,
        reqId: anchorPostSource.post.reqId,
      })
    }
  })

  const isRoot = !!anchor && anchor.value.post.record.reply === undefined
  const canReply = !anchor?.value.post?.viewer?.replyDisabled
  const alsoLikedFeedEnabled = useAlsoLikedFeedEnabled()
  const alsoLikedCollapseByDefault = useAlsoLikedCollapseByDefault()
  const alsoLikedAnchorUri =
    anchor?.type === 'threadPost' && isRoot ? anchor.value.post.uri : undefined
  const [deferParents, setDeferParents] = useState(true)
  const [alsoLikedCollapsed, setAlsoLikedCollapsed] = useState(
    alsoLikedCollapseByDefault,
  )
  const [maxAlsoLikedCount, setMaxAlsoLikedCount] =
    useState(ALSO_LIKED_PAGE_SIZE)
  useEffect(() => {
    setAlsoLikedCollapsed(alsoLikedCollapseByDefault)
  }, [alsoLikedAnchorUri, alsoLikedCollapseByDefault])
  useEffect(() => {
    setMaxAlsoLikedCount(ALSO_LIKED_PAGE_SIZE)
  }, [alsoLikedAnchorUri])
  const alsoLikedVisible =
    Boolean(alsoLikedAnchorUri) &&
    alsoLikedFeedEnabled &&
    !thread.state.isPlaceholderData &&
    !deferParents
  const alsoLikedEnabled = alsoLikedVisible && !alsoLikedCollapsed
  const alsoLiked = usePostAlsoLikedQuery(alsoLikedAnchorUri, {
    enabled: alsoLikedEnabled,
  })
  const alsoLikedPosts = useMemo(() => {
    const seen = new Set<string>()
    return (alsoLiked.data?.pages ?? [])
      .flatMap(page => page.posts)
      .filter(post => {
        if (seen.has(post.uri)) return false
        seen.add(post.uri)
        return true
      })
  }, [alsoLiked.data])
  const blockedOrMutedAlsoLikedAuthors =
    usePostViewAuthorShadowFilter(alsoLikedPosts)
  const filteredAlsoLikedPosts = useMemo(() => {
    return alsoLikedPosts.filter(post => {
      if (blockedOrMutedAlsoLikedAuthors.includes(post.author.did)) {
        return false
      }
      if (!moderationOpts) {
        return true
      }
      return !moderatePost(post, moderationOpts).ui('contentList').filter
    })
  }, [alsoLikedPosts, blockedOrMutedAlsoLikedAuthors, moderationOpts])
  const visibleAlsoLikedPosts = useMemo(() => {
    return filteredAlsoLikedPosts.slice(0, maxAlsoLikedCount)
  }, [filteredAlsoLikedPosts, maxAlsoLikedCount])
  const [maxParentCount, setMaxParentCount] = useState(PARENT_CHUNK_SIZE)
  const [maxChildrenCount, setMaxChildrenCount] = useState(CHILDREN_CHUNK_SIZE)
  const totalParentCount = useRef(0) // recomputed below
  const totalChildrenCount = useRef(thread.data.items.length) // recomputed below
  const listRef = useRef<ListMethods>(null)
  const anchorRef = useRef<View | null>(null)
  const headerRef = useRef<View | null>(null)
  const alsoLikedHeaderRef = useRef<View | null>(null)
  const currentScrollOffsetRef = useRef(0)
  const scrollStateRequestIdRef = useRef(0)
  const scrollStateAnimationFrameRef = useRef<number | null>(null)
  const contentSizeAnimationFrameRef = useRef<number | null>(null)
  const [isAlsoLikedFocused, setIsAlsoLikedFocused] = useState(false)

  useEffect(() => {
    if (!alsoLikedVisible || alsoLikedCollapsed) {
      setIsAlsoLikedFocused(false)
    }
  }, [alsoLikedCollapsed, alsoLikedVisible])

  useEffect(() => {
    if (alsoLikedCollapsed) {
      setMaxAlsoLikedCount(ALSO_LIKED_PAGE_SIZE)
    }
  }, [alsoLikedCollapsed])

  /*
   * On a cold load, parents are not prepended until the anchor post has
   * rendered as the first item in the list. This gives us a consistent
   * reference point for which to pin the anchor post to the top of the screen.
   *
   * We simulate a cold load any time the user changes the view or sort params
   * so that this handling is consistent.
   *
   * On native, `maintainVisibleContentPosition={{minIndexForVisible: 0}}` gives
   * us this for free, since the anchor post is the first item in the list.
   *
   * On web, `onContentSizeChange` is used to get ahead of next paint and handle
   * this scrolling.
   */
  /**
   * Used to flag whether we should scroll to the anchor post. On a cold load,
   * this is always true. And when a user changes thread parameters, we also
   * manually set this to true.
   */
  const shouldHandleScroll = useRef(true)
  /**
   * Called any time the content size of the list changes. Could be a fresh
   * render, items being added to the list, or any resize that changes the
   * scrollable size of the content.
   *
   * We want this to fire every time we change params (which will reset
   * `deferParents` via `onLayout` on the anchor post, due to the key change),
   * or click into a new post (which will result in a fresh `deferParents`
   * hook).
   *
   * The result being: any intentional change in view by the user will result
   * in the anchor being pinned as the first item.
   */
  const onContentSizeChangeWebOnly = web(
    useNonReactiveCallback(() => {
      if (contentSizeAnimationFrameRef.current !== null) {
        cancelAnimationFrame(contentSizeAnimationFrameRef.current)
      }

      contentSizeAnimationFrameRef.current = requestAnimationFrame(() => {
        contentSizeAnimationFrameRef.current = null
        const list = listRef.current
        const anchorElement = anchorRef.current as unknown as Element | null
        const header = headerRef.current as unknown as Element | null

        if (list && anchorElement && header && shouldHandleScroll.current) {
          const anchorOffsetTop = anchorElement.getBoundingClientRect().top
          const headerHeight = header.getBoundingClientRect().height

          /*
           * `deferParents` is `true` on a cold load, and always reset to
           * `true` when params change via `prepareForParamsUpdate`.
           *
           * On a cold load or a push to a new post, on the first pass of this
           * logic, the anchor post is the first item in the list. Therefore
           * `anchorOffsetTop - headerHeight` will be 0.
           *
           * When a user changes thread params, on the first pass of this logic,
           * the anchor post may not move (if there are no parents above it), or it
           * may have gone off the screen above, because of the sudden lack of
           * parents due to `deferParents === true`. This negative value (minus
           * `headerHeight`) will result in a _negative_ `offset` value, which will
           * scroll the anchor post _down_ to the top of the screen.
           *
           * However, `prepareForParamsUpdate` also resets scroll to `0`, so when a user
           * changes params, the anchor post's offset will actually be equivalent
           * to the `headerHeight` because of how the DOM is stacked on web.
           * Therefore, `anchorOffsetTop - headerHeight` will once again be 0,
           * which means the first pass in this case will result in no scroll.
           *
           * Then, once parents are prepended, this will fire again. Now, the
           * `anchorOffsetTop` will be positive, which minus the header height,
           * will give us a _positive_ offset, which will scroll the anchor post
           * back _up_ to the top of the screen.
           */
          const offset = anchorOffsetTop - headerHeight
          list.scrollToOffset({offset})

          /*
           * After we manage to do a positive adjustment, we need to ensure this
           * doesn't run again until scroll handling is requested again via
           * `shouldHandleScroll.current === true` and a params change via
           * `prepareForParamsUpdate`.
           *
           * The `isRoot` here is needed because if we're looking at the anchor
           * post, this handler will not fire after `deferParents` is set to
           * `false`, since there are no parents to render above it. In this case,
           * we want to make sure `shouldHandleScroll` is set to `false` right away
           * so that subsequent size changes unrelated to a params change (like
           * pagination) do not affect scroll.
           */
          if (offset > 0 || isRoot) shouldHandleScroll.current = false
        }
      })
    }),
  )

  /**
   * Ditto the above, but for native.
   */
  const onContentSizeChangeNativeOnly = native(() => {
    const list = listRef.current
    const anchorElement = anchorRef.current

    if (list && anchorElement && shouldHandleScroll.current) {
      /*
       * `prepareForParamsUpdate` is called any time the user changes thread params like
       * `view` or `sort`, which sets `deferParents(true)` and resets the
       * scroll to the top of the list. However, there is a split second
       * where the top of the list is wherever the parents _just were_. So if
       * there were parents, the anchor is not at the top of the list just
       * prior to this handler being called.
       *
       * Once this handler is called, the anchor post is the first item in
       * the list (because of `deferParents` being `true`), and so we can
       * synchronously scroll the list back to the top of the list (which is
       * 0 on native, no need to handle `headerHeight`).
       */
      list.scrollToOffset({
        animated: false,
        offset: 0,
      })

      /*
       * After this first pass, `deferParents` will be `false`, and those
       * will render in. However, the anchor post will retain its position
       * because of `maintainVisibleContentPosition` handling on native. So we
       * don't need to let this handler run again, like we do on web.
       */
      shouldHandleScroll.current = false
    }
  })

  /**
   * Called any time the user changes thread params, such as `view` or `sort`.
   * Prepares the UI for repositioning of the scroll so that the anchor post is
   * always at the top after a params change.
   *
   * No need to handle max parents here, deferParents will handle that and we
   * want it to re-render with the same items above the anchor.
   */
  const prepareForParamsUpdate = useCallback(() => {
    /**
     * Truncate list so that anchor post is the first item in the list. Manual
     * scroll handling on web is predicated on this, and on native, this allows
     * `maintainVisibleContentPosition` to do its thing.
     */
    setDeferParents(true)
    // reset this to a lower value for faster re-render
    setMaxChildrenCount(CHILDREN_CHUNK_SIZE)
    // set flag
    shouldHandleScroll.current = true
  }, [setDeferParents, setMaxChildrenCount])

  const setSortWrapped = useCallback(
    (sort: string) => {
      prepareForParamsUpdate()
      thread.actions.setSort(sort)
    },
    [thread, prepareForParamsUpdate],
  )

  const setViewWrapped = useCallback(
    (view: ThreadViewOption) => {
      prepareForParamsUpdate()
      thread.actions.setView(view)
    },
    [thread, prepareForParamsUpdate],
  )

  const onStartReached = () => {
    if (thread.state.isFetching) return
    // can be true after `prepareForParamsUpdate` is called
    if (deferParents) return
    // prevent any state mutations if we know we're done
    if (maxParentCount >= totalParentCount.current) return
    setMaxParentCount(n => n + PARENT_CHUNK_SIZE)
  }

  const onEndReached = () => {
    if (thread.state.isFetching) return
    // can be true after `prepareForParamsUpdate` is called
    if (deferParents) return
    if (maxChildrenCount < totalChildrenCount.current) {
      setMaxChildrenCount(prev => prev + CHILDREN_CHUNK_SIZE)
      return
    }
    if (alsoLikedAnchorUri && alsoLikedEnabled) {
      if (visibleAlsoLikedPosts.length < filteredAlsoLikedPosts.length) {
        setMaxAlsoLikedCount(current =>
          Math.min(
            current + ALSO_LIKED_PAGE_SIZE,
            filteredAlsoLikedPosts.length,
          ),
        )
        return
      }

      if (
        !alsoLiked.isLoading &&
        !alsoLiked.isFetchingNextPage &&
        alsoLiked.hasNextPage
      ) {
        void alsoLiked.fetchNextPage()
      }
    }
  }

  const slices = useMemo(() => {
    const results: ThreadItem[] = []

    if (!thread.data.items.length) return results

    /*
     * Pagination hack, tracks the # of items below the anchor post.
     */
    let childrenCount = 0

    for (let i = 0; i < thread.data.items.length; i++) {
      const item = thread.data.items[i]
      /*
       * Need to check `depth`, since not found or blocked posts are not
       * `threadPost`s, but still have `depth`.
       */
      const hasDepth = 'depth' in item

      /*
       * Handle anchor post.
       */
      if (hasDepth && item.depth === 0) {
        results.push(item)

        // Recalculate total parents current index.
        totalParentCount.current = i
        // Recalculate total children using (length - 1) - current index.
        totalChildrenCount.current = thread.data.items.length - 1 - i

        /*
         * Walk up the parents, limiting by `maxParentCount`
         */
        if (!deferParents) {
          const start = i - 1
          if (start >= 0) {
            const limit = Math.max(0, start - maxParentCount)
            for (let pi = start; pi >= limit; pi--) {
              results.unshift(thread.data.items[pi])
            }
          }
        }
      } else {
        // ignore any parent items
        if (item.type === 'readMoreUp' || (hasDepth && item.depth < 0)) continue
        // can exit early if we've reached the max children count
        if (childrenCount > maxChildrenCount) break

        results.push(item)
        childrenCount++
      }
    }

    return results
  }, [thread, deferParents, maxParentCount, maxChildrenCount])

  /**
   * Defer rendering reply skeletons so that the anchor post (from cache)
   * can paint without being blocked by skeleton layout work. On mount,
   * skeletons are filtered out. After the first render, they're added
   * back via a low-priority transition.
   */
  const [showReplySkeletons, setShowReplySkeletons] = useState(false)
  useEffect(() => {
    if (thread.state.isPlaceholderData && !showReplySkeletons) {
      startTransition(() => {
        setShowReplySkeletons(true)
      })
    }
  }, [thread.state.isPlaceholderData, showReplySkeletons])

  const deferredSlices = useMemo(() => {
    if (showReplySkeletons) return slices
    return slices.filter(
      item => !(item.type === 'skeleton' && item.item === 'reply'),
    )
  }, [slices, showReplySkeletons])

  const isTombstoneView = useMemo(() => {
    if (deferredSlices.length > 1) return false
    return deferredSlices.every(
      s => s.type === 'threadPostBlocked' || s.type === 'threadPostNotFound',
    )
  }, [deferredSlices])

  const renderItem = useCallback(
    ({item, index}: {item: ThreadItem; index: number}) => {
      if (item.type === 'threadPost') {
        if (item.depth < 0) {
          return (
            <ThreadItemPost
              item={item}
              threadgateRecord={thread.data.threadgate?.record ?? undefined}
              overrides={{
                topBorder: index === 0,
              }}
              onPostSuccess={optimisticOnPostReply}
            />
          )
        } else if (item.depth === 0) {
          return (
            /*
             * Keep this view wrapped so that the anchor post is always index 0
             * in the list and `maintainVisibleContentPosition` can do its
             * thing.
             */
            <View collapsable={false}>
              <View
                /*
                 * IMPORTANT: this is a load-bearing key on all platforms. We
                 * want to force `onLayout` to fire any time the thread params
                 * change so that `deferParents` is always reset to `false` once
                 * the anchor post is rendered.
                 *
                 * If we ever add additional thread params to this screen, they
                 * will need to be added here.
                 */
                key={item.uri + thread.state.view + thread.state.sort}
                ref={anchorRef}
                onLayout={() => setDeferParents(false)}
              />
              <ThreadItemAnchor
                item={item}
                threadgateRecord={thread.data.threadgate?.record ?? undefined}
                onPostSuccess={optimisticOnPostReply}
                postSource={anchorPostSource}
              />
            </View>
          )
        } else {
          if (thread.state.view === 'tree') {
            return (
              <ThreadItemTreePost
                item={item}
                threadgateRecord={thread.data.threadgate?.record ?? undefined}
                overrides={{
                  moderation: thread.state.otherItemsVisible && item.depth > 0,
                }}
                onPostSuccess={optimisticOnPostReply}
              />
            )
          } else {
            return (
              <ThreadItemPost
                item={item}
                threadgateRecord={thread.data.threadgate?.record ?? undefined}
                overrides={{
                  moderation: thread.state.otherItemsVisible && item.depth > 0,
                }}
                onPostSuccess={optimisticOnPostReply}
              />
            )
          }
        }
      } else if (item.type === 'threadPostNoUnauthenticated') {
        if (item.depth < 0) {
          return <ThreadItemPostNoUnauthenticated item={item} />
        } else if (item.depth === 0) {
          return <ThreadItemAnchorNoUnauthenticated />
        }
      } else if (item.type === 'readMore') {
        return (
          <ThreadItemReadMore
            item={item}
            view={thread.state.view === 'tree' ? 'tree' : 'linear'}
          />
        )
      } else if (item.type === 'readMoreUp') {
        return <ThreadItemReadMoreUp item={item} />
      } else if (item.type === 'threadPostBlocked') {
        return <ThreadItemPostTombstone type="blocked" />
      } else if (item.type === 'threadPostNotFound') {
        return <ThreadItemPostTombstone type="not-found" />
      } else if (item.type === 'replyComposer') {
        return (
          <View>
            {gtMobile && (
              <ThreadComposePrompt onPressCompose={onReplyToAnchor} />
            )}
          </View>
        )
      } else if (item.type === 'showOtherReplies') {
        return <ThreadItemShowOtherReplies onPress={item.onPress} />
      } else if (item.type === 'skeleton') {
        if (item.item === 'anchor') {
          return <ThreadItemAnchorSkeleton />
        } else if (item.item === 'reply') {
          if (thread.state.view === 'linear') {
            return <ThreadItemPostSkeleton index={index} />
          } else {
            return <ThreadItemTreePostSkeleton index={index} />
          }
        } else if (item.item === 'replyComposer') {
          return <ThreadItemReplyComposerSkeleton />
        }
      }
      return null
    },
    [
      thread,
      optimisticOnPostReply,
      onReplyToAnchor,
      gtMobile,
      anchorPostSource,
    ],
  )

  const defaultListFooterHeight = hasParents ? windowHeight - 200 : undefined
  const retryAlsoLiked = useCallback(() => {
    if (alsoLikedPosts.length > 0) {
      void alsoLiked.fetchNextPage()
    } else {
      void alsoLiked.refetch()
    }
  }, [alsoLiked, alsoLikedPosts.length])
  const toggleAlsoLikedCollapsed = useCallback(() => {
    setAlsoLikedCollapsed(current => !current)
  }, [])
  const runAlsoLikedScrollStateUpdate = useNonReactiveCallback(
    (requestId: number) => {
      scrollStateAnimationFrameRef.current = null

      if (
        !alsoLikedVisible ||
        alsoLikedCollapsed ||
        !alsoLikedHeaderRef.current ||
        !headerRef.current
      ) {
        setIsAlsoLikedFocused(false)
        return
      }

      measureViewRect(headerRef.current, headerRect => {
        if (requestId !== scrollStateRequestIdRef.current) return
        if (!headerRect) {
          setIsAlsoLikedFocused(false)
          return
        }

        measureViewRect(alsoLikedHeaderRef.current, alsoLikedRect => {
          if (requestId !== scrollStateRequestIdRef.current) return
          if (!alsoLikedRect) {
            setIsAlsoLikedFocused(false)
            return
          }

          const headerBottom = headerRect.y + headerRect.height
          const focused = alsoLikedRect.y <= headerBottom

          setIsAlsoLikedFocused(current =>
            current === focused ? current : focused,
          )
        })
      })
    },
  )
  const scheduleAlsoLikedScrollStateUpdate = useNonReactiveCallback(() => {
    if (scrollStateAnimationFrameRef.current !== null) {
      cancelAnimationFrame(scrollStateAnimationFrameRef.current)
    }

    const requestId = scrollStateRequestIdRef.current
    scrollStateAnimationFrameRef.current = requestAnimationFrame(() => {
      runAlsoLikedScrollStateUpdate(requestId)
    })
  })
  const handleScrollOffsetChange = useNonReactiveCallback((offsetY: number) => {
    currentScrollOffsetRef.current = offsetY
    if (offsetY <= 1) {
      scrollStateRequestIdRef.current += 1
      setIsAlsoLikedFocused(false)
      return
    }
    if (!alsoLikedVisible || alsoLikedCollapsed) {
      return
    }
    scrollStateRequestIdRef.current += 1
    scheduleAlsoLikedScrollStateUpdate()
  })

  useEffect(() => {
    return () => {
      if (scrollStateAnimationFrameRef.current !== null) {
        cancelAnimationFrame(scrollStateAnimationFrameRef.current)
      }
      if (contentSizeAnimationFrameRef.current !== null) {
        cancelAnimationFrame(contentSizeAnimationFrameRef.current)
      }
    }
  }, [])

  useEffect(() => {
    scrollStateRequestIdRef.current += 1
    scheduleAlsoLikedScrollStateUpdate()
  }, [
    alsoLikedCollapsed,
    alsoLikedPosts.length,
    alsoLikedVisible,
    scheduleAlsoLikedScrollStateUpdate,
  ])

  return (
    <PostThreadContextProvider context={thread.context}>
      <Layout.Header.Outer headerRef={headerRef}>
        <Layout.Header.BackButton />
        <Layout.Header.Content>
          <Layout.Header.TitleText>
            {isAlsoLikedFocused ? (
              <Trans>Posts also liked</Trans>
            ) : (
              <Trans context="description">Post</Trans>
            )}
          </Layout.Header.TitleText>
        </Layout.Header.Content>
        <Layout.Header.Slot>
          <HeaderDropdown
            sort={thread.state.sort}
            setSort={setSortWrapped}
            view={thread.state.view}
            setView={setViewWrapped}
          />
        </Layout.Header.Slot>
      </Layout.Header.Outer>

      {thread.state.error ? (
        <ThreadError
          error={thread.state.error}
          onRetry={thread.actions.refetch}
        />
      ) : (
        <List
          ref={listRef}
          data={deferredSlices}
          renderItem={renderItem}
          keyExtractor={keyExtractor}
          onContentSizeChange={platform({
            web: onContentSizeChangeWebOnly,
            default: onContentSizeChangeNativeOnly,
          })}
          onStartReached={onStartReached}
          onEndReached={onEndReached}
          onEndReachedThreshold={2}
          onStartReachedThreshold={1}
          onScrollOffsetChange={handleScrollOffsetChange}
          onItemSeen={item => {
            // Track post:view for parent posts and replies (non-anchor posts)
            if (item.type === 'threadPost' && item.depth !== 0) {
              trackThreadItemView(item.value.post)
            }
          }}
          /**
           * NATIVE ONLY
           * {@link https://reactnative.dev/docs/scrollview#maintainvisiblecontentposition}
           */
          maintainVisibleContentPosition={{minIndexForVisible: 0}}
          desktopFixedHeight
          sideBorders={false}
          ListFooterComponent={
            <ThreadAlsoLiked
              posts={visibleAlsoLikedPosts}
              visible={alsoLikedVisible}
              collapsed={alsoLikedCollapsed}
              isLoading={alsoLiked.isLoading}
              showLoadingState={
                !alsoLikedCollapsed &&
                !alsoLiked.error &&
                visibleAlsoLikedPosts.length === 0 &&
                (!alsoLiked.isFetched ||
                  alsoLiked.isLoading ||
                  (alsoLiked.hasNextPage && alsoLiked.isFetchingNextPage))
              }
              isFetchingNextPage={alsoLiked.isFetchingNextPage}
              error={alsoLiked.error}
              onRetry={retryAlsoLiked}
              headerRef={alsoLikedHeaderRef}
              onToggleCollapsed={toggleAlsoLikedCollapsed}
              spacerHeight={platform({
                web: defaultListFooterHeight,
                default: deferParents
                  ? windowHeight * 2
                  : defaultListFooterHeight,
              })}
              isTombstoneView={isTombstoneView}
            />
          }
          initialNumToRender={initialNumToRender}
          /**
           * Default: 21
           *
           * Smaller for placeholder data so we don't waste time rendering skeletons
           */
          windowSize={thread.state.isPlaceholderData ? 1 : 7}
          /**
           * Default: 10
           */
          maxToRenderPerBatch={5}
          /**
           * Default: 50
           */
          updateCellsBatchingPeriod={100}
        />
      )}

      {!gtMobile && canReply && hasSession && (
        <MobileComposePrompt onPressReply={onReplyToAnchor} />
      )}
    </PostThreadContextProvider>
  )
}

function MobileComposePrompt({onPressReply}: {onPressReply: () => unknown}) {
  const {footerHeight} = useShellLayout()

  const animatedStyle = useAnimatedStyle(() => {
    return {
      bottom: footerHeight.get(),
    }
  })

  return (
    <Animated.View style={[a.fixed, a.left_0, a.right_0, animatedStyle]}>
      <ThreadComposePrompt onPressCompose={onPressReply} />
    </Animated.View>
  )
}

const keyExtractor = (item: ThreadItem) => {
  return item.key
}

type ViewRect = {
  x: number
  y: number
  width: number
  height: number
}

function measureViewRect(
  view: View | null,
  cb: (rect: ViewRect | null) => void,
) {
  const target = view as
    | (View & {
        measureInWindow?: (
          callback: (
            x: number,
            y: number,
            width: number,
            height: number,
          ) => void,
        ) => void
        getBoundingClientRect?: () => DOMRect
        measure?: (
          callback: (
            x: number,
            y: number,
            width: number,
            height: number,
            pageX: number,
            pageY: number,
          ) => void,
        ) => void
      })
    | null
  if (!target) {
    cb(null)
    return
  }

  if (typeof target.measureInWindow === 'function') {
    target.measureInWindow(
      (x: number, y: number, width: number, height: number) => {
        cb({x, y, width, height})
      },
    )
    return
  }

  if (typeof target.getBoundingClientRect === 'function') {
    const rect = target.getBoundingClientRect()
    cb({
      x: rect.left,
      y: rect.top,
      width: rect.width,
      height: rect.height,
    })
    return
  }

  if (typeof target.measure === 'function') {
    target.measure(
      (
        _x: number,
        _y: number,
        width: number,
        height: number,
        pageX: number,
        pageY: number,
      ) => {
        cb({x: pageX, y: pageY, width, height})
      },
    )
    return
  }

  cb(null)
}
