import * as THREE from 'three';
import { effectEmitterTime, effectPresentation } from './effect-presentation';
import { matrixRows, nifToWeb, runtimeControllerTime, sampleRuntimeKeys } from './effect-path';
import type { EffectPathRuntime, ReferenceRuntimeSource, RuntimeController, RuntimeKeyData } from './effect-path';

const MAX_RUNTIME_PARTICLES = 4096;
// Audited Origin sources only. A newly extracted system is not automatically supported.
const PARTICLE_REFERENCE_SOURCES = new Map([
  ['item/model/w420.nif', {
    sha256: 'ce6d65ef2137d78f0aca7844d9f5359e7aa7aeaabb843eb0e73b8c3bc0a4204e',
    blocks: new Map([[107, 'PArray01'], [127, 'SuperSpray01']]),
  }],
  ['item/model/w320.nif', {
    sha256: '4b2937ee03d337518328158afe5a32b529d9f949f8e2d56fbc6a55db7c6b0512',
    blocks: new Map([[18, 'PArray01']]),
  }],
  ['item/model/wmf01.nif', {
    sha256: '8eb440e56cd0bea386dd464263c8a4a7d57e3a5b302014db8dcd147238d326e5',
    blocks: new Map([[166, 'PArray02'], [190, 'PArray03'], [214, 'PArray04']]),
  }],
  ['char/model/x6an6.nif', {
    sha256: '9d899c89c58de6cd9d9c04833cbf7aa2da28531637be15f5445494a270e4b97a',
    blocks: new Map([[104, 'PArray01'], [126, 'PArray02']]),
  }],
]);
const SUPPORTED_MODIFIERS = new Set([
  'NiPSysAgeDeathModifier', 'NiPSysMeshEmitter', 'NiPSysBoxEmitter', 'NiPSysSpawnModifier',
  'NiPSysGrowFadeModifier', 'NiPSysColorModifier', 'NiPSysRotationModifier',
  'NiPSysPositionModifier', 'NiPSysBoundUpdateModifier',
]);

type RawEnum = { rawValue?: number; name?: string };
type RawKey = {
  time?: number;
  valueRaw?: number | number[];
  valueBool?: boolean;
  forwardRaw?: number | number[];
  backwardRaw?: number | number[];
};
type RawKeyData = { interpolationType?: RawEnum; keys?: RawKey[] };
type RawModifier = {
  blockIndex?: number;
  type?: string;
  parametersRaw?: Record<string, unknown>;
  dataRaw?: { dataBlockIndex?: number; interpolationType?: RawEnum; keys?: RawKey[] };
};
export interface RawOriginParticleSystem {
  blockIndex: number;
  type: string;
  name: string;
  sourceMatrixWorldRows?: number[][];
  worldSpaceRaw: number;
  dataRaw?: {
    numVerticesRaw?: number;
    numParticlesRaw?: number;
    numActiveRaw?: number;
    vertexArrayCount?: number;
    particleInfoArrayCount?: number;
    maxParticlesFieldPresent?: boolean;
  };
  controllers?: {
    controllerBlockIndex?: number;
    type?: string;
    flagsRaw?: number;
    cycleType?: RawEnum;
    active?: boolean;
    playBackwards?: boolean;
    frequencyRaw?: number;
    phaseRaw?: number;
    startTimeRaw?: number;
    stopTimeRaw?: number;
    modifierBlockIndex?: number;
    emissionRateInterpolator?: { valueRaw?: number };
    visibilityInterpolator?: { data?: RawKeyData };
  }[];
  emitterSources?: {
    kind?: string;
    modifierBlockIndex?: number;
    emitterMeshBlockIndex?: number;
    emitterMeshName?: string;
    emitterMeshSourceWorldRows?: number[][];
    emitterObjectBlockIndex?: number;
    emitterObjectName?: string;
    emitterObjectSourceWorldRows?: number[][];
  }[];
  modifiers?: RawModifier[];
  properties?: unknown[];
  inheritedProperties?: unknown[];
  propertyScopeCounts?: { direct?: number; inherited?: number };
}
export interface RawOriginEmitterMeshSource {
  blockIndex: number;
  verticesRaw: number[][];
  normalsRaw: number[][];
  sourceMatrixWorldRows: number[][];
  triangulation?: { triangleIndices?: number[][] | number[] };
}
export type OriginParticleRuntimeSource = ReferenceRuntimeSource & {
  rawParticleSystems?: RawOriginParticleSystem[];
  emitterMeshSources?: RawOriginEmitterMeshSource[];
};

export interface OriginEffectParticleRuntime {
  readonly diagnostics: string[];
  readonly supportedSystemBlocks: number[];
  readonly liveParticleCount: number;
  tick(delta: number, elapsed: number, camera: THREE.Camera, viewportHeight: number): void;
  dispose(): void;
}

export type OriginParticleMaterialBuilder = (
  system: RawOriginParticleSystem,
) => Promise<THREE.ShaderMaterial | null>;

type Particle = {
  birthTime: number;
  lifeSpan: number;
  startPosition: THREE.Vector3;
  velocity: THREE.Vector3;
  diameter: number;
  initialColor: [number, number, number, number];
  rotationAngle: number;
  rotationSpeed: number;
};
type SurfaceTriangle = { a: number; b: number; c: number; cumulativeArea: number };
type PreparedMesh = { mesh: RawOriginEmitterMeshSource; triangles: SurfaceTriangle[]; totalArea: number };
type EmissionWindow = { start: number; end: number };
type RuntimeSystem = {
  source: RawOriginParticleSystem;
  controller: NonNullable<RawOriginParticleSystem['controllers']>[number];
  runtimeController: RuntimeController;
  visibility: RuntimeKeyData;
  windows: EmissionWindow[];
  rate: number;
  capacity: number;
  particles: Particle[];
  points: THREE.Points<THREE.BufferGeometry, THREE.ShaderMaterial>;
  geometry: THREE.BufferGeometry;
  material: THREE.ShaderMaterial;
  attributes: { position: THREE.BufferAttribute; color: THREE.BufferAttribute; diameter: THREE.BufferAttribute; rotation: THREE.BufferAttribute };
  emitter: NonNullable<RawOriginParticleSystem['emitterSources']>[number];
  emitterModifier: RawModifier;
  colorKeys?: RuntimeKeyData;
  growTime: number;
  fadeTime: number;
  baseScale: number;
  rotationModifier?: RawModifier;
  preparedMesh?: PreparedMesh;
  systemWorld: THREE.Matrix4;
  systemWorldInverse: THREE.Matrix4;
  lastElapsed: number;
  randomState: number;
  capDiagnosticAdded: boolean;
};

