---
import { ScrollArea } from '@/components/ui/scroll-area'
import type { TOCSection } from '@/lib/data-utils'
import { getParentId, isSubpost } from '@/lib/data-utils'
import { cn, getHeadingMargin } from '@/lib/utils'

type Props = {
  sections: TOCSection[]
  currentPostId: string
}

const { sections, currentPostId } = Astro.props
const isCurrentSubpost = isSubpost(currentPostId)
const parentId = isCurrentSubpost ? getParentId(currentPostId) : currentPostId
---

{
  sections.length > 0 && (
    <div
      id="toc-sidebar-container"
      class="sticky top-20 col-start-1 row-span-1 mr-8 ml-auto hidden h-[calc(100vh-5rem)] max-w-md xl:block"
    >
      <ScrollArea
        client:load
        className="flex max-h-[calc(100vh-8rem)] flex-col overflow-y-auto"
        type="hover"
        data-toc-scroll-area
      >
        <div class="flex flex-col gap-2 px-4">
          <span class="text-lg font-medium">Table of Contents</span>
          {sections.map((section, index) => {
            const isFirstSubpost =
              section.type === 'subpost' &&
              (index === 0 || sections[index - 1].type === 'parent')

            return (
              <>
                {isFirstSubpost && (
                  <div class="mt-2 flex items-center gap-2">
                    <div class="bg-border h-px flex-1" />
                    <span class="text-muted-foreground text-xs font-medium">
                      Subposts
                    </span>
                    <div class="bg-border h-px flex-1" />
                  </div>
                )}

                {section.type === 'parent' ? (
                  <ul class="flex list-none flex-col gap-y-2">
                    {section.headings.map((heading) => (
                      <li
                        class={cn(
                          'text-sm',
                          getHeadingMargin(heading.depth),
                          isCurrentSubpost
                            ? 'text-foreground/40'
                            : 'text-foreground/60',
                        )}
                      >
                        <a
                          href={
                            isCurrentSubpost
                              ? `/blog/${parentId}#${heading.slug}`
                              : `#${heading.slug}`
                          }
                          class="marker:text-foreground/30 list-none underline decoration-transparent underline-offset-[3px] transition-colors duration-200 hover:decoration-inherit"
                          data-heading-link={heading.slug}
                        >
                          {heading.text}
                        </a>
                      </li>
                    ))}
                  </ul>
                ) : (
                  <div
                    class={cn(
                      'rounded-md border p-2',
                      section.subpostId === currentPostId ? 'bg-muted/50' : '',
                    )}
                  >
                    <ul class="flex list-none flex-col gap-y-2">
                      <li
                        class={cn(
                          'text-xs font-medium',
                          section.subpostId === currentPostId
                            ? 'text-foreground'
                            : 'text-foreground/60',
                        )}
                      >
                        <a
                          href={
                            section.subpostId === currentPostId
                              ? '#'
                              : `/blog/${section.subpostId}`
                          }
                          class="marker:text-foreground/30 list-none underline decoration-transparent underline-offset-[3px] transition-colors duration-200 hover:decoration-inherit"
                          data-heading-link={
                            section.subpostId === currentPostId
                              ? 'top'
                              : `${section.subpostId}-top`
                          }
                        >
                          {section.title}
                        </a>
                      </li>
                      {section.headings.map((heading) => (
                        <li
                          class={cn(
                            'text-xs',
                            getHeadingMargin(heading.depth),
                            section.subpostId === currentPostId
                              ? 'text-foreground/60'
                              : 'text-foreground/30',
                          )}
                        >
                          <a
                            href={
                              section.subpostId === currentPostId
                                ? `#${heading.slug}`
                                : `/blog/${section.subpostId}#${heading.slug}`
                            }
                            class="marker:text-foreground/30 hover:text-foreground/60 list-none underline decoration-transparent underline-offset-[3px] transition-colors duration-200 hover:decoration-inherit"
                            data-heading-link={
                              section.subpostId === currentPostId
                                ? heading.slug
                                : `${section.subpostId}-${heading.slug}`
                            }
                          >
                            {heading.text}
                          </a>
                        </li>
                      ))}
                    </ul>
                  </div>
                )}
              </>
            )
          })}
        </div>
      </ScrollArea>
    </div>
  )
}

