import { useEffect, useRef, useState } from 'react';

import type { WhiteboardStroke } from '@chat-app/shared/chat';

export type WhiteboardTool = 'pen' | 'eraser';
export type WhiteboardColor = '#000000' | '#ef4444' | '#3b82f6' | '#22c55e' | '#facc15' | '#a855f7';
export type WhiteboardWidth = 2 | 4 | 8;

export interface WhiteboardStrokePayload {
  tool: WhiteboardTool;
  color: WhiteboardColor;
  width: WhiteboardWidth;
  // [x, y, t-ms-since-stroke-start]
  points: Array<[number, number, number]>;
}

interface Props {
  strokes: WhiteboardStroke[];
  tool: WhiteboardTool;
  color: WhiteboardColor;
  width: WhiteboardWidth;
  onStroke: (payload: WhiteboardStrokePayload) => void;
  logicalWidth?: number;
  logicalHeight?: number;
}

const DEFAULT_LOGICAL_W = 1280;
const DEFAULT_LOGICAL_H = 720;

export function WhiteboardCanvas({
  strokes,
  tool,
  color,
  width,
  onStroke,
  logicalWidth = DEFAULT_LOGICAL_W,
  logicalHeight = DEFAULT_LOGICAL_H,
}: Props) {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const draftRef = useRef<WhiteboardStrokePayload | null>(null);
  const strokeStartRef = useRef<number>(0);
  const [, forceTick] = useState(0);

  useEffect(() => {
    const cv = canvasRef.current;
    if (!cv) return;
    cv.width = logicalWidth;
    cv.height = logicalHeight;
    const ctx = cv.getContext('2d');
    if (!ctx) return;
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, cv.width, cv.height);
    for (const s of strokes) {
      const payload = s.strokeJson as Partial<WhiteboardStrokePayload> | null;
      if (payload) renderStroke(ctx, payload);
    }
    if (draftRef.current) renderStroke(ctx, draftRef.current);
  });

  function canvasPoint(e: React.PointerEvent<HTMLCanvasElement>): [number, number] {
    const cv = canvasRef.current!;
    const rect = cv.getBoundingClientRect();
    const scaleX = cv.width / rect.width;
    const scaleY = cv.height / rect.height;
    return [(e.clientX - rect.left) * scaleX, (e.clientY - rect.top) * scaleY];
  }

  const handlePointerDown = (e: React.PointerEvent<HTMLCanvasElement>) => {
    const cv = canvasRef.current;
    if (!cv) return;
    cv.setPointerCapture(e.pointerId);
    const [x, y] = canvasPoint(e);
    strokeStartRef.current = Date.now();
    draftRef.current = {
      tool,
      color,
      width,
      points: [[x, y, 0]],
    };
    forceTick((n) => n + 1);
  };

  const handlePointerMove = (e: React.PointerEvent<HTMLCanvasElement>) => {
    if (!draftRef.current) return;
    const [x, y] = canvasPoint(e);
    draftRef.current.points.push([x, y, Date.now() - strokeStartRef.current]);
    forceTick((n) => n + 1);
  };

  const handlePointerUp = (e: React.PointerEvent<HTMLCanvasElement>) => {
    const cv = canvasRef.current;
    if (cv && cv.hasPointerCapture(e.pointerId)) cv.releasePointerCapture(e.pointerId);
    const draft = draftRef.current;
    draftRef.current = null;
    if (!draft) return;
    if (draft.points.length < 2) {
      forceTick((n) => n + 1);
      return;
    }
    onStroke(draft);
    forceTick((n) => n + 1);
  };

  return (
    <canvas
      ref={canvasRef}
      onPointerDown={handlePointerDown}
      onPointerMove={handlePointerMove}
      onPointerUp={handlePointerUp}
      onPointerLeave={handlePointerUp}
      className="block max-h-full max-w-full cursor-crosshair touch-none rounded-lg border border-line/40 bg-white shadow-2xl"
      style={{ aspectRatio: logicalWidth + ' / ' + logicalHeight, width: '100%' }}
    />
  );
}

function renderStroke(
  ctx: CanvasRenderingContext2D,
  s: Partial<WhiteboardStrokePayload>,
): void {
  const points = Array.isArray(s.points) ? s.points : null;
  if (!points || points.length < 1) return;

  ctx.save();
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';
  ctx.lineWidth = typeof s.width === 'number' ? s.width : 4;
  if (s.tool === 'eraser') {
    ctx.strokeStyle = '#ffffff';
    ctx.lineWidth = Math.max(8, (typeof s.width === 'number' ? s.width : 4) * 4);
  } else {
    ctx.strokeStyle = typeof s.color === 'string' ? s.color : '#000000';
  }

  ctx.beginPath();
  ctx.moveTo(points[0]![0], points[0]![1]);
  for (let i = 1; i < points.length; i++) {
    ctx.lineTo(points[i]![0], points[i]![1]);
  }
  ctx.stroke();
  ctx.restore();
}
