import cn from 'classnames'
import Konva from 'konva'
import { nanoid } from 'nanoid'
import { forwardRef,
	 useEffect,
	 useImperativeHandle,
	 useRef,
	 useState } from 'react'
import { FaRedo, FaUndo } from 'react-icons/fa'
import { Layer, Line, Rect, Stage, Image } from 'react-konva'

import type { ChangeEventHandler } from 'react'

type ImageItem = { src: string }

type Layer = {
    id:       string
    name:     string
    lines:    Line[]
    history:  Line[][]
    future:   Line[][]
    image?:   ImageItem }

type Line =
    | { mode:         typeof Mode.Pen | typeof Mode.Rubber
	points:       number[]
	stroke:       string
	strokeWidth:  number }
    | { mode:     typeof Mode.Paint
	points:   number[]
	stroke:   string,
	imageSrc: string }

const Mode = {
    Pen:    'Pen',
    Rubber: 'Rubber',
    Paint:  'Paint' } as const
type Mode = (typeof Mode)[keyof typeof Mode]


const colourDiffMax = (c1: readonly number[], c2: readonly number[]) => (
    Math.max (...[0, 1, 2, 3].map (i => Math.abs (c1[i] - c2[i]))))


const floodFill = (
    imgData:    ImageData,
    sx:         number,
    sy:         number,
    fill:       { r:  number
		  g:  number
		  b:  number
		  a:  number },
    tolerance:  number          = 16) => {
  const { data, width: W, height: H } = imgData
  const start = getRGBA (data, sx, sy, W)
  const visited = new Uint8Array (W * H)

  if (colourDiffMax (start, [fill.r, fill.g, fill.b, fill.a]) <= tolerance)
    return

  const stack: [number, number][] = [[sx, sy]]
  while (stack.length > 0)
    {
      const [x, y] = stack.pop ()!
      let lx = x
      while (lx >= 0
	     && !(visited[y * W + lx])
	     && colourDiffMax (getRGBA (data, lx, y, W), start) <= tolerance)
	--lx
      ++lx
      let rx = x
      while (rx < W
	     && !(visited[y * W + rx])
	     && colourDiffMax (getRGBA (data, rx, y, W), start) <= tolerance)
	++rx
      for (let i = lx; i < rx; ++i)
	{
	  const idx = y * W + i
	  visited[idx] = 1
	  const p = idx * 4
	  data[p] = fill.r
	  data[p + 1] = fill.g
	  data[p + 2] = fill.b
	  data[p + 3] = fill.a
	  if (y > 0
	      && !(visited[(y - 1) * W + i])
	      && colourDiffMax (getRGBA (data, i, y - 1, W), start) <= tolerance)
	    stack.push ([i, y - 1])
	  if (y < H - 1
	      && !(visited[(y + 1) * W + i])
	      && colourDiffMax (getRGBA (data, i, y + 1, W), start) <= tolerance)
	    stack.push ([i, y + 1])
	}
    }
}


const getRGBA = (data: Uint8ClampedArray, x: number, y: number, w: number) => {
  const i = (y * w + x) * 4
  return [data[i], data[i + 1], data[i + 2], data[i + 3]] as const
}


const hexToRgba = (hex: string, alpha = 255): [number, number, number, number] => {
  const m = /^#?([0-9a-f]{6})$/i.exec (hex)
  const n = parseInt (m![1], 16)
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255, alpha]
}


const isLayer = (obj: unknown): obj is Layer => (
    typeof obj === 'object'
    && obj !== null
    && typeof (obj as Layer).id === 'string'
    && typeof (obj as Layer).name === 'string'
    && Array.isArray ((obj as Layer).lines)
    && Array.isArray ((obj as Layer).history)
    && Array.isArray ((obj as Layer).future))


export type ThreadCanvasHandle = {
    exportAll:  () => Promise<{ dataURL:  string
			        width:    number
			        height:   number }>
    clear:      () => void }

type Props = { visible: boolean }


