import { isCanvasState, type CanvasState } from "@dada/shared-contracts";

import { DYNAMIC_RENDER_MODELS } from "./dynamic-render-models.js";
import type { DynamicTemplateId } from "./dynamic-provider.js";

type CanvasElement = CanvasState["elements"][number];

export interface CanvasPoint {
  x: number;
  y: number;
}

export interface CanvasRect extends CanvasPoint {
  height: number;
  width: number;
}

export interface CanvasElementIdentity {
  createdAt: string;
  elementId: string;
}

export type CanvasLayerCommand = "back" | "backward" | "forward" | "front";

interface StaticStickerInput {
  assetId: string;
  identity: CanvasElementIdentity;
  opacity?: number;
  position: CanvasPoint;
  resourceVersion: string;
  rotation?: number;
  scale?: CanvasElement["scale"];
  style_parameters?: CanvasElement["style_parameters"];
  z_index?: number;
  zIndex: number;
}

const snapThreshold = 0.015;
const hitHalfExtent = 0.08;

function styleNumber(element: CanvasElement, key: string, fallback: number) {
  const value = element.style_parameters?.[key];
  return typeof value === "number" ? value : fallback;
}

function textLineUnits(line: string) {
  return Array.from(line).reduce((total, character) => total + (/^[\x00-\x7F]$/.test(character) ? 0.62 : 1), 0);
}

export function elementHalfExtents(state: CanvasState, element: CanvasElement): CanvasPoint {
  if (element.type === "color_card") {
    const vertical = element.style_id === "style_01" || element.style_id === "style_02";
    return {
      x: Math.max(hitHalfExtent, (vertical ? 62 : 175) / state.pixel_width) * element.scale.x,
      y: Math.max(hitHalfExtent, (vertical ? 155 : 50) / state.pixel_height) * element.scale.y,
    };
  }
  if (element.type === "dynamic_sticker") {
    const model = DYNAMIC_RENDER_MODELS[element.template_or_asset_id as DynamicTemplateId];
    const halfWidth = model?.halfSize.width ?? 170;
    const halfHeight = model?.halfSize.height ?? 62;
    return {
      x: Math.max(hitHalfExtent, halfWidth / state.pixel_width) * element.scale.x,
      y: Math.max(hitHalfExtent, halfHeight / state.pixel_height) * element.scale.y,
    };
  }
  if (element.type !== "text_template") {
    return { x: hitHalfExtent * element.scale.x, y: hitHalfExtent * element.scale.y };
  }
  const lines = (element.content ?? "").split("\n");
  const fontSize = element.font_size ?? 48;
  const letterSpacing = styleNumber(element, "letter_spacing", 1);
  const lineHeight = styleNumber(element, "line_height", 1.2);
  const strokeWidth = styleNumber(element, "stroke_width", 0);
  const longestLine = Math.max(1, ...lines.map((line) => textLineUnits(line)));
  const longestCharacterCount = Math.max(1, ...lines.map((line) => Array.from(line).length));
  const widthPixels = longestLine * fontSize + (longestCharacterCount - 1) * letterSpacing + 32 + strokeWidth * 2;
  const heightPixels = Math.max(1, lines.length) * fontSize * lineHeight + 32 + strokeWidth * 2;
  return {
    x: Math.max(hitHalfExtent, widthPixels / state.pixel_width / 2) * element.scale.x,
    y: Math.max(hitHalfExtent, heightPixels / state.pixel_height / 2) * element.scale.y,
  };
}

function cloneState(state: CanvasState) {
  return structuredClone(state);
}

function requireState(state: CanvasState) {
  if (!isCanvasState(state)) throw new Error("canvas_state_invalid");
  return cloneState(state);
}

function clamp(value: number, minimum: number, maximum: number) {
  return Math.max(minimum, Math.min(maximum, value));
}

function normalizedRotation(value: number) {
  const normalized = ((value + 180) % 360 + 360) % 360 - 180;
  return normalized === -180 ? 180 : normalized;
}

function samePoint(left: CanvasPoint | undefined, right: CanvasPoint) {
  return Boolean(left && Math.abs(left.x - right.x) < 0.001 && Math.abs(left.y - right.y) < 0.001);
}

function reindex(elements: CanvasElement[]) {
  return elements.map((element, zIndex) => ({ ...element, z_index: zIndex }));
}

export function createStaticStickerElement(input: StaticStickerInput): CanvasElement {
  return {
    created_at: input.identity.createdAt,
    element_id: input.identity.elementId,
    opacity: input.opacity ?? 1,
    position: structuredClone(input.position),
    resource_version: input.resourceVersion,
    rotation: input.rotation ?? 0,
    scale: structuredClone(input.scale ?? { x: 1, y: 1 }),
    style_parameters: structuredClone(input.style_parameters ?? { flip_horizontal: false }),
    template_or_asset_id: input.assetId,
    type: "static_sticker",
    z_index: input.z_index ?? input.zIndex,
  };
}

