import {
  useCallback,
  useEffect,
  useRef,
  useState,
  forwardRef,
  type ReactNode,
  type CSSProperties,
} from 'react'
import { clsx } from 'clsx'
import { Tooltip } from './tooltip'

export interface EllipsisTextProps {
  /** Text or inline content to display. */
  children: ReactNode
  /**
   * Maximum visible lines before truncation.
   * @default 1
   */
  lines?: number
  /** Extra className for the text node. */
  className?: string
  /**
   * Tooltip placement when overflow is detected.
   * @default 'top'
   */
  placement?: 'top' | 'bottom' | 'left' | 'right'
  /** Extra className for Tooltip content. */
  tooltipClassName?: string
  /** Custom tooltip content. Defaults to children. */
  tooltipContent?: ReactNode
  /**
   * Disable tooltip even when content overflows.
   * @default false
   */
  disabled?: boolean
  /** Inline style for the text node. */
  style?: CSSProperties
  /**
   * Rendered text element.
   * @default 'span'
   */
  as?: 'span' | 'div' | 'p'
}

function setRef<T>(ref: React.Ref<T> | undefined, value: T | null) {
  if (!ref) return
  if (typeof ref === 'function') ref(value)
  else (ref as React.MutableRefObject<T | null>).current = value
}

export const EllipsisText = forwardRef<HTMLElement, EllipsisTextProps>(
  (
    {
      children,
      lines = 1,
      className = '',
      placement = 'top',
      tooltipClassName = '',
      tooltipContent,
      disabled = false,
      style,
      as: Component = 'span',
    },
    ref
  ) => {
    const textRef = useRef<HTMLElement>(null)
    const [isOverflowing, setIsOverflowing] = useState(false)

    const checkOverflow = useCallback(() => {
      const el = textRef.current
      if (!el) return
      const overflowing =
        lines === 1
          ? el.scrollWidth > el.clientWidth
          : el.scrollHeight > el.clientHeight
      setIsOverflowing(overflowing)
    }, [lines])

    useEffect(() => {
      const raf = requestAnimationFrame(() => checkOverflow())
      const el = textRef.current
      if (!el) return () => cancelAnimationFrame(raf)
      const ro = new ResizeObserver(() => checkOverflow())
      ro.observe(el)
      window.addEventListener('resize', checkOverflow)
      return () => {
        cancelAnimationFrame(raf)
        ro.disconnect()
        window.removeEventListener('resize', checkOverflow)
      }
    }, [checkOverflow, children])

    const ellipsisStyle: CSSProperties =
      lines === 1
        ? {
            display: 'block',
            overflow: 'hidden',
            textOverflow: 'ellipsis',
            whiteSpace: 'nowrap',
            ...style,
          }
        : {
            overflow: 'hidden',
            display: '-webkit-box',
            WebkitLineClamp: lines,
            WebkitBoxOrient: 'vertical',
            ...style,
          }

    const refCallback = useCallback(
      (el: HTMLElement | null) => {
        setRef(textRef, el)
        setRef(ref, el)
      },
      [ref]
    )

    const triggerNode = (
      <Component
        ref={refCallback}
        className={className}
        style={ellipsisStyle}
      >
        {children}
      </Component>
    )

    const useTooltip = isOverflowing && !disabled
    if (useTooltip) {
      return (
        <Tooltip
          content={tooltipContent ?? children}
          side={placement}
          contentClassName={clsx(tooltipClassName)}
        >
          <span
            ref={refCallback}
            className={className}
            style={ellipsisStyle}
          >
            {children}
          </span>
        </Tooltip>
      )
    }
    return triggerNode
  }
)

EllipsisText.displayName = 'EllipsisText'