const pointVertexShader = [
  'attribute vec4 color;',
  'attribute float particleDiameter;',
  'attribute float particleRotation;',
  'uniform float viewportHeight;',
  'uniform int orthographicCamera;',
  'varying vec4 vEffectColor;',
  'varying float vParticleRotation;',
  'void main() {',
  '  vEffectColor = color;',
  '  vParticleRotation = particleRotation;',
  '  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);',
  '  float modelScale = max(length(modelMatrix[0].xyz), max(length(modelMatrix[1].xyz), length(modelMatrix[2].xyz)));',
  '  float viewDepth = orthographicCamera == 1 ? 1.0 : max(0.0001, -mvPosition.z);',
  '  gl_PointSize = max(1.0, particleDiameter * modelScale * projectionMatrix[1][1] * viewportHeight * 0.5 / viewDepth);',
  '  gl_Position = projectionMatrix * mvPosition;',
  '}',
].join('\n');

const pointFragmentShader = [
  '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 mat3 uvMatrix;',
  '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 vec4 vEffectColor;',
  'varying float vParticleRotation;',
  'void main() {',
  '  vec2 centered = gl_PointCoord - vec2(0.5);',
  '  float c = cos(vParticleRotation);',
  '  float s = sin(vParticleRotation);',
  '  vec2 spriteUv = mat2(c, -s, s, c) * centered + vec2(0.5);',
  '  vec2 effectUv = (uvMatrix * vec3(spriteUv, 1.0)).xy;',
  '  vec4 texel = texture2D(map, effectUv);',
  '  if (darkMapEnabled == 1) texel *= texture2D(darkMap, (darkUvMatrix * vec3(spriteUv, 1.0)).xy);',
  '  if (detailMapEnabled == 1) { vec4 detail = texture2D(detailMap, (detailUvMatrix * vec3(spriteUv, 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');

function isFiniteNumber(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value);
}

function rawVector(value: unknown, size: number): number[] | null {
  if (!Array.isArray(value) || value.length < size || !value.slice(0, size).every(isFiniteNumber)) return null;
  return value.slice(0, size) as number[];
}

function keyData(rawData?: RawKeyData): RuntimeKeyData | null {
  if (!rawData?.interpolationType || !Array.isArray(rawData.keys)) return null;
  const keys = rawData.keys.flatMap(key => {
    if (!isFiniteNumber(key.time) || (!isFiniteNumber(key.valueRaw) && !Array.isArray(key.valueRaw))) return [];
    return [{
      time: key.time,
      valueRaw: key.valueRaw as number | number[],
      ...(key.forwardRaw !== undefined ? { forwardRaw: key.forwardRaw } : {}),
      ...(key.backwardRaw !== undefined ? { backwardRaw: key.backwardRaw } : {}),
    }];
  });
  return { interpolationType: rawData.interpolationType, keys };
}

function findModifier(system: RawOriginParticleSystem, type: string): RawModifier | undefined {
  return system.modifiers?.find(modifier => modifier.type === type && modifier.parametersRaw?.activeRaw !== false);
}

function supportedConfiguration(system: RawOriginParticleSystem, diagnostics: string[]): boolean {
  let supported = true;
  for (const controller of system.controllers ?? []) {
    if (controller.active === false) continue;
    if (controller.type !== 'NiPSysEmitterCtlr' && controller.type !== 'NiPSysUpdateCtlr') {
      diagnostics.push(`PARTICLE_ACTIVE_CONTROLLER_UNSUPPORTED:system=${system.blockIndex}:type=${controller.type}`);
      supported = false;
    }
  }
  for (const modifier of system.modifiers ?? []) {
    if (modifier.parametersRaw?.activeRaw === false) continue;
    const parameters = modifier.parametersRaw ?? {};
    if (!SUPPORTED_MODIFIERS.has(modifier.type ?? '')) {
      diagnostics.push(`PARTICLE_ACTIVE_MODIFIER_UNSUPPORTED:system=${system.blockIndex}:type=${modifier.type}`);
      supported = false;
    }
    if ((modifier.type === 'NiPSysAgeDeathModifier' && parameters.spawn_on_deathRaw !== 0) ||
        (modifier.type === 'NiPSysSpawnModifier' && parameters.num_spawn_generationsRaw !== 0)) {
      diagnostics.push(`PARTICLE_SPAWN_GENERATIONS_UNSUPPORTED:system=${system.blockIndex}:modifier=${modifier.blockIndex}`);
      supported = false;
    }
    if (modifier.type === 'NiPSysGrowFadeModifier' &&
        (parameters.grow_generationRaw !== 0 || parameters.fade_generationRaw !== 0)) {
      diagnostics.push(`PARTICLE_GROW_FADE_GENERATIONS_UNSUPPORTED:system=${system.blockIndex}`);
      supported = false;
    }
    if (modifier.type === 'NiPSysColorModifier' &&
        ![1, 2, 5].includes(modifier.dataRaw?.interpolationType?.rawValue ?? -1)) {
      diagnostics.push(`PARTICLE_COLOR_INTERPOLATION_UNSUPPORTED:system=${system.blockIndex}`);
      supported = false;
    }
  }
  return supported;
}

