import type { ResolvedRenderColor } from '#core/color/management'
/* eslint-disable max-lines -- SkiaRenderer facade owns CanvasKit state and delegates domain drawing */
import {
  SELECTION_COLOR,
  COMPONENT_COLOR,
  CANVAS_BG_COLOR,
  DEFAULT_FONT_SIZE,
  COMPONENT_SET_DASH,
  COMPONENT_SET_DASH_GAP,
  COMPONENT_SET_BORDER_WIDTH,
  IS_BROWSER
} from '#core/constants'
import type { EditorState } from '#core/editor/types'
import { RenderProfiler } from '#core/profiler'
import type { SceneNode, SceneGraph, Fill, Stroke } from '#core/scene-graph'
import type { SnapGuide } from '#core/scene-graph/snap'
import type { TextEditor } from '#core/text/editor'
import type { Color, Rect, Vector } from '#core/types'

import { LabelCache } from './labels/cache'
import * as LabelHitTest from './labels/hit-test'
import * as RenderColors from './renderer/colors'
import * as RendererFonts from './renderer/fonts'
import { destroyRenderer } from './renderer/lifecycle'
import { installRendererDomainMethods } from './renderer/methods'
import { initializeRendererPaints } from './renderer/paints'
import * as RenderPipeline from './renderer/pipeline'
import * as RendererState from './renderer/state'
import * as RenderText from './text'
export type { RenderOverlays, RulerTheme } from './renderer/types'
import type {
  Image as CKImage,
  Path,
  CanvasKit,
  Surface,
  Canvas,
  Paint,
  Font,
  FontMgr,
  TypefaceFontProvider,
  SkPicture,
  ImageFilter,
  MaskFilter,
  Paragraph
} from 'canvaskit-wasm'

export interface SubtreePictureCacheEntry {
  picture: SkPicture
  pageId: string | null
  sceneVersion: number
  positionPreviewVersion: number
}

import type { RenderOverlays, RulerTheme } from './renderer/types'

export class SkiaRenderer {
  ck: CanvasKit
  surface: Surface
  declare fillPaint: Paint
  declare strokePaint: Paint
  declare selectionPaint: Paint
  declare parentOutlinePaint: Paint
  declare snapPaint: Paint
  declare auxFill: Paint
  declare auxStroke: Paint
  declare opacityPaint: Paint
  declare effectLayerPaint: Paint
  imageFilterCache = new Map<string, ImageFilter | null>()
  maskFilterCache = new Map<number, MaskFilter | null>()
  _tmpColor = new Float32Array(4)
  _tmpRect = new Float32Array(4)
  textFont: Font | null = null
  labelFont: Font | null = null
  sizeFont: Font | null = null
  sectionTitleFont: Font | null = null
  componentLabelFont: Font | null = null
  fontMgr: FontMgr | null = null
  fontProvider: TypefaceFontProvider | null = null
  fontsLoaded = false
  imageCache = new Map<string, CKImage>()
  vectorPathCache = new Map<string, Path[]>()
  vectorStrokePathCache = new Map<string, Path[]>()
  vectorStrokeOutlineCache = new Map<string, Path[]>()
  fillGeometryCache = new Map<string, Path[]>()
  strokeGeometryCache = new Map<string, Path[]>()
  scenePicture: SkPicture | null = null
  scenePictureVersion = -1
  scenePicturePositionPreviewVersion = -1
  scenePicturePageId: string | null = null
  sceneBacking: {
    image: CKImage
    pageId: string | null
    sceneVersion: number
    positionPreviewVersion: number
    panX: number
    panY: number
    zoom: number
    width: number
    height: number
    dpr: number
    worldX: number
    worldY: number
    worldWidth: number
    worldHeight: number
  } | null = null
  sceneBackingPreviewUntil = 0
  sceneBackingNeedsCrispRender = false
  sceneBackingBuild: {
    surface: Surface
    graph: SceneGraph
    childIds: string[]
    index: number
    startedAt: number
    pageId: string | null
    sceneVersion: number
    positionPreviewVersion: number
    panX: number
    panY: number
    zoom: number
    width: number
    height: number
    dpr: number
    worldX: number
    worldY: number
    worldWidth: number
    worldHeight: number
  } | null = null
  sceneBackingAverageRecordMs = 40
  sceneBackingAverageViewportIntervalMs = 80
  sceneBackingLastViewportEventAt = 0
  lastSceneViewport: { panX: number; panY: number; zoom: number } | null = null
  nodePictureCache = new Map<string, SkPicture | null>()
  subtreePictureCache = new Map<string, SubtreePictureCacheEntry>()
  subtreePictureCachePageId: string | null = null
  subtreePictureCacheSceneVersion = -1
  subtreePictureCachePositionPreviewVersion = -1
  readonly labelCache = new LabelCache()
  readonly profiler: RenderProfiler