export class CanvasElementController {
  private current: CanvasState;
  private selection: string[] = [];
  private cyclePoint: CanvasPoint | undefined = undefined;
  private cycleSignature = "";
  private cycleIndex = -1;

  constructor(initial: CanvasState) {
    this.current = requireState(initial);
  }

  get value() {
    return cloneState(this.current);
  }

  get selectedIds() {
    return [...this.selection];
  }

  replaceState(next: CanvasState) {
    this.current = requireState(next);
    const existing = new Set(this.current.elements.map((element) => element.element_id));
    this.selection = this.selection.filter((elementId) => existing.has(elementId));
    this.pointerMoved();
  }

  clearSelection() {
    this.selection = [];
    this.pointerMoved();
  }

  pointerMoved() {
    this.cyclePoint = undefined;
    this.cycleSignature = "";
    this.cycleIndex = -1;
  }

  selectById(elementId: string, append = false) {
    if (!this.current.elements.some((element) => element.element_id === elementId)) return this.selectedIds;
    if (!append) this.selection = [elementId];
    else if (this.selection.includes(elementId)) this.selection = this.selection.filter((selected) => selected !== elementId);
    else this.selection = [...this.selection, elementId];
    return this.selectedIds;
  }

  selectIds(elementIds: readonly string[]) {
    const existing = new Set(this.current.elements.map((element) => element.element_id));
    this.selection = [...new Set(elementIds.filter((elementId) => existing.has(elementId)))];
    this.pointerMoved();
    return this.selectedIds;
  }

  candidatesAt(point: CanvasPoint) {
    return this.current.elements
      .filter((element) => {
        const { x: halfWidth, y: halfHeight } = elementHalfExtents(this.current, element);
        return point.x >= element.position.x - halfWidth && point.x <= element.position.x + halfWidth
          && point.y >= element.position.y - halfHeight && point.y <= element.position.y + halfHeight;
      })
      .sort((left, right) => right.z_index - left.z_index)
      .map((element) => structuredClone(element));
  }

  selectAt(point: CanvasPoint, options: { append?: boolean } = {}) {
    const candidates = this.candidatesAt(point);
    if (candidates.length === 0) {
      if (!options.append) this.selection = [];
      this.pointerMoved();
      return this.selectedIds;
    }
    if (options.append) {
      this.pointerMoved();
      return this.selectById(candidates[0]!.element_id, true);
    }
    const signature = candidates.map((candidate) => candidate.element_id).join("|");
    if (samePoint(this.cyclePoint, point) && signature === this.cycleSignature) this.cycleIndex = (this.cycleIndex + 1) % candidates.length;
    else this.cycleIndex = 0;
    this.cyclePoint = { ...point };
    this.cycleSignature = signature;
    this.selection = [candidates[this.cycleIndex]!.element_id];
    return this.selectedIds;
  }

  marqueeSelect(rectangle: CanvasRect, append = false) {
    const minimumX = Math.min(rectangle.x, rectangle.x + rectangle.width);
    const maximumX = Math.max(rectangle.x, rectangle.x + rectangle.width);
    const minimumY = Math.min(rectangle.y, rectangle.y + rectangle.height);
    const maximumY = Math.max(rectangle.y, rectangle.y + rectangle.height);
    const hits = this.current.elements
      .filter((element) => element.position.x >= minimumX && element.position.x <= maximumX
        && element.position.y >= minimumY && element.position.y <= maximumY)
      .sort((left, right) => left.z_index - right.z_index)
      .map((element) => element.element_id);
    this.selection = append ? [...new Set([...this.selection, ...hits])] : hits;
    this.pointerMoved();
    return this.selectedIds;
  }

  add(element: CanvasElement) {
    if (this.current.elements.length >= 50) throw new Error("canvas_element_limit_reached");
    const nextElement = structuredClone(element);
    nextElement.z_index = this.current.elements.length;
    this.current = requireState({ ...this.current, elements: [...this.current.elements, nextElement] });
    this.selection = [nextElement.element_id];
    this.pointerMoved();
    return this.value;
  }

  replaceElement(element: CanvasElement) {
    const index = this.current.elements.findIndex((candidate) => candidate.element_id === element.element_id);
    if (index < 0) throw new Error("canvas_element_not_found");
    const elements = [...this.current.elements];
    elements[index] = structuredClone(element);
    this.current = requireState({ ...this.current, elements });
    this.selection = [element.element_id];
    this.pointerMoved();
    return this.value;
  }

  private updateSelected(mapper: (element: CanvasElement) => CanvasElement) {
    const selected = new Set(this.selection);
    this.current = requireState({
      ...this.current,
      elements: this.current.elements.map((element) => selected.has(element.element_id) ? mapper(structuredClone(element)) : element),
    });
    this.pointerMoved();
    return this.value;
  }

