import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { CSSProperties, HTMLAttributes, MouseEventHandler } from 'react'
import { cn } from '@/lib/utils'

export type DotmSquare3Pattern = 'full' | 'diamond' | 'outline' | 'cross'
export type DotmSquare3DotShape = 'circle' | 'square' | 'diamond'
export type DotmSquare3Phase = 'idle' | 'collapse' | 'hoverRipple' | 'loadingRipple'

export interface DotmSquare3Props extends Omit<HTMLAttributes<HTMLDivElement>, 'color'> {
  size?: number
  dotSize?: number
  color?: string
  speed?: number
  ariaLabel?: string
  pattern?: DotmSquare3Pattern
  muted?: boolean
  bloom?: boolean
  halo?: number
  animated?: boolean
  hoverAnimated?: boolean
  dotClassName?: string
  dotShape?: DotmSquare3DotShape
  opacityBase?: number
  opacityMid?: number
  opacityPeak?: number
  cellPadding?: number
  minSize?: number
}

const GRID_SIZE = 5
const DOT_COUNT = GRID_SIZE * GRID_SIZE
const DOT_INDEXES = Array.from({ length: DOT_COUNT }, (_, index) => index)
const FULL_INDEXES = new Set(DOT_INDEXES)
const DIAMOND_INDEXES = new Set([2, 6, 7, 8, 10, 11, 12, 13, 14, 16, 17, 18, 22])
const OUTLINE_INDEXES = new Set([0, 1, 2, 3, 4, 5, 9, 10, 14, 15, 19, 20, 21, 22, 23, 24])
const CROSS_INDEXES = new Set([2, 7, 10, 11, 12, 13, 14, 17, 22])
const PATTERN_INDEXES: Record<DotmSquare3Pattern, Set<number>> = {
  full: FULL_INDEXES,
  diamond: DIAMOND_INDEXES,
  outline: OUTLINE_INDEXES,
  cross: CROSS_INDEXES,
}

const DOTM_SQUARE_3_CSS = `
.spark-dotm-square-3 {
  --spark-dotm-cycle: 1500ms;
  --spark-dotm-speed: 1;
  --spark-dotm-fill: var(--color-primary);
  --spark-dotm-opacity-base: 0.16;
  --spark-dotm-opacity-mid: 0.32;
  --spark-dotm-opacity-peak: 1;
  --spark-dotm-halo-level: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  color: var(--spark-dotm-fill);
  line-height: 0;
}
.spark-dotm-square-3-grid {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  grid-template-rows: repeat(5, minmax(0, 1fr));
  width: 100%;
  height: 100%;
}
.spark-dotm-square-3-cell {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  min-height: 0;
}
.spark-dotm-square-3-dot {
  display: block;
  width: var(--spark-dotm-dot-size);
  height: var(--spark-dotm-dot-size);
  background: currentColor;
  opacity: var(--spark-dotm-idle-opacity, var(--spark-dotm-opacity-mid));
  will-change: opacity, filter;
}
.spark-dotm-square-3-dot--circle {
  border-radius: var(--radius-full, 999px);
}
.spark-dotm-square-3-dot--square {
  border-radius: min(var(--radius-xs, 2px), 25%);
}
.spark-dotm-square-3-dot--diamond {
  border-radius: min(var(--radius-xs, 2px), 20%);
  transform: rotate(45deg) scale(0.88);
}
.spark-dotm-square-3-dot--inactive {
  opacity: 0;
}
.spark-dotm-square-3--muted {
  opacity: 0.56;
}
.spark-dotm-square-3--bloom .spark-dotm-square-3-dot {
  filter: drop-shadow(0 0 calc(var(--spark-dotm-halo-level) * 1px) currentColor);
}
.spark-dotm-square-3-dot--spiral {
  animation-name: spark-dotm-square-3-spiral;
  animation-duration: calc(var(--spark-dotm-cycle) * var(--spark-dotm-speed));
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  animation-delay: calc(var(--spark-dotm-spiral-order, 0) * 60ms * var(--spark-dotm-speed));
}
@keyframes spark-dotm-square-3-spiral {
  0% { opacity: var(--spark-dotm-opacity-base); }
  10% { opacity: var(--spark-dotm-opacity-base); }
  20% { opacity: var(--spark-dotm-opacity-mid); }
  30% { opacity: var(--spark-dotm-opacity-peak); }
  42% { opacity: var(--spark-dotm-opacity-mid); }
  58% { opacity: var(--spark-dotm-opacity-base); }
  100% { opacity: var(--spark-dotm-opacity-base); }
}
@media (prefers-reduced-motion: reduce) {
  .spark-dotm-square-3-dot--spiral {
    animation: none !important;
  }
}
`

function getSpiralOrder(): number[] {
  const order: number[] = []
  let top = 0
  let bottom = GRID_SIZE - 1
  let left = 0
  let right = GRID_SIZE - 1

  while (top <= bottom && left <= right) {
    for (let col = left; col <= right; col += 1) order.push(top * GRID_SIZE + col)
    for (let row = top + 1; row <= bottom; row += 1) order.push(row * GRID_SIZE + right)
    if (top < bottom) {
      for (let col = right - 1; col >= left; col -= 1) order.push(bottom * GRID_SIZE + col)
    }
    if (left < right) {
      for (let row = bottom - 1; row > top; row -= 1) order.push(row * GRID_SIZE + left)
    }
    top += 1
    bottom -= 1
    left += 1
    right -= 1
  }

  return order
}

const SPIRAL_ORDER = getSpiralOrder()
const SPIRAL_POSITION = new Map(SPIRAL_ORDER.map((dotIndex, orderIndex) => [dotIndex, orderIndex]))

export function spiralInwardOrderValue(index: number): number {
  return SPIRAL_POSITION.get(index) ?? index
}