function createRuntimeController(system: RawOriginParticleSystem): RuntimeSystem['runtimeController'] | null {
  const controller = system.controllers?.find(candidate => candidate.type === 'NiPSysEmitterCtlr');
  if (!controller || !isFiniteNumber(controller.frequencyRaw) || !isFiniteNumber(controller.phaseRaw) ||
      !isFiniteNumber(controller.startTimeRaw) || !isFiniteNumber(controller.stopTimeRaw)) return null;
  return {
    blockIndex: controller.controllerBlockIndex ?? -1,
    targetBlockIndex: system.blockIndex,
    frequencyRaw: controller.frequencyRaw,
    phaseRaw: controller.phaseRaw,
    startTimeRaw: controller.startTimeRaw,
    stopTimeRaw: controller.stopTimeRaw,
    flags: {
      active: controller.active,
      cycleType: controller.cycleType,
      playBackwards: controller.playBackwards,
    },
  };
}

function buildVisibleWindows(system: RawOriginParticleSystem, controller: RuntimeSystem['controller'], diagnostics: string[]): EmissionWindow[] | null {
  const data = controller.visibilityInterpolator?.data;
  if (!data || data.interpolationType?.rawValue !== 5 || !Array.isArray(data.keys) || data.keys.length === 0) {
    diagnostics.push(`VISIBILITY_INTERPOLATOR_UNSUPPORTED:system=${system.blockIndex}`);
    return null;
  }
  const start = controller.startTimeRaw ?? 0;
  const stop = controller.stopTimeRaw ?? 0;
  const keys = data.keys.filter(key => isFiniteNumber(key.time) && (typeof key.valueBool === 'boolean' || isFiniteNumber(key.valueRaw)))
    .sort((a, b) => (a.time ?? 0) - (b.time ?? 0));
  if (keys.length === 0) {
    diagnostics.push(`VISIBILITY_KEYS_INVALID:system=${system.blockIndex}`);
    return null;
  }
  let cursor = start;
  let visible = keys[0].valueBool ?? Number(keys[0].valueRaw) !== 0;
  const windows: EmissionWindow[] = [];
  for (const key of keys) {
    const keyTime = THREE.MathUtils.clamp(key.time!, start, stop);
    if (keyTime > cursor && visible) windows.push({ start: cursor, end: keyTime });
    cursor = Math.max(cursor, keyTime);
    visible = key.valueBool ?? Number(key.valueRaw) !== 0;
  }
  if (stop > cursor && visible) windows.push({ start: cursor, end: stop });
  return windows;
}

function buildSurface(mesh: RawOriginEmitterMeshSource, diagnostics: string[], systemBlock: number): PreparedMesh | null {
  const vertexCount = mesh.verticesRaw?.length ?? 0;
  const normalsCount = mesh.normalsRaw?.length ?? 0;
  const rawTriangles = mesh.triangulation?.triangleIndices;
  const triangles: number[][] = Array.isArray(rawTriangles) && rawTriangles.length > 0 && Array.isArray(rawTriangles[0])
    ? rawTriangles as number[][]
    : Array.isArray(rawTriangles) ? Array.from({ length: Math.floor(rawTriangles.length / 3) }, (_, i) => rawTriangles.slice(i * 3, i * 3 + 3) as number[]) : [];
  let totalArea = 0;
  const cumulative: SurfaceTriangle[] = [];
  for (const triangle of triangles) {
    if (triangle.length < 3 || !triangle.slice(0, 3).every(index => Number.isInteger(index) && index >= 0 && index < vertexCount)) continue;
    const a = rawVector(mesh.verticesRaw[triangle[0]], 3);
    const b = rawVector(mesh.verticesRaw[triangle[1]], 3);
    const c = rawVector(mesh.verticesRaw[triangle[2]], 3);
    if (!a || !b || !c) continue;
    const area = new THREE.Vector3().subVectors(new THREE.Vector3(...b as [number, number, number]), new THREE.Vector3(...a as [number, number, number]))
      .cross(new THREE.Vector3().subVectors(new THREE.Vector3(...c as [number, number, number]), new THREE.Vector3(...a as [number, number, number])))
      .length() * 0.5;
    if (!(area > 0)) continue;
    totalArea += area;
    cumulative.push({ a: triangle[0], b: triangle[1], c: triangle[2], cumulativeArea: totalArea });
  }
  if (vertexCount === 0 || normalsCount !== vertexCount || cumulative.length === 0 || !(totalArea > 0)) {
    diagnostics.push(`MESH_EMITTER_SOURCE_INVALID:system=${systemBlock}:mesh=${mesh.blockIndex}:vertices=${vertexCount}:normals=${normalsCount}:triangles=${cumulative.length}`);
    return null;
  }
  return { mesh, triangles: cumulative, totalArea };
}

function pointOnSurface(surface: PreparedMesh, random: () => number): { position: THREE.Vector3; normal: THREE.Vector3 } {
  const target = random() * surface.totalArea;
  let low = 0, high = surface.triangles.length - 1;
  while (low < high) {
    const middle = (low + high) >>> 1;
    if (surface.triangles[middle].cumulativeArea < target) low = middle + 1;
    else high = middle;
  }
  const triangle = surface.triangles[low];
  const a = new THREE.Vector3(...surface.mesh.verticesRaw[triangle.a] as [number, number, number]);
  const b = new THREE.Vector3(...surface.mesh.verticesRaw[triangle.b] as [number, number, number]);
  const c = new THREE.Vector3(...surface.mesh.verticesRaw[triangle.c] as [number, number, number]);
  const root = Math.sqrt(random());
  const baryA = 1 - root;
  const baryB = root * (1 - random());
  const baryC = root - baryB;
  const position = a.multiplyScalar(baryA).addScaledVector(b, baryB).addScaledVector(c, baryC);
  const normalA = new THREE.Vector3(...surface.mesh.normalsRaw[triangle.a] as [number, number, number]);
  const normalB = new THREE.Vector3(...surface.mesh.normalsRaw[triangle.b] as [number, number, number]);
  const normalC = new THREE.Vector3(...surface.mesh.normalsRaw[triangle.c] as [number, number, number]);
  const normal = normalA.multiplyScalar(baryA).addScaledVector(normalB, baryB).addScaledVector(normalC, baryC).normalize();
  return { position, normal };
}