export default forwardRef<ThreadCanvasHandle, Props> (({ visible }, ref) => {
  const drawingRef = useRef (false)
  const fileInputRef = useRef<HTMLInputElement> (null)
  const layersRef = useRef<Record<string, Konva.Layer>> ({ })
  const stageRef = useRef<Konva.Stage | null> (null)

  const [activeLayerId, setActiveLayerId] = useState<string | null> (null)
  const [colour, setColour] = useState ('#000000')
  const [images, setImages] = useState<Record<string, HTMLImageElement>> ({ })
  const [layerCnt, setLayerCnt] = useState (0)
  const [layers, setLayers] = useState<Layer[]> ([])
  const [mode, setMode] = useState<Mode> (Mode.Pen)
  const [pointSize, setPointSize] = useState (3)
  const [singleLayer, setSingleLayer] = useState (false)
  const [stageHeight, setStageHeight] = useState (480)
  const [stageWidth, setStageWidth] = useState (480)

  const activeLayer = layers.find (l => l.id === activeLayerId)

  const handleMouseDown = (ev: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => {
    if (!(activeLayer) || (mode !== Mode.Pen && mode !== Mode.Rubber))
      return

    drawingRef.current = true

    updateActiveLayerHistory ([...activeLayer.history, activeLayer.lines])
    updateActiveLayerFuture ([])

    const pos = ev.target.getStage ()?.getPointerPosition ()
    if (!(pos))
      return

    const lines: Line[] = [
	...activeLayer.lines,
	{ mode,
	  points:      [pos.x, pos.y],
	  stroke:      colour,
	  strokeWidth: pointSize }]
    updateActiveLayerLines (lines)
  }

  const handleMouseMove = (ev: Konva.KonvaEventObject<MouseEvent | TouchEvent>) => {
    if (!(drawingRef.current) || !(activeLayer))
      return

    const stage = ev.target.getStage ()
    const point = stage?.getPointerPosition ()
    if (!(point))
      return

    const lastLine = activeLayer.lines[activeLayer.lines.length - 1]
    const updatedLine = { ...lastLine,
			  points: [...lastLine.points, point.x, point.y] }
    const newLines = [...activeLayer.lines.slice (0, -1), updatedLine]
    updateActiveLayerLines (newLines)
  }

  const handleMouseUp = () => {
    drawingRef.current = false
  }

  const handleUndo = () => {
    if (!(activeLayer) || activeLayer.lines.length === 0)
      return

    const prev = activeLayer.history[activeLayer.history.length - 1]
    updateActiveLayerFuture ([...activeLayer.future, activeLayer.lines])
    updateActiveLayerLines (prev)
    updateActiveLayerHistory (activeLayer.history.slice (0, -1))
  }

  const handleRedo = () => {
    if (!(activeLayer) || activeLayer.future.length === 0)
      return

    const next = activeLayer.future[activeLayer.future.length - 1]
    updateActiveLayerHistory ([...activeLayer.history, activeLayer.lines])
    updateActiveLayerLines (next)
    updateActiveLayerFuture (activeLayer.future.slice (0, -1))
  }

  const handleImportImage: ChangeEventHandler<HTMLInputElement> = ev => {
    const file = ev.target.files?.[0]
    if (!(file))
      return

    const reader = new FileReader ()
    reader.onload = () => {
      const src = reader.result?.toString ()
      if (!(src))
	return
      addLayer ({ src })
    }
    reader.readAsDataURL (file)

    if (fileInputRef.current)
      fileInputRef.current.value = ''
  }

  const handlePaint = (ev: Konva.KonvaEventObject<PointerEvent | MouseEvent | TouchEvent>) => {
    const layer = activeLayer
    if (!(layer) || !(layersRef.current?.[layer.id]))
      return

    const pos = ev.target.getStage ()?.getPointerPosition ()
    if (!(pos))
      return

    const stage = layersRef.current[layer.id]
    if (!(stage))
      return

    if (drawingRef.current)
      return

    drawingRef.current = true

    const off = stage.toCanvas ({ pixelRatio: 1 })
    const ctx = off.getContext ('2d')!
    const imgData = ctx.getImageData (0, 0, off.width, off.height)

    const [r, g, b, a] = hexToRgba (colour, 255)
    floodFill (imgData, Math.floor (pos.x), Math.floor (pos.y), { r, g, b, a })
    ctx.putImageData (imgData, 0, 0)

    updateActiveLayerHistory ([...layer.history, layer.lines])
    updateActiveLayerFuture ([])

    setLayers (prev => prev.map (l => l.id === activeLayerId ? ({
	...l,
	lines: [...l.lines,
		{ mode:     Mode.Paint,
		  points:   [pos.x, pos.y],
		  stroke:   colour,
		  imageSrc: off.toDataURL ('image/png') }] }) : l))

    drawingRef.current = false
  }

  const updateActiveLayerLines = (lines: Line[]) => {
    setLayers (prev => (
	prev.map (layer => (layer.id === activeLayerId
			    ? { ...layer, lines }
			    : layer))))
  }

  const updateActiveLayerHistory = (history: Line[][]) => {
    setLayers (prev => (
	prev.map (layer => (layer.id === activeLayerId
			    ? { ...layer, history }
			    : layer))))
  }

  const updateActiveLayerFuture = (future: Line[][]) => {
    setLayers (prev => (
	prev.map (layer => (layer.id === activeLayerId
			    ? { ...layer, future }
			    : layer))))
  }

  const addLayer = (image?: ImageItem) => {
    const layer: Layer = { id:      nanoid (),
			   name:    `Layer ${ layerCnt + 1 }`,
			   lines:   [],
			   history: [],
			   future:  [],
			   image }
    setLayers(prev => [...prev, layer])
    setActiveLayerId (layer.id)
    setLayerCnt (n => n + 1)

    return layer
  }

  const canMoveLayer = (direction: number) => {
    const idx = layers.findIndex (l => l.id === activeLayerId)
    if (idx < 0)
      return false

    const target = idx + direction
    return 0 <= target && target < layers.length
  }

  const moveLayer = (direction: number) => {
    if (!(canMoveLayer (direction)))
      return

    const idx = layers.findIndex (l => l.id === activeLayerId)
    const target = idx + direction
    const next = [...layers]
    const [item] = next.splice (idx, 1)
    next.splice (target, 0, item)
    setLayers(next)
  }

  const clearCanvas = () => {
    const layer: Layer = { id:      nanoid (),
			   name:    `Layer 1`,
			   lines:   [],
			   history: [],
			   future:  [] }
    setLayers([layer])
    setActiveLayerId (layer.id)
    setLayerCnt (1)
    setStageWidth (480)
    setStageHeight (480)
  }

  const exportAllLayers = async (): Promise<string> => {
    const container = document.createElement ('div')
    const stage = new Konva.Stage ({
	container,
	width:      stageWidth,
	height:     stageHeight })

    const bgLayer = new Konva.Layer
    bgLayer.add (new Konva.Rect ({
	x:          0,
	y:          0,
	width:      stageWidth,
	height:     stageHeight,
	fill:       'white',
	listening:  false }))
    stage.add (bgLayer)

    layers.forEach (layer => {
      const kLayer = new Konva.Layer

      const baseImg = images[layer.id]
      if (baseImg)
	{
	  kLayer.add (new Konva.Image ({
	      image:      baseImg,
	      x:          0,
	      y:          0,
	      scaleX:     stageWidth / baseImg.width,
	      scaleY:     stageHeight / baseImg.height,
	      listening:  false }))
	}

      layer.lines.forEach (line => {
	if (line.mode === Mode.Paint)
	  {
	    const img = images[line.imageSrc]
	    img && kLayer.add (new Konva.Image ({
		image:      img,
		x:          0,
		y:          0,
		listening:  false }))
	  }
	else
	  {
	    kLayer.add (new Konva.Line ({
		points:                   line.points,
		stroke:                   line.mode === Mode.Rubber ? 'black' : line.stroke,
		strokeWidth:              line.strokeWidth,
		tension:                  .5,
		lineCap:                  'round',
		globalCompositeOperation: line.mode === Mode.Rubber ? 'destination-out' : 'source-over',
		listening:                false }))
	  }
      })

      stage.add (kLayer)
    })

    const dataURL = stage.toDataURL ({ mimeType: 'image/png', pixelRatio: 1 })
    stage.destroy ()
    return dataURL
  }

  useEffect (() => {
    try
    {
      const raw = localStorage.getItem ('paint')
      if (!(raw))
	throw new Error

      const parsed: unknown = JSON.parse (raw)
      if (!(Array.isArray (parsed)))
	throw new Error

      const restored: Layer[] = parsed.filter (isLayer)
      if (restored.length === 0)
	throw new Error

      setLayers (restored)
      setActiveLayerId (restored[0].id)
      setLayerCnt (restored.length)
    }
    catch
    {
      clearCanvas ()
    }
  }, [])

  useEffect (() => {
    localStorage.setItem ('paint', JSON.stringify (layers))

    layers.forEach (layer => {
      if (layer.image && !(layer.id in images))
	{
	  const image = new window.Image
	  image.crossOrigin = 'anonymous'
	  image.src = layer.image.src
	  image.onload = () => {
	    setImages (prev => ({ ...prev, [layer.id]: image }))
	  }
	}

      layer.lines.forEach (line => {
	if (line.mode === Mode.Paint && !(line.imageSrc in images))
	  {
	    const image = new window.Image
	    image.crossOrigin = 'anonymous'
	    image.src = line.imageSrc
	    image.onload = () => {
	      setImages (prev => ({ ...prev, [line.imageSrc]: image }))
	    }
	  }
      })
    })
  }, [layers])

  useImperativeHandle (ref, () => ({
      exportAll:  async () => ({ dataURL: await exportAllLayers (),
				 width:   stageWidth,
				 height:  stageHeight }),
      clear:	  clearCanvas }))

  return (
      <div className={cn (!(visible) && 'hidden')}>
	<div>
	  <div>
	    <button onClick={() => {
	      if (!(confirm ('作成中の絵を消してもよろしいですか?')))
		return

	      clearCanvas ()
	    }}>
	      初期化
	    </button>

	    <button onClick={() => fileInputRef.current?.click ()}>
	      画像
	    </button>
	    <input ref={fileInputRef}
		   type="file"
		   accept="image/*"
		   className="hidden"
		   onChange={handleImportImage} />

	    <button onClick={() => {
	      const stage = stageRef.current
	      if (!(stage))
		return
	      const mimeType = 'image/png'
	      const dataURL = stage.toDataURL ({ mimeType, pixelRatio: 2 })

	      const $a = document.createElement ('a')
	      $a.href = dataURL
	      $a.download = `kekec-bbs_${ (new Date).toLocaleString ('ja-JP-u-ca-japanese').replaceAll (/[\/:]/g, '-').replaceAll (/\s/g, '_') }.png`
	      document.body.appendChild ($a)
	      $a.click ()
	      $a.remove ()
	    }}>
	      保存
	    </button>

	    <div>
	      <label>
		幅（{stageWidth} px）：
		<input type="range"
		       min={32}
		       max={640}
		       value={stageWidth}
		       onChange={ev => setStageWidth (+ev.target.value)} />
	      </label>
	      <label>
		高さ（{stageHeight} px）：
		<input type="range"
		       min={24}
		       max={480}
		       value={stageHeight}
		       onChange={ev => setStageHeight (+ev.target.value)} />
	      </label>
	    </div>
	  </div>

	  <div>
	    <label>モード：</label>
	    <label>
	      <input type="radio"
		     name="mode"
		     value="pen"
		     checked={mode === Mode.Pen}
		     onChange={() => setMode (Mode.Pen)} />
	      ペン
	    </label>

	    <label>
	      <input type="radio"
		     name="mode"
		     value="rubber"
		     checked={mode === Mode.Rubber}
		     onChange={() => setMode (Mode.Rubber)} />
	      消しゴム
	    </label>

	    <label>
	      <input type="radio"
		     name="mode"
		     value="paint"
		     checked={mode === Mode.Paint}
		     onChange={() => setMode (Mode.Paint)} />
	      塗りつぶし
	    </label>
	  </div>

	  <div>
	    <button onClick={handleUndo}
		    disabled={!(activeLayer) || activeLayer.lines.length === 0}>
	      <FaUndo />
	    </button>
	    <button onClick={handleRedo}
		    disabled={!(activeLayer) || activeLayer.future.length === 0}>
	      <FaRedo />
	    </button>
	  </div>

	  <div>
	    <label>色：</label>
	    <input type="color"
		   value={colour}
		   onChange={ev => setColour (ev.target.value)} />
	  </div>

	  <div>
	    <label>サイズ：</label>
	    <input type="range"
		   min={1}
		   max={57}
		   value={pointSize}
		   onChange={ev => setPointSize (+ev.target.value)} />
	    {pointSize}
	  </div>

	  <div>
	    <label>レイア：</label>
	    {layers.map (layer => (
		<label key={layer.id}>
		  <input type="radio"
			 name="layer"
			 value={layer.id}
			 checked={layer.id === activeLayerId}
			 onChange={() => setActiveLayerId (layer.id)} />
		  {layer.name}
		</label>))}
	    <button disabled={layers.length <= 1} onClick={() => {
	      if (layers.length <= 1)
		{
		  alert ('何を消そうというのだ．')
		  return
		}

	      if (!(confirm ('選択中のレイアを削除するがよろしいか?')))
		return

	      let idx = layers.findIndex (l => l.id === activeLayerId) - 1
	      if (idx < 0)
		idx = 0
	      const newLayers = layers.filter (l => l.id !== activeLayerId)
	      setActiveLayerId (newLayers[idx].id)
	      setLayers (newLayers)
	    }}>
	      削除
	    </button>
	    <button disabled={!(canMoveLayer (-1))} onClick={() => {
	      moveLayer (-1)
	    }}>
	      下へ
	    </button>
	    <button disabled={!(canMoveLayer (1))} onClick={() => {
	      moveLayer (1)
	    }}>
	      上へ
	    </button>
	    <button onClick={() => addLayer ()}>
	      追加
	    </button>
	  </div>

	  <label>
	    <input type="checkbox"
		   checked={singleLayer}
		   onChange={ev => setSingleLayer (ev.target.checked)} />
	    レイアを分けて表示
	  </label>
	</div>

	<div className="w-full flex items-center justify-center">
	  <div className="border border-black dark:border-white">
	    <Stage className="touch-none"
		   ref={node => {
		     stageRef.current = node
		   }}
		   width={stageWidth}
		   height={stageHeight}
		   onMouseDown={mode === Mode.Paint ? handlePaint : handleMouseDown}
		   onMouseMove={mode === Mode.Paint ? undefined : handleMouseMove}
		   onMouseUp={mode === Mode.Paint ? undefined : handleMouseUp}
		   onTouchStart={mode === Mode.Paint ? handlePaint : handleMouseDown}
		   onTouchMove={mode === Mode.Paint ? undefined : handleMouseMove}
		   onTouchEnd={mode === Mode.Paint ? undefined : handleMouseUp}>
	      <Layer>
		<Rect x={0}
		      y={0}
		      width={stageWidth}
		      height={stageHeight}
		      fill="white"
		      listening={false} />
	      </Layer>

	      {layers.map (layer => (!(singleLayer) || layer.id === activeLayerId) && (
		  <Layer key={layer.id} ref={node => {
		    if (node)
		      layersRef.current = { ...layersRef.current, [layer.id]: node }
		  }}>
		    {images[layer.id] && (
			<Image image={images[layer.id]}
			       x={0}
			       y={0}
			       scaleX={stageWidth / images[layer.id].width}
			       scaleY={stageHeight / images[layer.id].height} />)}

		    {layer.lines.map ((line, i) => {
		      switch (line.mode)
		      {
		      case Mode.Pen:
		      case Mode.Rubber:
			return (
			  <Line key={i}
				points={line.points}
				stroke={line.mode === Mode.Rubber ? 'black' : line.stroke}
				strokeWidth={line.strokeWidth}
				tension={.5}
				lineCap="round"
				globalCompositeOperation={line.mode === Mode.Rubber
							  ? 'destination-out'
							  : 'source-over'} />)
		      case Mode.Paint:
			return (
			    <Image key={i}
				   image={images[line.imageSrc]}
				   x={0}
				   y={0}
				   scaleX={1}
				   scaleY={1} />)
		      }
		    })}
		  </Layer>))}
	    </Stage>
	  </div>
	</div>
      </div>)
})
