// A material instance over creator-gl. Uniforms are set through a Proxy that lowers each JS value to
// the right native uniform call. The common materials are static factories (Material.lit/unlit/
// video/shadow); Material.load brings in a custom compiled shader. Ported from worker/src/Material.ts,
// with the Lit/Unlit/... subclasses collapsed into factories and color accepting any ColorInput.

import { Color, type ColorInput } from "../core/color"
import { fetch, type FetchResponse } from "../runtime/fetch"
import { Texture } from "./Texture"
// Type-only (erased at build) so a 3D material never pulls the 2D-canvas runtime; the guard is inline.
import type { Canvas } from "../canvas/Canvas"

/** Duck-type a Canvas without importing it (keeps 3D material free of 2D-canvas code). */
const isCanvasSource = (v: unknown): v is Canvas =>
  typeof v === "object" && v !== null && (v as { __canvas?: unknown }).__canvas === true

type UniformValue = string | Texture | number | boolean | number[] | Float32Array | null

const applyUniform = (id: number, key: string, value: UniformValue): void => {
  if (typeof value === "string") {
    if (value.length > 7) _creator.setUniformRgba(id, key, parseInt(value.slice(1), 16))
    else _creator.setUniformRgb(id, key, parseInt(value.slice(1), 16))
  } else if (typeof value === "number") {
    _creator.setUniformFloat(id, key, value)
  } else if (typeof value === "boolean") {
    _creator.setUniformBoolean(id, key, value)
  } else if (value instanceof Texture) {
    _creator.setUniformTexture(id, key, value._id, value.wrapS, value.wrapT)
  } else if (value instanceof Float32Array) {
    _creator.setUniformArray(id, key, value)
  } else if (Array.isArray(value)) {
    _creator.setUniformArray(id, key, Float32Array.of(...value))
  } else {
    _creator.setUniformNull(id, key)
  }
}

export type MaterialColorOptions = { color?: ColorInput, map?: Texture | Canvas | null }

export class Material {
  /** @internal native material-instance handle. */
  _id: number
  readonly shader: FetchResponse | "unknown"
  readonly uniforms: Record<string, UniformValue>

  private _colorKey: string | null = null
  private _useMapFlag = true

  constructor(shader: FetchResponse | number) {
    if (typeof shader === "number") {
      this.shader = "unknown"
      this._id = shader
    } else {
      this.shader = shader
      this._id = _creator.createMaterial((shader as any)._id)
    }
    const store: Record<string, UniformValue> = {}
    const id = () => this._id
    this.uniforms = new Proxy(store, {
      get: (t, k: string) => t[k],
      set: (t, k: string, v: UniformValue) => { t[k] = v; applyUniform(id(), k, v); return true },
    })
  }

  /** Set a uniform (chainable). */
  set(key: string, value: UniformValue): this {
    this.uniforms[key] = value
    return this
  }

  set color(c: ColorInput) {
    if (this._colorKey) this.uniforms[this._colorKey] = Color.toHexString(c)
  }

  set map(value: Texture | Canvas | null) {
    if (value === null) {
      if (this._useMapFlag) this.uniforms.useBaseColorMap = false
    } else {
      // A Canvas bakes to a (cached) 3D texture on assign — same as Sprite accepting a Canvas in 2D.
      this.uniforms.baseColorMap = isCanvasSource(value) ? Texture.fromCanvas(value) : value
      if (this._useMapFlag) this.uniforms.useBaseColorMap = true
    }
  }

  // --- factories ---

  /** PBR lit material. */
  static lit(options: MaterialColorOptions = {}): Material {
    // The fetchLocal literal must be inline so the compiler's preload header captures the shader name.
    const m = new Material({ _id: _creatorUtils.fetchLocal("lit.filamat") } as any)
    m._colorKey = "baseColor"
    if (options.color !== undefined) m.color = options.color
    if (options.map) m.map = options.map
    return m
  }

  /** Unlit (flat) material. */
  static unlit(options: MaterialColorOptions = {}): Material {
    const m = new Material({ _id: _creatorUtils.fetchLocal("unlit.filamat") } as any)
    m._colorKey = "baseColor"
    if (options.color !== undefined) m.color = options.color
    if (options.map) m.map = options.map
    return m
  }

  /** Material that samples a VideoPlayer's texture. */
  static video(map?: Texture): Material {
    const m = new Material({ _id: _creatorUtils.fetchLocal("video.filamat") } as any)
    m._useMapFlag = false
    if (map) m.uniforms.baseColorMap = map
    return m
  }

  /** Shadow-catcher material (transparent except where shadows fall). */
  static shadow(color: ColorInput = "#000000aa"): Material {
    const m = new Material({ _id: _creatorUtils.fetchLocal("shadow.filamat") } as any)
    m._colorKey = "color"
    m.color = color
    return m
  }

  /** Load a custom compiled shader (.mat URL) as a material. */
  static load(url: string): Promise<Material> {
    return fetch(url, { useOnce: true }).then((resp) => {
      if (resp.status >= 400) return Promise.reject(new Error(`Failed to fetch shader from ${url}. HTTP ${resp.status}`))
      return new Material(resp)
    })
  }

  /** @internal */
  static idOf(material: Material): number { return material._id }
}
