import React from 'react'
import { getCaClassFromParams, supportsCaClass } from './caRuntime.js'
import type { CaClass, Cells, SceneParams } from './types.js'
import { Voxel3DRenderer } from './voxel3d/Voxel3DRenderer.js'

interface CaRendererProps {
  caption: string
  cells: Cells
  settings: SceneParams
  onCellsChange: (cells: Cells) => void
}

export interface CaRendererRuntime {
  id: string
  label: string
  aliases?: string[]
  supportedClasses: CaClass[]
  Component: React.ComponentType<CaRendererProps>
}

const rendererRuntimes = new Map<string, CaRendererRuntime>()

export function registerCaRendererRuntime(runtime: CaRendererRuntime, options: { replace?: boolean } = {}) {
  if (!runtime.id.trim()) throw new Error('CA renderer runtime id is required')
  if (rendererRuntimes.has(runtime.id) && !options.replace) {
    throw new Error(`CA renderer runtime already registered: ${runtime.id}`)
  }
  rendererRuntimes.set(runtime.id, runtime)
}

export function listCaRendererRuntimes() {
  return [...rendererRuntimes.values()]
}

export function listCompatibleCaRendererRuntimes(caClass: CaClass | undefined) {
  return listCaRendererRuntimes().filter((runtime) => supportsCaClass(runtime, caClass))
}

export function getCaRendererRuntime(rendererId: string) {
  return (
    rendererRuntimes.get(rendererId) ??
    [...rendererRuntimes.values()].find((runtime) => runtime.aliases?.includes(rendererId)) ??
    null
  )
}

export function CaRenderer({ caption, cells, settings, onCellsChange }: CaRendererProps) {
  const rendererId = settings.renderer?.id ?? '2d-canvas'
  const runtime = getCaRendererRuntime(rendererId)
  const caClass = getCaClassFromParams(settings)

  if (runtime && (!caClass || supportsCaClass(runtime, caClass))) {
    const RendererComponent = runtime.Component
    return <RendererComponent caption={caption} cells={cells} settings={settings} onCellsChange={onCellsChange} />
  }

  return (
    <div className="board-wrap">
      {caption.trim() ? <div className="renderer-caption">{caption}</div> : null}
      <div className="renderer-empty">
        {runtime ? `Renderer ${rendererId} does not support this CA space` : `Renderer unavailable: ${rendererId}`}
      </div>
    </div>
  )
}

function Voxel3DRendererAdapter({ caption, settings }: CaRendererProps) {
  return <Voxel3DRenderer caption={caption} settings={settings} />
}

function rendererNumber(value: unknown, fallback: number, min: number, max: number) {
  return typeof value === 'number' && Number.isFinite(value) ? Math.max(min, Math.min(max, value)) : fallback
}

function parseElementaryRule(settings: SceneParams) {
  const rendererRule = settings.renderer?.elementaryRule
  if (typeof rendererRule === 'number') return Math.max(0, Math.min(255, Math.floor(rendererRule)))

  const ruleId = settings.simulation?.ruleId ?? ''
  const match = /(?:rule[-_\s]*)?(\d{1,3})/i.exec(ruleId)
  if (!match) return 110
  return Math.max(0, Math.min(255, Number(match[1])))
}

function nextElementaryRow(row: boolean[], rule: number) {
  return row.map((_, index) => {
    const left = row[(index - 1 + row.length) % row.length] ? 1 : 0
    const center = row[index] ? 1 : 0
    const right = row[(index + 1) % row.length] ? 1 : 0
    const pattern = (left << 2) | (center << 1) | right
    return ((rule >> pattern) & 1) === 1
  })
}

function firstLiveRowIndex(cells: Cells) {
  const index = cells.findIndex((row) => row.some(Boolean))
  return index === -1 ? 0 : index
}

