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

import { startMipMapRenderFrame } from '@/components/studio/canvasRegions/waveform/mipMapCache';

import { frameTicker } from './FrameTicker';
import { Rectangle } from './canvasRendererTypes';
import getRelativeCoords from './getRelativeCoords';
import getScaledPixelRatio from './getScaledPixelRatio';
import setCursor from './setCursor';

export type CanvasRegion = {
  touchTarget?: {
    bounds: Rectangle;
    onMouseDown?: (e: MouseEvent) => void;
    onDoubleClick?: (e: MouseEvent) => void;
    hoverCursor?: string;
    dragCursor?: string;
    passthrough?: boolean;
    hoverGroup?: string;
  };

  render?: (ctx: CanvasRenderingContext2D, hovered: boolean) => void;
  renderAnimation?: (ctx: CanvasRenderingContext2D, hovered: boolean) => void;
  // New GL layer: rendered between the base 2D background and other overlays
  renderMiddleGL?: (gl: WebGL2RenderingContext) => void;
  renderAboveGL?: (gl: WebGL2RenderingContext) => void;
  renderTop?: (ctx: CanvasRenderingContext2D, hovered: boolean) => void;
};

let webglContextGets = 0;

const CanvasRenderer = ({
  getCanvasRegions,
  frameCountRef,
  withWebGL = false,
  allowRightClicks = false,
}: {
  getCanvasRegions: (width: number, height: number) => CanvasRegion[];
  frameCountRef: RefObject<number>;
  withWebGL?: boolean;
  allowRightClicks?: boolean;
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const middleGLCanvasRef = useRef<HTMLCanvasElement>(null);
  const aboveGLCanvasRef = useRef<HTMLCanvasElement>(null);
  const topCanvasRef = useRef<HTMLCanvasElement>(null);
  const [middleGL, setMiddleGL] = useState<WebGL2RenderingContext | null>(null);
  const [aboveGL, setAboveGL] = useState<WebGL2RenderingContext | null>(null);
  const lastFrameCountRef = useRef(-Infinity);
  const lastMousePositionRef = useRef({ x: -Infinity, y: -Infinity });
  const lastTargetRegionsRef = useRef<CanvasRegion[]>([]);
  const unsetCursorRef = useRef<(() => void) | null>(null);
  const lastRegionsRef = useRef<CanvasRegion[]>([]);

  const receiveAboveGLCanvasRef = useCallback(
    (canvas: HTMLCanvasElement) => {
      aboveGLCanvasRef.current = canvas;
      if (withWebGL && canvas && !aboveGL) {
        const aboveGL = canvas.getContext('webgl2');
        console.log('got webGL context', webglContextGets++, 'time(s)');
        if (aboveGL) {
          aboveGL.clearColor(0, 0, 0, 0);
          setAboveGL(aboveGL);
        }
      }
    },
    [withWebGL, aboveGL]
  );

  const receiveMiddleGLCanvasRef = useCallback(
    (canvas: HTMLCanvasElement) => {
      middleGLCanvasRef.current = canvas;
      if (withWebGL && canvas && !middleGL) {
        const ctx = canvas.getContext('webgl2');
        if (ctx) {
          ctx.clearColor(0, 0, 0, 0);
          setMiddleGL(ctx);
        }
      }
    },
    [withWebGL, middleGL]
  );

  useEffect(() => {
    if (canvasRef.current && topCanvasRef.current) {
      const canvas = canvasRef.current;
      const ctx = canvas.getContext('2d');

      const topCanvas = topCanvasRef.current;
      const topCtx = topCanvas.getContext('2d');

      const getMouseTargetRegions = ({ x, y }: { x: number; y: number }) => {
        const regions = lastRegionsRef.current;
        const targetRegions: CanvasRegion[] = [];
        for (let i = regions.length - 1; i >= 0; i--) {
          const region = regions[i];
          if (
            region.touchTarget &&
            x >= region.touchTarget.bounds.left &&
            x <= region.touchTarget.bounds.right &&
            y >= region.touchTarget.bounds.top &&
            y <= region.touchTarget.bounds.bottom
          ) {
            targetRegions.push(region);
            if (!region.touchTarget.passthrough) break;
          }
        }

        return targetRegions;
      };

      const handleDoubleClick = (e: MouseEvent) => {
        const { x, y } = getRelativeCoords(canvas, e.clientX, e.clientY);
        const regions = getMouseTargetRegions({ x, y });
        regions.forEach((region) => {
          region.touchTarget?.onDoubleClick?.(e);
        });
      };

      const handleMouseDown = (e: MouseEvent) => {
        if (e.button !== 0 && !allowRightClicks) return;
        const { x, y } = getRelativeCoords(canvas, e.clientX, e.clientY);
        const regions = getMouseTargetRegions({ x, y });
        regions.forEach((region) => {
          if (region?.touchTarget?.onMouseDown) {
            region.touchTarget.onMouseDown(e);
            document.body.style.userSelect = 'none';
            const unsetCursor =
              region.touchTarget?.dragCursor &&
              setCursor(region.touchTarget.dragCursor, true);
            const handleMouseUp = () => {
              document.body.style.userSelect = '';
              window.removeEventListener('mouseup', handleMouseUp);
              if (unsetCursor) unsetCursor();
            };
            window.addEventListener('mouseup', handleMouseUp);
          }
        });
      };

      const handleTouchStart = (e: TouchEvent) => {
        const { x, y } = getRelativeCoords(
          canvas,
          e.touches[0].clientX,
          e.touches[0].clientY
        );
        const regions = getMouseTargetRegions({ x, y });
        regions.forEach((region) => {
          if (region?.touchTarget?.onMouseDown) {
            region.touchTarget.onMouseDown(
              new MouseEvent('mousedown', {
                clientX: e.touches[0].clientX,
                clientY: e.touches[0].clientY,
                button: 0,
                buttons: 1,
                ctrlKey: e.ctrlKey,
                shiftKey: e.shiftKey,
                altKey: e.altKey,
                metaKey: e.metaKey,
              })
            );
            document.body.style.userSelect = 'none';
          }
        });
      };

      const handleMouseMove = (e: MouseEvent) => {
        lastMousePositionRef.current = { x: e.clientX, y: e.clientY };
        const { x, y } = getRelativeCoords(canvas, e.clientX, e.clientY);
        const targetRegions = getMouseTargetRegions({ x, y });
        let matches = 0;
        targetRegions.forEach((targetRegion) => {
          if (!lastTargetRegionsRef.current.includes(targetRegion)) {
            if (
              (ctx && targetRegion.render) ||
              (topCtx && targetRegion.renderTop)
            ) {
              frameCountRef.current++;
            }

            if (unsetCursorRef.current) {
              unsetCursorRef.current();
              unsetCursorRef.current = null;
            }

            if (targetRegion?.touchTarget?.hoverCursor) {
              unsetCursorRef.current = setCursor(
                targetRegion.touchTarget.hoverCursor
              );
            }
          } else {
            matches++;
          }
        });

        if (matches !== targetRegions.length) {
          frameCountRef.current++;
        }

        lastTargetRegionsRef.current = targetRegions;
      };

      const handleMouseLeave = () => {
        if (unsetCursorRef.current) {
          unsetCursorRef.current();
          unsetCursorRef.current = null;
        }
        frameCountRef.current++;
        lastTargetRegionsRef.current = [];
        lastMousePositionRef.current = { x: -Infinity, y: -Infinity };
      };

      canvasRef.current.addEventListener('mousedown', handleMouseDown);
      canvasRef.current.addEventListener('mousemove', handleMouseMove);
      canvasRef.current.addEventListener('mouseout', handleMouseLeave);
      canvasRef.current.addEventListener('touchstart', handleTouchStart);
      canvasRef.current.addEventListener('dblclick', handleDoubleClick);

      let parentObserver: ResizeObserver;

      if (ctx && topCtx) {
        const render = () => {
          if (lastFrameCountRef.current === frameCountRef.current) {
            return;
          }

          // Start new render frame for generation-based cancellation
          startMipMapRenderFrame();

          ctx.save();
          topCtx.save();
          const scaledPixelRatio = getScaledPixelRatio();

          ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
          ctx.scale(scaledPixelRatio, scaledPixelRatio);

          topCtx.clearRect(0, 0, topCtx.canvas.width, topCtx.canvas.height);
          topCtx.scale(scaledPixelRatio, scaledPixelRatio);

          lastFrameCountRef.current = frameCountRef.current;

          const regions = getCanvasRegions(
            canvas.width / scaledPixelRatio,
            canvas.height / scaledPixelRatio
          );
          lastRegionsRef.current = regions;

          const { x: mouseX, y: mouseY } = getRelativeCoords(
            canvas,
            lastMousePositionRef.current.x,
            lastMousePositionRef.current.y
          );

          const targetRegions = getMouseTargetRegions({ x: mouseX, y: mouseY });
          const hoverGroups: string[] = targetRegions
            .map((t) => t.touchTarget?.hoverGroup)
            .filter((h) => h !== undefined);

          for (let i = 0; i < regions.length; i++) {
            const region = regions[i];
            const hovered: boolean = Boolean(
              targetRegions.includes(region) ||
                (region.touchTarget?.hoverGroup &&
                  hoverGroups.includes(region.touchTarget?.hoverGroup))
            );

            if (region.render) {
              region.render(ctx, hovered);
            }

            if (region.renderTop) {
              region.renderTop(topCtx, hovered);
            }

            // Only run 2D animations here. WebGL layers are drawn once per
            // frame later in a dedicated pass without forcing continuous
            // re-renders.
            if (region.renderAnimation) {
              region.renderAnimation(ctx, hovered);
              frameCountRef.current++;
            }
          }

          ctx.restore();
          topCtx.restore();

          // Render WebGL regions
          if (!withWebGL) return;
          if (middleGL) {
            middleGL.viewport(
              0,
              0,
              middleGL.drawingBufferWidth,
              middleGL.drawingBufferHeight
            );
            middleGL.clear(middleGL.COLOR_BUFFER_BIT);
            regions.forEach((r) => r.renderMiddleGL?.(middleGL));
          }
          if (aboveGL) {
            aboveGL.viewport(
              0,
              0,
              aboveGL.drawingBufferWidth,
              aboveGL.drawingBufferHeight
            );
            aboveGL.clear(aboveGL.COLOR_BUFFER_BIT);
            regions.forEach((r) => r.renderAboveGL?.(aboveGL));
          }
        };

        // Subscribe this canvas to the shared ticker
        const unsubscribe = frameTicker.subscribe(render);

        parentObserver = new ResizeObserver((entries) => {
          if (frameCountRef) {
            frameCountRef.current++;
          }
          const size = entries[0].contentBoxSize[0];
          canvas.width = size.inlineSize * window.devicePixelRatio;
          canvas.height = size.blockSize * window.devicePixelRatio;
          canvas.style.transform = `scale(${1 / window.devicePixelRatio})`;
          canvas.style.transformOrigin = '0 0';

          topCanvas.width = size.inlineSize * window.devicePixelRatio;
          topCanvas.height = size.blockSize * window.devicePixelRatio;
          topCanvas.style.transform = `scale(${1 / window.devicePixelRatio})`;
          topCanvas.style.transformOrigin = '0 0';

          if (middleGLCanvasRef.current) {
            const dpr = window.devicePixelRatio || 1;
            const glCanvas = middleGLCanvasRef.current;
            const targetW = Math.max(1, Math.floor(size.inlineSize * dpr));
            const targetH = Math.max(1, Math.floor(size.blockSize * dpr));
            if (glCanvas.width !== targetW || glCanvas.height !== targetH) {
              glCanvas.width = targetW;
              glCanvas.height = targetH;
            }
            glCanvas.style.width = `${size.inlineSize}px`;
            glCanvas.style.height = `${size.blockSize}px`;
            glCanvas.style.transformOrigin = '0 0';
          }

          if (!aboveGLCanvasRef.current) return;
          // HiDPI: render the WebGL overlay at device pixels, keep CSS size stable.
          const dpr = window.devicePixelRatio || 1;
          const glCanvas = aboveGLCanvasRef.current;
          const targetW = Math.max(1, Math.floor(size.inlineSize * dpr));
          const targetH = Math.max(1, Math.floor(size.blockSize * dpr));
          if (glCanvas.width !== targetW || glCanvas.height !== targetH) {
            glCanvas.width = targetW;
            glCanvas.height = targetH;
          }
          // Preserve layout size in CSS pixels.
          glCanvas.style.width = `${size.inlineSize}px`;
          glCanvas.style.height = `${size.blockSize}px`;
          glCanvas.style.transformOrigin = '0 0';

          // Request a frame after resize (will run on next tick)
          render();
        });

        const parentEl = canvas.parentElement as Element | null;
        if (parentEl) parentObserver.observe(parentEl);
        return () => {
          // Unsubscribe from ticker and remove listeners/observers
          unsubscribe();
          canvas.removeEventListener('mousedown', handleMouseDown);
          canvas.removeEventListener('mousemove', handleMouseMove);
          canvas.removeEventListener('mouseout', handleMouseMove);
          canvas.removeEventListener('touchstart', handleTouchStart);
          canvas.removeEventListener('dblclick', handleDoubleClick);
          if (parentObserver) {
            const parent = canvas.parentElement as Element | null;
            if (parent) parentObserver.unobserve(parent);
            parentObserver.disconnect();
          }
        };
      }

      return () => {
        canvas.removeEventListener('mousedown', handleMouseDown);
        canvas.removeEventListener('mousemove', handleMouseMove);
        canvas.removeEventListener('mouseout', handleMouseMove);
        canvas.removeEventListener('touchstart', handleTouchStart);
        canvas.removeEventListener('dblclick', handleDoubleClick);
        if (parentObserver) {
          const parent = canvas.parentElement as Element | null;
          if (parent) parentObserver.unobserve(parent);
          parentObserver.disconnect();
        }
      };
    }
  }, [
    frameCountRef,
    getCanvasRegions,
    allowRightClicks,
    withWebGL,
    middleGL,
    aboveGL,
  ]);

  return (
    <div style={{ position: 'relative', width: '100%', height: '100%' }}>
      {/* 2-D canvas */}
      <canvas ref={canvasRef} style={{ display: 'block' }} />
      {/* Middle WebGL (waveforms) above base 2D, below everything else */}
      {withWebGL && (
        <canvas
          ref={receiveMiddleGLCanvasRef}
          style={{
            position: 'absolute',
            top: 0,
            left: 0,
            display: 'block',
            pointerEvents: 'none',
          }}
        />
      )}
      {/* WebGL above canvas */}
      {withWebGL && (
        <canvas
          ref={receiveAboveGLCanvasRef}
          style={{
            position: 'absolute',
            top: 0,
            left: 0,
            display: 'block',
            pointerEvents: 'none',
          }}
        />
      )}
      {/* top canvas */}
      <canvas
        ref={topCanvasRef}
        style={{
          position: 'absolute',
          top: 0,
          left: 0,
          display: 'block',
          pointerEvents: 'none',
        }}
      />
    </div>
  );
};

export default CanvasRenderer;