  moveSelected(delta: CanvasPoint, options: { snap?: boolean } = {}) {
    const selected = new Set(this.selection);
    const primary = this.current.elements.find((element) => selected.has(element.element_id));
    if (!primary) return { guides: [] as string[], state: this.value };
    let nextX = primary.position.x + delta.x;
    let nextY = primary.position.y + delta.y;
    const guides: string[] = [];
    const snapAxis = (value: number, axis: "x" | "y") => {
      const canvasTargets = [0, 0.5, 1];
      const target = canvasTargets.find((candidate) => Math.abs(value - candidate) <= snapThreshold);
      if (target !== undefined) {
        guides.push(target === 0.5 ? `canvas-center-${axis}` : `canvas-edge-${axis}`);
        return target;
      }
      const other = this.current.elements.find((element) => !selected.has(element.element_id) && Math.abs(value - element.position[axis]) <= snapThreshold);
      if (other) {
        guides.push(`element-${axis}:${other.element_id}`);
        return other.position[axis];
      }
      return value;
    };
    if (options.snap !== false) {
      nextX = snapAxis(nextX, "x");
      nextY = snapAxis(nextY, "y");
    }
    const adjusted = { x: nextX - primary.position.x, y: nextY - primary.position.y };
    const state = this.updateSelected((element) => ({
      ...element,
      position: { x: clamp(element.position.x + adjusted.x, 0, 1), y: clamp(element.position.y + adjusted.y, 0, 1) },
    }));
    return { guides, state };
  }

  scaleSelected(factor: number) {
    if (!Number.isFinite(factor) || factor <= 0) throw new Error("canvas_scale_invalid");
    return this.updateSelected((element) => ({
      ...element,
      scale: { x: clamp(element.scale.x * factor, 0.01, 100), y: clamp(element.scale.y * factor, 0.01, 100) },
    }));
  }

  rotateSelected(degrees: number) {
    if (!Number.isFinite(degrees)) throw new Error("canvas_rotation_invalid");
    return this.updateSelected((element) => ({ ...element, rotation: normalizedRotation(element.rotation + degrees) }));
  }

  flipSelectedHorizontal() {
    return this.updateSelected((element) => element.type === "static_sticker" ? {
      ...element,
      style_parameters: { ...element.style_parameters, flip_horizontal: element.style_parameters?.flip_horizontal !== true },
    } : element);
  }

  setSelectedOpacity(opacity: number) {
    if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) throw new Error("canvas_opacity_invalid");
    return this.updateSelected((element) => element.type === "static_sticker" ? { ...element, opacity } : element);
  }

  copySelected() {
    const selected = new Set(this.selection);
    return this.current.elements.filter((element) => selected.has(element.element_id)).map((element) => structuredClone(element));
  }

  pasteElements(elements: readonly CanvasElement[], identityFactory: (source: CanvasElement, index: number) => CanvasElementIdentity) {
    if (this.current.elements.length + elements.length > 50) throw new Error("canvas_element_limit_reached");
    const pasted = elements.map((source, index) => {
      const identity = identityFactory(source, index);
      return {
        ...structuredClone(source),
        created_at: identity.createdAt,
        element_id: identity.elementId,
        position: { x: clamp(source.position.x + 0.02, 0, 1), y: clamp(source.position.y + 0.02, 0, 1) },
        z_index: this.current.elements.length + index,
      };
    });
    this.current = requireState({ ...this.current, elements: [...this.current.elements, ...pasted] });
    this.selection = pasted.map((element) => element.element_id);
    this.pointerMoved();
    return this.value;
  }

  duplicateSelected(identityFactory: (source: CanvasElement, index: number) => CanvasElementIdentity) {
    return this.pasteElements(this.copySelected(), identityFactory);
  }

  deleteSelected() {
    const selected = new Set(this.selection);
    this.current = requireState({ ...this.current, elements: reindex(this.current.elements.filter((element) => !selected.has(element.element_id))) });
    this.selection = [];
    this.pointerMoved();
    return this.value;
  }

  changeLayer(command: CanvasLayerCommand) {
    const selected = new Set(this.selection);
    const ordered = [...this.current.elements].sort((left, right) => left.z_index - right.z_index);
    if (command === "front" || command === "back") {
      const selectedElements = ordered.filter((element) => selected.has(element.element_id));
      const unselectedElements = ordered.filter((element) => !selected.has(element.element_id));
      ordered.splice(0, ordered.length, ...(command === "front" ? [...unselectedElements, ...selectedElements] : [...selectedElements, ...unselectedElements]));
    }
    else if (command === "forward") {
      for (let index = ordered.length - 2; index >= 0; index -= 1) {
        if (selected.has(ordered[index]!.element_id) && !selected.has(ordered[index + 1]!.element_id)) [ordered[index], ordered[index + 1]] = [ordered[index + 1]!, ordered[index]!];
      }
    } else {
      for (let index = 1; index < ordered.length; index += 1) {
        if (selected.has(ordered[index]!.element_id) && !selected.has(ordered[index - 1]!.element_id)) [ordered[index - 1], ordered[index]] = [ordered[index]!, ordered[index - 1]!];
      }
    }
    this.current = requireState({ ...this.current, elements: reindex(ordered) });
    this.pointerMoved();
    return this.value;
  }
}