function Canvas2DRenderer({
  caption,
  cells,
  onCellsChange
}: {
  caption: string
  cells: Cells
  onCellsChange: (cells: Cells) => void
}) {
  const canvasRef = React.useRef<HTMLCanvasElement | null>(null)
  const cellsRef = React.useRef(cells)
  const paintValueRef = React.useRef<boolean | null>(null)
  const [canvasRevision, setCanvasRevision] = React.useState(0)
  const width = cells[0]?.length ?? 0
  const height = cells.length

  React.useEffect(() => {
    cellsRef.current = cells
  }, [cells])

  React.useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return

    const observer = new ResizeObserver(() => setCanvasRevision((revision) => revision + 1))
    observer.observe(canvas)
    return () => observer.disconnect()
  }, [])

  React.useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas || width === 0 || height === 0) return

    const context = canvas.getContext('2d')
    if (!context) return

    const rect = canvas.getBoundingClientRect()
    const dpr = window.devicePixelRatio || 1
    const pixelWidth = Math.max(1, Math.floor(rect.width * dpr))
    const pixelHeight = Math.max(1, Math.floor(rect.height * dpr))

    if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
      canvas.width = pixelWidth
      canvas.height = pixelHeight
    }

    context.setTransform(dpr, 0, 0, dpr, 0, 0)
    context.clearRect(0, 0, rect.width, rect.height)
    context.fillStyle = '#071206'
    context.fillRect(0, 0, rect.width, rect.height)

    const cellSize = Math.min(rect.width / width, rect.height / height)
    const boardWidth = cellSize * width
    const boardHeight = cellSize * height
    const offsetX = (rect.width - boardWidth) / 2
    const offsetY = (rect.height - boardHeight) / 2
    const gap = Math.max(1, Math.min(2, cellSize * 0.08))

    context.strokeStyle = '#1e3519'
    context.lineWidth = 1

    for (let y = 0; y < height; y += 1) {
      for (let x = 0; x < width; x += 1) {
        const px = offsetX + x * cellSize + gap / 2
        const py = offsetY + y * cellSize + gap / 2
        const size = Math.max(1, cellSize - gap)

        if (cells[y][x]) {
          context.fillStyle = '#70f45f'
          context.shadowColor = 'rgba(112, 244, 95, 0.42)'
          context.shadowBlur = Math.max(0, cellSize * 0.38)
          context.fillRect(px, py, size, size)
          context.shadowBlur = 0
          context.strokeStyle = '#b7ffab'
        } else {
          context.fillStyle = '#071206'
          context.fillRect(px, py, size, size)
          context.strokeStyle = '#1e3519'
        }
        context.strokeRect(px + 0.5, py + 0.5, Math.max(0, size - 1), Math.max(0, size - 1))
      }
    }
  }, [canvasRevision, cells, height, width])

  function cellFromPointer(event: React.PointerEvent<HTMLCanvasElement>) {
    const canvas = canvasRef.current
    if (!canvas || width === 0 || height === 0) return null

    const rect = canvas.getBoundingClientRect()
    const cellSize = Math.min(rect.width / width, rect.height / height)
    const boardWidth = cellSize * width
    const boardHeight = cellSize * height
    const offsetX = (rect.width - boardWidth) / 2
    const offsetY = (rect.height - boardHeight) / 2
    const x = Math.floor((event.clientX - rect.left - offsetX) / cellSize)
    const y = Math.floor((event.clientY - rect.top - offsetY) / cellSize)

    if (x < 0 || y < 0 || x >= width || y >= height) return null
    return { x, y }
  }

  function paintCell(x: number, y: number, value: boolean) {
    const current = cellsRef.current
    if (current[y]?.[x] === value) return

    const next = current.map((row, rowIndex) =>
      rowIndex === y ? row.map((alive, columnIndex) => (columnIndex === x ? value : alive)) : row
    )
    cellsRef.current = next
    onCellsChange(next)
  }

  function handlePointerDown(event: React.PointerEvent<HTMLCanvasElement>) {
    const cell = cellFromPointer(event)
    if (!cell) return

    event.currentTarget.setPointerCapture(event.pointerId)
    const nextValue = !cellsRef.current[cell.y][cell.x]
    paintValueRef.current = nextValue
    paintCell(cell.x, cell.y, nextValue)
  }

  function handlePointerMove(event: React.PointerEvent<HTMLCanvasElement>) {
    if (paintValueRef.current === null) return
    const cell = cellFromPointer(event)
    if (!cell) return
    paintCell(cell.x, cell.y, paintValueRef.current)
  }

  function stopPainting(event: React.PointerEvent<HTMLCanvasElement>) {
    paintValueRef.current = null
    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId)
    }
  }

  return (
    <div className="board-wrap">
      {caption.trim() ? <div className="renderer-caption">{caption}</div> : null}
      <canvas
        ref={canvasRef}
        aria-label="Game of Life cell editor"
        className="board-canvas"
        role="img"
        onPointerCancel={stopPainting}
        onPointerDown={handlePointerDown}
        onPointerLeave={stopPainting}
        onPointerMove={handlePointerMove}
        onPointerUp={stopPainting}
      />
    </div>
  )
}

