import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn } from '@/lib/utils'
import { useEffect, useRef, useState } from 'react'

import type { FC } from 'react'

type Props = {
    text:      string
    className?: string
    truncateOnMobile?: boolean
    title?:    string }

const DESKTOP_MARQUEE_MEDIA =
    '(min-width: 768px) and (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)'
const MARQUEE_START_DELAY_MS = 1600
const MARQUEE_END_HOLD_MS = 2400
const MARQUEE_SCROLL_PX_PER_SECOND = 43
const MIN_MARQUEE_OVERFLOW_PX = 1


const ResponsiveMarqueeText: FC<Props> = (
    { text, className, truncateOnMobile = false, title },
) => {
  const behaviourSettings = useClientBehaviourSettings ()
  const animationMode = behaviourSettings.animation ?? 'normal'
  const outerRef = useRef<HTMLSpanElement | null> (null)
  const staticRef = useRef<HTMLSpanElement | null> (null)
  const animatedRef = useRef<HTMLSpanElement | null> (null)
  const animationRef = useRef<Animation | null> (null)
  const timeoutRef = useRef<number | null> (null)
  const rafRef = useRef<number | null> (null)
  const [overflowPx, setOverflowPx] = useState (0)
  const [desktopMarqueeEnabled, setDesktopMarqueeEnabled] = useState (false)
  const [active, setActive] = useState (false)
  const [marqueeVisible, setMarqueeVisible] = useState (false)

  useEffect (() => {
    const outer = outerRef.current
    const inner = staticRef.current

    if (!(outer) || !(inner))
      return

    const measure = () => {
      const nextOverflow = Math.max (0, Math.ceil (inner.scrollWidth - outer.clientWidth))
      setOverflowPx (prev => Math.abs (prev - nextOverflow) <= 1 ? prev : nextOverflow)
    }

    measure ()

    const resizeObserver =
	typeof ResizeObserver === 'undefined'
	? null
	: new ResizeObserver (() => {
	    measure ()
	  })

    resizeObserver?.observe (outer)
    resizeObserver?.observe (inner)
    addEventListener ('resize', measure)

    return () => {
      resizeObserver?.disconnect ()
      removeEventListener ('resize', measure)
    }
  }, [text])

  useEffect (() => {
    if (typeof window === 'undefined' || typeof window.matchMedia !== 'function')
      return

    const media = window.matchMedia (DESKTOP_MARQUEE_MEDIA)
    const update = () => {
      setDesktopMarqueeEnabled (media.matches)
    }

    update ()
    media.addEventListener ('change', update)

    return () => {
      media.removeEventListener ('change', update)
    }
  }, [])

  useEffect (() => {
    const clearScheduled = () => {
      if (timeoutRef.current != null)
        {
          clearTimeout (timeoutRef.current)
          timeoutRef.current = null
        }
      if (rafRef.current != null)
        {
          cancelAnimationFrame (rafRef.current)
          rafRef.current = null
        }
      animationRef.current?.cancel ()
      animationRef.current = null
    }

    const animated = animatedRef.current
    const canMarquee = (
        active
        && animationMode !== 'off'
        && desktopMarqueeEnabled
        && overflowPx >= MIN_MARQUEE_OVERFLOW_PX
        && animated != null)

    const resetAnimated = () => {
      const node = animatedRef.current

      animationRef.current?.cancel ()
      animationRef.current = null
      node?.getAnimations?.().forEach (animation => {
        animation.cancel ()
      })

      if (!(node))
        return

      node.style.transition = 'none'
      node.style.transform = 'translateX(0)'
    }

    if (!canMarquee)
      {
        clearScheduled ()
        resetAnimated ()
        setMarqueeVisible (false)
        return
      }

    let cancelled = false

    const sleep = (ms: number) =>
	new Promise<void> (resolve => {
	  timeoutRef.current = window.setTimeout (() => {
	    timeoutRef.current = null
	    resolve ()
	  }, ms)
	})

    const runLoop = async () => {
      while (!cancelled)
        {
          resetAnimated ()
          setMarqueeVisible (true)

          await sleep (MARQUEE_START_DELAY_MS)
          if (cancelled || !(animatedRef.current))
            break

          const moveDurationMs =
	      overflowPx / MARQUEE_SCROLL_PX_PER_SECOND * 1000
          const node = animatedRef.current

          if (typeof node.animate === 'function')
            {
              const animation = node.animate (
		  [
		    { transform: 'translateX(0)' },
		    { transform: `translateX(-${ overflowPx }px)` },
		  ],
		  { duration: moveDurationMs, easing: 'linear', fill: 'forwards' })
              animationRef.current = animation
              try
              {
                await animation.finished
              }
              catch
              {
                break
              }

              node.style.transform = `translateX(-${ overflowPx }px)`
              animation.cancel ()
              if (animationRef.current === animation)
                animationRef.current = null
            }
          else
            {
              node.style.transition = `transform ${ moveDurationMs }ms linear`
              await new Promise<void> (resolve => {
                rafRef.current = requestAnimationFrame (() => {
                  rafRef.current = null
                  node.style.transform = `translateX(-${ overflowPx }px)`
                  resolve ()
                })
              })
              await sleep (moveDurationMs)
              node.style.transition = 'none'
              node.style.transform = `translateX(-${ overflowPx }px)`
            }

          if (cancelled)
            break

          await sleep (MARQUEE_END_HOLD_MS)
          if (cancelled)
            break

          resetAnimated ()
        }
    }

    void runLoop ()

    return () => {
      cancelled = true
      clearScheduled ()
      resetAnimated ()
      setMarqueeVisible (false)
    }
  }, [active, animationMode, desktopMarqueeEnabled, overflowPx, text])

  return (
      <span
	  ref={outerRef}
	  title={title ?? text}
	  onMouseEnter={() => setActive (true)}
	  onMouseLeave={() => setActive (false)}
	  onFocus={() => setActive (true)}
	  onBlur={() => setActive (false)}
	  className={cn (
	      'tag-marquee inline-block max-w-full min-w-0 align-bottom',
	      (truncateOnMobile
	       ? 'overflow-hidden text-ellipsis whitespace-nowrap'
	       : 'whitespace-normal [overflow-wrap:anywhere]'),
	      'md:overflow-hidden md:whitespace-nowrap',
	      className)}>
	<span
	    ref={staticRef}
	    className={cn (
		'tag-marquee__static block max-w-full',
		(truncateOnMobile
		 ? 'overflow-hidden text-ellipsis whitespace-nowrap'
		 : 'whitespace-normal [overflow-wrap:anywhere]'),
		'md:overflow-hidden md:text-ellipsis md:whitespace-nowrap',
		marqueeVisible && 'md:opacity-0')}>
	  {text}
	</span>
	{overflowPx >= MIN_MARQUEE_OVERFLOW_PX && (
	    <span
		ref={animatedRef}
		aria-hidden="true"
		className={cn (
		    'tag-marquee__animated hidden md:block',
		    marqueeVisible ? 'md:opacity-100' : 'md:opacity-0')}>
	      {text}
	    </span>)}
      </span>)
}

export default ResponsiveMarqueeText