function variation(base: unknown, amount: unknown, random: () => number, fallback: number): number {
  const center = isFiniteNumber(base) ? base : fallback;
  const spread = isFiniteNumber(amount) ? Math.abs(amount) : 0;
  return center + (random() * 2 - 1) * spread;
}

function rgba(value: unknown, fallback: [number, number, number, number]): [number, number, number, number] {
  const components = rawVector(value, 4);
  return components ? [components[0], components[1], components[2], components[3]] : [...fallback];
}

function eventTimes(
  runtimeController: RuntimeController,
  windows: EmissionWindow[],
  rate: number,
  fromElapsed: number,
  toElapsed: number,
  diagnostics: string[],
  systemBlock: number,
): number[] {
  if (!(rate > 0) || runtimeController.flags.active === false) return [];
  const frequency = runtimeController.frequencyRaw;
  const start = runtimeController.startTimeRaw;
  const stop = runtimeController.stopTimeRaw;
  const cycle = runtimeController.flags.cycleType?.rawValue ?? 0;
  if (!(frequency > 0) || !(stop > start) || cycle !== 0) {
    diagnostics.push(`EMITTER_CONTROLLER_CYCLE_UNSUPPORTED:system=${systemBlock}:cycle=${cycle}:frequency=${frequency}`);
    return [];
  }
  if (runtimeController.flags.playBackwards) {
    diagnostics.push(`EMITTER_CONTROLLER_PLAYBACK_REVERSE_UNSUPPORTED:system=${systemBlock}`);
    return [];
  }
  const period = stop - start;
  const scaledFrom = fromElapsed * frequency + runtimeController.phaseRaw;
  const scaledTo = toElapsed * frequency + runtimeController.phaseRaw;
  const firstCycle = Math.max(0, Math.floor((scaledFrom - start) / period) - 1);
  const lastCycle = Math.max(firstCycle, Math.floor((scaledTo - start) / period) + 1);
  if (lastCycle - firstCycle > 20000) {
    diagnostics.push(`EMITTER_TIMELINE_RANGE_TRUNCATED:system=${systemBlock}:cycles=${lastCycle - firstCycle + 1}`);
  }
  const boundedFirst = Math.max(firstCycle, lastCycle - 20000);
  const births: number[] = [];
  for (let cycleIndex = boundedFirst; cycleIndex <= lastCycle; cycleIndex++) {
    const cycleOffset = cycleIndex * period;
    for (const window of windows) {
      const windowStart = (window.start + cycleOffset - runtimeController.phaseRaw) / frequency;
      const windowEnd = (window.end + cycleOffset - runtimeController.phaseRaw) / frequency;
      const visibleStart = Math.max(0, windowStart);
      const visibleEnd = windowEnd;
      if (visibleEnd <= visibleStart || visibleEnd <= fromElapsed || visibleStart > toElapsed) continue;
      const firstIndex = Math.max(1, Math.floor((Math.max(fromElapsed, visibleStart) - visibleStart) * rate + 1e-8) + 1);
      const lastIndex = Math.floor((Math.min(toElapsed, visibleEnd) - visibleStart) * rate + 1e-8);
      for (let index = firstIndex; index <= lastIndex; index++) {
        const birth = visibleStart + index / rate;
        if (birth > fromElapsed + 1e-9 && birth <= toElapsed + 1e-9 && birth < visibleEnd + 1e-9) births.push(birth);
      }
    }
  }
  births.sort((a, b) => a - b);
  return births;
}

function colorKeyData(system: RawOriginParticleSystem): RuntimeKeyData | undefined {
  const modifier = findModifier(system, 'NiPSysColorModifier');
  const data = keyData(modifier?.dataRaw);
  return data ?? undefined;
}

function makePointMaterial(material: THREE.ShaderMaterial): void {
  const uniforms = material.uniforms as Record<string, THREE.IUniform<unknown>>;
  if (!uniforms.uvMatrix) uniforms.uvMatrix = { value: new THREE.Matrix3() };
  if (!uniforms.map) uniforms.map = { value: null };
  if (!uniforms.darkMap) uniforms.darkMap = { value: null };
  if (!uniforms.detailMap) uniforms.detailMap = { value: null };
  if (!uniforms.darkMapEnabled) uniforms.darkMapEnabled = { value: 0 };
  if (!uniforms.detailMapEnabled) uniforms.detailMapEnabled = { value: 0 };
  if (!uniforms.darkUvMatrix) uniforms.darkUvMatrix = { value: new THREE.Matrix3() };
  if (!uniforms.detailUvMatrix) uniforms.detailUvMatrix = { value: new THREE.Matrix3() };
  if (!uniforms.diffuseContribution) uniforms.diffuseContribution = { value: effectPresentation.diffuseContribution };
  if (!uniforms.diffuseColor) uniforms.diffuseColor = { value: new THREE.Color(1, 1, 1) };
  if (!uniforms.emissiveColor) uniforms.emissiveColor = { value: new THREE.Color(0, 0, 0) };
  if (!uniforms.emissiveMultiplier) uniforms.emissiveMultiplier = { value: 1 };
  if (!uniforms.previewIntensity) uniforms.previewIntensity = { value: 1 };
  if (!uniforms.sourceVertexMode) uniforms.sourceVertexMode = { value: 2 };
  if (!uniforms.lightingMode) uniforms.lightingMode = { value: 1 };
  if (!uniforms.vertexColorsEnabled) uniforms.vertexColorsEnabled = { value: 1 };
  if (!uniforms.opacity) uniforms.opacity = { value: 1 };
  if (!uniforms.alphaThreshold) uniforms.alphaThreshold = { value: 0 };
  if (!uniforms.alphaTestEnabled) uniforms.alphaTestEnabled = { value: 0 };
  if (!uniforms.alphaTestFunction) uniforms.alphaTestFunction = { value: 4 };
  uniforms.viewportHeight = { value: 1 };
  uniforms.orthographicCamera = { value: 0 };
  material.vertexShader = pointVertexShader;
  material.fragmentShader = pointFragmentShader;
  material.needsUpdate = true;
}

