import { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent } from "react";
import type { CanvasState } from "@dada/shared-contracts";

import { cssFilterForBackground } from "./editor-canvas.js";
import { DYN012_RENDER_LAYOUT, dyn012DisplayParts } from "./dynamic-provider.js";
import type { DynamicTemplateId } from "./dynamic-provider.js";
import { DYNAMIC_RENDER_MODELS, dynamicFontOptionsFor, dynamicImageUrl, dynamicTextValue } from "./dynamic-render-models.js";
import type { CanvasPoint, CanvasRect } from "./editor-elements.js";
import { fontFamilyName, type ArchivedFontStatus } from "./text-font-loader.js";
import { fontIdForTextElement } from "./text-assets.js";
import { drawColorCard } from "./palette-provider.js";

interface Gesture {
  append: boolean;
  hit: boolean;
  longPressOpened: boolean;
  pointerId: number;
  start: CanvasPoint;
}

interface EditorStageProps {
  assetId: string | null;
  canvasState: CanvasState;
  guides: readonly string[];
  fontStatuses: Readonly<Record<string, ArchivedFontStatus>>;
  onCandidates: (point: CanvasPoint) => void;
  onClearSelection: () => void;
  onCopy: () => void;
  onDelete: () => void;
  onMarquee: (rectangle: CanvasRect, append: boolean) => void;
  onMoveCommit: () => void;
  onMovePreview: (delta: CanvasPoint) => void;
  onNudge: (delta: CanvasPoint) => void;
  onPaste: () => void;
  onPointerMoved: () => void;
  onSelect: (point: CanvasPoint, append: boolean) => boolean;
  projectId: string;
  selectedIds: readonly string[];
}

function pointFromEvent(event: PointerEvent<HTMLCanvasElement>): CanvasPoint {
  const bounds = event.currentTarget.getBoundingClientRect();
  return {
    x: Math.max(0, Math.min(1, (event.clientX - bounds.left) / bounds.width)),
    y: Math.max(0, Math.min(1, (event.clientY - bounds.top) / bounds.height)),
  };
}

function styleValue<T extends string | number | boolean>(element: CanvasState["elements"][number], key: string, fallback: T): T {
  const value = element.style_parameters?.[key];
  return typeof value === typeof fallback ? value as T : fallback;
}

interface TextPixelGeometry {
  height: number;
  width: number;
}

function prepareTextContext(context: CanvasRenderingContext2D, element: CanvasState["elements"][number], fontId: string) {
  const fontSize = element.font_size ?? 48;
  const letterSpacing = styleValue(element, "letter_spacing", 1);
  context.font = `${fontSize}px "${fontFamilyName(fontId)}"`;
  (context as CanvasRenderingContext2D & { letterSpacing: string }).letterSpacing = `${letterSpacing}px`;
}

function measureTextElement(context: CanvasRenderingContext2D, element: CanvasState["elements"][number], fontId: string): TextPixelGeometry {
  prepareTextContext(context, element, fontId);
  const fontSize = element.font_size ?? 48;
  const lineHeight = styleValue(element, "line_height", 1.2);
  const letterSpacing = styleValue(element, "letter_spacing", 1);
  const strokeWidth = styleValue(element, "stroke_width", 0);
  const lines = (element.content ?? "").split("\n");
  const widths = lines.map((line) => context.measureText(line).width + Math.max(0, Array.from(line).length - 1) * letterSpacing);
  return {
    height: Math.max(fontSize * lineHeight, lines.length * fontSize * lineHeight) + 32 + strokeWidth * 2,
    width: Math.max(1, ...widths) + 32 + strokeWidth * 2,
  };
}