  declare rulerBgPaint: Paint
  declare rulerTickPaint: Paint
  declare rulerTextPaint: Paint
  declare rulerHlPaint: Paint
  declare rulerBadgePaint: Paint
  declare rulerLabelPaint: Paint
  declare penPathPaint: Paint
  declare penLiveStrokePaint: Paint
  declare penHandlePaint: Paint
  declare penVertexFill: Paint
  declare penVertexStroke: Paint

  panX = 0
  panY = 0
  zoom = 1
  dpr = 1
  viewportWidth = 0
  viewportHeight = 0
  showRulers = true
  pageColor = CANVAS_BG_COLOR
  rulerTheme: RulerTheme | null = null
  pageId: string | null = null

  worldViewport = { x: 0, y: 0, w: 0, h: 0 }
  _nodeCount = 0
  _culledCount = 0
  _flashes: Array<{ nodeId: string; startTime: number }> = []
  _flashPaint: Paint | null = null
  _aiActiveNodes: Set<string> = new Set()
  _aiDoneFlashes: Array<{ nodeId: string; startTime: number }> = []

  readonly DEFAULT_FONT_SIZE = DEFAULT_FONT_SIZE
  readonly COMPONENT_SET_BORDER_WIDTH = COMPONENT_SET_BORDER_WIDTH
  readonly COMPONENT_SET_DASH = COMPONENT_SET_DASH
  readonly COMPONENT_SET_DASH_GAP = COMPONENT_SET_DASH_GAP