function sourceProperties(system: RawOriginParticleSystem): { growTime: number; fadeTime: number; baseScale: number; rotationModifier?: RawModifier } {
  const growFade = findModifier(system, 'NiPSysGrowFadeModifier')?.parametersRaw;
  return {
    growTime: isFiniteNumber(growFade?.grow_timeRaw) ? growFade.grow_timeRaw : 0,
    fadeTime: isFiniteNumber(growFade?.fade_timeRaw) ? growFade.fade_timeRaw : 0,
    baseScale: isFiniteNumber(growFade?.base_scaleRaw) ? growFade.base_scaleRaw : 1,
    rotationModifier: findModifier(system, 'NiPSysRotationModifier'),
  };
}

function makeSystemRuntime(
  source: RawOriginParticleSystem,
  controller: RuntimeSystem['controller'],
  runtimeController: RuntimeController,
  visibility: RuntimeKeyData,
  windows: EmissionWindow[],
  rate: number,
  emitter: RuntimeSystem['emitter'],
  emitterModifier: RawModifier,
  points: THREE.Points<THREE.BufferGeometry, THREE.ShaderMaterial>,
  capacity: number,
  preparedMesh: PreparedMesh | undefined,
  seed: number,
): RuntimeSystem | null {
  if (!source.sourceMatrixWorldRows) return null;
  const systemWorld = matrixRows(source.sourceMatrixWorldRows);
  const determinant = systemWorld.determinant();
  if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-12) return null;
  const growFade = sourceProperties(source);
  const rawColorData = colorKeyData(source);
  const geometry = points.geometry;
  const position = geometry.getAttribute('position') as THREE.BufferAttribute;
  const color = geometry.getAttribute('color') as THREE.BufferAttribute;
  const diameter = geometry.getAttribute('particleDiameter') as THREE.BufferAttribute;
  const rotation = geometry.getAttribute('particleRotation') as THREE.BufferAttribute;
  return {
    source, controller, runtimeController, visibility, windows, rate, capacity,
    particles: [], points, geometry, material: points.material,
    attributes: { position, color, diameter, rotation },
    emitter, emitterModifier, colorKeys: rawColorData,
    growTime: growFade.growTime, fadeTime: growFade.fadeTime, baseScale: growFade.baseScale,
    rotationModifier: growFade.rotationModifier, preparedMesh,
    systemWorld, systemWorldInverse: systemWorld.clone().invert(), lastElapsed: 0,
    randomState: (seed >>> 0) || 0xA341316C, capDiagnosticAdded: false,
  };
}

function randomFor(runtime: RuntimeSystem): () => number {
  return () => {
    let x = runtime.randomState | 0;
    x ^= x << 13; x ^= x >>> 17; x ^= x << 5;
    runtime.randomState = x | 0;
    return (x >>> 0) / 0x100000000;
  };
}

function spawnParticle(
  runtime: RuntimeSystem,
  birthTime: number,
  pathRuntime: Pick<EffectPathRuntime, 'deltaForNode'> | undefined,
  groupWorld: THREE.Matrix4,
  effectFramesPerSecond: number,
): Particle | null {
  const random = randomFor(runtime);
  const parameters = runtime.emitterModifier.parametersRaw ?? {};
  const kind = runtime.emitter.kind;
  const lifespan = Math.max(1e-4, variation(parameters.life_spanRaw, parameters.life_span_variationRaw, random, 1));
  const radius = Math.max(0, variation(parameters.initial_radiusRaw, parameters.radius_variationRaw, random, 0.05));
  const speed = Math.max(0, variation(parameters.speedRaw, parameters.speed_variationRaw, random, 0));
  const initialColor = rgba(parameters.initial_colorRaw, [1, 1, 1, 1]);
  let position: THREE.Vector3;
  let velocity = new THREE.Vector3();
  let diameter = radius * 2;

  if (kind === 'NiPSysMeshEmitter') {
    if (!runtime.preparedMesh) return null;
    const surfacePoint = pointOnSurface(runtime.preparedMesh, random);
    const originalMeshWorld = matrixRows(runtime.preparedMesh.mesh.sourceMatrixWorldRows);
    const meshDelta = pathRuntime?.deltaForNode(runtime.preparedMesh.mesh.blockIndex, birthTime) ?? new THREE.Matrix4();
    const meshWorld = meshDelta.multiply(originalMeshWorld);
    position = surfacePoint.position.applyMatrix4(meshWorld);
    const normalMatrix = new THREE.Matrix3().getNormalMatrix(meshWorld);
    const normal = surfacePoint.normal.applyMatrix3(normalMatrix).normalize();
    velocity.copy(normal).multiplyScalar(speed);
    if (runtime.source.worldSpaceRaw === 1) {
      const sourceToScene = groupWorld.clone().multiply(nifToWeb);
      position.applyMatrix4(sourceToScene);
      velocity.applyMatrix3(new THREE.Matrix3().setFromMatrix4(sourceToScene));
      diameter *= sourceToScene.getMaxScaleOnAxis();
    } else {
      position.applyMatrix4(runtime.systemWorldInverse);
      velocity.applyMatrix3(new THREE.Matrix3().setFromMatrix4(runtime.systemWorldInverse));
    }
    const declinationVariation = parameters.declination_variationRaw;
    const planarVariation = parameters.planar_angle_variationRaw;
    if ((isFiniteNumber(declinationVariation) && declinationVariation !== 0) ||
        (isFiniteNumber(planarVariation) && planarVariation !== 0)) {
      // Origin stores these angular ranges, but the client-specific cone distribution is not verified.
      // Keep the authored normal direction and report the omitted variation below.
    }
  } else if (kind === 'NiPSysBoxEmitter') {
    const objectRows = runtime.emitter.emitterObjectSourceWorldRows;
    if (!objectRows || !isFiniteNumber(runtime.emitter.emitterObjectBlockIndex)) return null;
    const emitterWorld = (pathRuntime?.deltaForNode(runtime.emitter.emitterObjectBlockIndex, effectEmitterTime(birthTime, effectFramesPerSecond)) ?? new THREE.Matrix4())
      .multiply(matrixRows(objectRows));
    const width = isFiniteNumber(parameters.widthRaw) ? parameters.widthRaw : 0;
    const height = isFiniteNumber(parameters.heightRaw) ? parameters.heightRaw : 0;
    const depth = isFiniteNumber(parameters.depthRaw) ? parameters.depthRaw : 0;
    const boxPosition = new THREE.Vector3(
      (random() - 0.5) * width,
      (random() - 0.5) * height,
      (random() - 0.5) * depth,
    ).applyMatrix4(emitterWorld);
    position = runtime.source.worldSpaceRaw === 0
      ? boxPosition.applyMatrix4(runtime.systemWorldInverse)
      : boxPosition;
  } else {
    return null;
  }

  const rotationParameters = runtime.rotationModifier?.parametersRaw ?? {};
  const rotationAngle = variation(rotationParameters.rotation_angleRaw, rotationParameters.rotation_angle_variationRaw, random, 0);
  let rotationSpeed = variation(rotationParameters.rotation_speedRaw, rotationParameters.rotation_speed_variationRaw, random, 0);
  if (rotationParameters.random_rot_speed_signRaw === 1 && random() < 0.5) rotationSpeed *= -1;
  return {
    birthTime, lifeSpan: lifespan, startPosition: position, velocity,
    diameter,
    initialColor,
    rotationAngle,
    rotationSpeed,
  };
}