<script>
  const HEADER_OFFSET = 150

  class TOCState {
    links: NodeListOf<Element> = document.querySelectorAll(
      '[data-heading-link]',
    )
    activeIds: string[] = []
    headings: HTMLElement[] = []
    regions: { id: string; start: number; end: number }[] = []
    scrollArea: HTMLElement | null = null
    tocScrollArea: HTMLElement | null = null

    reset() {
      this.links = document.querySelectorAll(
        '#toc-sidebar-container [data-heading-link]',
      )
      this.activeIds = []
      this.headings = []
      this.regions = []
      const tocContainer = document.getElementById('toc-sidebar-container')
      this.scrollArea =
        tocContainer?.querySelector('[data-radix-scroll-area-viewport]') || null
      this.tocScrollArea =
        tocContainer?.querySelector('[data-toc-scroll-area]') || null
    }
  }

  const state = new TOCState()

  class HeadingRegions {
    static build() {
      state.headings = Array.from(
        document.querySelectorAll<HTMLElement>(
          '.prose h2, .prose h3, .prose h4, .prose h5, .prose h6',
        ),
      )

      if (state.headings.length === 0) {
        state.regions = []
        return
      }

      state.regions = state.headings.map((heading, index) => {
        const nextHeading = state.headings[index + 1]
        return {
          id: heading.id,
          start: heading.offsetTop,
          end: nextHeading ? nextHeading.offsetTop : document.body.scrollHeight,
        }
      })
    }

    static getVisibleIds(): string[] {
      if (state.headings.length === 0) return []

      const viewportTop = window.scrollY + HEADER_OFFSET
      const viewportBottom = window.scrollY + window.innerHeight
      const visibleIds = new Set<string>()

      const isInViewport = (top: number, bottom: number) =>
        (top >= viewportTop && top <= viewportBottom) ||
        (bottom >= viewportTop && bottom <= viewportBottom) ||
        (top <= viewportTop && bottom >= viewportBottom)

      state.headings.forEach((heading) => {
        const headingBottom = heading.offsetTop + heading.offsetHeight
        if (isInViewport(heading.offsetTop, headingBottom)) {
          visibleIds.add(heading.id)
        }
      })

      state.regions.forEach((region) => {
        if (region.start <= viewportBottom && region.end >= viewportTop) {
          const heading = document.getElementById(region.id)
          if (heading) {
            const headingBottom = heading.offsetTop + heading.offsetHeight
            if (
              region.end > headingBottom &&
              (headingBottom < viewportBottom || viewportTop < region.end)
            ) {
              visibleIds.add(region.id)
            }
          }
        }
      })

      return Array.from(visibleIds)
    }
  }

  class TOCScrollMask {
    static update() {
      if (!state.scrollArea || !state.tocScrollArea) return

      const { scrollTop, scrollHeight, clientHeight } = state.scrollArea
      const threshold = 5
      const isAtTop = scrollTop <= threshold
      const isAtBottom = scrollTop >= scrollHeight - clientHeight - threshold

      state.tocScrollArea.classList.toggle('mask-t-from-90%', !isAtTop)
      state.tocScrollArea.classList.toggle('mask-b-from-90%', !isAtBottom)
    }
  }

  class TOCLinks {
    static update(headingIds: string[]) {
      state.links.forEach((link) => {
        link.classList.remove('text-foreground')
      })

      headingIds.forEach((id) => {
        if (id) {
          const activeLink = document.querySelector(
            `#toc-sidebar-container [data-heading-link="${id}"]`,
          )
          if (activeLink) {
            activeLink.classList.add('text-foreground')
          }
        }
      })

      this.scrollToActive(headingIds)
    }

    static scrollToActive(headingIds: string[]) {
      if (!state.scrollArea || !headingIds.length) return

      const activeLink = document.querySelector(
        `#toc-sidebar-container [data-heading-link="${headingIds[0]}"]`,
      )
      if (!activeLink) return

      const { top: areaTop, height: areaHeight } =
        state.scrollArea.getBoundingClientRect()
      const { top: linkTop, height: linkHeight } =
        activeLink.getBoundingClientRect()

      const currentLinkTop = linkTop - areaTop + state.scrollArea.scrollTop
      const targetScroll = Math.max(
        0,
        Math.min(
          currentLinkTop - (areaHeight - linkHeight) / 2,
          state.scrollArea.scrollHeight - state.scrollArea.clientHeight,
        ),
      )

      if (Math.abs(targetScroll - state.scrollArea.scrollTop) > 5) {
        state.scrollArea.scrollTop = targetScroll
      }
    }
  }

  class TOCController {
    static handleScroll() {
      const newActiveIds = HeadingRegions.getVisibleIds()

      if (JSON.stringify(newActiveIds) !== JSON.stringify(state.activeIds)) {
        state.activeIds = newActiveIds
        TOCLinks.update(state.activeIds)
      }
    }

    static handleTOCScroll = () => TOCScrollMask.update()

    static handleResize() {
      HeadingRegions.build()
      const newActiveIds = HeadingRegions.getVisibleIds()

      if (JSON.stringify(newActiveIds) !== JSON.stringify(state.activeIds)) {
        state.activeIds = newActiveIds
        TOCLinks.update(state.activeIds)
      }

      TOCScrollMask.update()
    }

    static init() {
      state.reset()
      HeadingRegions.build()

      if (state.headings.length === 0) {
        TOCLinks.update([])
        return
      }

      this.handleScroll()
      setTimeout(TOCScrollMask.update, 100)

      const options = { passive: true }
      window.addEventListener('scroll', this.handleScroll, options)
      window.addEventListener('resize', this.handleResize, options)
      state.scrollArea?.addEventListener(
        'scroll',
        this.handleTOCScroll,
        options,
      )
    }

    static cleanup() {
      window.removeEventListener('scroll', this.handleScroll)
      window.removeEventListener('resize', this.handleResize)
      state.scrollArea?.removeEventListener('scroll', this.handleTOCScroll)

      Object.assign(state, {
        activeIds: [],
        headings: [],
        regions: [],
        scrollArea: null,
        tocScrollArea: null,
      })
    }
  }

  document.addEventListener('astro:page-load', () => TOCController.init())
  document.addEventListener('astro:after-swap', () => {
    TOCController.cleanup()
    TOCController.init()
  })
  document.addEventListener('astro:before-swap', () => TOCController.cleanup())
</script>
