import * as THREE from 'three';
import { effectPresentation, effectFrameTime, clipsEffectTexture } from './effect-presentation';
import type { EffectPresentationProfile } from './effect-presentation';
import { createEffectPathRuntime, webDelta } from './effect-path';
import type { EffectPathRuntime, ReferenceRuntimeCatalog } from './effect-path';
import { createOriginEffectBillboardRuntime } from './effects-billboard';
import type { OriginEffectBillboardRuntime, OriginEffectBillboardSource } from './effects-billboard';
import { createEffectParticleRuntime } from './effect-particles';
import type { OriginEffectParticleRuntime, OriginParticleRuntimeSource } from './effect-particles';
import type {
  OriginEffectBinding, OriginEffectController, OriginEffectMesh, OriginEffectModel,
  OriginEffectPreviewSettings, OriginEffectProperty, OriginEffectReceipt, OriginEffectReceiptBinding,
  OriginEffectsCatalog, OriginEffectTextureSlot, RawEnum,
} from './effects-format';

export interface OriginEffectRendererOptions {
  presentation?: EffectPresentationProfile;
  effectSourceUrl: string;
  assetBaseUrl: string;
  runtimeSourceUrl?: string;
  viewportHeight?: () => number;
}

export interface OriginEffectRenderer {
  setEffects(bindings: OriginEffectBinding[]): Promise<OriginEffectReceipt>;
  setIntensity(intensity: number): OriginEffectPreviewSettings;
  tick(delta: number, camera: THREE.Camera, playingEffects: boolean | ReadonlySet<string>): void;
  clearBindings(): void;
  dispose(): void;
}

type RuntimeTrack = { controller: OriginEffectController; mesh: THREE.Mesh; material: THREE.ShaderMaterial; applied: boolean };
type RuntimeBinding = { instanceKey: string; elapsed: number; tracks: RuntimeTrack[];
  path?: EffectPathRuntime; billboards?: OriginEffectBillboardRuntime; surfaces: THREE.Mesh[];
  particles?: OriginEffectParticleRuntime; particleMaterials: THREE.ShaderMaterial[];
  receipt: OriginEffectReceiptBinding };
type MeshBuildResult = { object: THREE.Mesh; urls: string[]; tracks: RuntimeTrack[]; diagnostics: string[] };

const vertexShader = [
  'uniform mat3 uvMatrix;',
  'varying vec2 vEffectUv;',
  'varying vec2 vSourceUv;',
  'varying vec4 vEffectColor;',
  'attribute vec4 color;',
  '#include <skinning_pars_vertex>',
  'void main() {',
  '  vEffectUv = (uvMatrix * vec3(uv, 1.0)).xy;',
  '  vSourceUv = uv;',
  '  vEffectColor = color;',
  '  #include <skinbase_vertex>',
  '  #include <begin_vertex>',
  '  #include <skinning_vertex>',
  '  #include <project_vertex>',
  '}',
].join('\n');

const fragmentShader = [
  'uniform sampler2D map;',
  'uniform sampler2D darkMap;',
  'uniform sampler2D detailMap;',
  'uniform int darkMapEnabled;',
  'uniform int detailMapEnabled;',
  'uniform mat3 darkUvMatrix;',
  'uniform mat3 detailUvMatrix;',
  'uniform float diffuseContribution;',
  'uniform int transparentOutsideUnitUv;',
  'uniform vec3 diffuseColor;',
  'uniform vec3 emissiveColor;',
  'uniform float emissiveMultiplier;',
  'uniform float previewIntensity;',
  'uniform int sourceVertexMode;',
  'uniform int lightingMode;',
  'uniform int vertexColorsEnabled;',
  'uniform float opacity;',
  'uniform float alphaThreshold;',
  'uniform int alphaTestEnabled;',
  'uniform int alphaTestFunction;',
  'varying vec2 vEffectUv;',
  'varying vec2 vSourceUv;',
  'varying vec4 vEffectColor;',
  'void main() {',
  '  if (transparentOutsideUnitUv == 1 && (any(lessThan(vEffectUv, vec2(0.0))) || any(greaterThan(vEffectUv, vec2(1.0))))) discard;',
  '  vec4 texel = texture2D(map, vEffectUv);',
  '  if (darkMapEnabled == 1) texel *= texture2D(darkMap, (darkUvMatrix * vec3(vSourceUv, 1.0)).xy);',
  '  if (detailMapEnabled == 1) { vec4 detail = texture2D(detailMap, (detailUvMatrix * vec3(vSourceUv, 1.0)).xy); texel *= vec4(detail.rgb * 2.0, detail.a); }',
  '  float alphaOpacity = opacity;',
  '  if (vertexColorsEnabled == 1 && sourceVertexMode == 2) alphaOpacity = vEffectColor.a;',
  '  float alpha = texel.a * alphaOpacity;',
  '  if (alphaTestEnabled == 1) {',
  '    bool reject = false;',
  '    if (alphaTestFunction == 0) reject = false;',
  '    else if (alphaTestFunction == 1) reject = !(alpha < alphaThreshold);',
  '    else if (alphaTestFunction == 2) reject = !(alpha == alphaThreshold);',
  '    else if (alphaTestFunction == 3) reject = !(alpha <= alphaThreshold);',
  '    else if (alphaTestFunction == 4) reject = !(alpha > alphaThreshold);',
  '    else if (alphaTestFunction == 5) reject = !(alpha != alphaThreshold);',
  '    else if (alphaTestFunction == 6) reject = !(alpha >= alphaThreshold);',
  '    else if (alphaTestFunction == 7) reject = true;',
  '    if (reject) discard;',
  '  }',
  '  vec3 diffuseTerm = diffuseColor;',
  '  vec3 emissiveTerm = emissiveColor * emissiveMultiplier;',
  '  if (vertexColorsEnabled == 1 && sourceVertexMode == 1) emissiveTerm = vEffectColor.rgb;',
  '  if (vertexColorsEnabled == 1 && sourceVertexMode == 2) diffuseTerm = vEffectColor.rgb;',
  '  if (lightingMode == 0) diffuseTerm = vec3(0.0);',
  '  diffuseTerm *= diffuseContribution;',
  '  vec3 rgb = texel.rgb * (diffuseTerm + emissiveTerm) * previewIntensity;',
  '  gl_FragColor = vec4(rgb, alpha);',
  '  #include <colorspace_fragment>',
  '}',
].join('\n');

const sourceFactorByRaw = new Map<number, THREE.BlendingSrcFactor>([
  [0, THREE.OneFactor], [1, THREE.ZeroFactor], [2, THREE.SrcColorFactor],
  [3, THREE.OneMinusSrcColorFactor], [4, THREE.DstColorFactor],
  [5, THREE.OneMinusDstColorFactor], [6, THREE.SrcAlphaFactor],
  [7, THREE.OneMinusSrcAlphaFactor], [8, THREE.DstAlphaFactor],
  [9, THREE.OneMinusDstAlphaFactor], [10, THREE.SrcAlphaSaturateFactor],
]);
const destinationFactorByRaw = new Map<number, THREE.BlendingDstFactor>([
  [0, THREE.OneFactor], [1, THREE.ZeroFactor], [2, THREE.SrcColorFactor],
  [3, THREE.OneMinusSrcColorFactor], [4, THREE.DstColorFactor],
  [5, THREE.OneMinusDstColorFactor], [6, THREE.SrcAlphaFactor],
  [7, THREE.OneMinusSrcAlphaFactor], [8, THREE.DstAlphaFactor],
  [9, THREE.OneMinusDstAlphaFactor],
]);
const depthFunctionByRaw = new Map<number, THREE.DepthModes>([
  [0, THREE.AlwaysDepth], [1, THREE.LessDepth], [2, THREE.EqualDepth],
  [3, THREE.LessEqualDepth], [4, THREE.GreaterDepth], [5, THREE.NotEqualDepth],
  [6, THREE.GreaterEqualDepth], [7, THREE.NeverDepth],
]);