function liveColor(runtime: RuntimeSystem, particle: Particle, age: number): [number, number, number, number] {
  const ageRatio = THREE.MathUtils.clamp(age / particle.lifeSpan, 0, 1);
  const keyValue = runtime.colorKeys ? sampleRuntimeKeys(runtime.colorKeys, ageRatio) : null;
  const modifierColor = keyValue && keyValue.length >= 4 ? keyValue : [1, 1, 1, 1];
  return [
    particle.initialColor[0] * modifierColor[0],
    particle.initialColor[1] * modifierColor[1],
    particle.initialColor[2] * modifierColor[2],
    particle.initialColor[3] * modifierColor[3],
  ];
}

function growFadeScale(runtime: RuntimeSystem, particle: Particle, age: number): number {
  const grow = runtime.growTime > 0 ? THREE.MathUtils.clamp(age / runtime.growTime, 0, 1) : 1;
  const fade = runtime.fadeTime > 0 ? THREE.MathUtils.clamp((particle.lifeSpan - age) / runtime.fadeTime, 0, 1) : 1;
  return Math.max(0, runtime.baseScale * grow * fade);
}

function updateParticles(runtime: RuntimeSystem, elapsed: number): void {
  const positions = runtime.attributes.position.array as Float32Array;
  const colors = runtime.attributes.color.array as Float32Array;
  const diameters = runtime.attributes.diameter.array as Float32Array;
  const rotations = runtime.attributes.rotation.array as Float32Array;
  let count = 0;
  for (let index = runtime.particles.length - 1; index >= 0; index--) {
    const particle = runtime.particles[index];
    const age = elapsed - particle.birthTime;
    if (age < 0 || age >= particle.lifeSpan) {
      runtime.particles.splice(index, 1);
      continue;
    }
  }
  for (const particle of runtime.particles) {
    if (count >= runtime.capacity) break;
    const age = Math.max(0, elapsed - particle.birthTime);
    const position = particle.startPosition.clone().addScaledVector(particle.velocity, age);
    const offset = count * 3;
    positions[offset] = position.x; positions[offset + 1] = position.y; positions[offset + 2] = position.z;
    const color = liveColor(runtime, particle, age);
    const colorOffset = count * 4;
    colors[colorOffset] = color[0]; colors[colorOffset + 1] = color[1]; colors[colorOffset + 2] = color[2]; colors[colorOffset + 3] = color[3];
    diameters[count] = particle.diameter * growFadeScale(runtime, particle, age);
    rotations[count] = particle.rotationAngle + particle.rotationSpeed * age;
    count++;
  }
  runtime.geometry.setDrawRange(0, count);
  runtime.attributes.position.needsUpdate = true;
  runtime.attributes.color.needsUpdate = true;
  runtime.attributes.diameter.needsUpdate = true;
  runtime.attributes.rotation.needsUpdate = true;
}

function setPointsTransform(
  runtime: RuntimeSystem,
  pathRuntime: Pick<EffectPathRuntime, 'deltaForNode'> | undefined,
  elapsed: number,
  groupWorldInverse: THREE.Matrix4,
): void {
  if (runtime.source.worldSpaceRaw === 1) {
    runtime.points.matrix.copy(groupWorldInverse);
  } else {
    const delta = pathRuntime?.deltaForNode(runtime.source.blockIndex, elapsed) ?? new THREE.Matrix4();
    runtime.points.matrix.copy(nifToWeb).multiply(delta).multiply(runtime.systemWorld);
  }
  runtime.points.matrixAutoUpdate = false;
  runtime.points.matrixWorldNeedsUpdate = true;
}

/**
 * Simulates audited weapon and X6AN6 accessory sources with their exact hashes.
 * This is a source-backed preview subset, not an Origin engine reproduction.
 */
