// .mat source → Web Lite material artifact.
//
// matc validates and compiles the .mat for the native platforms; the lite web runtime
// (packages/viewer-lite) has no Filament, so it gets this JSON artifact instead: the parsed
// material definition (parameters, shading model, blending) plus the author's fragment body with
// `materialParams.x` rewritten to `materialParams_x`. viewer-lite wraps that body in its own GLSL
// template (src/gl/customMaterial.ts mirrors the artifact type) — GLSL knowledge stays there, this
// module only owns .mat syntax.
//
// The parsed `params`/`samplers` lists are deliberately structured (filament type names kept
// verbatim): they are also the metadata a future "typed materials" codegen needs — generating a
// Material subclass with typed uniform setters straight from the shader's parameter list.
//
// This module is dependency-free and must never throw: an exotic material becomes an
// `unsupported` artifact (viewer-lite falls back to unlit + warning), never a failed shader save.

export type LiteMaterialParam = {
  name: string
  /** filament type name: float, float2..4, int..4, bool..4, float3x3, float4x4 */
  type: string
  /** array parameters ("float[9]") */
  size?: number
}

export type LiteMaterialArtifact = {
  lecodesLiteMaterial: 1
  shadingModel: "lit" | "unlit"
  blending: "opaque" | "transparent" | "fade" | "add" | "masked"
  maskThreshold: number
  doubleSided: boolean
  culling: "back" | "front" | "none"
  depthWrite: boolean
  /** Filament's shadow-catcher flag (unlit only, transparency mandatory — matc enforces both):
   *  the fragment shows baseColor scaled by the received shadow amount, transparent elsewhere. */
  shadowMultiplier: boolean
  params: LiteMaterialParam[]
  /** sampler2d / samplerExternal parameters — all become sampler2D in lite */
  samplers: { name: string }[]
  /** the author's fragment block body, `materialParams.x` rewritten to `materialParams_x` */
  fragmentCode: string
  /** the fragment writes MaterialInputs.normal — the template adds the tangent-frame transform */
  writesNormal: boolean
  /** a vertex{} block was present and dropped (lite ignores custom vertex stages) */
  hasVertexBlock: boolean
  /** set when the material can't run in lite at all (parse failure, unsupported param type) */
  unsupported?: string
}

// ---- .mat block extraction ----------------------------------------------------------------------

/** Split a .mat file into its top-level `name { ... }` blocks (material / fragment / vertex),
 *  matching braces while skipping comments so `{` inside GLSL or comments can't derail it. */
export const extractMatBlocks = (text: string): Record<string, string> => {
  const blocks: Record<string, string> = {}
  let i = 0
  const skipNoise = (): void => {
    for (;;) {
      while (i < text.length && /\s/.test(text[i]!)) i++
      if (text[i] === "/" && text[i + 1] === "/") { while (i < text.length && text[i] !== "\n") i++ }
      else if (text[i] === "/" && text[i + 1] === "*") {
        i += 2
        while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++
        i += 2
      } else return
    }
  }
  while (i < text.length) {
    skipNoise()
    const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text.slice(i))
    if (!m) { i++; continue }
    i += m[0].length
    skipNoise()
    if (text[i] !== "{") continue
    // brace-match the block body, comment-aware
    let depth = 0
    const start = i
    while (i < text.length) {
      const c = text[i]
      if (c === "/" && text[i + 1] === "/") { while (i < text.length && text[i] !== "\n") i++ }
      else if (c === "/" && text[i + 1] === "*") {
        i += 2
        while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++
        i++ // lands on '/', loop increment steps past
      }
      else if (c === "{") depth++
      else if (c === "}") { depth--; if (depth === 0) break }
      i++
    }
    blocks[m[0]] = text.slice(start + 1, i)
    i++
  }
  return blocks
}

// ---- "jsonish" material-block parser --------------------------------------------------------------
// matc's material block is relaxed JSON: unquoted keys/values, comments, optional commas, and both
// `:` and `=` as separators. Small recursive-descent parser over that grammar.

export type JsonishValue = string | number | boolean | JsonishValue[] | { [key: string]: JsonishValue }