export function spiralInwardNormFromIndex(index: number): number {
  return spiralInwardOrderValue(index) / Math.max(1, DOT_COUNT - 1)
}

function usePrefersReducedMotion(): boolean {
  const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)

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

    const media = window.matchMedia('(prefers-reduced-motion: reduce)')
    setPrefersReducedMotion(media.matches)
    const handleChange = () => setPrefersReducedMotion(media.matches)
    media.addEventListener?.('change', handleChange)

    return () => media.removeEventListener?.('change', handleChange)
  }, [])

  return prefersReducedMotion
}

function useDotMatrixPhases(animated: boolean, hoverAnimated: boolean, cycleMs: number) {
  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const [phase, setPhase] = useState<DotmSquare3Phase>(animated ? 'loadingRipple' : 'idle')

  useEffect(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
      timeoutRef.current = null
    }

    setPhase(animated ? 'loadingRipple' : 'idle')
  }, [animated])

  const queueReturn = useCallback(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current)
    }

    timeoutRef.current = setTimeout(() => {
      setPhase(animated ? 'loadingRipple' : 'idle')
      timeoutRef.current = null
    }, Math.max(240, cycleMs * 0.72))
  }, [animated, cycleMs])

  const onMouseEnter: MouseEventHandler<HTMLDivElement> = useCallback(() => {
    if (!hoverAnimated) return
    setPhase('hoverRipple')
    queueReturn()
  }, [hoverAnimated, queueReturn])

  const onMouseLeave: MouseEventHandler<HTMLDivElement> = useCallback(() => {
    if (!hoverAnimated) return
    queueReturn()
  }, [hoverAnimated, queueReturn])

  useEffect(
    () => () => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current)
      }
    },
    []
  )

  return { phase, onMouseEnter, onMouseLeave }
}

function getIdleOpacity(index: number, base: number, peak: number): number {
  return Number((base + spiralInwardNormFromIndex(index) * (peak - base)).toFixed(3))
}

export function DotmSquare3({
  size = 36,
  dotSize = 5,
  color = 'var(--color-primary)',
  speed = 1.35,
  ariaLabel = 'Loading',
  pattern = 'full',
  muted = false,
  bloom = false,
  halo = 3,
  animated = true,
  hoverAnimated = false,
  dotClassName,
  dotShape = 'circle',
  opacityBase = 0.16,
  opacityMid = 0.32,
  opacityPeak = 1,
  cellPadding = 1,
  minSize = 20,
  className,
  style,
  onMouseEnter,
  onMouseLeave,
  ...props
}: DotmSquare3Props) {
  const prefersReducedMotion = usePrefersReducedMotion()
  const cycleMs = Math.max(320, 1500 * speed)
  const { phase, onMouseEnter: handlePhaseEnter, onMouseLeave: handlePhaseLeave } = useDotMatrixPhases(
    animated,
    hoverAnimated,
    cycleMs
  )
  const activeIndexes = PATTERN_INDEXES[pattern] ?? FULL_INDEXES
  const resolvedSize = Math.max(minSize, size)
  const resolvedDotSize = Math.max(1, Math.min(dotSize, (resolvedSize - cellPadding * 2) / GRID_SIZE))
  const rootStyle = {
    '--spark-dotm-fill': color,
    '--spark-dotm-speed': speed,
    '--spark-dotm-dot-size': `${resolvedDotSize}px`,
    '--spark-dotm-opacity-base': opacityBase,
    '--spark-dotm-opacity-mid': opacityMid,
    '--spark-dotm-opacity-peak': opacityPeak,
    '--spark-dotm-halo-level': bloom ? halo : 0,
    width: resolvedSize,
    height: resolvedSize,
    ...style,
  } as CSSProperties
  const isAnimatedPhase = animated && !prefersReducedMotion && phase !== 'idle'
  const dots = useMemo(
    () =>
      DOT_INDEXES.map((index) => {
        const isActive = activeIndexes.has(index)
        const idleOpacity = getIdleOpacity(index, opacityBase, opacityPeak)
        const dotStyle = {
          '--spark-dotm-idle-opacity': idleOpacity,
          '--spark-dotm-spiral-order': spiralInwardOrderValue(index),
        } as CSSProperties

        return (
          <span key={index} className="spark-dotm-square-3-cell" aria-hidden="true">
            <span
              className={cn(
                'spark-dotm-square-3-dot',
                `spark-dotm-square-3-dot--${dotShape}`,
                !isActive && 'spark-dotm-square-3-dot--inactive',
                isActive && isAnimatedPhase && 'spark-dotm-square-3-dot--spiral',
                dotClassName
              )}
              style={dotStyle}
            />
          </span>
        )
      }),
    [activeIndexes, dotClassName, dotShape, isAnimatedPhase, opacityBase, opacityPeak]
  )

  const handleMouseEnter: MouseEventHandler<HTMLDivElement> = (event) => {
    handlePhaseEnter(event)
    onMouseEnter?.(event)
  }

  const handleMouseLeave: MouseEventHandler<HTMLDivElement> = (event) => {
    handlePhaseLeave(event)
    onMouseLeave?.(event)
  }

  return (
    <div
      role="status"
      aria-label={ariaLabel}
      aria-live="polite"
      className={cn(
        'spark-dotm-square-3',
        muted && 'spark-dotm-square-3--muted',
        (bloom || halo > 0) && 'spark-dotm-square-3--bloom',
        className
      )}
      style={rootStyle}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      {...props}
    >
      <style>{DOTM_SQUARE_3_CSS}</style>
      <span className="spark-dotm-square-3-grid">{dots}</span>
    </div>
  )
}

DotmSquare3.displayName = 'DotmSquare3'
