// .mat source → the shader's PARAMETER SCHEMA: what the scene editor's material card renders and
// what `lecodes shaders` prints. Filament's material block carries name + type only, so the rest
// (widget kind, range, default, tooltip) comes from ANNOTATIONS in the comment that follows each
// parameter entry — the same trailing comments shader authors already write:
//
//   parameters : [
//       { type : float3,    name : tint },     // @color what the coating does to the world behind it
//       { type : float,     name : opacity },  // @range(0, 1) @default(0.22) how much the glass darkens
//       { type : sampler2d, name : mask },     // the reticle mask, white on alpha
//   ]
//
// Tags: `@color` (float3/float4 = a colour picker; `@vec` forces the plain vector — without either,
// a float3/float4 whose NAME reads like a colour is one), `@range(min, max)` (slider), `@step(s)`,
// `@default(v)` (a number, `#hex`, `[x, y, z]`, `true`/`false`). Whatever text is left is the
// tooltip. A full-line comment directly above the entry counts too. Unknown tags are ignored; a
// malformed one lands in `errors` and the parameter still lists.
//
// Built on liteMaterial's block extraction + relaxed-JSON parser (the ONLY .mat syntax knowledge
// in the repo — do not fork it). Dependency-free, browser-safe, never throws.

import { extractMatBlocks, parseJsonish, type JsonishValue } from "./liteMaterial"

export type ShaderParamEditor =
  | "number" | "int" | "switch" | "vec2" | "vec3" | "vec4" | "color" | "texture" | "matrix" | "array"

export type ShaderParamValue = number | boolean | string | number[] | null

export type ShaderParam = {
  name: string
  /** filament type name, verbatim (float, float3, sampler2d, …) */
  type: string
  /** array parameters ("float[9]") */
  size?: number
  editor: ShaderParamEditor
  /** The value a new material starts with (from `@default`, else the type's zero — white for a colour). */
  default: ShaderParamValue
  min?: number
  max?: number
  step?: number
  tooltip?: string
}

export type ShaderSchema = {
  name: string
  shadingModel: string
  blending: string
  params: ShaderParam[]
  errors: string[]
}

const SAMPLER_TYPES = new Set([ "sampler2d", "samplerExternal", "sampler2dArray", "samplerCubemap", "sampler3d" ])
const COLOR_NAME = /colou?r|tint|albedo|emissive|glare|glow/i

const zeroFor = (type: string, editor: ShaderParamEditor): ShaderParamValue => {
  switch (editor) {
    case "number": return 0
    case "int": return 0
    case "switch": return false
    case "vec2": return [ 0, 0 ]
    case "vec3": return [ 0, 0, 0 ]
    case "vec4": return [ 0, 0, 0, 0 ]
    case "color": return type === "float4" ? "#ffffffff" : "#ffffff"
    case "texture": return null
    default: return null
  }
}

/** Parse one `@default(...)` payload. */
const parseDefault = (raw: string, editor: ShaderParamEditor): ShaderParamValue | undefined => {
  const s = raw.trim()
  if (s === "true") return true
  if (s === "false") return false
  if (/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(s)) return s.toLowerCase()
  if (/^-?\d+(\.\d+)?$/.test(s)) return editor === "switch" ? Number(s) !== 0 : Number(s)
  const arr = /^\[(.*)\]$/.exec(s)
  if (arr) {
    const nums = arr[1]!.split(",").map((p) => Number(p.trim()))
    if (nums.length > 0 && nums.every((n) => Number.isFinite(n))) return nums
  }
  return undefined
}

type Annotation = { color?: boolean, vec?: boolean, range?: [number, number], step?: number, default?: string, tooltip: string, errors: string[] }

/** Split a comment into tags + the remaining tooltip text. */
const parseAnnotation = (comment: string): Annotation => {
  const out: Annotation = { tooltip: "", errors: [] }
  let rest = comment
  rest = rest.replace(/@(color|vec)\b/g, (_m, tag: string) => { out[tag as "color" | "vec"] = true; return " " })
  rest = rest.replace(/@(range|step|default)\s*\(([^)]*)\)/g, (_m, tag: string, body: string) => {
    if (tag === "range") {
      const parts = body.split(",").map((p) => Number(p.trim()))
      if (parts.length === 2 && parts.every((n) => Number.isFinite(n))) out.range = [ parts[0]!, parts[1]! ]
      else out.errors.push(`@range expects two numbers, got "${body.trim()}"`)
    } else if (tag === "step") {
      const n = Number(body.trim())
      if (Number.isFinite(n) && n > 0) out.step = n
      else out.errors.push(`@step expects a positive number, got "${body.trim()}"`)
    } else {
      out.default = body
    }
    return " "
  })
  rest = rest.replace(/@\w+(\([^)]*\))?/g, " ")   // unknown tags: dropped
  out.tooltip = rest.replace(/\s+/g, " ").trim()
  return out
}