function finite(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value);
}
function raw(enumValue?: RawEnum): number | undefined {
  return finite(enumValue?.rawValue) ? enumValue.rawValue : undefined;
}
function textureSlotIndex(slot: OriginEffectTextureSlot): number {
  return slot.slot === 'DARK_MAP' ? 1 : slot.slot === 'DETAIL_MAP' ? 2 : 0;
}
function disposeShaderTextures(shader: THREE.ShaderMaterial): void {
  for (const name of ['map', 'darkMap', 'detailMap']) {
    (shader.uniforms?.[name]?.value as THREE.Texture | undefined)?.dispose();
  }
}
function rowMatrix(rows?: number[][]): THREE.Matrix4 | null {
  if (!rows || rows.length !== 4 || rows.some(row => row.length !== 4 || row.some(v => !finite(v)))) return null;
  return new THREE.Matrix4().set(
    rows[0][0], rows[0][1], rows[0][2], rows[0][3],
    rows[1][0], rows[1][1], rows[1][2], rows[1][3],
    rows[2][0], rows[2][1], rows[2][2], rows[2][3],
    rows[3][0], rows[3][1], rows[3][2], rows[3][3],
  );
}
function sourceTransform(mesh: OriginEffectMesh): THREE.Matrix4 | null {
  const source = rowMatrix(mesh.sourceMatrixWorldRows);
  if (!source) return null;
  const axisAndUnitConversion = new THREE.Matrix4().makeRotationX(-Math.PI / 2)
    .multiply(new THREE.Matrix4().makeScale(0.1, 0.1, 0.1));
  return axisAndUnitConversion.multiply(source);
}
function meshProperties(mesh: OriginEffectMesh): OriginEffectProperty[] {
  const direct = Array.isArray(mesh.properties) ? mesh.properties : [];
  const inherited = Array.isArray(mesh.inheritedProperties) ? mesh.inheritedProperties : [];
  const result: OriginEffectProperty[] = [];
  const seenTypes = new Set<string>();
  // NifSkope resolves active properties from the mesh outward; a closer property
  // of the same type takes precedence over one on an ancestor.
  for (const property of [...direct, ...inherited]) {
    if (property.type && seenTypes.has(property.type)) continue;
    result.push(property);
    if (property.type) seenTypes.add(property.type);
  }
  return result;
}
function decodedMode(property: OriginEffectProperty | undefined, field: 'vertexMode' | 'lightingMode'): number | undefined {
  const enumValue = raw(property?.[field]);
  if (enumValue !== undefined) return enumValue;
  if (!finite(property?.flagsRaw)) return undefined;
  return field === 'vertexMode'
    ? (property.flagsRaw >>> 4) & 0x03
    : (property.flagsRaw >>> 3) & 0x01;
}
function zBufferValue(
  property: OriginEffectProperty | undefined,
  field: 'depthTest' | 'depthWrite',
): boolean | undefined {
  if (typeof property?.[field] === 'boolean') return property[field];
  if (!finite(property?.flagsRaw)) return undefined;
  return field === 'depthTest' ? (property.flagsRaw & 1) !== 0 : (property.flagsRaw & 2) !== 0;
}
function zBufferCompare(property: OriginEffectProperty | undefined): number | undefined {
  const compare = raw(property?.compareFunction);
  if (compare !== undefined) return compare;
  return finite(property?.flagsRaw) ? (property.flagsRaw >>> 2) & 0x07 : undefined;
}
function baseSlot(mesh: OriginEffectMesh): OriginEffectTextureSlot | null {
  const rows = Array.isArray(mesh.textures) ? mesh.textures : [];
  for (const row of rows) {
    const slot = row.slots?.find(candidate => candidate.slot === 'BASE_MAP' && candidate.assetStatus === 'written');
    if (slot) return slot;
  }
  for (const property of meshProperties(mesh)) {
    const slot = property.slots?.find(candidate => candidate.slot === 'BASE_MAP' && candidate.assetStatus === 'written');
    if (slot) return slot;
  }
  return null;
}
function namedColor(values: number[] | undefined, fallback: [number, number, number]): THREE.Vector3 {
  if (!values || values.length < 3 || !values.slice(0, 3).every(finite)) return new THREE.Vector3(...fallback);
  return new THREE.Vector3(values[0], values[1], values[2]);
}
function rawVector3(values: unknown): THREE.Vector3 | null {
  if (!Array.isArray(values) || values.length < 3 || !values.slice(0, 3).every(finite)) return null;
  return new THREE.Vector3(values[0], values[1], values[2]);
}
function positiveModulo(value: number, modulus: number): number {
  return ((value % modulus) + modulus) % modulus;
}
function evaluateKeys(controller: OriginEffectController, sampleTime: number): number | null {
  const keys = (controller.keys ?? []).filter(key =>
    finite(key.time) && finite(key.value) && (key.forward === undefined || key.forward === null || finite(key.forward)) &&
    (key.backward === undefined || key.backward === null || finite(key.backward)));
  if (keys.length === 0) return null;
  if (sampleTime <= keys[0].time!) return keys[0].value!;
  if (sampleTime >= keys[keys.length - 1].time!) return keys[keys.length - 1].value!;
  let index = 0;
  while (index + 1 < keys.length && keys[index + 1].time! < sampleTime) index++;
  const a = keys[index];
  const b = keys[index + 1];
  const duration = b.time! - a.time!;
  if (!(duration > 0)) return a.value!;
  const u = (sampleTime - a.time!) / duration;
  const interpolation = raw(controller.interpolationType);
  if (interpolation === 1) return a.value! + (b.value! - a.value!) * u;
  if (interpolation === 2) {
    const u2 = u * u;
    const u3 = u2 * u;
    const backward = finite(a.backward) ? a.backward : 0;
    const forward = finite(b.forward) ? b.forward : 0;
    return (2 * u3 - 3 * u2 + 1) * a.value!
      + (-2 * u3 + 3 * u2) * b.value!
      + (u3 - 2 * u2 + u) * backward
      + (u3 - u2) * forward;
  }
  if (interpolation === 5) return u < 1 ? a.value! : b.value!;
  return null;
}
function evaluateVector3Keys(controller: OriginEffectController, sampleTime: number): THREE.Vector3 | null {
  const validTangent = (tangent: unknown) => tangent === undefined || tangent === null || rawVector3(tangent) !== null;
  const keys = (controller.keys ?? []).flatMap(key => {
    const value = rawVector3(key.valueRaw);
    if (!finite(key.time) || !value || !validTangent(key.forwardRaw) || !validTangent(key.backwardRaw)) return [];
    return [{ time: key.time, value, forward: rawVector3(key.forwardRaw), backward: rawVector3(key.backwardRaw) }];
  });
  if (keys.length === 0) return null;
  if (sampleTime <= keys[0].time) return keys[0].value.clone();
  if (sampleTime >= keys[keys.length - 1].time) return keys[keys.length - 1].value.clone();
  let index = 0;
  while (index + 1 < keys.length && keys[index + 1].time < sampleTime) index++;
  const a = keys[index];
  const b = keys[index + 1];
  const duration = b.time - a.time;
  if (!(duration > 0)) return a.value.clone();
  const u = (sampleTime - a.time) / duration;
  const interpolation = raw(controller.interpolationType);
  if (interpolation === 1) return a.value.clone().lerp(b.value, u);
  if (interpolation === 2) {
    const u2 = u * u;
    const u3 = u2 * u;
    const backward = a.backward ?? new THREE.Vector3();
    const forward = b.forward ?? new THREE.Vector3();
    return a.value.clone().multiplyScalar(2 * u3 - 3 * u2 + 1)
      .addScaledVector(b.value, -2 * u3 + 3 * u2)
      .addScaledVector(backward, u3 - 2 * u2 + u)
      .addScaledVector(forward, u3 - u2);
  }
  if (interpolation === 5) return (u < 1 ? a.value : b.value).clone();
  return null;
}
function controllerTime(controller: OriginEffectController, elapsed: number): number | null {
  const keys = (controller.keys ?? []).filter(key => finite(key.time));
  const keyStart = keys.length ? keys[0].time! : 0;
  const keyStop = keys.length ? keys[keys.length - 1].time! : keyStart;
  const declaredStart = controller.startTimeRaw;
  const declaredStop = controller.stopTimeRaw;
  const start = finite(declaredStart) && Math.abs(declaredStart) < 1e20 ? declaredStart : keyStart;
  const stop = finite(declaredStop) && Math.abs(declaredStop) < 1e20 && declaredStop > start ? declaredStop : keyStop;
  const duration = stop - start;
  if (!(duration > 0)) return start;
  const flags = controller.flags;
  if (flags?.active === false) return null;
  const frequency = finite(controller.frequencyRaw) ? controller.frequencyRaw : 1;
  const phase = finite(controller.phaseRaw) ? controller.phaseRaw : 0;
  const offset = flags?.computeScaledTime === false ? elapsed : elapsed * frequency + phase;
  let time = start + offset;
  if (flags?.playBackwards) time = start + stop - time;
  const cycle = raw(flags?.cycleType);
  if (cycle === 0) time = start + positiveModulo(time - start, duration);
  else if (cycle === 1) {
    const position = positiveModulo(time - start, duration * 2);
    time = position <= duration ? start + position : stop - (position - duration);
  } else if (cycle === 2 || cycle === undefined) {
    time = Math.max(start, Math.min(stop, time));
  } else return null;
  return time;
}
function diagnosticCode(value: unknown): string {
  if (typeof value === 'string') return value;
  if (value && typeof value === 'object') {
    const row = value as Record<string, unknown>;
    return String(row.code ?? row.message ?? row.diagnostic ?? 'SOURCE_DIAGNOSTIC');
  }
  return 'SOURCE_DIAGNOSTIC';
}
function textureUrl(baseUrl: string, assetUrl: string): string {
  if (/^(?:https?:|data:|blob:|\/)/i.test(assetUrl)) return assetUrl;
  return baseUrl.replace(/\/?$/, '/') + assetUrl.replace(/^\/+/, '');
}

