import { type JSX, createEffect, createSignal, onCleanup, onMount, splitProps } from "solid-js"
import "./scroll-fade.css"

export interface ScrollFadeProps extends JSX.HTMLAttributes<HTMLDivElement> {
  direction?: "horizontal" | "vertical"
  fadeStartSize?: number
  fadeEndSize?: number
  trackTransformSelector?: string
  ref?: (el: HTMLDivElement) => void
}

export function ScrollFade(props: ScrollFadeProps) {
  const [local, others] = splitProps(props, [
    "children",
    "direction",
    "fadeStartSize",
    "fadeEndSize",
    "trackTransformSelector",
    "class",
    "style",
    "ref",
  ])

  const direction = () => local.direction ?? "vertical"
  const fadeStartSize = () => local.fadeStartSize ?? 20
  const fadeEndSize = () => local.fadeEndSize ?? 20

  const getTransformOffset = (element: Element): number => {
    const style = getComputedStyle(element)
    const transform = style.transform
    if (!transform || transform === "none") return 0

    const match = transform.match(/matrix(?:3d)?\(([^)]+)\)/)
    if (!match) return 0

    const values = match[1].split(",").map((v) => parseFloat(v.trim()))
    const isHorizontal = direction() === "horizontal"

    if (transform.startsWith("matrix3d")) {
      return isHorizontal ? -(values[12] || 0) : -(values[13] || 0)
    } else {
      return isHorizontal ? -(values[4] || 0) : -(values[5] || 0)
    }
  }

  let containerRef: HTMLDivElement | undefined

  const [fadeStart, setFadeStart] = createSignal(0)
  const [fadeEnd, setFadeEnd] = createSignal(0)
  const [isScrollable, setIsScrollable] = createSignal(false)

  let lastScrollPos = 0
  let lastTransformPos = 0
  let lastScrollSize = 0
  let lastClientSize = 0

  const updateFade = () => {
    if (!containerRef) return

    const isHorizontal = direction() === "horizontal"
    const scrollPos = isHorizontal ? containerRef.scrollLeft : containerRef.scrollTop
    const scrollSize = isHorizontal ? containerRef.scrollWidth : containerRef.scrollHeight
    const clientSize = isHorizontal ? containerRef.clientWidth : containerRef.clientHeight

    let transformPos = 0
    if (local.trackTransformSelector) {
      const transformElement = containerRef.querySelector(local.trackTransformSelector)
      if (transformElement) {
        transformPos = getTransformOffset(transformElement)
      }
    }

    const effectiveScrollPos = Math.max(scrollPos, transformPos)

    if (
      effectiveScrollPos === lastScrollPos &&
      transformPos === lastTransformPos &&
      scrollSize === lastScrollSize &&
      clientSize === lastClientSize
    ) {
      return
    }

    lastScrollPos = effectiveScrollPos
    lastTransformPos = transformPos
    lastScrollSize = scrollSize
    lastClientSize = clientSize

    const maxScroll = scrollSize - clientSize
    const canScroll = maxScroll > 1

    setIsScrollable(canScroll)

    if (!canScroll) {
      setFadeStart(0)
      setFadeEnd(0)
      return
    }

    const progress = maxScroll > 0 ? effectiveScrollPos / maxScroll : 0

    const startProgress = Math.min(progress / 0.1, 1)
    setFadeStart(startProgress * fadeStartSize())

    const endProgress = progress > 0.9 ? (1 - progress) / 0.1 : 1
    setFadeEnd(Math.max(0, endProgress) * fadeEndSize())
  }

  onMount(() => {
    if (!containerRef) return

    updateFade()

    let rafId: number | undefined
    let isPolling = false
    let pollTimeout: ReturnType<typeof setTimeout> | undefined

    const startPolling = () => {
      if (isPolling) return
      isPolling = true

      const pollScroll = () => {
        updateFade()
        rafId = requestAnimationFrame(pollScroll)
      }
      rafId = requestAnimationFrame(pollScroll)
    }

    const stopPolling = () => {
      if (!isPolling) return
      isPolling = false
      if (rafId !== undefined) {
        cancelAnimationFrame(rafId)
        rafId = undefined
      }
    }

    const schedulePollingStop = () => {
      if (pollTimeout !== undefined) clearTimeout(pollTimeout)
      pollTimeout = setTimeout(stopPolling, 1000)
    }

    const onActivity = () => {
      updateFade()
      if (local.trackTransformSelector) {
        startPolling()
        schedulePollingStop()
      }
    }

    containerRef.addEventListener("scroll", onActivity, { passive: true })

    const resizeObserver = new ResizeObserver(() => {
      lastScrollSize = 0
      lastClientSize = 0
      onActivity()
    })
    resizeObserver.observe(containerRef)

    const mutationObserver = new MutationObserver(() => {
      lastScrollSize = 0
      lastClientSize = 0
      requestAnimationFrame(onActivity)
    })
    mutationObserver.observe(containerRef, {
      childList: true,
      subtree: true,
      characterData: true,
    })

    onCleanup(() => {
      containerRef?.removeEventListener("scroll", onActivity)
      resizeObserver.disconnect()
      mutationObserver.disconnect()
      stopPolling()
      if (pollTimeout !== undefined) clearTimeout(pollTimeout)
    })
  })

  createEffect(() => {
    local.children
    requestAnimationFrame(updateFade)
  })

  return (
    <div
      ref={(el) => {
        containerRef = el
        local.ref?.(el)
      }}
      data-component="scroll-fade"
      data-direction={direction()}
      data-scrollable={isScrollable() || undefined}
      data-fade-start={fadeStart() > 0 || undefined}
      data-fade-end={fadeEnd() > 0 || undefined}
      class={local.class}
      style={{
        ...(typeof local.style === "object" ? local.style : {}),
        "--scroll-fade-start": `${fadeStart()}px`,
        "--scroll-fade-end": `${fadeEnd()}px`,
      }}
      {...others}
    >
      {local.children}
    </div>
  )
}