  declare drawHoverHighlight: (
    canvas: Canvas,
    graph: SceneGraph,
    hoveredNodeId?: string | null
  ) => void
  declare drawEnteredContainer: (
    canvas: Canvas,
    graph: SceneGraph,
    enteredContainerId?: string | null
  ) => void
  declare drawSelection: (
    canvas: Canvas,
    graph: SceneGraph,
    selectedIds: Set<string>,
    overlays: RenderOverlays
  ) => void
  declare drawNodeSelection: (
    canvas: Canvas,
    node: SceneNode,
    rotation: number,
    graph: SceneGraph
  ) => void
  declare drawSelectionLabels: (
    canvas: Canvas,
    graph: SceneGraph,
    selectedIds: Set<string>,
    overlays?: RenderOverlays
  ) => void
  declare drawParentFrameOutlines: (
    canvas: Canvas,
    graph: SceneGraph,
    selectedIds: Set<string>
  ) => void
  declare drawNodeOutline: (
    canvas: Canvas,
    node: SceneNode,
    rotation: number,
    graph: SceneGraph
  ) => void
  declare drawGroupBounds: (canvas: Canvas, nodes: SceneNode[], graph: SceneGraph) => void
  declare getRotatedCorners: (node: SceneNode, abs: Vector) => Vector[]
  declare drawHandle: (canvas: Canvas, x: number, y: number) => void
  declare drawSnapGuides: (canvas: Canvas, guides?: SnapGuide[]) => void
  declare drawMarquee: (canvas: Canvas, marquee?: Rect | null) => void
  declare drawFlashes: (canvas: Canvas, graph: SceneGraph) => void
  declare drawLayoutInsertIndicator: (
    canvas: Canvas,
    indicator?: RenderOverlays['layoutInsertIndicator']
  ) => void
  declare drawAutoLayoutHover: (
    canvas: Canvas,
    graph: SceneGraph,
    hover?: RenderOverlays['autoLayoutHover']
  ) => void
  declare drawTextEditOverlay: (canvas: Canvas, node: SceneNode, editor: TextEditor) => void
  declare drawNodeEditOverlay: (
    canvas: Canvas,
    graph: SceneGraph,
    editState?: RenderOverlays['nodeEditState']
  ) => void
  declare drawPenOverlay: (canvas: Canvas, penState: RenderOverlays['penState']) => void
  declare drawRemoteCursors: (
    canvas: Canvas,
    graph: SceneGraph,
    cursors?: RenderOverlays['remoteCursors']
  ) => void
  declare drawRulers: (canvas: Canvas, graph: SceneGraph, selectedIds: Set<string>) => void
  declare drawSectionTitles: (canvas: Canvas, graph: SceneGraph) => void
  declare drawComponentLabels: (canvas: Canvas, graph: SceneGraph) => void
  declare renderNode: (
    canvas: Canvas,
    graph: SceneGraph,
    nodeId: string,
    overlays: RenderOverlays,
    parentAbsX?: number,
    parentAbsY?: number
  ) => void
  declare renderSection: (canvas: Canvas, node: SceneNode, graph: SceneGraph) => void
  declare renderComponentSet: (canvas: Canvas, node: SceneNode, graph: SceneGraph) => void
  declare renderShape: (canvas: Canvas, node: SceneNode, graph: SceneGraph) => void
  declare renderShapeUncached: (canvas: Canvas, node: SceneNode, graph: SceneGraph) => void
  declare renderEffects: (
    canvas: Canvas,
    node: SceneNode,
    rect: Float32Array,
    hasRadius: boolean,
    pass: 'behind' | 'front',
    shadowShapeChild?: SceneNode | null
  ) => void
  declare renderText: (canvas: Canvas, node: SceneNode, fill?: Fill) => void
  declare drawNodeFill: (
    canvas: Canvas,
    node: SceneNode,
    rect: Float32Array,
    hasRadius: boolean,
    fill?: Fill
  ) => void
  declare applyFill: (fill: Fill, node: SceneNode, graph: SceneGraph, fillIndex?: number) => boolean
  declare applyGradientFill: (fill: Fill, node: SceneNode, graph: SceneGraph) => void
  declare applyImageFill: (fill: Fill, node: SceneNode, graph: SceneGraph) => boolean
  declare drawArc: (canvas: Canvas, node: SceneNode, paint: Paint) => void
  declare drawNodeStroke: (
    canvas: Canvas,
    node: SceneNode,
    rect: Float32Array,
    hasRadius: boolean
  ) => void
  declare drawStrokeWithAlign: (
    canvas: Canvas,
    node: SceneNode,
    rect: Float32Array,
    hasRadius: boolean,
    align: 'INSIDE' | 'CENTER' | 'OUTSIDE'
  ) => void
  declare drawRRectStrokeWithAlign: (
    canvas: Canvas,
    rrect: Float32Array,
    node: SceneNode,
    stroke: Stroke
  ) => void
  declare drawIndividualSideStrokes: (
    canvas: Canvas,
    node: SceneNode,
    align: 'INSIDE' | 'CENTER' | 'OUTSIDE'
  ) => void
  declare strokeNodeShape: (canvas: Canvas, node: SceneNode, paint: Paint) => void
  declare makeNodeShapePath: (node: SceneNode, rect: Float32Array, hasRadius: boolean) => Path
  declare makePolygonPath: (node: SceneNode) => Path
  declare makeRRect: (node: SceneNode) => Float32Array
  declare makeRRectWithSpread: (node: SceneNode, spread: number) => Float32Array
  declare makeRRectWithOffset: (
    node: SceneNode,
    ox: number,
    oy: number,
    spread: number
  ) => Float32Array
  declare clipNodeShape: (
    canvas: Canvas,
    node: SceneNode,
    rect: Float32Array,
    hasRadius: boolean
  ) => void
  declare getVectorPaths: (node: SceneNode) => Path[] | null
  declare getFillGeometry: (node: SceneNode) => Path[] | null
  declare getStrokeGeometry: (node: SceneNode) => Path[] | null
  declare getCachedDropShadow: (
    dx: number,
    dy: number,
    sigma: number,
    color: Float32Array
  ) => ImageFilter
  declare getCachedBlur: (sigma: number) => ImageFilter
  declare getCachedDecalBlur: (sigma: number) => ImageFilter
  declare getCachedMaskBlur: (sigma: number) => MaskFilter
  declare applyClippedBlur: (
    canvas: Canvas,
    node: SceneNode,
    rect: Float32Array,
    hasRadius: boolean,
    sigma: number
  ) => void
  color4f(r: number, g: number, b: number, a: number): Float32Array {
    const c = this._tmpColor
    c[0] = r
    c[1] = g
    c[2] = b
    c[3] = a
    return c
  }