function Elementary1DRenderer({ caption, cells, settings, onCellsChange }: CaRendererProps) {
  const canvasRef = React.useRef<HTMLCanvasElement | null>(null)
  const cellsRef = React.useRef(cells)
  const [canvasRevision, setCanvasRevision] = React.useState(0)
  const width = cells[0]?.length ?? 0
  const sourceRowIndex = Math.min(cells.length - 1, Math.max(0, firstLiveRowIndex(cells)))
  const sourceRow = cells[sourceRowIndex] ?? []
  const rule = parseElementaryRule(settings)
  const historyRows = rendererNumber(settings.renderer?.historyRows, 96, 8, 512)

  React.useEffect(() => {
    cellsRef.current = cells
  }, [cells])

  React.useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return

    const observer = new ResizeObserver(() => setCanvasRevision((revision) => revision + 1))
    observer.observe(canvas)
    return () => observer.disconnect()
  }, [])

  React.useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas || width === 0 || sourceRow.length === 0) return

    const context = canvas.getContext('2d')
    if (!context) return

    const rect = canvas.getBoundingClientRect()
    const dpr = window.devicePixelRatio || 1
    const pixelWidth = Math.max(1, Math.floor(rect.width * dpr))
    const pixelHeight = Math.max(1, Math.floor(rect.height * dpr))

    if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
      canvas.width = pixelWidth
      canvas.height = pixelHeight
    }

    context.setTransform(dpr, 0, 0, dpr, 0, 0)
    context.clearRect(0, 0, rect.width, rect.height)
    context.fillStyle = '#061006'
    context.fillRect(0, 0, rect.width, rect.height)

    const cellWidth = rect.width / width
    const cellHeight = rect.height / historyRows
    let row = [...sourceRow]

    for (let y = 0; y < historyRows; y += 1) {
      const age = y / Math.max(1, historyRows - 1)
      for (let x = 0; x < width; x += 1) {
        if (row[x]) {
          const green = Math.round(190 + age * 55)
          const red = Math.round(112 + age * 80)
          context.fillStyle = `rgb(${red} ${green} 92)`
          context.fillRect(x * cellWidth, y * cellHeight, Math.ceil(cellWidth), Math.ceil(cellHeight))
        }
      }
      row = nextElementaryRow(row, rule)
    }

    context.strokeStyle = 'rgba(240, 201, 74, 0.32)'
    context.lineWidth = 1
    context.strokeRect(0.5, 0.5, Math.max(0, rect.width - 1), Math.max(0, rect.height - 1))
  }, [canvasRevision, historyRows, rule, sourceRow, width])

  function toggleColumn(event: React.PointerEvent<HTMLCanvasElement>) {
    const canvas = canvasRef.current
    if (!canvas || width === 0) return

    const rect = canvas.getBoundingClientRect()
    const x = Math.floor(((event.clientX - rect.left) / rect.width) * width)
    if (x < 0 || x >= width) return

    const current = cellsRef.current
    const next = current.map((row, rowIndex) =>
      rowIndex === sourceRowIndex ? row.map((alive, columnIndex) => (columnIndex === x ? !alive : alive)) : row
    )
    cellsRef.current = next
    onCellsChange(next)
  }

  return (
    <div className="board-wrap elementary-renderer-wrap">
      {caption.trim() ? <div className="renderer-caption">{caption}</div> : null}
      <canvas
        ref={canvasRef}
        aria-label={`Elementary cellular automaton rule ${rule}`}
        className="elementary-canvas"
        role="img"
        onPointerDown={toggleColumn}
      />
    </div>
  )
}

