// Pixel engine — crisp nearest-neighbor canvas rendering. Ported from prototype/js/pixel.jsx.
import { useEffect, useRef, useState } from 'preact/hooks'
import type { JSX } from 'preact'

import { type Pal, drawMap } from './draw'

export type ScenePainter = (
  ctx: CanvasRenderingContext2D,
  w: number,
  h: number,
  t: number,
) => void

interface PixelCanvasProps {
  frames?: string[][]
  map?: string[]
  pal?: Pal
  px?: number
  fps?: number
  className?: string
  style?: JSX.CSSProperties
  playing?: boolean
  onClick?: (e: MouseEvent) => void
}

/** A sprite, optionally animated across frames. */
export function PixelCanvas({
  frames,
  map,
  pal,
  px = 4,
  fps = 4,
  className,
  style,
  playing = true,
  onClick,
}: PixelCanvasProps) {
  const ref = useRef<HTMLCanvasElement>(null)
  const allFrames = frames || [map as string[]]
  const cols = allFrames[0][0].length
  const rows = allFrames[0].length
  const [f, setF] = useState(0)

  useEffect(() => {
    if (!playing || allFrames.length < 2) return
    const id = setInterval(() => setF((v) => (v + 1) % allFrames.length), 1000 / fps)
    return () => clearInterval(id)
  }, [playing, allFrames.length, fps])

  useEffect(() => {
    const cv = ref.current
    if (!cv) return
    const ctx = cv.getContext('2d')!
    ctx.imageSmoothingEnabled = false
    ctx.clearRect(0, 0, cv.width, cv.height)
    drawMap(ctx, allFrames[f], pal, px)
  }, [f, px, pal, allFrames])

  return (
    <canvas
      ref={ref}
      width={cols * px}
      height={rows * px}
      onClick={onClick}
      class={className}
      style={{ imageRendering: 'pixelated', display: 'block', ...style }}
    />
  )
}

interface SceneCanvasProps {
  paint: ScenePainter
  w?: number
  h?: number
  className?: string
  style?: JSX.CSSProperties
  deps?: unknown[]
  anim?: boolean
  full?: boolean
  rain?: boolean
}

/** Procedural painter at low internal res. Static scenes paint once to an offscreen
 *  buffer; anim scenes blit the buffer + a cheap rain overlay so we never re-dither
 *  the whole canvas every frame. `full` forces a true per-frame repaint. */
export function SceneCanvas({
  paint,
  w = 240,
  h = 135,
  className,
  style,
  deps = [],
  anim = false,
  full = false,
  rain = true,
}: SceneCanvasProps) {
  const ref = useRef<HTMLCanvasElement>(null)
  const bufRef = useRef<HTMLCanvasElement | null>(null)
  const tRef = useRef(0)
  useEffect(() => {
    const cv = ref.current
    if (!cv) return
    const ctx = cv.getContext('2d')!
    ctx.imageSmoothingEnabled = false

    const buf = document.createElement('canvas')
    buf.width = w
    buf.height = h
    const bctx = buf.getContext('2d')!
    bctx.imageSmoothingEnabled = false
    paint(bctx, w, h, 0)
    bufRef.current = buf

    let raf = 0
    // always paint a first frame synchronously (rAF is paused in background iframes)
    ctx.clearRect(0, 0, w, h)
    ctx.drawImage(buf, 0, 0)
    if (full) {
      let last = 0
      const loop = (ts: number) => {
        if (ts - last > 110) {
          last = ts
          tRef.current += 1
          ctx.clearRect(0, 0, w, h)
          paint(ctx, w, h, tRef.current)
        }
        raf = requestAnimationFrame(loop)
      }
      raf = requestAnimationFrame(loop)
    } else if (anim && rain) {
      let last = 0
      const loop = (ts: number) => {
        if (ts - last > 70) {
          last = ts
          tRef.current += 1
          ctx.clearRect(0, 0, w, h)
          ctx.drawImage(buf, 0, 0)
          ctx.fillStyle = 'rgba(176,196,206,0.26)'
          const t = tRef.current
          for (let i = 0; i < 36; i++) {
            const x = (i * 41 + t * 5) % w
            const y = (i * 57 + t * 9) % h
            ctx.fillRect(Math.floor(x), Math.floor(y), 1, 3)
          }
        }
        raf = requestAnimationFrame(loop)
      }
      raf = requestAnimationFrame(loop)
    } else {
      ctx.clearRect(0, 0, w, h)
      ctx.drawImage(buf, 0, 0)
    }
    return () => cancelAnimationFrame(raf)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps)
  return (
    <canvas
      ref={ref}
      width={w}
      height={h}
      class={className}
      style={{ imageRendering: 'pixelated', display: 'block', width: '100%', height: '100%', ...style }}
    />
  )
}

/** Full-screen pixel rain on a canvas. */
export function RainFX({ density = 90 }: { density?: number }) {
  const ref = useRef<HTMLCanvasElement>(null)
  useEffect(() => {
    const cv = ref.current
    if (!cv) return
    const ctx = cv.getContext('2d')!
    let W = 0
    let H = 0
    let drops: { x: number; y: number; v: number; len: number }[] = []
    let raf = 0
    const resize = () => {
      W = cv.width = Math.ceil(window.innerWidth / 3)
      H = cv.height = Math.ceil(window.innerHeight / 3)
      cv.style.width = window.innerWidth + 'px'
      cv.style.height = window.innerHeight + 'px'
      drops = Array.from({ length: density }, () => ({
        x: Math.random() * W,
        y: Math.random() * H,
        v: 2 + Math.random() * 3,
        len: 3 + Math.random() * 5,
      }))
    }
    resize()
    window.addEventListener('resize', resize)
    let last = 0
    const loop = (ts: number) => {
      if (ts - last > 33) {
        last = ts
        ctx.clearRect(0, 0, W, H)
        ctx.fillStyle = 'rgba(180,200,210,0.30)'
        for (const d of drops) {
          ctx.fillRect(Math.floor(d.x), Math.floor(d.y), 1, Math.floor(d.len))
          d.y += d.v
          d.x += 0.4
          if (d.y > H) {
            d.y = -d.len
            d.x = Math.random() * W
          }
        }
      }
      raf = requestAnimationFrame(loop)
    }
    raf = requestAnimationFrame(loop)
    return () => {
      cancelAnimationFrame(raf)
      window.removeEventListener('resize', resize)
    }
  }, [density])
  return <canvas ref={ref} class="fx-rain" style={{ position: 'fixed', inset: 0, imageRendering: 'pixelated' }} />
}

/** True if the user asked the OS to reduce motion. */
export function prefersReducedMotion(): boolean {
  return typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
}

/** Typewriter hook: returns [visibleText, done]. Honors prefers-reduced-motion (instant). */
export function useTypewriter(text: string, speed = 28, on = true): [string, boolean] {
  if (prefersReducedMotion()) on = false
  const [out, setOut] = useState(on ? '' : text)
  const [done, setDone] = useState(!on)
  useEffect(() => {
    if (!on) {
      setOut(text)
      setDone(true)
      return
    }
    setOut('')
    setDone(false)
    if (!text) {
      setDone(true)
      return
    }
    let i = 0
    const id = setInterval(() => {
      i++
      setOut(text.slice(0, i))
      if (i >= text.length) {
        clearInterval(id)
        setDone(true)
      }
    }, speed)
    return () => clearInterval(id)
  }, [text, speed, on])
  return [out, done]
}