function drawTextElement(context: CanvasRenderingContext2D, element: CanvasState["elements"][number], fontStatuses: Readonly<Record<string, ArchivedFontStatus>>) {
  const fontId = fontIdForTextElement(element);
  if (!fontId || fontStatuses[fontId] !== "ready") {
    context.fillStyle = "#e5e7eb";
    context.fillRect(-110, -34, 220, 68);
    context.fillStyle = "#9f1d1d";
    context.font = "600 22px Microsoft YaHei UI, sans-serif";
    context.textAlign = "center";
    context.fillText("字体不可用", 0, 8);
    return;
  }
  const fontSize = element.font_size ?? 48;
  const lineHeight = styleValue(element, "line_height", 1.2);
  const letterSpacing = styleValue(element, "letter_spacing", 1);
  const align = styleValue(element, "text_align", "center") as CanvasTextAlign;
  const lines = (element.content ?? "").split("\n");
  prepareTextContext(context, element, fontId);
  context.textAlign = align;
  context.textBaseline = "middle";
  const widths = lines.map((line) => context.measureText(line).width + Math.max(0, line.length - 1) * letterSpacing);
  const textWidth = Math.max(1, ...widths);
  const textHeight = Math.max(fontSize * lineHeight, lines.length * fontSize * lineHeight);
  const padding = 16;
  const backgroundEnabled = styleValue(element, "background_enabled", false);
  if (backgroundEnabled) {
    const elementOpacity = context.globalAlpha;
    context.globalAlpha = elementOpacity * styleValue(element, "background_opacity", 1);
    context.fillStyle = styleValue(element, "background_color", "#FFE62C");
    context.fillRect(-textWidth / 2 - padding, -textHeight / 2 - padding, textWidth + padding * 2, textHeight + padding * 2);
    context.globalAlpha = elementOpacity;
  }
  const firstY = -((lines.length - 1) * fontSize * lineHeight) / 2;
  const anchorX = align === "left" ? -textWidth / 2 : align === "right" ? textWidth / 2 : 0;
  context.fillStyle = styleValue(element, "fill_color", "#111111");
  context.strokeStyle = styleValue(element, "stroke_color", "#000000");
  context.lineWidth = styleValue(element, "stroke_width", 0);
  lines.forEach((line, index) => {
    const y = firstY + index * fontSize * lineHeight;
    if (styleValue(element, "stroke_enabled", false) && context.lineWidth > 0) context.strokeText(line, anchorX, y);
    context.fillText(line, anchorX, y);
  });
}

function drawDynamicUnavailable(context: CanvasRenderingContext2D, message: string) {
  context.fillStyle = "#e5e7eb";
  context.fillRect(-140, -42, 280, 84);
  context.fillStyle = "#9f1d1d";
  context.font = "600 18px Microsoft YaHei UI, sans-serif";
  context.textAlign = "center";
  context.textBaseline = "middle";
  context.fillText(message, 0, 0);
}

export function drawDynamicSticker(
  context: CanvasRenderingContext2D,
  element: CanvasState["elements"][number],
  fontStatuses: Readonly<Record<string, ArchivedFontStatus>>,
  resourceImages: Readonly<Record<string, HTMLImageElement>>,
) {
  const id = element.template_or_asset_id as DynamicTemplateId;
  const model = DYNAMIC_RENDER_MODELS[id];
  if (!model) {
    drawDynamicUnavailable(context, "动态贴纸模型不可用");
    return;
  }
  if (dynamicFontOptionsFor(id).some((font) => fontStatuses[font.fontId] !== "ready")) {
    drawDynamicUnavailable(context, id === "DYN012" ? "FONT081 替代字体不可用" : "原版字体不可用");
    return;
  }
  if (model.imageLayers.some((layer) => !resourceImages[layer.assetId])) {
    drawDynamicUnavailable(context, "原版图片资源不可用");
    return;
  }
  context.textBaseline = "middle";
  for (const layer of model.imageLayers) {
    const image = resourceImages[layer.assetId]!;
    context.drawImage(image, layer.x - layer.width / 2, layer.y - layer.height / 2, layer.width, layer.height);
  }
  if (id === "DYN012") {
    const parts = dyn012DisplayParts(element);
    const layout = DYN012_RENDER_LAYOUT;
    context.fillStyle = layout.divider.color;
    context.fillRect(layout.divider.x - layout.divider.width / 2, layout.divider.y - layout.divider.height / 2, layout.divider.width, layout.divider.height);
    for (const [part, layer] of [[parts.hour, layout.hour], [parts.minute, layout.minute], [parts.meridiem, layout.meridiem]] as const) {
      context.fillStyle = layer.color;
      context.font = `${layer.fontSize}px "${fontFamilyName(layer.fontId)}"`;
      context.textAlign = "center";
      context.fillText(part, layer.x, layer.y);
    }
    return;
  }
  for (const layer of model.textLayers) {
    context.fillStyle = layer.color;
    context.font = `${layer.fontSize}px "${fontFamilyName(layer.fontId)}"`;
    context.textAlign = layer.align;
    context.fillText(dynamicTextValue(element, layer.value), layer.x, layer.y);
  }
}