function Wildfire2DRenderer({ caption, cells, onCellsChange }: CaRendererProps) {
  const canvasRef = React.useRef<HTMLCanvasElement | null>(null)
  const cellsRef = React.useRef(cells)
  const paintValueRef = React.useRef<boolean | null>(null)
  const [canvasRevision, setCanvasRevision] = React.useState(0)
  const width = cells[0]?.length ?? 0
  const height = cells.length

  React.useEffect(() => {
    cellsRef.current = cells
  }, [cells])

  React.useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas) return

    const observer = new ResizeObserver(() => setCanvasRevision((revision) => revision + 1))
    observer.observe(canvas)
    return () => observer.disconnect()
  }, [])

  React.useEffect(() => {
    const canvas = canvasRef.current
    if (!canvas || width === 0 || height === 0) return

    const context = canvas.getContext('2d')
    if (!context) return

    const rect = canvas.getBoundingClientRect()
    const dpr = window.devicePixelRatio || 1
    const pixelWidth = Math.max(1, Math.floor(rect.width * dpr))
    const pixelHeight = Math.max(1, Math.floor(rect.height * dpr))

    if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
      canvas.width = pixelWidth
      canvas.height = pixelHeight
    }

    context.setTransform(dpr, 0, 0, dpr, 0, 0)
    context.clearRect(0, 0, rect.width, rect.height)

    const cellSize = Math.min(rect.width / width, rect.height / height)
    const boardWidth = cellSize * width
    const boardHeight = cellSize * height
    const offsetX = (rect.width - boardWidth) / 2
    const offsetY = (rect.height - boardHeight) / 2

    context.fillStyle = '#061006'
    context.fillRect(0, 0, rect.width, rect.height)

    for (let y = 0; y < height; y += 1) {
      for (let x = 0; x < width; x += 1) {
        const px = offsetX + x * cellSize
        const py = offsetY + y * cellSize

        if (cells[y][x]) {
          const gradient = context.createRadialGradient(
            px + cellSize * 0.5,
            py + cellSize * 0.52,
            cellSize * 0.08,
            px + cellSize * 0.5,
            py + cellSize * 0.52,
            cellSize * 0.62
          )
          gradient.addColorStop(0, '#fff5b9')
          gradient.addColorStop(0.45, '#f0c94a')
          gradient.addColorStop(1, '#d9481e')
          context.fillStyle = gradient
        } else {
          context.fillStyle = (x + y) % 2 === 0 ? '#143f1b' : '#0d2b14'
        }
        context.fillRect(px, py, Math.ceil(cellSize), Math.ceil(cellSize))
      }
    }
  }, [canvasRevision, cells, height, width])

  function cellFromPointer(event: React.PointerEvent<HTMLCanvasElement>) {
    const canvas = canvasRef.current
    if (!canvas || width === 0 || height === 0) return null

    const rect = canvas.getBoundingClientRect()
    const cellSize = Math.min(rect.width / width, rect.height / height)
    const boardWidth = cellSize * width
    const boardHeight = cellSize * height
    const offsetX = (rect.width - boardWidth) / 2
    const offsetY = (rect.height - boardHeight) / 2
    const x = Math.floor((event.clientX - rect.left - offsetX) / cellSize)
    const y = Math.floor((event.clientY - rect.top - offsetY) / cellSize)

    if (x < 0 || y < 0 || x >= width || y >= height) return null
    return { x, y }
  }

  function paintCell(x: number, y: number, value: boolean) {
    const current = cellsRef.current
    if (current[y]?.[x] === value) return

    const next = current.map((row, rowIndex) =>
      rowIndex === y ? row.map((alive, columnIndex) => (columnIndex === x ? value : alive)) : row
    )
    cellsRef.current = next
    onCellsChange(next)
  }

  function handlePointerDown(event: React.PointerEvent<HTMLCanvasElement>) {
    const cell = cellFromPointer(event)
    if (!cell) return

    event.currentTarget.setPointerCapture(event.pointerId)
    const nextValue = !cellsRef.current[cell.y][cell.x]
    paintValueRef.current = nextValue
    paintCell(cell.x, cell.y, nextValue)
  }

  function handlePointerMove(event: React.PointerEvent<HTMLCanvasElement>) {
    if (paintValueRef.current === null) return
    const cell = cellFromPointer(event)
    if (!cell) return
    paintCell(cell.x, cell.y, paintValueRef.current)
  }

  function stopPainting(event: React.PointerEvent<HTMLCanvasElement>) {
    paintValueRef.current = null
    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId)
    }
  }

  return (
    <div className="board-wrap wildfire-renderer-wrap">
      {caption.trim() ? <div className="renderer-caption">{caption}</div> : null}
      <canvas
        ref={canvasRef}
        aria-label="Wildfire cellular automaton editor"
        className="wildfire-canvas"
        role="img"
        onPointerCancel={stopPainting}
        onPointerDown={handlePointerDown}
        onPointerLeave={stopPainting}
        onPointerMove={handlePointerMove}
        onPointerUp={stopPainting}
      />
    </div>
  )
}

registerCaRendererRuntime(
  {
    id: '2d-canvas',
    label: '2D Canvas',
    supportedClasses: [{ dimensions: 2, states: 2 }],
    Component: Canvas2DRenderer
  },
  { replace: true }
)

registerCaRendererRuntime(
  {
    id: 'elementary-1d',
    label: 'Elementary 1D',
    aliases: ['elementary-ca', '1d-canvas'],
    supportedClasses: [{ dimensions: 1, states: 2 }],
    Component: Elementary1DRenderer
  },
  { replace: true }
)

registerCaRendererRuntime(
  {
    id: 'wildfire-2d',
    label: 'Wildfire 2D',
    aliases: ['forest-fire-2d'],
    supportedClasses: [{ dimensions: 2, states: 2 }],
    Component: Wildfire2DRenderer
  },
  { replace: true }
)

registerCaRendererRuntime(
  {
    id: 'voxel-3d',
    label: 'Voxel 3D',
    aliases: ['three-voxel'],
    supportedClasses: [{ dimensions: 3, states: 2 }],
    Component: Voxel3DRendererAdapter
  },
  { replace: true }
)