/** The comment attached to each parameter entry, by parameter name: the trailing `//` on the
 *  entry's line, else the full-line comment(s) directly above it. Scans the raw block text
 *  (the relaxed-JSON parser drops comments). */
const collectComments = (materialBlock: string): Map<string, string> => {
  const out = new Map<string, string>()
  const start = materialBlock.search(/\bparameters\s*[:=]/)
  if (start < 0) return out
  const lines = materialBlock.slice(start).split("\n")
  let above: string[] = []
  for (const line of lines) {
    const trimmed = line.trim()
    const name = /\bname\s*[:=]\s*"?([A-Za-z_]\w*)"?/.exec(line)?.[1]
    if (name) {
      const trailing = /\}[^/\n]*\/\/(.*)$/.exec(line)?.[1] ?? /\/\/(.*)$/.exec(line.slice(line.indexOf("}") + 1))?.[1]
      const text = trailing !== undefined ? trailing : above.join(" ")
      if (text.trim() !== "") out.set(name, text.trim())
      above = []
    } else if (trimmed.startsWith("//")) {
      above.push(trimmed.slice(2).trim())
    } else if (trimmed !== "" && trimmed !== "[" && trimmed !== "],") {
      above = []
    }
  }
  return out
}

/** Parse a `.mat` source into its parameter schema. Never throws: an unreadable file yields an
 *  empty schema whose `errors` say why. */
export const parseShaderSchema = (text: string): ShaderSchema => {
  const schema: ShaderSchema = { name: "", shadingModel: "lit", blending: "opaque", params: [], errors: [] }
  let block: string | undefined
  let def: Record<string, JsonishValue>
  try {
    block = extractMatBlocks(text).material
    if (block === undefined) { schema.errors.push("no material block"); return schema }
    def = parseJsonish("{" + block + "}")
  } catch (e) {
    schema.errors.push(e instanceof Error ? e.message : String(e))
    return schema
  }
  schema.name = typeof def.name === "string" ? def.name : ""
  schema.shadingModel = typeof def.shadingModel === "string" ? def.shadingModel : "lit"
  schema.blending = typeof def.blending === "string" ? def.blending : "opaque"

  const comments = collectComments(block)
  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)) { schema.errors.push("malformed parameter entry"); continue }
    const entry = raw as Record<string, JsonishValue>
    const name = String(entry.name ?? "")
    const typeRaw = String(entry.type ?? "")
    const typeMatch = /^(\w+)(?:\[(\d+)\])?$/.exec(typeRaw)
    if (!name || !typeMatch) { schema.errors.push(`malformed parameter "${name || typeRaw}"`); continue }
    const type = typeMatch[1]!
    const size = typeMatch[2] ? Number(typeMatch[2]) : undefined
    const ann = parseAnnotation(comments.get(name) ?? "")
    for (const e of ann.errors) schema.errors.push(`${name}: ${e}`)

    let editor: ShaderParamEditor
    if (size !== undefined) editor = "array"
    else if (SAMPLER_TYPES.has(type)) editor = "texture"
    else if (type === "float") editor = "number"
    else if (type === "int" || type === "uint") editor = "int"
    else if (type === "bool") editor = "switch"
    else if (type === "float2") editor = "vec2"
    else if (type === "float3" || type === "float4") {
      const isColor = ann.color === true || (ann.vec !== true && COLOR_NAME.test(name))
      editor = isColor ? "color" : type === "float3" ? "vec3" : "vec4"
    } else if (/^(int|uint|bool)[234]$/.test(type)) editor = `vec${type.slice(-1)}` as ShaderParamEditor
    else if (/^float[234]x[234]$/.test(type)) editor = "matrix"
    else editor = "array"

    const param: ShaderParam = { name, type, editor, default: zeroFor(type, editor) }
    if (size !== undefined) param.size = size
    if (ann.range) { param.min = ann.range[0]; param.max = ann.range[1] }
    if (ann.step !== undefined) param.step = ann.step
    if (ann.default !== undefined) {
      const v = parseDefault(ann.default, editor)
      if (v === undefined) schema.errors.push(`${name}: unreadable @default(${ann.default.trim()})`)
      else param.default = v
    }
    if (ann.tooltip) param.tooltip = ann.tooltip
    schema.params.push(param)
  }
  return schema
}

/** The parameter values a NEW material starts with for this shader (the schema defaults, textures
 *  left out — an unset sampler stays the engine's white). */
export const shaderDefaults = (schema: ShaderSchema): Record<string, ShaderParamValue> => {
  const out: Record<string, ShaderParamValue> = {}
  for (const p of schema.params) if (p.editor !== "texture" && p.default !== null) out[p.name] = p.default
  return out
}