export const parseJsonish = (src: string): Record<string, JsonishValue> => {
  let i = 0
  const fail = (msg: string): never => { throw new Error(`material block: ${msg} at offset ${i}`) }
  const skip = (): void => {
    for (;;) {
      while (i < src.length && /[\s,]/.test(src[i]!)) i++
      if (src[i] === "/" && src[i + 1] === "/") { while (i < src.length && src[i] !== "\n") i++ }
      else if (src[i] === "/" && src[i + 1] === "*") {
        i += 2
        while (i < src.length && !(src[i] === "*" && src[i + 1] === "/")) i++
        i += 2
      } else return
    }
  }
  const quoted = (q: string): string => {
    i++
    const start = i
    while (i < src.length && src[i] !== q) i++
    return src.slice(start, i++)
  }
  const word = (): string => {
    const m = /^[^\s{}[\]:,="']+/.exec(src.slice(i))
    if (!m) fail("expected a value")
    i += m![0].length
    return m![0]
  }
  const value = (): JsonishValue => {
    skip()
    const c = src[i]
    if (c === "{") return object()
    if (c === "[") return array()
    if (c === '"' || c === "'") return quoted(c)
    const w = word()
    if (w === "true") return true
    if (w === "false") return false
    const n = Number(w)
    return Number.isNaN(n) ? w : n
  }
  const array = (): JsonishValue[] => {
    i++ // [
    const out: JsonishValue[] = []
    for (;;) {
      skip()
      if (i >= src.length) fail("unterminated array")
      if (src[i] === "]") { i++; return out }
      out.push(value())
    }
  }
  const object = (): Record<string, JsonishValue> => {
    i++ // {
    const out: Record<string, JsonishValue> = {}
    for (;;) {
      skip()
      if (i >= src.length) fail("unterminated object")
      if (src[i] === "}") { i++; return out }
      const key = src[i] === '"' || src[i] === "'" ? quoted(src[i]!) : word()
      skip()
      if (src[i] !== ":" && src[i] !== "=") fail(`expected ':' after key "${key}"`)
      i++
      out[key] = value()
    }
  }
  skip()
  if (src[i] !== "{") fail("expected '{'")
  return object()
}

// ---- artifact builder -----------------------------------------------------------------------------

const PARAM_TYPES = new Set([
  "float", "float2", "float3", "float4",
  "int", "int2", "int3", "int4",
  "bool", "bool2", "bool3", "bool4",
  "float3x3", "float4x4",
])
const SAMPLER_TYPES = new Set(["sampler2d", "samplerExternal"])

const unsupported = (reason: string): LiteMaterialArtifact => ({
  lecodesLiteMaterial: 1, unsupported: reason,
  shadingModel: "unlit", blending: "opaque", maskThreshold: 0.4,
  doubleSided: false, culling: "back", depthWrite: true, shadowMultiplier: false,
  params: [], samplers: [], fragmentCode: "", writesNormal: false, hasVertexBlock: false,
})

export const buildLiteMaterialArtifact = (text: string): LiteMaterialArtifact => {
  try {
    const blocks = extractMatBlocks(text)
    if (blocks.material === undefined) return unsupported("no material block")
    if (blocks.fragment === undefined) return unsupported("no fragment block")
    const def = parseJsonish("{" + blocks.material + "}")

    const params: LiteMaterialParam[] = []
    const samplers: { name: string }[] = []
    const rawParams = def.parameters === undefined ? []
      : Array.isArray(def.parameters) ? def.parameters : [def.parameters]
    for (const raw of rawParams) {
      if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return unsupported("malformed parameter entry")
      const name = String((raw as Record<string, JsonishValue>).name ?? "")
      const typeRaw = String((raw as Record<string, JsonishValue>).type ?? "")
      const typeMatch = /^(\w+)(?:\[(\d+)\])?$/.exec(typeRaw)
      if (!name || !typeMatch) return unsupported(`malformed parameter "${name || typeRaw}"`)
      const [, type, size] = typeMatch
      if (SAMPLER_TYPES.has(type!)) {
        if (size) return unsupported(`sampler arrays are not supported ("${name}")`)
        samplers.push({ name })
      } else if (PARAM_TYPES.has(type!)) {
        params.push(size ? { name, type: type!, size: Number(size) } : { name, type: type! })
      } else {
        return unsupported(`parameter type "${typeRaw}" ("${name}")`)
      }
    }

    // cloth/subsurface/specularGlossiness approximate as lit — better than refusing outright.
    const shadingModel = def.shadingModel === "unlit" ? "unlit" : "lit"
    const blendingRaw = String(def.blending ?? "opaque")
    const blending =
      blendingRaw === "transparent" || blendingRaw === "fade" || blendingRaw === "add" || blendingRaw === "masked"
        ? blendingRaw
        : blendingRaw === "multiply" || blendingRaw === "screen" ? "transparent" // approximate
        : "opaque"
    const culling = def.culling === "none" ? "none" : def.culling === "front" ? "front" : "back"
    // Filament defaults: depth write on except for transparent/fade blending.
    const depthWrite = typeof def.depthWrite === "boolean" ? def.depthWrite
      : blending !== "transparent" && blending !== "fade" && blending !== "add"

    const fragmentCode = blocks.fragment.replace(/\bmaterialParams\./g, "materialParams_")
    if (!/\bvoid\s+material\s*\(/.test(fragmentCode)) return unsupported("fragment block has no material() function")

    return {
      lecodesLiteMaterial: 1,
      shadingModel,
      blending,
      maskThreshold: typeof def.maskThreshold === "number" ? def.maskThreshold : 0.4,
      doubleSided: def.doubleSided === true,
      culling,
      depthWrite,
      shadowMultiplier: def.shadowMultiplier === true,
      params,
      samplers,
      fragmentCode,
      writesNormal: /\.normal\s*=/.test(fragmentCode),
      hasVertexBlock: blocks.vertex !== undefined,
    }
  } catch (e) {
    return unsupported(e instanceof Error ? e.message : String(e))
  }
}