  ltrb(l: number, t: number, r: number, b: number): Float32Array {
    const rc = this._tmpRect
    rc[0] = l
    rc[1] = t
    rc[2] = r
    rc[3] = b
    return rc
  }

  selColor(alpha = 1) {
    return this.ck.Color4f(SELECTION_COLOR.r, SELECTION_COLOR.g, SELECTION_COLOR.b, alpha)
  }

  compColor(alpha = 1) {
    return this.ck.Color4f(COMPONENT_COLOR.r, COMPONENT_COLOR.g, COMPONENT_COLOR.b, alpha)
  }

  isComponentType(type: string): boolean {
    return type === 'COMPONENT' || type === 'COMPONENT_SET' || type === 'INSTANCE'
  }

  isRectangularType(type: string): boolean {
    return (
      type === 'FRAME' ||
      type === 'RECTANGLE' ||
      type === 'ROUNDED_RECTANGLE' ||
      type === 'COMPONENT' ||
      type === 'INSTANCE' ||
      type === 'SECTION' ||
      type === 'GROUP'
    )
  }

  effectOverflow(node: SceneNode): number {
    let expand = 0
    for (const e of node.effects) {
      if (!e.visible) continue
      const blur = e.radius
      const spread = e.spread
      const ox = Math.abs(e.offset.x)
      const oy = Math.abs(e.offset.y)
      expand = Math.max(expand, blur + spread + ox, blur + spread + oy)
    }
    return expand
  }

  constructor(ck: CanvasKit, surface: Surface, gl?: WebGL2RenderingContext | null) {
    this.ck = ck
    this.surface = surface
    this.profiler = new RenderProfiler(ck, gl ?? null)
    initializeRendererPaints(this)
  }

  getFontProvider(): TypefaceFontProvider | null {
    return RendererFonts.getFontProvider(this)
  }

  isDestroyed(): boolean {
    return this.destroyed
  }

  async loadFonts(onFallbackFontsLoaded?: () => void): Promise<void> {
    await RendererFonts.loadFonts(this, onFallbackFontsLoaded)
  }

  async prepareForExport(
    graph: SceneGraph,
    pageId: string,
    nodeIds: string[]
  ): Promise<() => void> {
    return RendererFonts.prepareForExport(this, graph, pageId, nodeIds)
  }

  replaceSurface(surface: Surface): void {
    this.surface.delete()
    this.surface = surface
    this.invalidateScenePicture()
  }

  invalidateScenePicture(): void {
    RendererState.invalidateScenePicture(this)
  }

  invalidateAllPictures(): void {
    RendererState.invalidateAllPictures(this)
  }

  invalidateNodePicture(nodeId: string): void {
    RendererState.invalidateNodePicture(this, nodeId)
  }

  flashNode(nodeId: string): void {
    RendererState.flashNode(this, nodeId)
  }

  aiMarkActive(nodeIds: string[]): void {
    RendererState.aiMarkActive(this, nodeIds)
  }

  aiMarkDone(nodeIds: string[]): void {
    RendererState.aiMarkDone(this, nodeIds)
  }

  aiFlashDone(nodeIds: string[]): void {
    RendererState.aiFlashDone(this, nodeIds)
  }

  aiClearActive(): void {
    RendererState.aiClearActive(this)
  }

  aiClearAll(): void {
    RendererState.aiClearAll(this)
  }

  get hasActiveFlashes(): boolean {
    return RendererState.hasActiveFlashes(this)
  }

  hitTestSectionTitle(graph: SceneGraph, canvasX: number, canvasY: number): SceneNode | null {
    return LabelHitTest.hitTestSectionTitle(
      graph,
      canvasX,
      canvasY,
      this.zoom,
      this.pageId ?? graph.rootId,
      this.sectionTitleFont,
      this.labelCache
    )
  }

  hitTestComponentLabel(graph: SceneGraph, canvasX: number, canvasY: number): SceneNode | null {
    return LabelHitTest.hitTestComponentLabel(
      graph,
      canvasX,
      canvasY,
      this.zoom,
      this.pageId ?? graph.rootId,
      this.componentLabelFont,
      this.labelCache
    )
  }