function elementSelectionHalfSize(
  context: CanvasRenderingContext2D,
  element: CanvasState["elements"][number],
  fontStatuses: Readonly<Record<string, ArchivedFontStatus>>,
) {
  if (element.type === "color_card") {
    if (element.style_id === "style_01") return { height: 76 * element.scale.y, width: 26 * element.scale.x };
    if (element.style_id === "style_02") return { height: 77 * element.scale.y, width: 18 * element.scale.x };
    if (element.style_id === "style_08") return { height: 10 * element.scale.y, width: 73 * element.scale.x };
    return { height: 9 * element.scale.y, width: 78 * element.scale.x };
  }
  if (element.type === "dynamic_sticker") {
    const model = DYNAMIC_RENDER_MODELS[element.template_or_asset_id as DynamicTemplateId];
    return { height: (model?.halfSize.height ?? 62) * element.scale.y, width: (model?.halfSize.width ?? 170) * element.scale.x };
  }
  if (element.type !== "text_template") return { height: 78 * element.scale.y, width: 78 * element.scale.x };
  const fontId = fontIdForTextElement(element);
  if (!fontId || fontStatuses[fontId] !== "ready") return { height: 34 * element.scale.y, width: 110 * element.scale.x };
  context.save();
  const geometry = measureTextElement(context, element, fontId);
  context.restore();
  return { height: geometry.height * element.scale.y / 2, width: geometry.width * element.scale.x / 2 };
}

function drawElement(
  context: CanvasRenderingContext2D,
  element: CanvasState["elements"][number],
  width: number,
  height: number,
  fontStatuses: Readonly<Record<string, ArchivedFontStatus>>,
  resourceImages: Readonly<Record<string, HTMLImageElement>>,
) {
  const x = element.position.x * width;
  const y = element.position.y * height;
  context.save();
  context.globalAlpha = element.opacity;
  context.translate(x, y);
  context.rotate((element.rotation * Math.PI) / 180);
  const flip = element.style_parameters?.flip_horizontal === true ? -1 : 1;
  context.scale(element.scale.x * flip, element.scale.y);
  if (element.type === "color_card") drawColorCard(context, element);
  else if (element.type === "static_sticker") {
    const image = resourceImages[element.template_or_asset_id];
    if (!image) drawDynamicUnavailable(context, "原版贴纸资源不可用");
    else {
      const scale = 156 / Math.max(image.naturalWidth, image.naturalHeight);
      const width = image.naturalWidth * scale;
      const height = image.naturalHeight * scale;
      context.drawImage(image, -width / 2, -height / 2, width, height);
    }
  } else if (element.type === "text_template") drawTextElement(context, element, fontStatuses);
  else if (element.type === "dynamic_sticker") drawDynamicSticker(context, element, fontStatuses, resourceImages);
  context.restore();
}

function loadCanvasImage(url: string) {
  return new Promise<HTMLImageElement | undefined>((resolve) => {
    const image = new Image();
    image.onload = () => resolve(image);
    image.onerror = () => resolve(undefined);
    image.src = url;
  });
}

function resourceUrlsForCanvas(canvasState: CanvasState) {
  const imageReferences = new Map<string, string>();
  for (const element of canvasState.elements) {
    if (element.type === "static_sticker") imageReferences.set(element.template_or_asset_id, dynamicImageUrl(element.resource_version, element.template_or_asset_id));
    if (element.type === "dynamic_sticker") {
      const model = DYNAMIC_RENDER_MODELS[element.template_or_asset_id as DynamicTemplateId];
      for (const layer of model?.imageLayers ?? []) imageReferences.set(layer.assetId, dynamicImageUrl(element.resource_version, layer.assetId));
    }
  }
  return imageReferences;
}

