"use client";

import {
  drawDrawing,
  findTopmostDrawingAt,
  type CoordinateBridge,
} from "@/lib/chart/drawings/canvas-render";
import { createDrawing } from "@/lib/chart/drawings/factory";
import { sanitizeTextInput } from "@/lib/chart/drawings/storage";
import { translateDrawing } from "@/lib/chart/drawings/transform";
import type {
  ChartDrawing,
  ChartInteractionTool,
  DrawingScope,
  DrawingToolType,
  PriceTimePoint,
} from "@/lib/chart/drawings/types";
import { snapPriceToNearestOhlc } from "@/lib/chart/snap-to-ohlc";
import type { ChartCandle } from "@/lib/market/types";
import {
  ONE_POINT_TOOLS,
  POLYLINE_TOOLS,
  THREE_POINT_TOOLS,
  TWO_POINT_TOOLS,
} from "@/lib/chart/drawings/types";
import { useDrawingStore } from "@/lib/chart/drawings/useDrawingStore";
import { DrawingTextEditorModal } from "@/components/chart/DrawingTextEditorModal";
import {
  forwardRef,
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from "react";

type LwcChart = {
  timeScale: () => {
    timeToCoordinate: (time: number) => number | null;
    coordinateToTime: (x: number) => number | null;
    subscribeVisibleLogicalRangeChange: (cb: () => void) => void;
    unsubscribeVisibleLogicalRangeChange: (cb: () => void) => void;
  };
  subscribeCrosshairMove: (cb: () => void) => void;
  unsubscribeCrosshairMove: (cb: () => void) => void;
};

type LwcSeries = {
  priceToCoordinate: (price: number) => number | null;
  coordinateToPrice: (y: number) => number | null;
};

export type ChartDrawingLayerHandle = {
  /** Returns true if the event should not reach the chart. */
  pointerDown: (x: number, y: number, tool: ChartInteractionTool) => boolean;
  pointerMove: (x: number, y: number, tool: ChartInteractionTool) => void;
  pointerUp: () => void;
  cancelPending: () => void;
  clearSelection: () => void;
  isInteractionSuspended: () => boolean;
  patchDrawingStyle: (
    drawingId: string,
    stylePatch: Partial<ChartDrawing["style"]>,
  ) => void;
};

type ChartDrawingLayerProps = {
  scope: DrawingScope;
  chart: LwcChart | null;
  series: LwcSeries | null;
  activeTool: ChartInteractionTool;
  width: number;
  height: number;
  visible?: boolean;
  drawingIdsWithAlerts?: ReadonlySet<string>;
  ohlcSnapCandles?: ChartCandle[];
  onSelectionChange?: (drawing: ChartDrawing | null) => void;
};

function isDrawingToolType(tool: ChartInteractionTool): tool is DrawingToolType {
  return tool !== "select" && tool !== "delete";
}

export const ChartDrawingLayer = forwardRef<ChartDrawingLayerHandle, ChartDrawingLayerProps>(
  function ChartDrawingLayer(
    {
      scope,
      chart,
      series,
      activeTool,
      width,
      height,
      visible = true,
      drawingIdsWithAlerts,
      ohlcSnapCandles,
      onSelectionChange,
    },
    ref,
  ) {
    const canvasRef = useRef<HTMLCanvasElement>(null);
    const {
      drawings,
      selectedId,
      setSelectedId,
      updateDrawing,
      addDrawing,
      removeDrawing,
    } = useDrawingStore(scope);

    const pendingAnchorsRef = useRef<PriceTimePoint[]>([]);
    const polylinePointsRef = useRef<PriceTimePoint[]>([]);
    const [previewPoint, setPreviewPoint] = useState<PriceTimePoint | null>(null);
    const [textEditor, setTextEditor] = useState<{
      tool: "text" | "callout";
      anchor: PriceTimePoint;
    } | null>(null);
    const textEditorRef = useRef(textEditor);
    textEditorRef.current = textEditor;

    const dragRef = useRef<{
      id: string;
      startX: number;
      startY: number;
      snapshot: ChartDrawing;
    } | null>(null);

    const buildBridge = useCallback((): CoordinateBridge | null => {
      if (!chart || !series) {
        return null;
      }
      return {
        timeToX: (timeSec) => chart.timeScale().timeToCoordinate(timeSec),
        priceToY: (price) => series.priceToCoordinate(price),
      };
    }, [chart, series]);

    const rawPointerToPoint = useCallback(
      (x: number, y: number): PriceTimePoint | null => {
        if (!chart || !series) {
          return null;
        }
        const time = chart.timeScale().coordinateToTime(x);
        const price = series.coordinateToPrice(y);
        if (time === null || price === null) {
          return null;
        }
        return { timeSec: time as number, price };
      },
      [chart, series],
    );

    const pointerToPoint = useCallback(
      (x: number, y: number, opts?: { magnet?: boolean }): PriceTimePoint | null => {
        const raw = rawPointerToPoint(x, y);
        if (!raw) {
          return null;
        }
        if (opts?.magnet && ohlcSnapCandles?.length) {
          return snapPriceToNearestOhlc(ohlcSnapCandles, raw, series, y);
        }
        return raw;
      },
      [ohlcSnapCandles, rawPointerToPoint, series],
    );

    const clearPending = useCallback(() => {
      pendingAnchorsRef.current = [];
      polylinePointsRef.current = [];
      setPreviewPoint(null);
    }, []);

    const redraw = useCallback(() => {
      const canvas = canvasRef.current;
      const bridge = buildBridge();
      if (!canvas || !bridge || width <= 0 || height <= 0) {
        return;
      }
      const dpr = window.devicePixelRatio || 1;
      canvas.width = Math.floor(width * dpr);
      canvas.height = Math.floor(height * dpr);
      canvas.style.width = `${width}px`;
      canvas.style.height = `${height}px`;
      const ctx = canvas.getContext("2d");
      if (!ctx) {
        return;
      }
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.clearRect(0, 0, width, height);
      if (!visible) {
        return;
      }
      for (const d of drawings) {
        if (d.visible !== false) {
          drawDrawing(
            ctx,
            d,
            bridge,
            d.id === selectedId,
            width,
            height,
            drawingIdsWithAlerts?.has(d.id),
          );
        }
      }

      const pending = pendingAnchorsRef.current;
      const preview = previewPoint;
      if (pending.length > 0 && preview && isDrawingToolType(activeTool)) {
        ctx.save();
        ctx.strokeStyle = "#58a6ff";
        ctx.setLineDash([6, 4]);
        ctx.lineWidth = 1;
        const last = pending[pending.length - 1]!;
        const p1 = bridge.timeToX(last.timeSec);
        const y1 = bridge.priceToY(last.price);
        const p2x = bridge.timeToX(preview.timeSec);
        const y2 = bridge.priceToY(preview.price);
        if (p1 !== null && y1 !== null && p2x !== null && y2 !== null) {
          ctx.beginPath();
          ctx.moveTo(p1, y1);
          ctx.lineTo(p2x, y2);
          ctx.stroke();
        }
        ctx.restore();
      }

      const poly = polylinePointsRef.current;
      if (poly.length >= 1 && preview && POLYLINE_TOOLS.includes(activeTool as DrawingToolType)) {
        ctx.save();
        ctx.strokeStyle = "#58a6ff";
        ctx.setLineDash([4, 3]);
        ctx.lineWidth = 1;
        let prevX: number | null = null;
        let prevY: number | null = null;
        for (const pt of poly) {
          const px = bridge.timeToX(pt.timeSec);
          const py = bridge.priceToY(pt.price);
          if (px !== null && py !== null) {
            if (prevX !== null && prevY !== null) {
              ctx.beginPath();
              ctx.moveTo(prevX, prevY);
              ctx.lineTo(px, py);
              ctx.stroke();
            }
            prevX = px;
            prevY = py;
          }
        }
        if (prevX !== null && prevY !== null && preview) {
          const px = bridge.timeToX(preview.timeSec);
          const py = bridge.priceToY(preview.price);
          if (px !== null && py !== null) {
            ctx.beginPath();
            ctx.moveTo(prevX, prevY);
            ctx.lineTo(px, py);
            ctx.stroke();
          }
        }
        ctx.restore();
      }
    }, [
      activeTool,
      buildBridge,
      drawingIdsWithAlerts,
      drawings,
      height,
      previewPoint,
      selectedId,
      visible,
      width,
    ]);

    useEffect(() => {
      redraw();
    }, [redraw]);

    useEffect(() => {
      if (!onSelectionChange) {
        return;
      }
      const d = selectedId ? drawings.find((x) => x.id === selectedId) ?? null : null;
      onSelectionChange(d);
    }, [drawings, onSelectionChange, selectedId]);

    useEffect(() => {
      if (!chart) {
        return;
      }
      const cb = () => redraw();
      chart.timeScale().subscribeVisibleLogicalRangeChange(cb);
      chart.subscribeCrosshairMove(cb);
      return () => {
        chart.timeScale().unsubscribeVisibleLogicalRangeChange(cb);
        chart.unsubscribeCrosshairMove(cb);
      };
    }, [chart, redraw]);

    useEffect(() => {
      clearPending();
    }, [activeTool, clearPending]);

    const deleteSelected = useCallback(() => {
      if (selectedId) {
        removeDrawing(selectedId);
      }
    }, [removeDrawing, selectedId]);

    useEffect(() => {
      const onKey = (e: KeyboardEvent) => {
        if (e.key === "Delete" || e.key === "Backspace") {
          deleteSelected();
        }
        if (e.key === "Escape") {
          clearPending();
        }
      };
      window.addEventListener("keydown", onKey);
      return () => window.removeEventListener("keydown", onKey);
    }, [clearPending, deleteSelected]);

    const finishPolyline = useCallback(
      (tool: "path" | "pen") => {
        const pts = polylinePointsRef.current;
        if (pts.length >= 2) {
          addDrawing(createDrawing(tool, { points: [...pts] }));
        }
        polylinePointsRef.current = [];
        setPreviewPoint(null);
      },
      [addDrawing],
    );

    const handleToolClick = useCallback(
      (tool: ChartInteractionTool, pt: PriceTimePoint): boolean => {
        if (!isDrawingToolType(tool)) {
          return false;
        }

        if (ONE_POINT_TOOLS.includes(tool)) {
          if (tool === "horizontal") {
            addDrawing(createDrawing("horizontal", { price: pt.price }));
            return true;
          }
          if (tool === "vertical") {
            addDrawing(createDrawing("vertical", { timeSec: pt.timeSec }));
            return true;
          }
          if (tool === "horizontal_ray" || tool === "vertical_ray" || tool === "marker") {
            addDrawing(createDrawing(tool, { anchor: pt }));
            return true;
          }
          if (tool === "text" || tool === "callout") {
            setTextEditor({ tool, anchor: pt });
            return true;
          }
        }

        if (POLYLINE_TOOLS.includes(tool)) {
          polylinePointsRef.current = [...polylinePointsRef.current, pt];
          return true;
        }

        if (THREE_POINT_TOOLS.includes(tool)) {
          pendingAnchorsRef.current = [...pendingAnchorsRef.current, pt];
          if (pendingAnchorsRef.current.length >= 3) {
            const [a, b, c] = pendingAnchorsRef.current;
            addDrawing(createDrawing("parallel_channel", { a: a!, b: b!, c: c! }));
            pendingAnchorsRef.current = [];
            setPreviewPoint(null);
          }
          return true;
        }

        if (TWO_POINT_TOOLS.includes(tool)) {
          pendingAnchorsRef.current = [...pendingAnchorsRef.current, pt];
          if (pendingAnchorsRef.current.length >= 2) {
            const [a, b] = pendingAnchorsRef.current;
            addDrawing(createDrawing(tool, { a: a!, b: b! }));
            pendingAnchorsRef.current = [];
            setPreviewPoint(null);
          }
          return true;
        }

        return false;
      },
      [addDrawing],
    );

    useImperativeHandle(
      ref,
      () => ({
        cancelPending: clearPending,
        clearSelection: () => {
          setSelectedId(null);
          onSelectionChange?.(null);
        },
        isInteractionSuspended: () => textEditorRef.current !== null,
        pointerDown: (x, y, tool) => {
          if (textEditorRef.current) {
            return false;
          }
          if (!chart || !series) {
            return false;
          }
          const bridge = buildBridge();
          if (!bridge) {
            return false;
          }

          const hit = findTopmostDrawingAt(x, y, drawings, bridge);
          if (hit) {
            setSelectedId(hit.id);
            onSelectionChange?.(hit);
            if (tool === "delete") {
              removeDrawing(hit.id);
              onSelectionChange?.(null);
              return true;
            }
            if (tool === "select") {
              dragRef.current = { id: hit.id, startX: x, startY: y, snapshot: hit };
            }
            return true;
          }

          if (tool === "select") {
            setSelectedId(null);
            onSelectionChange?.(null);
            return false;
          }

          const pt = pointerToPoint(x, y, { magnet: true });
          if (!pt) {
            return true;
          }
          return handleToolClick(tool, pt);
        },
        pointerMove: (x, y, tool) => {
          const pt = pointerToPoint(x, y, { magnet: isDrawingToolType(tool) });
          if (
            pt &&
            (pendingAnchorsRef.current.length > 0 ||
              polylinePointsRef.current.length > 0) &&
            isDrawingToolType(tool)
          ) {
            setPreviewPoint(pt);
          }

          const drag = dragRef.current;
          if (!drag || tool !== "select" || !chart || !series) {
            return;
          }
          const startPt = rawPointerToPoint(drag.startX, drag.startY);
          const curPt = rawPointerToPoint(x, y);
          if (!startPt || !curPt) {
            return;
          }
          const dt = curPt.timeSec - startPt.timeSec;
          const dp = curPt.price - startPt.price;
          updateDrawing(drag.id, translateDrawing(drag.snapshot, dt, dp));
        },
        pointerUp: () => {
          dragRef.current = null;
        },
        patchDrawingStyle: (drawingId, stylePatch) => {
          const d = drawings.find((x) => x.id === drawingId);
          if (!d) {
            return;
          }
          const next = {
            ...d,
            style: { ...d.style, ...stylePatch },
          };
          updateDrawing(drawingId, next);
          onSelectionChange?.(next);
        },
      }),
      [
        buildBridge,
        chart,
        clearPending,
        drawings,
        handleToolClick,
        pointerToPoint,
        removeDrawing,
        series,
        setSelectedId,
        updateDrawing,
        onSelectionChange,
      ],
    );

    useEffect(() => {
      const onDblClick = () => {
        if (activeTool === "path" || activeTool === "pen") {
          finishPolyline(activeTool);
        }
      };
      window.addEventListener("dblclick", onDblClick);
      return () => window.removeEventListener("dblclick", onDblClick);
    }, [activeTool, finishPolyline]);

    return (
      <>
        <canvas
          ref={canvasRef}
          className="chart-drawing-layer"
          aria-hidden
          style={{ pointerEvents: "none" }}
        />
        <DrawingTextEditorModal
          open={textEditor !== null}
          title={textEditor?.tool === "callout" ? "Callout text" : "Chart text"}
          onCancel={() => setTextEditor(null)}
          onSubmit={({ text, color, fontSize }) => {
            if (!textEditor) {
              return;
            }
            const safe = sanitizeTextInput(text);
            if (!safe) {
              setTextEditor(null);
              return;
            }
            const d = createDrawing(textEditor.tool, {
              anchor: textEditor.anchor,
              text: safe,
            });
            addDrawing({
              ...d,
              style: { ...d.style, color, fontSize },
            });
            setTextEditor(null);
          }}
        />
      </>
    );
  },
);
