"use client"

import * as React from "react"
import { ChevronDown, ChevronUp, Search } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  MAX_SEARCH_MATCHES,
  findMatchingLines,
  tokenizeXmlLine,
  visibleWindow,
  type XmlTokenKind,
} from "@/components/xml-apis/shared/xml-source"
import { cn } from "@/lib/utils"

/** Fixed row height: what lets a 66,000-line file be drawn as ~60 rows. Keep in step with `leading-5`. */
export const XML_ROW_HEIGHT = 20
const OVERSCAN_ROWS = 12
/** Before the viewport is measured (server render, first paint) draw enough rows to fill a tall screen. */
const INITIAL_VIEWPORT_HEIGHT = 900
const MAX_LINE_WIDTH_CH = 4000

const TOKEN_CLASS: Record<XmlTokenKind, string> = {
  punct: "text-zinc-500",
  tag: "text-sky-300",
  attr: "text-amber-300",
  value: "text-emerald-300",
  text: "text-zinc-100",
  comment: "text-zinc-500 italic",
  decl: "text-violet-300",
}

/** One line of XML in colour. */
function XmlLine({ line }: { line: string }) {
  const tokens = tokenizeXmlLine(line)
  if (tokens.length === 0) return <>{"​"}</>
  return (
    <>
      {tokens.map((token, index) => (
        <span key={index} className={TOKEN_CLASS[token.kind]}>
          {token.text}
        </span>
      ))}
    </>
  )
}

type XmlSourceViewerProps = {
  lines: readonly string[]
  className?: string
}

/**
 * Read-only XML source: line numbers, syntax colours and search, drawn by window so a file of any size
 * scrolls smoothly (only the rows on screen are in the page).
 */
export function XmlSourceViewer({ lines, className }: XmlSourceViewerProps) {
  const scrollerRef = React.useRef<HTMLDivElement>(null)
  const [scrollTop, setScrollTop] = React.useState(0)
  const [viewportHeight, setViewportHeight] = React.useState(INITIAL_VIEWPORT_HEIGHT)
  const [query, setQuery] = React.useState("")
  const [activeMatch, setActiveMatch] = React.useState(0)

  // Measure the scroll area (the observer reports its size right after it starts watching, and on every resize).
  React.useEffect(() => {
    const scroller = scrollerRef.current
    if (!scroller || typeof ResizeObserver === "undefined") return
    const observer = new ResizeObserver(([entry]) => {
      if (entry) setViewportHeight(entry.contentRect.height)
    })
    observer.observe(scroller)
    return () => observer.disconnect()
  }, [])

  const matches = React.useMemo(() => findMatchingLines(lines, query), [lines, query])
  const matchSet = React.useMemo(() => new Set(matches), [matches])
  const current = matches.length === 0 ? -1 : Math.min(activeMatch, matches.length - 1)
  const currentLine = current === -1 ? -1 : matches[current]!

  const total = lines.length
  const gutterDigits = String(Math.max(total, 1)).length
  const widestLine = React.useMemo(() => lines.reduce((max, line) => Math.max(max, line.length), 0), [lines])
  const contentWidthCh = Math.min(MAX_LINE_WIDTH_CH, widestLine) + gutterDigits + 4

  const { start, end } = visibleWindow({
    scrollTop,
    viewportHeight,
    rowHeight: XML_ROW_HEIGHT,
    total,
    overscan: OVERSCAN_ROWS,
  })

  const scrollToLine = React.useCallback(
    (line: number) => {
      const scroller = scrollerRef.current
      if (!scroller) return
      scroller.scrollTo({ top: Math.max(0, line * XML_ROW_HEIGHT - scroller.clientHeight / 2) })
    },
    [],
  )

  const go = (index: number) => {
    if (matches.length === 0) return
    const next = (index + matches.length) % matches.length
    setActiveMatch(next)
    scrollToLine(matches[next]!)
  }

  const onQueryChange = (value: string) => {
    setQuery(value)
    setActiveMatch(0)
    const first = findMatchingLines(lines, value, 1)[0]
    if (first !== undefined) scrollToLine(first)
  }

  const rows: React.ReactNode[] = []
  for (let index = start; index < end; index += 1) {
    const isCurrent = index === currentLine
    rows.push(
      <div
        key={index}
        data-line={index + 1}
        className={cn(
          "absolute left-0 flex h-5 w-max min-w-full",
          matchSet.has(index) && "bg-amber-400/10",
          isCurrent && "bg-amber-400/25",
        )}
        style={{ top: index * XML_ROW_HEIGHT }}
      >
        <span
          className="sticky left-0 z-10 shrink-0 select-none bg-zinc-950 pr-3 pl-3 text-right text-zinc-600"
          style={{ width: `${gutterDigits + 3}ch` }}
        >
          {index + 1}
        </span>
        <span className="whitespace-pre pr-6">
          <XmlLine line={lines[index]!} />
        </span>
      </div>,
    )
  }

  return (
    <div className={cn("flex min-h-0 flex-1 flex-col bg-zinc-950", className)}>
      <div className="flex shrink-0 items-center gap-2 border-b border-zinc-800 px-3 py-1.5">
        <Search className="size-3.5 shrink-0 text-zinc-500" aria-hidden />
        <input
          type="search"
          value={query}
          onChange={(event) => onQueryChange(event.target.value)}
          onKeyDown={(event) => {
            if (event.key === "Enter") {
              event.preventDefault()
              go(event.shiftKey ? current - 1 : current + 1)
            }
          }}
          placeholder="Search the XML…"
          aria-label="Search the XML"
          className="h-7 min-w-0 flex-1 bg-transparent text-sm text-zinc-100 outline-none placeholder:text-zinc-600"
        />
        {query.trim() ? (
          <span className="shrink-0 text-xs text-zinc-400" aria-live="polite">
            {matches.length === 0
              ? "No matches"
              : `${current + 1} of ${matches.length}${matches.length >= MAX_SEARCH_MATCHES ? "+" : ""}`}
          </span>
        ) : null}
        <Button
          type="button"
          variant="ghost"
          size="icon-sm"
          className="text-zinc-400 hover:text-zinc-100"
          aria-label="Previous match"
          disabled={matches.length === 0}
          onClick={() => go(current - 1)}
        >
          <ChevronUp className="size-4" />
        </Button>
        <Button
          type="button"
          variant="ghost"
          size="icon-sm"
          className="text-zinc-400 hover:text-zinc-100"
          aria-label="Next match"
          disabled={matches.length === 0}
          onClick={() => go(current + 1)}
        >
          <ChevronDown className="size-4" />
        </Button>
      </div>

      <div
        ref={scrollerRef}
        data-slot="xml-source-scroller"
        className="min-h-0 flex-1 overflow-auto font-mono text-[13px] leading-5"
        onScroll={(event) => setScrollTop(event.currentTarget.scrollTop)}
        tabIndex={0}
        role="region"
        aria-label="XML source"
      >
        <div className="relative" style={{ height: total * XML_ROW_HEIGHT, width: `${contentWidthCh}ch`, minWidth: "100%" }}>
          {rows}
        </div>
      </div>
    </div>
  )
}
