import type { FetchResponse } from "../runtime/fetch"
import type { SvgSourceValue as SvgSource } from "../runtime/misc"
import type { File } from "../runtime/fetch"
import { Canvas, isCanvas } from "../canvas/Canvas"
import { Element, type AnimateStyle, type Color, type ElementStyle, type OnLayoutCallback, type Style } from "./UINode"

export type UIImageStyle = ElementStyle & { borderRadius?: number, objectFit?: "cover" | "contain" | "fill", tintColor?: Color }

/** A baked Canvas surface as an image source. The UI backend resolves it to a UI texture — see
 *  docs/canvas-contract.md ("Pending"). */
export type CanvasImageSource = { canvasSurface: number }

/** Accepted image sources. A Canvas is baked to a surface marker on assign. */
export type ImageSource = string | FetchResponse | File | SvgSource | Canvas

const toSrc = (src: ImageSource): string | FetchResponse | File | SvgSource | CanvasImageSource =>
  isCanvas(src) ? { canvasSurface: src._bakeSurface() } : src

/** The stored/resolved image source (a Canvas is resolved to a surface marker). */
export type ResolvedImageSource = string | FetchResponse | File | SvgSource | CanvasImageSource

export interface UIImage {
  readonly type: "image",
  get src(): ResolvedImageSource
  set src(value: ImageSource)
  style: Style<this, UIImageStyle>
  animateTo: AnimateStyle<this, UIImageStyle>
  animateFrom: AnimateStyle<this, UIImageStyle>

  // Render only a sub-rectangle of the source texture (coords/size in texture pixels).
  // This is the atlas / spritesheet primitive: the initial crop is the first call, sprite
  // animation is swapping the rect per frame. The rect's w/h become the node's intrinsic
  // size (one frame, not the whole atlas) and it overrides objectFit — the cropped frame
  // always fills the box.
  setSourceRect(x: number, y: number, w: number, h: number): this

  onLayout(onLayout: OnLayoutCallback): this

  setClass(name: string, enabled: boolean): this
  toggleClass(name: string): this
  hasClass(name: string): boolean
  bindClass(name: string, fn: () => boolean): this
}

class ImageElement extends Element<"image"> {
  private _src: ResolvedImageSource
  // Current source sub-rect; undefined until the first setSourceRect. Kept on the node (not
  // just sent as a command) so a crop set while detached (_id == 0) is reapplied at mount —
  // the viewer reads it when it creates the element.
  private _sourceRect?: [number, number, number, number]
  constructor(style: any, src: ImageSource) {
    super("image", style)
    this._src = toSrc(src)
  }

  get src(): ResolvedImageSource {
    return this._src
  }
  set src(value: ImageSource) {
    this._src = toSrc(value)
    if (this._id !== 0) {
      _creatorUI.updateNode(this._id, "src", this._src)
    }
  }

  setSourceRect(x: number, y: number, w: number, h: number): this {
    // Stash on the node so a crop set before mount is reapplied when the element is created.
    this._sourceRect = [x, y, w, h]
    // Live mutation: fast channel (not _style → no PX_VALUE density scaling: these are texture
    // pixels, a different coordinate space). On a detached node there is nothing to mutate yet —
    // the stash above carries it to mount. The viewer decides relayout vs draw-only by comparing
    // w/h against the rect it has stashed on the element.
    if (this._id !== 0) {
      _creatorUI.setSourceRect(this._id, x, y, w, h)
    }
    return this
  }
}

export function UIImage(src: ImageSource): UIImage
export function UIImage(style: UIImageStyle, src: ImageSource): UIImage
export function UIImage(...args: [ImageSource] | [UIImageStyle, ImageSource]): UIImage {
  if (args.length === 1) {
    return new ImageElement({}, args[0])
  } else {
    return new ImageElement(args[0], args[1])
  }
}