export function createOriginEffectRenderer(options: OriginEffectRendererOptions): OriginEffectRenderer {
  const textureLoader = new THREE.TextureLoader();
  const textureCache = new Map<string, Promise<THREE.Texture>>();
  const installedGroups = new Set<THREE.Group>();
  const runtimeBindings = new Map<string, RuntimeBinding>();
  let catalogPromise: Promise<OriginEffectsCatalog> | undefined;
  let runtimeCatalogPromise: Promise<ReferenceRuntimeCatalog> | undefined;
  let revision = 0;
  let disposed = false;
  // Viewer-only RGB gain. This never rewrites the extracted NIF material values.
  const presentation = options.presentation ?? effectPresentation;
  let previewIntensity = presentation.brightnessMultiplier;
  // Original effect size restored at the user's request; RGB intensity is independent.
  const previewEffectScale = 1;
  let latestReceipt: OriginEffectReceipt | undefined;
  const previewMaterials = new Set<THREE.ShaderMaterial>();
  const sourceUrl = options.effectSourceUrl;

  function currentPreviewSettings(): OriginEffectPreviewSettings {
    return {
      intensity: previewIntensity,
      effectFramesPerSecond: presentation.effectFramesPerSecond,
      calibrationProfileRevision: presentation.revision,
      effectScale: previewEffectScale,
      effectScalePivot: 'RENDERED_FX_BOUNDS_CENTER',
      sourceGeometryScaleChanged: false,
      sourceMaterialValuesChanged: false,
      engineAppearanceParityValidated: false,
    };
  }
  function rememberReceipt(receipt: Omit<OriginEffectReceipt, 'previewSettings'>): OriginEffectReceipt {
    const completeReceipt: OriginEffectReceipt = {
      ...receipt,
      previewSettings: currentPreviewSettings(),
    };
    latestReceipt = completeReceipt;
    return completeReceipt;
  }
  function setIntensity(intensity: number): OriginEffectPreviewSettings {
    if (!finite(intensity) || intensity < 0 || intensity > 1) {
      throw new RangeError('Effect preview intensity must be a finite number from 0 to 1.');
    }
    previewIntensity = intensity;
    for (const material of previewMaterials) {
      if (material.uniforms?.previewIntensity) material.uniforms.previewIntensity.value = intensity;
      material.uniformsNeedUpdate = true;
    }
    const settings = currentPreviewSettings();
    if (latestReceipt) {
      latestReceipt.previewSettings = settings;
      for (const binding of latestReceipt.bindings) for (const material of binding.runtime?.particleMaterials ?? []) material.intensity = intensity;
    }
    return settings;
  }

  function loadCatalog(): Promise<OriginEffectsCatalog> {
    if (!catalogPromise) {
      catalogPromise = fetch(sourceUrl).then(async response => {
        if (!response.ok) throw new Error('Effects manifest HTTP ' + response.status);
        const catalog = await response.json() as OriginEffectsCatalog;
        if (catalog.clientId !== 'origin') throw new Error('Effects manifest clientId must be origin');
        return catalog;
      }).catch(error => {
        catalogPromise = undefined;
        throw error;
      });
    }
    return catalogPromise;
  }
  function loadTexture(url: string): Promise<THREE.Texture> {
    let promise = textureCache.get(url);
    if (!promise) {
      promise = textureLoader.loadAsync(url).then(texture => {
        texture.colorSpace = THREE.SRGBColorSpace;
        texture.flipY = false;
        texture.generateMipmaps = true;
        return texture;
      });
      textureCache.set(url, promise);
    }
    return promise;
  }
  function disposeGroup(group: THREE.Group): void {
    group.removeFromParent();
    group.traverse(object => {
      const mesh = object as THREE.Mesh;
      if (!mesh.isMesh) return;
      mesh.geometry.dispose();
      const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
      materials.forEach(material => {
        const shader = material as THREE.ShaderMaterial;
        previewMaterials.delete(shader);
        disposeShaderTextures(shader);
        material.dispose();
      });
    });
  }
  function clearBindings(): void {
    revision++;
    for (const runtime of runtimeBindings.values()) {
      runtime.particles?.dispose();
      for (const material of runtime.particleMaterials) {
        previewMaterials.delete(material);
        disposeShaderTextures(material);
        material.dispose();
      }
    }
    for (const group of installedGroups) disposeGroup(group);
    installedGroups.clear();
    runtimeBindings.clear();
  }
  function skippedMeshReceipt(mesh: OriginEffectMesh, reason: string): OriginEffectReceiptBinding['meshes'][number] {
    return {
      blockIndex: finite(mesh.blockIndex) ? mesh.blockIndex : null,
      name: mesh.name ?? mesh.meshId ?? 'unnamed',
      status: 'SKIPPED', textureUrls: [], animatedControllers: 0, diagnostics: [reason],
    };
  }
  function createGeometry(mesh: OriginEffectMesh, diagnostics: string[]): THREE.BufferGeometry | null {
    const transform = sourceTransform(mesh);
    if (!transform) { diagnostics.push('INVALID_SOURCE_MATRIX'); return null; }
    const sourcePositions = mesh.positionsNifUnits;
    const triangles = mesh.triangleIndices;
    if (!Array.isArray(sourcePositions) || sourcePositions.length < 3 || !Array.isArray(triangles)) {
      diagnostics.push('MISSING_EXTRACTED_TRIANGLE_GEOMETRY'); return null;
    }
    const positions: number[] = [];
    const normals: number[] = [];
    const uv = Array.isArray(mesh.uv0Raw) ? mesh.uv0Raw : [];
    const sourceNormals = Array.isArray(mesh.normalsNif) ? mesh.normalsNif : [];
    const normalMatrix = new THREE.Matrix3().getNormalMatrix(transform);
    for (let index = 0; index < sourcePositions.length; index++) {
      const point = sourcePositions[index];
      if (!Array.isArray(point) || point.length < 3 || !point.slice(0, 3).every(finite)) {
        diagnostics.push('INVALID_SOURCE_POSITION'); return null;
      }
      const converted = new THREE.Vector3(point[0], point[1], point[2]).applyMatrix4(transform);
      positions.push(converted.x, converted.y, converted.z);
      const normal = sourceNormals[index];
      if (Array.isArray(normal) && normal.length >= 3 && normal.slice(0, 3).every(finite)) {
        const convertedNormal = new THREE.Vector3(normal[0], normal[1], normal[2]).applyMatrix3(normalMatrix).normalize();
        normals.push(convertedNormal.x, convertedNormal.y, convertedNormal.z);
      } else normals.push(0, 0, 1);
    }
    const indices: number[] = [];
    for (const triangle of triangles) {
      if (!Array.isArray(triangle) || triangle.length !== 3 || !triangle.every(v => Number.isInteger(v) && v >= 0 && v < sourcePositions.length)) {
        diagnostics.push('INVALID_TRIANGLE_INDEX'); return null;
      }
      indices.push(triangle[0], triangle[1], triangle[2]);
    }
    if (uv.length === sourcePositions.length && uv.every(row => Array.isArray(row) && row.length >= 2 && row.slice(0, 2).every(finite))) {
      const uvValues = uv.flatMap(row => [row[0], row[1]]);
      const geometry = new THREE.BufferGeometry();
      geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
      geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
      geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvValues, 2));
      geometry.setIndex(indices);
      const colors = mesh.vertexColorsRaw ?? [];
      if (colors.length === sourcePositions.length && colors.every(row => Array.isArray(row) && row.length >= 3 && row.slice(0, 3).every(finite))) {
        geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors.flatMap(row =>
          [row[0], row[1], row[2], finite(row[3]) ? row[3] : 1]), 4));
      }
      geometry.computeBoundingSphere();
      return geometry;
    }
    diagnostics.push('MISSING_OR_INVALID_UV0');
    return null;
  }
  function createUvMatrix(slot: OriginEffectTextureSlot, controllers: OriginEffectController[], sampleTimes: Map<number, number>): THREE.Matrix3 {
    const transform = slot.staticTextureTransform;
    let tx = finite(transform?.translationRaw?.[0]) ? transform.translationRaw[0] : 0;
    let ty = finite(transform?.translationRaw?.[1]) ? transform.translationRaw[1] : 0;
    let sx = finite(transform?.scaleRaw?.[0]) ? transform.scaleRaw[0] : 1;
    let sy = finite(transform?.scaleRaw?.[1]) ? transform.scaleRaw[1] : 1;
    let rotation = finite(transform?.rotationRaw) ? transform.rotationRaw : 0;
    const centerX = finite(transform?.centerRaw?.[0]) ? transform.centerRaw[0] : 0.5;
    const centerY = finite(transform?.centerRaw?.[1]) ? transform.centerRaw[1] : 0.5;
    for (const controller of controllers) {
      if (controller.kind !== 'uv' || raw(controller.textureSlot) !== textureSlotIndex(slot)) continue;
      const time = sampleTimes.get(controller.controllerBlockIndex ?? -1);
      if (time === undefined) continue;
      const value = evaluateKeys(controller, time);
      if (!finite(value)) continue;
      switch (raw(controller.operation)) {
        case 0: tx = value; break;
        case 1: ty = value; break;
        case 2: rotation = value; break;
        case 3: sx = value; break;
        case 4: sy = value; break;
      }
    }
    return new THREE.Matrix3().setUvTransform(tx, ty, sx, sy, rotation, centerX, centerY);
  }
  async function attachTextureLayers(shader: THREE.ShaderMaterial, properties: OriginEffectProperty[], baseUrl: string,
      diagnostics: string[]): Promise<boolean> {
    const slots = properties.find(property => property.type === 'NiTexturingProperty')?.slots ?? [];
    const layers: OriginEffectTextureSlot[] = [];
    for (const slot of slots) {
      if (slot.slot === 'BASE_MAP') continue;
      if (slot.slot !== 'DARK_MAP' && slot.slot !== 'DETAIL_MAP') {
        diagnostics.push(`UNSUPPORTED_TEXTURE_LAYER:${slot.slot}`);
        continue;
      }
      if (slot.uvSetRaw !== undefined && slot.uvSetRaw !== 0) {
        diagnostics.push(`TEXTURE_LAYER_UV_SET_NOT_EXTRACTED:${slot.slot}:uv=${slot.uvSetRaw}`);
        return false;
      }
      if (!slot.assetUrl) {
        diagnostics.push(`TEXTURE_LAYER_ASSET_MISSING:${slot.slot}`);
        return false;
      }
      const sourceTexture = await loadTexture(textureUrl(baseUrl, slot.assetUrl));
      const map = sourceTexture.clone();
      const clamp = raw(slot.clampMode);
      map.wrapS = clamp === 0 || clamp === 1 ? THREE.ClampToEdgeWrapping : THREE.RepeatWrapping;
      map.wrapT = clamp === 0 || clamp === 2 ? THREE.ClampToEdgeWrapping : THREE.RepeatWrapping;
      const filter = raw(slot.filterMode);
      if (filter === 0) map.minFilter = map.magFilter = THREE.NearestFilter;
      else if (filter === 1) map.minFilter = map.magFilter = THREE.LinearFilter;
      else if (filter === 2) { map.magFilter = THREE.LinearFilter; map.minFilter = THREE.LinearMipmapLinearFilter; }
      map.needsUpdate = true;
      const prefix = slot.slot === 'DARK_MAP' ? 'dark' : 'detail';
      shader.uniforms[prefix + 'Map'].value = map;
      shader.uniforms[prefix + 'MapEnabled'].value = 1;
      shader.uniforms[prefix + 'UvMatrix'].value = createUvMatrix(slot, [], new Map());
      layers.push(slot);
    }
    shader.userData.originTextureLayers = layers;
    return true;
  }
  function updateTextureMatrices(shader: THREE.ShaderMaterial, baseSlot: OriginEffectTextureSlot,
      controllers: OriginEffectController[], sampleTimes: Map<number, number>): void {
    shader.uniforms.uvMatrix.value = createUvMatrix(baseSlot, controllers, sampleTimes);
    for (const slot of (shader.userData.originTextureLayers ?? []) as OriginEffectTextureSlot[]) {
      const prefix = slot.slot === 'DARK_MAP' ? 'dark' : 'detail';
      shader.uniforms[prefix + 'UvMatrix'].value = createUvMatrix(slot, controllers, sampleTimes);
    }
  }
  function createMaterial(
    slot: OriginEffectTextureSlot,
    properties: OriginEffectProperty[],
    texture: THREE.Texture,
    hasVertexColors: boolean,
    diagnostics: string[],
  ): THREE.ShaderMaterial | null {
    const alpha = properties.find(property => property.type === 'NiAlphaProperty');
    const material = properties.find(property => property.type === 'NiMaterialProperty');
    const vertexColor = properties.find(property => property.type === 'NiVertexColorProperty');
    const zBuffer = properties.find(property => property.type === 'NiZBufferProperty');
    const texturing = properties.find(property => property.type === 'NiTexturingProperty');
    const applyMode = raw(texturing?.applyMode);
    if (applyMode !== undefined && applyMode !== 2) {
      diagnostics.push('UNSUPPORTED_BASE_MAP_APPLY_MODE:' + applyMode);
      return null;
    }
    let sourceVertexMode = decodedMode(vertexColor, 'vertexMode');
    let lightingMode = decodedMode(vertexColor, 'lightingMode');
    if (sourceVertexMode === undefined) {
      if (vertexColor) diagnostics.push('VERTEX_COLOR_MODE_NOT_EXPORTED_DEFAULTED_TO_NIF_DEFAULT');
      sourceVertexMode = 2;
    }
    if (sourceVertexMode < 0 || sourceVertexMode > 2) {
      diagnostics.push('UNSUPPORTED_VERTEX_COLOR_MODE:' + sourceVertexMode + ':DEFAULTED_TO_AMB_DIF');
      sourceVertexMode = 2;
    }
    if (lightingMode === undefined) {
      if (vertexColor) diagnostics.push('LIGHTING_MODE_NOT_EXPORTED_DEFAULTED_TO_NIF_DEFAULT');
      lightingMode = 1;
    }
    if (lightingMode < 0 || lightingMode > 1) {
      diagnostics.push('UNSUPPORTED_LIGHTING_MODE:' + lightingMode + ':DEFAULTED_TO_EMI_AMB_DIF');
      lightingMode = 1;
    }
    if (lightingMode === 1) diagnostics.push('PREVIEW_DIFFUSE_LIGHT_UNMODELED');
    const alphaEnabled = alpha?.alphaBlend === true;
    const srcRaw = raw(alpha?.sourceBlendMode);
    const dstRaw = raw(alpha?.destinationBlendMode);
    const blendSrc = srcRaw === undefined ? undefined : sourceFactorByRaw.get(srcRaw);
    const blendDst = dstRaw === undefined ? undefined : destinationFactorByRaw.get(dstRaw);
    // The game blends RGB into an opaque framebuffer. Our transparent canvas also
    // carries coverage for browser/CSS compositing: an additive black texel must
    // not make that canvas opaque. Preserve coverage while keeping source RGB factors.
    const preserveCanvasAlpha = alphaEnabled && srcRaw === 6 && dstRaw === 0;
    if (alphaEnabled && (blendSrc === undefined || blendDst === undefined)) {
      diagnostics.push('UNSUPPORTED_ALPHA_BLEND_FACTOR');
      return null;
    }
    if (alpha?.alphaTest === true && ![0, 1, 2, 3, 4, 5, 6, 7].includes(raw(alpha.testFunction) ?? -1)) {
      diagnostics.push('UNSUPPORTED_ALPHA_TEST_FUNCTION');
      return null;
    }
    const clamp = raw(slot.clampMode);
    texture.wrapS = clamp === 0 || clamp === 1 ? THREE.ClampToEdgeWrapping : THREE.RepeatWrapping;
    texture.wrapT = clamp === 0 || clamp === 2 ? THREE.ClampToEdgeWrapping : THREE.RepeatWrapping;
    const filter = raw(slot.filterMode);
    if (filter === 0) texture.minFilter = texture.magFilter = THREE.NearestFilter;
    else if (filter === 1) texture.minFilter = texture.magFilter = THREE.LinearFilter;
    else if (filter === 2) { texture.magFilter = THREE.LinearFilter; texture.minFilter = THREE.LinearMipmapLinearFilter; }
    const clonedTexture = texture.clone();
    clonedTexture.needsUpdate = true;
    const diffuse = namedColor(material?.diffuseColorRaw, [1, 1, 1]);
    const emissive = namedColor(material?.emissiveColorRaw, [0, 0, 0]);
    const opacity = finite(material?.alphaRaw) ? material.alphaRaw : 1;
    const threshold = finite(alpha?.thresholdRaw) ? alpha.thresholdRaw / 255 : 0;
    const depthTest = zBufferValue(zBuffer, 'depthTest') ?? true;
    const depthWrite = zBufferValue(zBuffer, 'depthWrite') ?? true;
    const depthCompareRaw = zBufferCompare(zBuffer);
    if (!zBuffer) diagnostics.push('NO_EXTRACTED_ZBUFFER_PROPERTY_DEFAULTS_USED');
    else if (depthCompareRaw === undefined) diagnostics.push('ZBUFFER_COMPARE_NOT_EXPORTED_DEFAULTED_TO_LEQUAL');
    const depthFunc = depthFunctionByRaw.get(depthCompareRaw ?? 3);
    if (depthFunc === undefined) {
      diagnostics.push('UNSUPPORTED_ZBUFFER_COMPARE_FUNCTION:' + depthCompareRaw);
      clonedTexture.dispose();
      return null;
    }
    const materialObject = new THREE.ShaderMaterial({
      uniforms: {
        map: { value: clonedTexture },
        darkMap: { value: null }, detailMap: { value: null },
        darkMapEnabled: { value: 0 }, detailMapEnabled: { value: 0 },
        darkUvMatrix: { value: new THREE.Matrix3() }, detailUvMatrix: { value: new THREE.Matrix3() },
        diffuseContribution: { value: presentation.diffuseContribution },
        transparentOutsideUnitUv: { value: 0 },
        uvMatrix: { value: new THREE.Matrix3() },
        diffuseColor: { value: diffuse },
        emissiveColor: { value: emissive },
        emissiveMultiplier: { value: finite(material?.emissiveMultRaw) ? material.emissiveMultRaw : 1 },
        previewIntensity: { value: previewIntensity },
        sourceVertexMode: { value: sourceVertexMode },
        lightingMode: { value: lightingMode },
        vertexColorsEnabled: { value: hasVertexColors ? 1 : 0 },
        opacity: { value: opacity },
        alphaThreshold: { value: threshold },
        alphaTestEnabled: { value: alpha?.alphaTest === true ? 1 : 0 },
        alphaTestFunction: { value: raw(alpha?.testFunction) ?? 0 },
      },
      vertexShader: hasVertexColors
        ? vertexShader
        : vertexShader.replace('attribute vec4 color;', '').replace('  vEffectColor = color;', '  vEffectColor = vec4(1.0);'),
      fragmentShader,
      vertexColors: false,
      transparent: alphaEnabled,
      blending: alphaEnabled ? THREE.CustomBlending : THREE.NoBlending,
      blendSrc: blendSrc ?? THREE.OneFactor,
      blendDst: blendDst ?? THREE.ZeroFactor,
      blendEquation: THREE.AddEquation,
      blendSrcAlpha: preserveCanvasAlpha ? THREE.ZeroFactor : blendSrc ?? THREE.OneFactor,
      blendDstAlpha: preserveCanvasAlpha ? THREE.OneFactor : blendDst ?? THREE.ZeroFactor,
      blendEquationAlpha: THREE.AddEquation,
      depthTest,
      depthWrite,
      depthFunc,
      side: THREE.DoubleSide,
      toneMapped: false,
    });
    materialObject.userData.originCanvasAlphaPolicy = preserveCanvasAlpha
      ? 'PRESERVE_DESTINATION_COVERAGE_FOR_ADDITIVE_RGB' : 'SOURCE_BLEND_FACTORS';
    if (alpha?.alphaBlend === true && alpha.alphaTest === true) diagnostics.push('ALPHA_BLEND_AND_TEST_BOTH_ENABLED');
    return materialObject;
  }
  async function buildMesh(
    sourceMesh: OriginEffectMesh,
    baseUrl: string,
    source: { path?: string; sha256?: string },
    skinTemplate?: THREE.SkinnedMesh,
  ): Promise<MeshBuildResult | null> {
    const diagnostics: string[] = [];
    const slot = baseSlot(sourceMesh);
    if (!slot?.assetUrl) { diagnostics.push('NO_RESOLVED_BASE_TEXTURE'); return null; }
    if (slot.uvSetRaw !== undefined && slot.uvSetRaw !== 0) {
      diagnostics.push(`TEXTURE_LAYER_UV_SET_NOT_EXTRACTED:BASE_MAP:uv=${slot.uvSetRaw}`); return null;
    }
    const url = textureUrl(baseUrl, slot.assetUrl);
    const sourceTexture = await loadTexture(url);
    // A native GLB template already owns source axes, authored vertex space and
    // per-mesh inverse binds. Do not bake the catalog's world matrix again.
    const geometry = skinTemplate ? skinTemplate.geometry.clone() : createGeometry(sourceMesh, diagnostics);
    if (!geometry) return null;
    const controllers = sourceMesh.controllers ?? [];
    const properties = meshProperties(sourceMesh);
    const targetBlocks = new Set(properties.map(property => property.blockIndex).filter(finite));
    const attachedControllers = controllers.filter(controller =>
      (controller.kind === 'uv' && targetBlocks.has(controller.targetBlockIndex ?? -1) &&
        properties.some(property => property.type === 'NiTexturingProperty' && property.slots?.some(candidate =>
          ['BASE_MAP', 'DARK_MAP', 'DETAIL_MAP'].includes(candidate.slot ?? '') && textureSlotIndex(candidate) === raw(controller.textureSlot)))) ||
      ((controller.kind === 'alpha' || controller.kind === 'material_color') &&
        targetBlocks.has(controller.targetBlockIndex ?? -1)));
    const unsupported = controllers.filter(controller => !attachedControllers.includes(controller));
    unsupported.forEach(controller => diagnostics.push('UNSUPPORTED_CONTROLLER:' +
      (controller.kind ?? controller.type ?? 'unknown') + ':block:' + (controller.controllerBlockIndex ?? '?')));
    const applySupported = (controller: OriginEffectController) => {
      const cycle = raw(controller.flags?.cycleType);
      const targetColor = raw(controller.targetColor);
      const kindSupported = controller.kind === 'uv' || controller.kind === 'alpha' ||
        (controller.kind === 'material_color' && [1, 3].includes(targetColor ?? -1));
      const cycleSupported = cycle === undefined || [0, 1, 2].includes(cycle);
      const validKeys = controller.kind === 'material_color'
        ? (controller.keys ?? []).length > 0 &&
          (controller.keys ?? []).every(key => finite(key.time) && rawVector3(key.valueRaw) !== null &&
            (key.forwardRaw == null || rawVector3(key.forwardRaw) !== null) &&
            (key.backwardRaw == null || rawVector3(key.backwardRaw) !== null))
        : true;
      return kindSupported && cycleSupported && validKeys &&
        finite(controller.interpolationType?.rawValue) && [1, 2, 5].includes(controller.interpolationType.rawValue) &&
        (controller.kind !== 'uv' || [0, 1, 2, 3, 4].includes(raw(controller.operation) ?? -1));
    };
    const supportedControllers = attachedControllers.filter(applySupported);
    attachedControllers.filter(controller => !applySupported(controller)).forEach(controller => {
      if (controller.kind === 'material_color' && ![1, 3].includes(raw(controller.targetColor) ?? -1)) {
        diagnostics.push('UNSUPPORTED_MATERIAL_COLOR_TARGET:target=' + (raw(controller.targetColor) ?? 'unknown') +
          ':block:' + (controller.controllerBlockIndex ?? '?'));
      } else if (controller.kind === 'material_color' &&
          (controller.keys ?? []).some(key => rawVector3(key.valueRaw) === null)) {
        diagnostics.push('UNSUPPORTED_MATERIAL_COLOR_KEY_DATA:block:' + (controller.controllerBlockIndex ?? '?'));
      } else {
        diagnostics.push('UNSUPPORTED_INTERPOLATION_OR_OPERATION:block:' + (controller.controllerBlockIndex ?? '?'));
      }
    });
    const hasVertexColors = geometry.getAttribute('color') !== undefined;
    const shader = createMaterial(slot, properties, sourceTexture, hasVertexColors, diagnostics);
    if (!shader) { geometry.dispose(); return null; }
    if (!(await attachTextureLayers(shader, properties, baseUrl, diagnostics))) {
      geometry.dispose(); disposeShaderTextures(shader); shader.dispose(); return null;
    }
    let meshObject: THREE.Mesh;
    if (skinTemplate) {
      const skinned = new THREE.SkinnedMesh(geometry, shader);
      skinned.bindMode = skinTemplate.bindMode;
      skinned.bind(skinTemplate.skeleton, skinTemplate.bindMatrix);
      skinned.bindMatrixInverse.copy(skinTemplate.bindMatrixInverse);
      skinned.userData.originEffectSkinTemplate = skinTemplate;
      // Shared bones and inverse binds remain owned by the source asset.
      skinned.userData.originEffectSkinBinding = {
        status: 'NATIVE_TEMPLATE_SHARED_SKELETON', sourceBlock: sourceMesh.blockIndex,
        bindMode: skinTemplate.bindMode, joints: skinTemplate.skeleton.bones.length,
      };
      diagnostics.push('NATIVE_SKIN_TEMPLATE_SHARED_BONES_AND_INVERSE_BINDS');
      meshObject = skinned;
    } else meshObject = new THREE.Mesh(geometry, shader);
    previewMaterials.add(shader);
    meshObject.name = sourceMesh.name ?? sourceMesh.meshId ?? 'OriginClient effect surface';
    meshObject.userData.originSource = {
      sourcePath: sourceMesh.sourcePath ?? null,
      blockIndex: sourceMesh.blockIndex ?? null,
      textureSourcePath: slot.sourceTexturePath ?? null,
      textureSourceSha256: slot.sourceTextureSha256 ?? null,
      texturePreviewSha256: slot.texture?.previewAsset?.sha256 ?? null,
    };
    meshObject.userData.originSourceMesh = sourceMesh;
    meshObject.userData.originTextureSlot = slot;
    // A static bind-space box must not cull a deformed overlay.
    meshObject.frustumCulled = !skinTemplate;
    const tracks = supportedControllers.map(controller => ({
      controller, mesh: meshObject, material: shader, applied: true,
    }));
    const initialTimes = new Map<number, number>();
    for (const controller of supportedControllers) {
      const initialTime = controllerTime(controller, 0);
      if (initialTime !== null && finite(controller.controllerBlockIndex)) initialTimes.set(controller.controllerBlockIndex, initialTime);
    }
    shader.uniforms.transparentOutsideUnitUv.value = clipsEffectTexture(source, sourceMesh.blockIndex, presentation) ? 1 : 0;
    if (shader.uniforms.transparentOutsideUnitUv.value === 1) {
      const map = shader.uniforms.map.value as THREE.Texture;
      map.wrapS = map.wrapT = THREE.ClampToEdgeWrapping;
      map.needsUpdate = true;
      diagnostics.push('USER_REFERENCE_TEXTURE_BORDER:TRANSPARENT_OUTSIDE_UNIT_UV');
    }
    updateTextureMatrices(shader, slot, supportedControllers, initialTimes);
    for (const controller of supportedControllers) {
      const initialTime = initialTimes.get(controller.controllerBlockIndex ?? -1);
      if (initialTime === undefined) continue;
      if (controller.kind === 'alpha') {
        const value = evaluateKeys(controller, initialTime);
        if (finite(value)) shader.uniforms.opacity.value = value;
      } else if (controller.kind === 'material_color') {
        const value = evaluateVector3Keys(controller, initialTime);
        const targetColor = raw(controller.targetColor);
        if (value && targetColor === 1) shader.uniforms.diffuseColor.value.copy(value);
        else if (value && targetColor === 3) shader.uniforms.emissiveColor.value.copy(value);
      }
    }
    for (const ancestor of sourceMesh.ancestorNodes ?? []) {
      if (ancestor.type === 'NiBillboardNode') {
        diagnostics.push('BILLBOARD_RUNTIME_UNSUPPORTED:mode=' + (ancestor.billboardMode?.name ?? raw(ancestor.billboardMode) ?? 'unknown'));
      }
    }
    return { object: meshObject, urls: [url], tracks, diagnostics };
  }
  function findModel(catalog: OriginEffectsCatalog, id: string): OriginEffectModel | undefined {
    return [...(catalog.weapons ?? []), ...(catalog.relatedModels ?? [])].find(row => row.id === id);
  }
  function synchronizeSkinTemplate(surface: THREE.Mesh): void {
    const template = surface.userData.originEffectSkinTemplate as THREE.SkinnedMesh | undefined;
    if (!template || !surface.parent) return;
    template.updateWorldMatrix(true, false);
    surface.parent.updateWorldMatrix(true, false);
    surface.matrix.copy(surface.parent.matrixWorld).invert().multiply(template.matrixWorld);
    surface.matrixAutoUpdate = false;
    surface.matrixWorldNeedsUpdate = true;
    // SkinnedMesh.updateMatrixWorld maintains the selected source bind mode.
    surface.updateMatrixWorld(true);
  }
  function emptyReceipt(bindings: OriginEffectBinding[], cancelled: boolean, error?: string): OriginEffectReceipt {
    const rows: OriginEffectReceiptBinding[] = bindings.map(binding => ({
      instanceKey: binding.instanceKey,
      effectId: binding.effectId,
      targetName: binding.target?.name ?? '',
      status: 'CANCELLED',
      provenance: binding.provenance,
      source: { path: null, sha256: null },
      meshes: [],
      unsupported: [],
      diagnostics: error ? [error] : ['STALE_SELECTION_CANCELLED'],
    }));
    return rememberReceipt({
      schemaVersion: 'gf-origin-effect-receipt/1',
      manifestSourceURL: sourceUrl,
      clientId: null,
      cancelled,
      summary: cancelled ? 'Efectos cancelados por un cambio de selección.' : error ?? 'Sin efectos renderizados.',
      stats: {
        requestedBindings: bindings.length, attachedBindings: 0, renderedMeshes: 0, animatedControllers: 0,
        unsupportedControllers: 0, unsupportedParticles: 0, failedMeshes: error ? bindings.length : 0,
      },
      bindings: rows,
    });
  }
  async function setEffects(bindings: OriginEffectBinding[]): Promise<OriginEffectReceipt> {
    clearBindings();
    const currentRevision = revision;
    if (disposed) return emptyReceipt(bindings, false, 'Renderer disposed.');
    try {
      const catalog = await loadCatalog();
      if (currentRevision !== revision) return emptyReceipt(bindings, true);
      const receiptBindings: OriginEffectReceiptBinding[] = [];
      const totals = {
        requestedBindings: bindings.length, attachedBindings: 0, renderedMeshes: 0,
        animatedControllers: 0, unsupportedControllers: 0, unsupportedParticles: 0, simulatedParticleSystems: 0, failedMeshes: 0,
      };
      for (const binding of bindings) {
        if (currentRevision !== revision) return emptyReceipt(bindings, true);
        const model = findModel(catalog, binding.effectId);
        if (!model) {
          receiptBindings.push({
            instanceKey: binding.instanceKey, effectId: binding.effectId, targetName: binding.target?.name ?? '',
            status: 'NO_EXTRACTED_MODEL', provenance: binding.provenance, source: { path: null, sha256: null },
            meshes: [], unsupported: [], diagnostics: ['NO_EXTRACTED_MODEL_FOR_EFFECT_ID'],
          });
          continue;
        }
        const group = new THREE.Group();
        group.name = 'OriginClient FX ' + binding.instanceKey;
        group.userData.originEffectId = model.id;
        const row: OriginEffectReceiptBinding = {
          instanceKey: binding.instanceKey, effectId: binding.effectId, targetName: binding.target?.name ?? '',
          status: 'NO_RENDERABLE_MESHES', provenance: binding.provenance,
          source: { path: model.source?.path ?? null, sha256: model.source?.sha256 ?? null },
          meshes: [], unsupported: [], diagnostics: [],
        };
        const runtime: RuntimeBinding = { instanceKey: binding.instanceKey, elapsed: 0, tracks: [], surfaces: [], particleMaterials: [], receipt: row };
        for (const particle of model.particleSystems ?? []) {
          row.unsupported.push('UNSUPPORTED_PARTICLE_RUNTIME');
          totals.unsupportedParticles++;
          row.diagnostics.push('PARTICLE_SYSTEM_PRESENT_BUT_NOT_SIMULATED:' + diagnosticCode(particle));
        }
        for (const sourceMesh of model.candidateMeshes ?? []) {
          if (currentRevision !== revision) { disposeGroup(group); return emptyReceipt(bindings, true); }
          if (sourceMesh.viewerRenderable !== true || sourceMesh.isParticleEmitterMesh === true) {
            const reason = sourceMesh.isParticleEmitterMesh ? 'PARTICLE_EMITTER_GEOMETRY_NOT_RENDERED' : 'SOURCE_MARKED_NOT_VIEWER_RENDERABLE';
            row.meshes.push(skippedMeshReceipt(sourceMesh, reason));
            continue;
          }
          let skinTemplate: THREE.SkinnedMesh | undefined;
          let templateCount = 0;
          binding.target.traverse(object => {
            if ((object as THREE.SkinnedMesh).isSkinnedMesh && object.userData.gfEffectSkinTemplate === true
                && Number(object.userData.gfSourceBlock) === sourceMesh.blockIndex) {
              templateCount++;
              skinTemplate = object as THREE.SkinnedMesh;
            }
          });
          if (templateCount > 1) {
            totals.failedMeshes++;
            row.meshes.push(skippedMeshReceipt(sourceMesh, 'AMBIGUOUS_NATIVE_EFFECT_SKIN_TEMPLATE'));
            continue;
          }
          if ((sourceMesh.skinned === true || sourceMesh.sourceSkinRequired === true) && !skinTemplate) {
            totals.failedMeshes++;
            row.meshes.push(skippedMeshReceipt(sourceMesh, 'REQUIRED_NATIVE_EFFECT_SKIN_TEMPLATE_NOT_FOUND'));
            continue;
          }
          if (skinTemplate && (!skinTemplate.geometry.getAttribute('skinIndex') || !skinTemplate.geometry.getAttribute('skinWeight')
              || !skinTemplate.skeleton?.bones.length || skinTemplate.skeleton.boneInverses.length !== skinTemplate.skeleton.bones.length)) {
            totals.failedMeshes++;
            row.meshes.push(skippedMeshReceipt(sourceMesh, 'INVALID_NATIVE_EFFECT_SKIN_TEMPLATE'));
            continue;
          }
          try {
            const result = await buildMesh(sourceMesh, options.assetBaseUrl, model.source ?? {}, skinTemplate);
            if (currentRevision !== revision) {
              if (result) { group.add(result.object); }
              disposeGroup(group);
              return emptyReceipt(bindings, true);
            }
            if (!result) {
              totals.failedMeshes++;
              row.meshes.push(skippedMeshReceipt(sourceMesh, 'MESH_BUILD_FAILED_OR_TEXTURE_UNAVAILABLE'));
              continue;
            }
            group.add(result.object);
            synchronizeSkinTemplate(result.object);
            runtime.surfaces.push(result.object);
            runtime.tracks.push(...result.tracks);
            totals.renderedMeshes++;
            totals.animatedControllers += result.tracks.length;
            const unsupportedControllers = (sourceMesh.controllers ?? []).length - result.tracks.length;
            totals.unsupportedControllers += Math.max(0, unsupportedControllers);
            result.diagnostics.forEach(message => {
              if (message.startsWith('UNSUPPORTED_')) row.unsupported.push(message);
              else row.diagnostics.push(message);
            });
            row.meshes.push({
              blockIndex: finite(sourceMesh.blockIndex) ? sourceMesh.blockIndex : null,
              name: sourceMesh.name ?? sourceMesh.meshId ?? 'unnamed',
              status: 'RENDERED', textureUrls: result.urls,
              animatedControllers: result.tracks.length, diagnostics: result.diagnostics,
            });
          } catch (error) {
            totals.failedMeshes++;
            row.meshes.push({
              blockIndex: finite(sourceMesh.blockIndex) ? sourceMesh.blockIndex : null,
              name: sourceMesh.name ?? sourceMesh.meshId ?? 'unnamed',
              status: 'FAILED', textureUrls: [], animatedControllers: 0,
              diagnostics: [error instanceof Error ? error.message : String(error)],
            });
          }
        }
        if (options.runtimeSourceUrl) {
          try {
            runtimeCatalogPromise ??= fetch(options.runtimeSourceUrl).then(async response => {
              if (!response.ok) throw new Error(`RUNTIME_SOURCE_HTTP_${response.status}`);
              return await response.json() as ReferenceRuntimeCatalog;
            });
            const supplement = await runtimeCatalogPromise;
            if (currentRevision !== revision) { disposeGroup(group); return emptyReceipt(bindings, true); }
            const source = supplement.sources.find(source => source.sourcePath === model.source?.path
              && source.sha256 === model.source?.sha256);
            if (source) {
              runtime.path = createEffectPathRuntime(source);
              runtime.billboards = createOriginEffectBillboardRuntime(runtime.surfaces.map(object => ({
                object, root: group, sourceMesh: object.userData.originSourceMesh as OriginEffectMesh,
              })), (source.billboards ?? []) as OriginEffectBillboardSource[], runtime.path);
              row.runtime = {
                sourceSha256: source.sha256, pathControllerBlocks: runtime.path.controllerBlocks,
                billboardMeshBlocks: runtime.surfaces.filter(object => object.userData.originBillboard)
                  .map(object => Number(object.userData.originSource.blockIndex)), elapsedSeconds: 0,
                engineAppearanceParityValidated: false, particleMaterials: [],
              };
              row.diagnostics.push(...runtime.path.diagnostics, ...runtime.billboards.diagnostics);
              const supportedPathControllers = new Set(runtime.path.controllerBlocks);
              for (const mesh of row.meshes) {
                if (runtime.surfaces.some(object => object.userData.originSource.blockIndex === mesh.blockIndex && object.userData.originBillboard)) {
                  mesh.diagnostics = mesh.diagnostics.filter(code => !code.startsWith('BILLBOARD_RUNTIME_UNSUPPORTED'));
                }
                const handledPath = (model.candidateMeshes?.find(candidate => candidate.blockIndex === mesh.blockIndex)?.controllers ?? [])
                  .filter(controller => supportedPathControllers.has(controller.controllerBlockIndex ?? -1));
                for (const controller of handledPath) {
                  const code = 'UNSUPPORTED_CONTROLLER:' + (controller.kind ?? controller.type ?? 'unknown') + ':block:' + controller.controllerBlockIndex;
                  mesh.diagnostics = mesh.diagnostics.filter(diagnostic => diagnostic !== code);
                  row.unsupported = row.unsupported.filter(diagnostic => diagnostic !== code);
                  totals.unsupportedControllers = Math.max(0, totals.unsupportedControllers - 1);
                  mesh.animatedControllers++; totals.animatedControllers++;
                }
              }
              row.diagnostics = row.diagnostics.filter(code => !code.startsWith('BILLBOARD_RUNTIME_UNSUPPORTED')
                || runtime.surfaces.some(object => !object.userData.originBillboard && (object.userData.originSourceMesh as OriginEffectMesh)
                  .ancestorNodes?.some(node => node.type === 'NiBillboardNode')));
              if (source.rawParticleSystems?.length) {
                runtime.particles = await createEffectParticleRuntime(source as OriginParticleRuntimeSource, runtime.path, group, async system => {
                  const properties = meshProperties({ properties: (system.properties ?? []) as OriginEffectProperty[],
                    inheritedProperties: (system.inheritedProperties ?? []) as OriginEffectProperty[] });
                  const slot = properties.find(property => property.type === 'NiTexturingProperty')?.slots?.find(slot => slot.slot === 'BASE_MAP');
                  if (!slot?.assetUrl) return null;
                  if (slot.uvSetRaw !== undefined && slot.uvSetRaw !== 0) {
                    row.diagnostics.push(`TEXTURE_LAYER_UV_SET_NOT_EXTRACTED:BASE_MAP:uv=${slot.uvSetRaw}`); return null;
                  }
                  const texture = await loadTexture(textureUrl(options.assetBaseUrl, slot.assetUrl));
                  const shader = createMaterial(slot, properties, texture, true, row.diagnostics);
                  if (shader) {
                    if (!(await attachTextureLayers(shader, properties, options.assetBaseUrl, row.diagnostics))) {
                      disposeShaderTextures(shader); shader.dispose(); return null;
                    }
                    previewMaterials.add(shader); runtime.particleMaterials.push(shader);
                    row.runtime?.particleMaterials?.push({ systemBlock: system.blockIndex,
                      textureUrl: textureUrl(options.assetBaseUrl, slot.assetUrl), depthTest: shader.depthTest,
                      depthWrite: shader.depthWrite, intensity: shader.uniforms.previewIntensity.value,
                      effectivePropertyBlocks: properties.map(property => ({ type: property.type ?? 'unknown', blockIndex: property.blockIndex ?? null })),
                      materialAudit: {
                        sourceVertexMode: shader.uniforms.sourceVertexMode.value,
                        lightingMode: shader.uniforms.lightingMode.value,
                        vertexColorsEnabled: shader.uniforms.vertexColorsEnabled.value,
                        diffuseColor: (shader.uniforms.diffuseColor.value as THREE.Vector3).toArray(),
                        emissiveColor: (shader.uniforms.emissiveColor.value as THREE.Vector3).toArray(),
                        emissiveMultiplier: shader.uniforms.emissiveMultiplier.value,
                        opacity: shader.uniforms.opacity.value,
                        sourceBlendMode: raw(properties.find(property => property.type === 'NiAlphaProperty')?.sourceBlendMode) ?? null,
                        destinationBlendMode: raw(properties.find(property => property.type === 'NiAlphaProperty')?.destinationBlendMode) ?? null,
                        textureColorSpace: (shader.uniforms.map.value as THREE.Texture).colorSpace,
                        alphaBlend: shader.blending === THREE.CustomBlending,
                        canvasAlphaPolicy: shader.userData.originCanvasAlphaPolicy,
                        alphaTestEnabled: shader.uniforms.alphaTestEnabled.value,
                        diffuseContribution: shader.uniforms.diffuseContribution.value,
                        textureLayers: ((shader.userData.originTextureLayers ?? []) as OriginEffectTextureSlot[]).map(layer => ({
                          slot: layer.slot ?? 'unknown', textureUrl: textureUrl(options.assetBaseUrl, layer.assetUrl!),
                          uvSet: layer.uvSetRaw ?? 0, rgbScale: layer.slot === 'DETAIL_MAP' ? 2 : 1, alphaScale: 1,
                        })),
                        diffuseLightingApproximation: shader.uniforms.lightingMode.value === 1
                          ? 'USER_REFERENCE_DIFFUSE_CONTRIBUTION_PLUS_SOURCE_EMISSIVE' : 'SOURCE_EMISSIVE_ONLY',
                      } });
                  }
                  return shader;
                }, presentation.effectFramesPerSecond);
                if (currentRevision !== revision) {
                  runtime.particles.dispose();
                  for (const material of runtime.particleMaterials) {
                    previewMaterials.delete(material); disposeShaderTextures(material);
                  }
                  disposeGroup(group); return emptyReceipt(bindings, true);
                }
                row.runtime.simulatedParticleSystemBlocks = runtime.particles.supportedSystemBlocks;
                row.runtime.liveParticleCount = 0;
                row.runtime.particleDiagnostics = runtime.particles.diagnostics;
                const simulated = runtime.particles.supportedSystemBlocks.length;
                totals.simulatedParticleSystems += simulated;
                totals.unsupportedParticles = Math.max(0, totals.unsupportedParticles - simulated);
                for (let i = 0; i < simulated; i++) {
                  const index = row.unsupported.indexOf('UNSUPPORTED_PARTICLE_RUNTIME');
                  if (index !== -1) row.unsupported.splice(index, 1);
                }
                if (simulated === model.particleSystems?.length) row.diagnostics = row.diagnostics.filter(code => !code.startsWith('PARTICLE_SYSTEM_PRESENT_BUT_NOT_SIMULATED'));
              }
            } else row.diagnostics.push('NO_SOURCE_HASH_MATCH_IN_RUNTIME_SUPPLEMENT');
          } catch (error) { row.diagnostics.push('RUNTIME_SUPPLEMENT_FAILED:' + String(error)); }
        }
        if (group.children.length > 0) {
          // Vertices are already baked into the source model's local frame.
          // Keep the complete layered effect centered if its preview scale changes.
          const sourceBounds = new THREE.Box3().setFromObject(group);
          if (!sourceBounds.isEmpty() && [...sourceBounds.min.toArray(), ...sourceBounds.max.toArray()].every(finite)) {
            const center = sourceBounds.getCenter(new THREE.Vector3());
            group.scale.setScalar(previewEffectScale);
            group.position.copy(center).multiplyScalar(1 - previewEffectScale);
            const previewBounds = new THREE.Box3().setFromObject(group);
            row.sizeAdjustment = {
              scale: previewEffectScale, pivot: 'RENDERED_FX_BOUNDS_CENTER',
              centerLocal: center.toArray(), sourceBoundsSize: sourceBounds.getSize(new THREE.Vector3()).toArray(),
              previewBoundsSize: previewBounds.getSize(new THREE.Vector3()).toArray(),
              previewCenterLocal: previewBounds.getCenter(new THREE.Vector3()).toArray(),
            };
          } else row.diagnostics.push('PREVIEW_EFFECT_SCALE_INVALID_BOUNDS');
          binding.target.add(group);
          for (const surface of runtime.surfaces) synchronizeSkinTemplate(surface);
          installedGroups.add(group);
          runtimeBindings.set(binding.instanceKey, runtime);
          totals.attachedBindings++;
          row.status = 'ATTACHED';
        } else if (row.meshes.some(mesh => mesh.status === 'FAILED')) {
          row.status = 'LOAD_FAILED';
        } else if (row.meshes.length === 0) {
          row.status = 'NO_RENDERABLE_MESHES';
          row.diagnostics.push('MODEL_HAS_NO_CANDIDATE_MESHES');
        }
        row.diagnostics.push(...(model.diagnostics ?? []).map(diagnosticCode));
        row.unsupported.push(...((model.particleSystems?.length ?? 0) > (runtime.particles?.supportedSystemBlocks.length ?? 0)
          ? ['PARTICLE_SYSTEMS_NOT_SPAWNED'] : []));
        receiptBindings.push(row);
      }
      const summary = totals.renderedMeshes > 0
        ? totals.renderedMeshes + ' meshes FX · ' + totals.animatedControllers + ' controllers animados · ' +
          totals.simulatedParticleSystems + ' sistemas de partículas activos · ' + totals.unsupportedParticles + ' no simulados'
        : 'No hay meshes FX renderizables para esta selección.';
      return rememberReceipt({
        schemaVersion: 'gf-origin-effect-receipt/1',
        manifestSourceURL: sourceUrl,
        clientId: catalog.clientId ?? null,
        cancelled: false,
        summary,
        stats: totals,
        bindings: receiptBindings,
      });
    } catch (error) {
      return emptyReceipt(bindings, false, error instanceof Error ? error.message : String(error));
    }
  }
  function tick(delta: number, camera: THREE.Camera, playingEffects: boolean | ReadonlySet<string>): void {
    if (disposed || !finite(delta) || delta <= 0) return;
    const cameraPosition = camera.position;
    void cameraPosition;
    const canPlay = (instanceKey: string) => typeof playingEffects === 'boolean'
      ? playingEffects : playingEffects.has(instanceKey);
    for (const runtime of runtimeBindings.values()) {
      // Follow authored template transforms even when FX time is paused.
      for (const surface of runtime.surfaces) synchronizeSkinTemplate(surface);
      if (!canPlay(runtime.instanceKey)) continue;
      runtime.elapsed += delta;
      const effectTime = effectFrameTime(runtime.elapsed, presentation.effectFramesPerSecond);
      if (runtime.receipt.runtime) runtime.receipt.runtime.elapsedSeconds = effectTime;
      for (const surface of runtime.surfaces) {
        if (surface.userData.originEffectSkinTemplate) continue;
        const blockIndex = Number(surface.userData.originSource.blockIndex);
        surface.matrix.copy(webDelta(runtime.path?.deltaForNode(blockIndex, effectTime) ?? new THREE.Matrix4()));
        surface.matrixAutoUpdate = false;
        surface.matrixWorldNeedsUpdate = true;
      }
      runtime.billboards?.tick(camera, effectTime);
      runtime.particles?.tick(delta, effectTime, camera, options.viewportHeight?.() ?? 1);
      if (runtime.receipt.runtime && runtime.particles) runtime.receipt.runtime.liveParticleCount = runtime.particles.liveParticleCount;
      const changedUvMeshes = new Set<THREE.Mesh>();
      for (const track of runtime.tracks) {
        const time = controllerTime(track.controller, effectTime);
        if (time === null) continue;
        if (track.controller.kind === 'material_color') {
          const value = evaluateVector3Keys(track.controller, time);
          const targetColor = raw(track.controller.targetColor);
          if (!value) continue;
          if (targetColor === 1) track.material.uniforms.diffuseColor.value.copy(value);
          else if (targetColor === 3) track.material.uniforms.emissiveColor.value.copy(value);
          else continue;
          track.material.uniformsNeedUpdate = true;
          continue;
        }
        const value = evaluateKeys(track.controller, time);
        if (!finite(value)) continue;
        if (track.controller.kind === 'alpha') {
          track.material.uniforms.opacity.value = value;
          track.material.uniformsNeedUpdate = true;
        } else if (track.controller.kind === 'uv') {
          changedUvMeshes.add(track.mesh);
        }
      }
      for (const mesh of changedUvMeshes) {
        const sourceMesh = mesh.userData.originSourceMesh as OriginEffectMesh | undefined;
        const slot = mesh.userData.originTextureSlot as OriginEffectTextureSlot | undefined;
        const material = mesh.material as THREE.ShaderMaterial;
        if (!sourceMesh || !slot || !material.uniforms?.uvMatrix) continue;
        const sampleTimes = new Map<number, number>();
        for (const track of runtime.tracks) {
          if (track.mesh !== mesh) continue;
          const time = controllerTime(track.controller, effectTime);
          if (time !== null && finite(track.controller.controllerBlockIndex)) {
            sampleTimes.set(track.controller.controllerBlockIndex, time);
          }
        }
        updateTextureMatrices(material, slot, sourceMesh.controllers ?? [], sampleTimes);
        material.uniformsNeedUpdate = true;
      }
    }
  }
  function dispose(): void {
    if (disposed) return;
    clearBindings();
    disposed = true;
    for (const promise of textureCache.values()) {
      void promise.then(texture => texture.dispose()).catch(() => undefined);
    }
    textureCache.clear();
    catalogPromise = undefined;
    runtimeCatalogPromise = undefined;
  }
  return { setEffects, setIntensity, tick, clearBindings, dispose };
}