  hitTestFrameTitle(
    graph: SceneGraph,
    canvasX: number,
    canvasY: number,
    selectedIds: Set<string>
  ): SceneNode | null {
    return LabelHitTest.hitTestFrameTitle(
      graph,
      canvasX,
      canvasY,
      this.zoom,
      selectedIds,
      this.labelFont
    )
  }

  renderSceneToCanvas(canvas: Canvas, graph: SceneGraph, pageId: string): void {
    RenderPipeline.renderSceneToCanvas(this, canvas, graph, pageId)
  }

  renderFromEditorState(
    state: EditorState,
    graph: SceneGraph,
    textEditor: unknown,
    viewportWidth: number,
    viewportHeight: number,
    showRulers = true,
    layer: RenderPipeline.RenderLayer = 'full'
  ): void {
    const dpr = IS_BROWSER ? window.devicePixelRatio || 1 : 1
    RenderPipeline.renderFromEditorState(
      this,
      state,
      graph,
      textEditor,
      viewportWidth,
      viewportHeight,
      showRulers,
      dpr,
      layer
    )
  }

  render(
    graph: SceneGraph,
    selectedIds: Set<string>,
    overlays: RenderOverlays = {},
    sceneVersion = -1,
    layer: RenderPipeline.RenderLayer = 'full'
  ): void {
    RenderPipeline.render(this, graph, selectedIds, overlays, sceneVersion, layer)
  }

  invalidateVectorPath(nodeId: string): void {
    for (const cache of [this.vectorPathCache, this.vectorStrokePathCache]) {
      const old = cache.get(nodeId)
      if (!old) continue
      for (const p of old) p.delete()
      cache.delete(nodeId)
    }
    for (const [key, paths] of this.vectorStrokeOutlineCache) {
      if (!key.startsWith(`${nodeId}|`)) continue
      for (const p of paths) p.delete()
      this.vectorStrokeOutlineCache.delete(key)
    }
    for (const cache of [this.fillGeometryCache, this.strokeGeometryCache]) {
      const oldGeom = cache.get(nodeId)
      if (oldGeom) {
        for (const p of oldGeom) p.delete()
        cache.delete(nodeId)
      }
    }
  }

  measureTextNode(node: SceneNode, maxWidth?: number): { width: number; height: number } | null {
    return RenderText.measureTextNode(this, node, maxWidth)
  }

  isNodeFontLoaded(node: SceneNode): boolean {
    return RenderText.isNodeFontLoaded(this, node)
  }

  buildTextPicture(node: SceneNode): Uint8Array | null {
    return RenderText.buildTextPicture(this, node)
  }

  buildParagraph(
    node: SceneNode,
    color?: Float32Array,
    opts?: { halfLeading?: boolean }
  ): Paragraph {
    return RenderText.buildParagraph(this, node, color, opts)
  }

  resolveFillColorInfo(
    fill: Fill,
    fillIndex: number,
    node: SceneNode,
    graph: SceneGraph
  ): ResolvedRenderColor {
    return RenderColors.resolveFillColorInfo(fill, fillIndex, node, graph)
  }

  resolveFillColor(fill: Fill, fillIndex: number, node: SceneNode, graph: SceneGraph): Color {
    return RenderColors.resolveFillColor(fill, fillIndex, node, graph)
  }

  resolveStrokeColorInfo(
    stroke: Stroke,
    strokeIndex: number,
    node: SceneNode,
    graph: SceneGraph
  ): ResolvedRenderColor {
    return RenderColors.resolveStrokeColorInfo(stroke, strokeIndex, node, graph)
  }

  resolveStrokeColor(
    stroke: Stroke,
    strokeIndex: number,
    node: SceneNode,
    graph: SceneGraph
  ): Color {
    return RenderColors.resolveStrokeColor(stroke, strokeIndex, node, graph)
  }

  screenToCanvas(sx: number, sy: number): Vector {
    return {
      x: (sx - this.panX) / this.zoom,
      y: (sy - this.panY) / this.zoom
    }
  }

  destroyed: boolean = false

  destroy(): void {
    destroyRenderer(this)
  }
}

installRendererDomainMethods(SkiaRenderer.prototype)