async function loadSceneResources(canvasState: CanvasState, projectId: string) {
  const background = canvasState.background.asset_id
    ? loadCanvasImage(`/api/v1/private-assets/projects/${encodeURIComponent(projectId)}/images/${encodeURIComponent(canvasState.background.asset_id)}`)
    : Promise.resolve(undefined);
  const resources = Promise.all([...resourceUrlsForCanvas(canvasState)].map(async ([assetId, url]) => [assetId, await loadCanvasImage(url)] as const));
  const [image, loaded] = await Promise.all([background, resources]);
  return {
    background: image,
    resourceImages: Object.fromEntries(loaded.filter((entry): entry is readonly [string, HTMLImageElement] => entry[1] !== undefined)),
  };
}

function renderEditorScene(
  context: CanvasRenderingContext2D,
  canvasState: CanvasState,
  fontStatuses: Readonly<Record<string, ArchivedFontStatus>>,
  background: HTMLImageElement | undefined,
  resourceImages: Readonly<Record<string, HTMLImageElement>>,
) {
  context.clearRect(0, 0, canvasState.pixel_width, canvasState.pixel_height);
  context.fillStyle = "#ffffff";
  context.fillRect(0, 0, canvasState.pixel_width, canvasState.pixel_height);
  context.filter = cssFilterForBackground(canvasState.background.adjustments);
  if (background) context.drawImage(background, 0, 0, canvasState.pixel_width, canvasState.pixel_height);
  context.filter = "none";
  for (const element of [...canvasState.elements].sort((left, right) => left.z_index - right.z_index)) {
    drawElement(context, element, canvasState.pixel_width, canvasState.pixel_height, fontStatuses, resourceImages);
  }
}

function requiredFontIds(canvasState: CanvasState) {
  return canvasState.elements.flatMap((element) => {
    if (element.type === "text_template") return [fontIdForTextElement(element)].filter((fontId): fontId is string => Boolean(fontId));
    if (element.type === "dynamic_sticker") return dynamicFontOptionsFor(element.template_or_asset_id).map((font) => font.fontId);
    return [];
  });
}

export async function renderCanvasStateForExport(input: {
  canvasState: CanvasState;
  fontStatuses: Readonly<Record<string, ArchivedFontStatus>>;
  projectId: string;
}) {
  const missingFont = requiredFontIds(input.canvasState).find((fontId) => input.fontStatuses[fontId] !== "ready");
  if (missingFont) throw new Error("export_font_unavailable");
  await document.fonts.ready;
  const resources = await loadSceneResources(input.canvasState, input.projectId);
  if (input.canvasState.background.asset_id && !resources.background) throw new Error("export_background_unavailable");
  if (Object.keys(resources.resourceImages).length !== resourceUrlsForCanvas(input.canvasState).size) throw new Error("export_asset_unavailable");
  const canvas = document.createElement("canvas");
  canvas.width = input.canvasState.pixel_width;
  canvas.height = input.canvasState.pixel_height;
  const context = canvas.getContext("2d", { colorSpace: "srgb" });
  if (!context) throw new Error("export_canvas_unavailable");
  renderEditorScene(context, input.canvasState, input.fontStatuses, resources.background, resources.resourceImages);
  return canvas;
}

