<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import TocTree from './TocTree.vue'

const pageRoute = useRoute()

// Interface for our TOC structure
interface TocLink {
  id: string
  text: string
  level: number
  children: TocLink[]
}

// Create a reactive reference for our TOC links
const tocLinks = ref<TocLink[]>([])
const pageTitle = ref('Table of Contents')

// Track visible headings and active heading
const visibleHeadings = ref<Set<string>>(new Set())
const activeHeading = ref<string | null>(null)

// Track the currently clicked heading for immediate highlight
const clickedHeading = ref<string | null>(null)

// Intersection observer for tracking visible headings
let headingObserver: IntersectionObserver | null = null

function setupIntersectionObserver() {
  if (typeof IntersectionObserver === 'undefined') return

  headingObserver = new IntersectionObserver(
    entries => {
      entries.forEach(entry => {
        const id = entry.target.id
        if (entry.isIntersecting) {
          visibleHeadings.value.add(id)
        } else {
          visibleHeadings.value.delete(id)
        }
      })

      // Set the active heading to the first visible one
      const allIds = getAllHeadingIds(tocLinks.value)
      for (const id of allIds) {
        if (visibleHeadings.value.has(id)) {
          activeHeading.value = id
          break
        }
      }
    },
    {
      rootMargin: '-80px 0px 0px 0px',
      threshold: 0,
    },
  )

  // Observe all headings (ids are auto-generated by scanHeadings if missing)
  const headings = document.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]')
  headings.forEach(heading => {
    headingObserver?.observe(heading)
  })
}

function getAllHeadingIds(links: TocLink[]): string[] {
  const ids: string[] = []
  for (const link of links) {
    ids.push(link.id)
    if (link.children.length) {
      ids.push(...getAllHeadingIds(link.children))
    }
  }
  return ids
}

function cleanupIntersectionObserver() {
  if (headingObserver) {
    headingObserver.disconnect()
    headingObserver = null
  }
  visibleHeadings.value.clear()
  activeHeading.value = null
}

// Function to scan the page for headings and build the TOC
function scanHeadings() {
  // Wait for the DOM to be ready
  if (typeof document === 'undefined') {
    return
  }

  // Find all headings (h1-h6), auto-generating ids for any that lack one
  const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6')
  const links: TocLink[] = []
  const usedIds = new Set<string>()

  // Get the page title from the first h1
  const h1 = document.querySelector('h1')
  if (h1) {
    pageTitle.value = h1.textContent || 'Table of Contents'
  }

  // Process each heading
  headings.forEach(heading => {
    // Skip the first h1 as it's the page title
    if (heading.tagName === 'H1' && heading === h1) {
      return
    }

    // Auto-generate id from text content if missing
    if (!heading.id) {
      const text = heading.textContent || ''
      const slug = text
        .toLowerCase()
        .trim()
        .replace(/[^\w\s-]/g, '')
        .replace(/[\s_]+/g, '-')
        .replace(/^-+|-+$/g, '')
      // Deduplicate
      let candidate = slug
      let n = 1
      while (usedIds.has(candidate)) {
        candidate = `${slug}-${n++}`
      }
      heading.id = candidate
    }
    usedIds.add(heading.id)

    const id = heading.id
    const level = Number.parseInt(heading.tagName.substring(1), 10)

    // Create a TOC link
    const link: TocLink = { id, text: heading.textContent || '', level, children: [] }
    links.push(link)
  })

  // Build a robust hierarchical structure in a single pass
  const hierarchicalLinks: TocLink[] = []
  const parents: TocLink[] = []

  for (const link of links) {
    // Always ensure children is initialized
    if (!('children' in link) || !Array.isArray(link.children)) {
      ;(link as TocLink).children = []
    }
    // Find the last parent of lower level
    while (parents.length > 0 && parents[parents.length - 1]!.level >= link.level) {
      parents.pop()
    }
    if (parents.length === 0) {
      hierarchicalLinks.push(link)
    } else {
      parents[parents.length - 1]!.children.push(link)
    }
    parents.push(link)
  }
  tocLinks.value = hierarchicalLinks
}

// Check if we're in the browser
let observer: MutationObserver | null = null

onMounted(() => {
  // Initial scan
  scanHeadings()

  // Initialize clickedHeading from URL hash if present
  if (window.location.hash) {
    clickedHeading.value = window.location.hash.slice(1)
  }

  // Setup intersection observer after initial scan
  nextTick(() => {
    setupIntersectionObserver()
  })

  // Re-scan when route changes (for SPA navigation)
  watch(
    () => pageRoute.path,
    () => {
      nextTick(() => {
        cleanupIntersectionObserver()
        scanHeadings()
        nextTick(() => {
          setupIntersectionObserver()
        })
      })
    },
  )

  // Watch for DOM mutations affecting headings
  observer = new MutationObserver(mutations => {
    let shouldRescan = false
    for (const mutation of mutations) {
      if (
        Array.from(mutation.addedNodes).some(isHeadingNode) ||
        Array.from(mutation.removedNodes).some(isHeadingNode) ||
        (mutation.type === 'attributes' && isHeadingNode(mutation.target))
      ) {
        shouldRescan = true
        break
      }
    }
    if (shouldRescan) {
      cleanupIntersectionObserver()
      scanHeadings()
      nextTick(() => {
        setupIntersectionObserver()
      })
    }
  })
  observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ['id'],
  })
})

onBeforeUnmount(() => {
  if (observer) {
    observer.disconnect()
    observer = null
  }
  cleanupIntersectionObserver()
})

function isHeadingNode(node: Node | EventTarget): boolean {
  if (!(node instanceof HTMLElement)) {
    return false
  }
  return /^H[1-6]$/.test(node.tagName)
}

function checkActive(hash: string) {
  return pageRoute.hash === hash ? 'active' : null
}

function toTop() {
  window.scrollTo({
    top: 0,
    behavior: 'smooth',
  })
  setTimeout(() => {
    window.location.hash = ''
  }, 600)
}

function toBottom() {
  window.scrollTo({
    top: document.body.scrollHeight,
    behavior: 'smooth',
  })
}

function scrollToHeading(id: string) {
  const el = document.getElementById(id)
  if (el) {
    // Immediately highlight the clicked item
    clickedHeading.value = id
    el.scrollIntoView({ behavior: 'smooth', block: 'start' })
    // Update the hash after the scroll
    history.replaceState(null, '', `#${id}`)
  }
}
</script>

<template>
  <Flex col>
    <Menu v-if="tocLinks.length" class="md:menu-sm xl:menu-md w-full">
      <NuxtLink href="#" custom>
        <template #default="{ route }">
          <MenuItem class="text-base-content cursor-pointer">
            <a
              class="flex flex-row items-center justify-between font-semibold bg-neutral/30"
              :class="checkActive(route?.hash || '')"
              @click="toTop"
            >
              {{ pageTitle }}
              <Icon name="feather:arrow-up" class="text-base" />
            </a>
          </MenuItem>
        </template>
      </NuxtLink>

      <TocTree
        :links="tocLinks"
        :check-active="checkActive"
        :scroll-to-heading="scrollToHeading"
        :visible-headings="visibleHeadings"
        :active-heading="activeHeading"
        :clicked-heading="clickedHeading"
      />

      <MenuItem>
        <a class="flex flex-row items-center justify-between hover:bg-accent/25 mt-6" @click="toBottom">
          scroll to bottom
          <Icon name="feather:arrow-down" class="text-base" />
        </a>
      </MenuItem>
    </Menu>
  </Flex>
</template>