export async function createEffectParticleRuntime(
  source: OriginParticleRuntimeSource,
  pathRuntime: Pick<EffectPathRuntime, 'deltaForNode'> | undefined,
  group: THREE.Group,
  buildMaterial: OriginParticleMaterialBuilder,
  effectFramesPerSecond = effectPresentation.effectFramesPerSecond,
): Promise<OriginEffectParticleRuntime> {
  const diagnostics: string[] = [];
  const systems: RuntimeSystem[] = [];
  const supportedSystemBlocks: number[] = [];
  const auditedSource = PARTICLE_REFERENCE_SOURCES.get(source.sourcePath.toLowerCase());
  if (!auditedSource || source.sha256.toLowerCase() !== auditedSource.sha256) {
    diagnostics.push(`PARTICLE_RUNTIME_SOURCE_UNSUPPORTED:path=${source.sourcePath}:sha256=${source.sha256}`);
    return { diagnostics, supportedSystemBlocks, get liveParticleCount() { return 0; }, tick() {}, dispose() {} };
  }
  diagnostics.push('PREVIEW_SUBSET_ONLY:sourceParticles=true:engineParityValidated=false');
  const meshSources = new Map((source.emitterMeshSources ?? []).map(mesh => [mesh.blockIndex, mesh]));
  const sourceSystems = source.rawParticleSystems ?? [];
  for (const system of sourceSystems) {
    const expectedName = auditedSource.blocks.get(system.blockIndex);
    if (!expectedName || system.type !== 'NiParticleSystem' || system.name !== expectedName) {
      diagnostics.push(`PARTICLE_SYSTEM_UNSUPPORTED:system=${system.blockIndex}:${system.type}:${system.name}`);
      continue;
    }
    if (!supportedConfiguration(system, diagnostics)) continue;
    if (system.worldSpaceRaw !== 0 && system.worldSpaceRaw !== 1) {
      diagnostics.push(`PARTICLE_WORLD_SPACE_UNSUPPORTED:system=${system.blockIndex}:raw=${system.worldSpaceRaw}`);
      continue;
    }
    const controller = system.controllers?.find(candidate => candidate.type === 'NiPSysEmitterCtlr');
    const runtimeController = createRuntimeController(system);
    const visibility = keyData(controller?.visibilityInterpolator?.data);
    const windows = controller && runtimeController ? buildVisibleWindows(system, controller, diagnostics) : null;
    const emitter = system.emitterSources?.find(candidate => candidate.modifierBlockIndex === controller?.modifierBlockIndex);
    const emitterModifier = system.modifiers?.find(modifier => modifier.blockIndex === controller?.modifierBlockIndex);
    const rate = controller?.emissionRateInterpolator?.valueRaw;
    if (!controller || !runtimeController || !visibility || !windows || !emitter || !emitterModifier || !isFiniteNumber(rate) || rate < 0) {
      diagnostics.push(`PARTICLE_SYSTEM_SOURCE_INCOMPLETE:system=${system.blockIndex}`);
      continue;
    }
    if ((runtimeController.flags.cycleType?.rawValue ?? -1) !== 0 || runtimeController.flags.playBackwards) {
      diagnostics.push(`PARTICLE_CONTROLLER_NOT_SUPPORTED:system=${system.blockIndex}`);
      continue;
    }
    const vertexArrayCount = system.dataRaw?.vertexArrayCount;
    const capacity = Math.max(1, Math.min(MAX_RUNTIME_PARTICLES,
      isFiniteNumber(vertexArrayCount) && vertexArrayCount > 0 ? Math.floor(vertexArrayCount) : MAX_RUNTIME_PARTICLES));
    if (system.dataRaw?.maxParticlesFieldPresent !== true) {
      diagnostics.push(`PREVIEW_CAP_FROM_SOURCE_VERTEX_ARRAY:system=${system.blockIndex}:count=${capacity}:field=maxParticles:present=false:engineParity=false`);
    }
    let preparedMesh: PreparedMesh | undefined;
    if (emitter.kind === 'NiPSysMeshEmitter') {
      const mesh = meshSources.get(emitter.emitterMeshBlockIndex ?? -1);
      if (!mesh) {
        diagnostics.push(`MESH_EMITTER_SOURCE_MISSING:system=${system.blockIndex}:mesh=${emitter.emitterMeshBlockIndex ?? '?'}`);
        continue;
      }
      const prepared = buildSurface(mesh, diagnostics, system.blockIndex);
      if (!prepared) continue;
      preparedMesh = prepared;
      const parameters = emitterModifier.parametersRaw ?? {};
      if ((parameters.emission_typeRaw as RawEnum | undefined)?.rawValue !== 3 ||
          (parameters.initial_velocity_typeRaw as RawEnum | undefined)?.rawValue !== 0) {
        diagnostics.push(`MESH_EMITTER_MODE_UNSUPPORTED:system=${system.blockIndex}`);
        continue;
      }
      if ((isFiniteNumber(parameters.declination_variationRaw) && parameters.declination_variationRaw !== 0) ||
          (isFiniteNumber(parameters.planar_angle_variationRaw) && parameters.planar_angle_variationRaw !== 0)) {
        diagnostics.push(`MESH_EMITTER_DIRECTION_VARIATION_OMITTED:system=${system.blockIndex}:declinationRaw=${String(parameters.declination_variationRaw)}:planarAngleRaw=${String(parameters.planar_angle_variationRaw)}:engineParity=false`);
      }
    } else if (emitter.kind !== 'NiPSysBoxEmitter') {
      diagnostics.push(`EMITTER_KIND_UNSUPPORTED:system=${system.blockIndex}:kind=${String(emitter.kind)}`);
      continue;
    }

    const material = await buildMaterial(system);
    if (!material) {
      diagnostics.push(`PARTICLE_SOURCE_MATERIAL_UNAVAILABLE:system=${system.blockIndex}`);
      continue;
    }
    makePointMaterial(material);
    const geometry = new THREE.BufferGeometry();
    const position = new THREE.BufferAttribute(new Float32Array(capacity * 3), 3);
    const color = new THREE.BufferAttribute(new Float32Array(capacity * 4), 4);
    const diameter = new THREE.BufferAttribute(new Float32Array(capacity), 1);
    const rotation = new THREE.BufferAttribute(new Float32Array(capacity), 1);
    geometry.setAttribute('position', position);
    geometry.setAttribute('color', color);
    geometry.setAttribute('particleDiameter', diameter);
    geometry.setAttribute('particleRotation', rotation);
    geometry.setDrawRange(0, 0);
    const points = new THREE.Points(geometry, material);
    points.name = `OriginParticleSystem-${system.blockIndex}-${system.name}`;
    points.frustumCulled = false;
    points.matrixAutoUpdate = false;
    points.userData.originParticleSource = {
      blockIndex: system.blockIndex,
      controllerBlockIndex: controller.controllerBlockIndex,
      emitterModifierBlockIndex: emitter.modifierBlockIndex,
      sourcePath: source.sourcePath,
      sourceSha256: source.sha256,
      previewOnly: true,
    };
    const runtime = makeSystemRuntime(system, controller, runtimeController, visibility, windows, rate, emitter, emitterModifier,
      points, capacity, preparedMesh, 0x9e3779b9 ^ system.blockIndex);
    if (!runtime) {
      geometry.dispose(); material.dispose();
      diagnostics.push(`PARTICLE_SYSTEM_TRANSFORM_INVALID:system=${system.blockIndex}`);
      continue;
    }
    runtime.growTime = sourceProperties(system).growTime;
    runtime.fadeTime = sourceProperties(system).fadeTime;
    runtime.baseScale = sourceProperties(system).baseScale;
    if (runtime.rotationModifier) {
      diagnostics.push(`ROTATION_MODIFIER_SCREEN_SPACE_PREVIEW:system=${system.blockIndex}:modifier=${runtime.rotationModifier.blockIndex ?? '?'}:engineParity=false`);
    }
    if (system.worldSpaceRaw === 1) diagnostics.push(`PARTICLE_WORLD_SPACE_BIRTHS_APPLIED_TO_SCENE_WORLD:system=${system.blockIndex}:engineParity=false`);
    group.add(points);
    systems.push(runtime);
    supportedSystemBlocks.push(system.blockIndex);
  }

  let disposed = false;
  function tick(delta: number, elapsed: number, camera: THREE.Camera, viewportHeight: number): void {
    if (disposed || !isFiniteNumber(elapsed) || !isFiniteNumber(delta) || delta < 0) return;
    const safeHeight = isFiniteNumber(viewportHeight) && viewportHeight > 0 ? viewportHeight : 1;
    if (!(isFiniteNumber(viewportHeight) && viewportHeight > 0) && !diagnostics.includes('VIEWPORT_HEIGHT_INVALID')) {
      diagnostics.push('VIEWPORT_HEIGHT_INVALID:pointSpritesUseMinimumPixelSize');
    }
    const orthographic = camera instanceof THREE.OrthographicCamera ? 1 : 0;
    group.updateWorldMatrix(true, false);
    const groupWorld = group.matrixWorld.clone();
    const groupDeterminant = groupWorld.determinant();
    const groupWorldInverse = Number.isFinite(groupDeterminant) && Math.abs(groupDeterminant) > 1e-12
      ? groupWorld.clone().invert() : new THREE.Matrix4();
    if ((!Number.isFinite(groupDeterminant) || Math.abs(groupDeterminant) <= 1e-12) &&
        !diagnostics.includes('GROUP_WORLD_MATRIX_SINGULAR')) {
      diagnostics.push('GROUP_WORLD_MATRIX_SINGULAR');
    }
    for (const runtime of systems) {
      const uniforms = runtime.material.uniforms as Record<string, THREE.IUniform<unknown>>;
      if (uniforms.viewportHeight) uniforms.viewportHeight.value = safeHeight;
      if (uniforms.orthographicCamera) uniforms.orthographicCamera.value = orthographic;
      runtime.material.uniformsNeedUpdate = true;
      if (elapsed + 1e-8 < runtime.lastElapsed) {
        runtime.particles = [];
        runtime.lastElapsed = 0;
        diagnostics.push(`PARTICLE_TIMELINE_RESET:system=${runtime.source.blockIndex}`);
      }
      const maxLife = runtime.emitterModifier.parametersRaw?.life_spanRaw;
      const lifeVariation = runtime.emitterModifier.parametersRaw?.life_span_variationRaw;
      const retention = Math.max(0.1, (isFiniteNumber(maxLife) ? maxLife : 1) + (isFiniteNumber(lifeVariation) ? lifeVariation : 0));
      const from = Math.max(runtime.lastElapsed, elapsed - retention - 0.05);
      const births = eventTimes(runtime.runtimeController, runtime.windows, runtime.rate, from, elapsed, diagnostics, runtime.source.blockIndex);
      for (const birthTime of births) {
        const particle = spawnParticle(runtime, birthTime, pathRuntime, groupWorld, effectFramesPerSecond);
        if (!particle) continue;
        runtime.particles.push(particle);
        if (runtime.particles.length > runtime.capacity) {
          runtime.particles.shift();
          if (!runtime.capDiagnosticAdded) {
            diagnostics.push(`PREVIEW_PARTICLE_CAP_REACHED:system=${runtime.source.blockIndex}:count=${runtime.capacity}:engineParity=false`);
            runtime.capDiagnosticAdded = true;
          }
        }
      }
      runtime.lastElapsed = elapsed;
      setPointsTransform(runtime, pathRuntime, elapsed, groupWorldInverse);
      updateParticles(runtime, elapsed);
    }
  }

  function dispose(): void {
    if (disposed) return;
    disposed = true;
    for (const runtime of systems) {
      runtime.points.removeFromParent();
      runtime.geometry.dispose();
      runtime.material.dispose();
      runtime.particles.length = 0;
    }
  }

  return {
    diagnostics,
    supportedSystemBlocks,
    get liveParticleCount() { return systems.reduce((sum, runtime) => sum + runtime.particles.length, 0); },
    tick,
    dispose,
  };
}