export function EditorStage(props: EditorStageProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const gestureRef = useRef<Gesture | undefined>(undefined);
  const longPressRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  const [marquee, setMarquee] = useState<CanvasRect>();

  useEffect(() => {
    let active = true;
    const canvas = canvasRef.current;
    if (!canvas) return undefined;
    canvas.width = props.canvasState.pixel_width;
    canvas.height = props.canvasState.pixel_height;
    const context = canvas.getContext("2d");
    if (!context) return undefined;
    const render = (image: HTMLImageElement | undefined, resourceImages: Readonly<Record<string, HTMLImageElement>>) => {
      renderEditorScene(context, props.canvasState, props.fontStatuses, image, resourceImages);
      context.lineWidth = 4;
      context.strokeStyle = "#005fcc";
      for (const element of props.canvasState.elements.filter((entry) => props.selectedIds.includes(entry.element_id))) {
        const { width: halfWidth, height: halfHeight } = elementSelectionHalfSize(context, element, props.fontStatuses);
        context.strokeRect(element.position.x * canvas.width - halfWidth, element.position.y * canvas.height - halfHeight, halfWidth * 2, halfHeight * 2);
      }
      context.save();
      context.strokeStyle = "#005fcc";
      context.lineWidth = 2;
      context.setLineDash([12, 8]);
      if (props.guides.some((guide) => guide.includes("-x"))) {
        context.beginPath(); context.moveTo(canvas.width / 2, 0); context.lineTo(canvas.width / 2, canvas.height); context.stroke();
      }
      if (props.guides.some((guide) => guide.includes("-y"))) {
        context.beginPath(); context.moveTo(0, canvas.height / 2); context.lineTo(canvas.width, canvas.height / 2); context.stroke();
      }
      if (marquee) context.strokeRect(marquee.x * canvas.width, marquee.y * canvas.height, marquee.width * canvas.width, marquee.height * canvas.height);
      context.restore();
    };
    void loadSceneResources(props.canvasState, props.projectId).then(({ background, resourceImages }) => {
      if (!active) return;
      render(background, resourceImages);
    });
    return () => { active = false; };
  }, [marquee, props.assetId, props.canvasState, props.fontStatuses, props.guides, props.projectId, props.selectedIds]);

  useEffect(() => () => { if (longPressRef.current) clearTimeout(longPressRef.current); }, []);

  function handlePointerDown(event: PointerEvent<HTMLCanvasElement>) {
    if (event.button !== 0) return;
    const start = pointFromEvent(event);
    const append = event.shiftKey;
    const hit = props.onSelect(start, append);
    gestureRef.current = { append, hit, longPressOpened: false, pointerId: event.pointerId, start };
    event.currentTarget.setPointerCapture(event.pointerId);
    longPressRef.current = setTimeout(() => {
      const gesture = gestureRef.current;
      if (!gesture || !gesture.hit) return;
      gesture.longPressOpened = true;
      props.onCandidates(gesture.start);
    }, 550);
  }

  function handlePointerMove(event: PointerEvent<HTMLCanvasElement>) {
    const gesture = gestureRef.current;
    if (!gesture || gesture.pointerId !== event.pointerId) {
      props.onPointerMoved();
      return;
    }
    const point = pointFromEvent(event);
    const delta = { x: point.x - gesture.start.x, y: point.y - gesture.start.y };
    if (Math.abs(delta.x) + Math.abs(delta.y) < 0.003) return;
    if (longPressRef.current) clearTimeout(longPressRef.current);
    props.onPointerMoved();
    if (gesture.hit && !gesture.longPressOpened) props.onMovePreview(delta);
    else if (!gesture.hit) setMarquee({ height: delta.y, width: delta.x, x: gesture.start.x, y: gesture.start.y });
  }

  function handlePointerUp(event: PointerEvent<HTMLCanvasElement>) {
    const gesture = gestureRef.current;
    if (!gesture || gesture.pointerId !== event.pointerId) return;
    if (longPressRef.current) clearTimeout(longPressRef.current);
    const point = pointFromEvent(event);
    const delta = { x: point.x - gesture.start.x, y: point.y - gesture.start.y };
    const moved = Math.abs(delta.x) + Math.abs(delta.y) >= 0.003;
    if (gesture.hit && moved && !gesture.longPressOpened) props.onMoveCommit();
    else if (!gesture.hit && moved) props.onMarquee({ height: delta.y, width: delta.x, x: gesture.start.x, y: gesture.start.y }, gesture.append);
    setMarquee(undefined);
    gestureRef.current = undefined;
    event.currentTarget.releasePointerCapture(event.pointerId);
  }

  function handleKeyDown(event: KeyboardEvent<HTMLCanvasElement>) {
    const step = event.shiftKey ? 0.01 : 0.002;
    if (event.key === "Delete" || event.key === "Backspace") { event.preventDefault(); props.onDelete(); }
    else if (event.key === "Escape") { event.preventDefault(); props.onClearSelection(); }
    else if (event.ctrlKey && event.key.toLowerCase() === "c") { event.preventDefault(); props.onCopy(); }
    else if (event.ctrlKey && event.key.toLowerCase() === "v") { event.preventDefault(); props.onPaste(); }
    else if (event.key.startsWith("Arrow")) {
      event.preventDefault();
      props.onNudge({ x: event.key === "ArrowLeft" ? -step : event.key === "ArrowRight" ? step : 0, y: event.key === "ArrowUp" ? -step : event.key === "ArrowDown" ? step : 0 });
    }
  }

  return <canvas aria-label="编辑画布" className="editor-canvas" onKeyDown={handleKeyDown} onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} ref={canvasRef} tabIndex={0} />;
}
