var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { AxesHelper, BackSide, FrontSide, LinearSRGBColorSpace, Material, Mesh, MeshBasicMaterial, NormalBlending, Object3D, PlaneGeometry, Quaternion, Vector3 } from "three";
import { BatchedParticleRenderer, ConstantColor, ConstantValue, ParticleSystem as _ParticleSystem, RenderMode, TrailParticle, Vector4 as QVector4 } from "three.quarks";
import { isDevEnvironment, showBalloonWarning } from "../../engine/debug/index.js";
import { Mathf } from "../../engine/engine_math.js";
// https://github.dev/creativelifeform/three-nebula
// import System, { Emitter, Position, Life, SpriteRenderer, Particle, Body, MeshRenderer, } from 'three-nebula.js';
import { serializable } from "../../engine/engine_serialization.js";
import { assign } from "../../engine/engine_serialization_core.js";
import { createFlatTexture } from "../../engine/engine_shaders.js";
import { getTempVector, getWorldPosition, getWorldQuaternion, getWorldScale } from "../../engine/engine_three_utils.js";
import { getParam } from "../../engine/engine_utils.js";
import { NEEDLE_progressive } from "../../engine/extensions/NEEDLE_progressive.js";
import { RGBAColor } from "../../engine/js-extensions/index.js";
import { Behaviour, GameObject } from "../Component.js";
import { ColorBySpeedModule, ColorOverLifetimeModule, EmissionModule, InheritVelocityModule, LimitVelocityOverLifetimeModule, MainModule, NoiseModule, ParticleBurst, ParticleSystemRenderMode, ParticleSystemScalingMode, ParticleSystemSimulationSpace, RotationBySpeedModule, RotationOverLifetimeModule, ShapeModule, SizeBySpeedModule, SizeOverLifetimeModule, TextureSheetAnimationModule, TrailModule, VelocityOverLifetimeModule } from "./ParticleSystemModules.js";
import { ParticleSubEmitter } from "./ParticleSystemSubEmitter.js";
const debug = getParam("debugparticles");
const suppressProgressiveLoading = getParam("noprogressive");
const debugProgressiveLoading = getParam("debugprogressive");
/**
 * Defines when a sub-emitter spawns particles relative to the parent particle's lifecycle.
 * Used to create complex effects like explosions on impact or trails following particles.
 */
export var SubEmitterType;
(function (SubEmitterType) {
    /** Sub-emitter triggers when the parent particle is born */
    SubEmitterType[SubEmitterType["Birth"] = 0] = "Birth";
    /** Sub-emitter triggers when the parent particle collides */
    SubEmitterType[SubEmitterType["Collision"] = 1] = "Collision";
    /** Sub-emitter triggers when the parent particle dies */
    SubEmitterType[SubEmitterType["Death"] = 2] = "Death";
    /** Sub-emitter triggers when the parent particle enters a trigger zone */
    SubEmitterType[SubEmitterType["Trigger"] = 3] = "Trigger";
    /** Sub-emitter is triggered manually via code */
    SubEmitterType[SubEmitterType["Manual"] = 4] = "Manual";
})(SubEmitterType || (SubEmitterType = {}));
/** @internal */
export class ParticleSystemRenderer extends Behaviour {
    renderMode;
    particleMaterial;
    trailMaterial;
    // @serializable(Mesh)
    particleMesh;
    maxParticleSize;
    minParticleSize;
    velocityScale;
    cameraVelocityScale;
    lengthScale;
    start() {
        if (this.maxParticleSize !== .5 && this.minParticleSize !== 0) {
            if (isDevEnvironment()) {
                const msg = `ParticleSystem \"${this.name}\" has non-default min/max particle size. This may not render correctly. Please set min size to 0 and the max size to 0.5 and use the \"StartSize\" setting instead`;
                console.warn(msg);
                // showBalloonWarning(msg);
            }
        }
    }
    get transparent() {
        const res = this.particleMaterial?.transparent ?? false;
        // console.log(res, this.particleMaterial);
        return res;
    }
    getMaterial(trailEnabled = false) {
        let material = (trailEnabled === true && this.trailMaterial) ? this.trailMaterial : this.particleMaterial;
        if (material) {
            if (material.type === "MeshStandardMaterial") {
                if (debug)
                    console.debug("ParticleSystemRenderer.getMaterial: MeshStandardMaterial detected, converting to MeshBasicMaterial. See https://github.com/Alchemist0823/three.quarks/issues/101");
                if ("map" in material && material.map) {
                    material.map.colorSpace = LinearSRGBColorSpace;
                    material.map.premultiplyAlpha = false;
                }
                const newMaterial = new MeshBasicMaterial();
                newMaterial.copy(material);
                if (trailEnabled)
                    this.trailMaterial = newMaterial;
                else
                    this.particleMaterial = newMaterial;
            }
            if (material.map) {
                material.map.colorSpace = LinearSRGBColorSpace;
                material.map.premultiplyAlpha = false;
            }
            if (trailEnabled) {
                // the particle material for trails must be DoubleSide or BackSide (since otherwise the trail is invisible)
                if (material.side === FrontSide) {
                    // don't modify the assigned material
                    material = material.clone();
                    material.side = BackSide;
                    if (trailEnabled)
                        this.trailMaterial = material;
                    else
                        this.particleMaterial = material;
                }
            }
        }
        // progressive load on start
        // TODO: figure out how to do this before particle system rendering so we only load textures for visible materials
        if (material && !suppressProgressiveLoading && material["_didRequestTextureLOD"] === undefined) {
            material["_didRequestTextureLOD"] = 0;
            if (debugProgressiveLoading) {
                console.log("Load material LOD", material.name);
            }
            NEEDLE_progressive.assignTextureLOD(material, 0);
        }
        return material;
    }
    getMesh(_renderMode) {
        let geo = null;
        if (!geo) {
            if (this.particleMesh instanceof Mesh) {
                geo = this.particleMesh.geometry;
            }
            if (geo === null) {
                geo = new PlaneGeometry(1, 1);
                // Flip UVs horizontally
                const uv = geo.attributes.uv;
                for (let i = 0; i < uv.count; i++) {
                    uv.setX(i, 1 - uv.getX(i));
                }
            }
        }
        const res = new Mesh(geo, this.getMaterial());
        return res;
    }
}
__decorate([
    serializable()
], ParticleSystemRenderer.prototype, "renderMode", void 0);
__decorate([
    serializable(Material)
], ParticleSystemRenderer.prototype, "particleMaterial", void 0);
__decorate([
    serializable(Material)
], ParticleSystemRenderer.prototype, "trailMaterial", void 0);
__decorate([
    serializable()
], ParticleSystemRenderer.prototype, "maxParticleSize", void 0);
__decorate([
    serializable()
], ParticleSystemRenderer.prototype, "minParticleSize", void 0);
__decorate([
    serializable()
], ParticleSystemRenderer.prototype, "velocityScale", void 0);
__decorate([
    serializable()
], ParticleSystemRenderer.prototype, "cameraVelocityScale", void 0);
__decorate([
    serializable()
], ParticleSystemRenderer.prototype, "lengthScale", void 0);
class MinMaxCurveFunction {
    _curve;
    _factor;
    constructor(curve, factor = 1) {
        this._curve = curve;
        this._factor = factor;
    }
    type = "function";
    startGen(_memory) {
        // ...
    }
    genValue(_memory, t) {
        return this._curve.evaluate(t, Math.random()) * this._factor;
    }
    toJSON() {
        throw new Error("Method not implemented.");
    }
    clone() {
        throw new Error("Method not implemented.");
    }
}
class MinMaxGradientFunction {
    _curve;
    constructor(curve) { this._curve = curve; }
    type = "function";
    startGen(_memory) {
        throw new Error("Method not implemented.");
    }
    genColor(_memory, color, t) {
        const col = this._curve.evaluate(t, Math.random());
        // TODO: incoming color should probably be blended?
        color.set(col.r, col.g, col.b, "alpha" in col ? col.alpha : 1);
        return color;
    }
    toJSON() {
        throw new Error("Method not implemented.");
    }
    clone() {
        throw new Error("Method not implemented.");
    }
}
class BaseValueGenerator {
    type = "value";
    toJSON() {
        throw new Error("Method not implemented.");
    }
    clone() {
        throw new Error("Method not implemented.");
    }
    startGen(_memory) { }
    system;
    constructor(system) {
        this.system = system;
    }
}
class TextureSheetStartFrameGenerator extends BaseValueGenerator {
    genValue() {
        return this.system.textureSheetAnimation.getStartIndex();
    }
}
class ParticleSystemEmissionOverTime extends BaseValueGenerator {
    _lastPosition = new Vector3();
    _lastDistance = 0;
    update() {
        const currentPosition = getWorldPosition(this.system.gameObject);
        this._lastDistance = this._lastPosition.distanceTo(currentPosition);
        this._lastPosition.copy(currentPosition);
    }
    genValue() {
        if (!this.system.isPlaying)
            return 0;
        if (!this.system.emission.enabled)
            return 0;
        if (this.system.currentParticles >= this.system.maxParticles)
            return 0;
        // emission over time
        let emission = this.system.emission.rateOverTime.evaluate(this.system.time / this.system.duration, Math.random());
        // if(this.system.currentParticles + emission > this.system.maxParticles) 
        //     emission = (this.system.maxParticles - this.system.currentParticles);
        // const res = Mathf.clamp(emission, 0, this.system.maxParticles - this.system.currentParticles);
        if (this.system.deltaTime > 0) {
            const distanceEmission = this.system.emission.rateOverDistance.evaluate(this.system.time / this.system.duration, Math.random());
            const meterPerSecond = this._lastDistance / this.system.deltaTime;
            let distanceEmissionValue = meterPerSecond * distanceEmission;
            if (!Number.isFinite(distanceEmissionValue))
                distanceEmissionValue = 0;
            emission += distanceEmissionValue;
        }
        const burst = this.system.emission.getBurst();
        if (burst > 0)
            emission += burst / this.system.deltaTime;
        const maxEmission = (this.system.maxParticles - this.system.currentParticles);
        return Mathf.clamp(emission, 0, maxEmission / this.system.deltaTime);
    }
}
class ParticleSystemEmissionOverDistance extends BaseValueGenerator {
    genValue() {
        if (!this.system.isPlaying)
            return 0;
        // this seems not be called yet
        return 0;
        // if (this.system.currentParticles >= this.system.maxParticles) return 0;
        // const emission = this.system.emission.rateOverDistance.evaluate(this.system.time / this.system.duration, Math.random());
        // return emission;
    }
}
/**
 * Base class for custom particle behaviors. Extend this to create custom particle logic.
 *
 * Override `initialize()` to set up per-particle state when particles spawn.
 * Override `update()` to modify particles each frame (position, velocity, color, size, etc.).
 * Override `frameUpdate()` for logic that runs once per frame (not per particle).
 *
 * @example Custom wind effect
 * ```ts
 * class WindBehaviour extends ParticleSystemBaseBehaviour {
 *   windStrength = 2;
 *   windDirection = new Vector3(1, 0, 0);
 *
 *   update(particle: Particle, delta: number) {
 *     particle.velocity.addScaledVector(this.windDirection, this.windStrength * delta);
 *   }
 * }
 * ```
 *
 * @see {@link ParticleSystem.addBehaviour} to register your custom behavior
 * @link https://github.com/Alchemist0823/three.quarks
 */
export class ParticleSystemBaseBehaviour {
    /** Reference to the particle system this behavior belongs to */
    system;
    /** Access to the engine context for timing, input, etc. */
    get context() { return this.system.context; }
    constructor(ps) {
        this.type = Object.getPrototypeOf(this).constructor.name || "ParticleSystemBaseBehaviour";
        if (ps)
            this.system = ps;
    }
    /** Behavior type identifier used by three.quarks */
    type;
    /** Called once when a particle is spawned. Use to initialize per-particle state. */
    initialize(_particle) { }
    /** Called every frame for each active particle. Use to update particle properties. */
    update(_particle, _delta) { }
    /** Called once per frame before particle updates. Use for shared calculations. */
    frameUpdate(_delta) { }
    toJSON() { throw new Error("Method not implemented."); }
    clone() { throw new Error("Method not implemented."); }
    /** Called when the particle system is reset. */
    reset() { }
}
const $startFrame = Symbol("startFrame");
class TextureSheetAnimationBehaviour extends ParticleSystemBaseBehaviour {
    type = "NeedleTextureSheet";
    // initialize(_particle: Particle): void {
    //     _particle[$startFrame] = this.system.textureSheetAnimation.getStartIndex();
    // }
    update(particle, _delta) {
        const sheet = this.system.textureSheetAnimation;
        if (sheet.enabled) {
            const t01 = particle.age / particle.life;
            const index = sheet.evaluate(t01);
            if (index !== undefined)
                particle.uvTile = index;
        }
    }
}
const $particleRotation = Symbol("particleRotation");
class RotationBehaviour extends ParticleSystemBaseBehaviour {
    type = "NeedleRotation";
    initialize(particle) {
        particle[$particleRotation] = Math.random();
    }
    update(particle, delta) {
        if (particle.rotation === undefined)
            return;
        const t = particle.age / particle.life;
        if (typeof particle.rotation === "number") {
            if (this.system.rotationOverLifetime.enabled) {
                particle.rotation += this.system.rotationOverLifetime.evaluate(t, particle[$particleRotation]) * delta;
            }
            else {
                if (this.system.renderer.renderMode === ParticleSystemRenderMode.Billboard)
                    particle.rotation = Math.PI;
            }
            if (this.system.rotationBySpeed.enabled) {
                const speed = particle.velocity.length();
                particle.rotation += this.system.rotationBySpeed.evaluate(t, speed) * delta;
            }
        }
        else {
            // const quat = particle.rotation as Quaternion;
            // TODO: implement rotation by speed for quaternions
        }
    }
}
const $sizeLerpFactor = Symbol("sizeLerpFactor");
const localScaleVec3 = new Vector3();
class SizeBehaviour extends ParticleSystemBaseBehaviour {
    type = "NeedleSize";
    _minSize = 0;
    _maxSize = 1;
    initialize(particle) {
        particle[$sizeLerpFactor] = Math.random();
        this._minSize = this.system.renderer.minParticleSize;
        this._maxSize = this.system.renderer.maxParticleSize;
    }
    update(particle, _delta) {
        const age01 = particle.age / particle.life;
        let size = 1;
        if (this.system.sizeOverLifetime.enabled)
            size *= this.system.sizeOverLifetime.evaluate(age01, undefined, particle[$sizeLerpFactor]).x;
        let scaleFactor = 1;
        if (this.system.renderer.renderMode !== ParticleSystemRenderMode.Mesh)
            scaleFactor = this.system.worldScale.x / this.system.cameraScale;
        const newSize = getTempVector(particle.startSize).multiplyScalar(size * scaleFactor);
        particle.size.set(newSize.x, newSize.y, newSize.z);
        if (this.system.localspace) {
            const scale = getLocalSimulationScale(this.system, localScaleVec3);
            particle.size.x *= scale.x;
            particle.size.y *= scale.y;
            particle.size.z *= scale.z;
        }
        // in Unity this is viewport size, we don't really support this yet (and the renderer is logging a warning)
        // so for now it's disabled again
        // particle.size = Mathf.clamp(particle.size, this._minSize, this._maxSize);
    }
}
export const $particleLife = Symbol("particleLife");
const $trailLifetime = Symbol("trailLifetime");
const $trailStartLength = Symbol("trailStartLength");
const $trailWidthRandom = Symbol("trailWidthRandom");
class TrailBehaviour extends ParticleSystemBaseBehaviour {
    type = "NeedleTrail";
    initialize(particle) {
        if (particle instanceof TrailParticle) {
            particle[$particleLife] = particle.life;
            if (this.system.trails.enabled && this.system.trails.dieWithParticles === false) {
                particle[$trailLifetime] = this.system.trails.lifetime.evaluate(Math.random(), Math.random());
                particle.life += particle[$trailLifetime];
            }
            particle[$trailStartLength] = particle.length;
            particle[$trailWidthRandom] = Math.random();
        }
    }
    update(particle) {
        if (this.system.trails?.enabled && particle instanceof TrailParticle) {
            const trailParticle = particle;
            const age01 = particle.age / particle[$particleLife];
            const iter = particle.previous.values();
            const length = particle.previous.length;
            // const maxAge = this.system.trails.lifetime.
            for (let i = 0; i < length; i++) {
                const cur = iter.next();
                const state = cur.value;
                const pos01 = 1 - (i / (length - 1));
                const size = particle.size;
                if (size.x <= 0 && !this.system.trails.sizeAffectsWidth) {
                    // Not sure where we get to 100* from, tested in SOC trong com
                    const newSize = 20 * this.system.trails.widthOverTrail.evaluate(.5, trailParticle[$trailWidthRandom]);
                    size.x = newSize;
                    size.y = newSize;
                    size.z = newSize;
                }
                state.size = this.system.trails.getWidth(size.x, age01, pos01, trailParticle[$trailWidthRandom]);
                state.color.copy(particle.color);
                this.system.trails.getColor(state.color, age01, pos01);
            }
            // particle.life = particle.age + .1;
            if (particle.age > particle[$particleLife]) {
                particle.velocity.set(0, 0, 0);
                const t = (particle.age - particle[$particleLife]) / particle[$trailLifetime];
                trailParticle.length = Mathf.lerp(particle[$trailStartLength], 0, t);
            }
        }
    }
}
const $startVelocity = Symbol("startVelocity");
const $gravityFactor = Symbol("gravityModifier");
const $gravitySpeed = Symbol("gravitySpeed");
const $velocityLerpFactor = Symbol("velocity lerp factor");
const temp3 = new Vector3();
const temp4 = new Quaternion();
class VelocityBehaviour extends ParticleSystemBaseBehaviour {
    type = "NeedleVelocity";
    _gravityDirection = new Vector3();
    initialize(particle) {
        const simulationSpeed = this.system.main.simulationSpeed;
        particle.startSpeed = this.system.main.startSpeed.evaluate(Math.random(), Math.random());
        const dir = this.system.shape.getDirection(particle, particle.position);
        particle.velocity.x = dir.x * particle.startSpeed;
        particle.velocity.y = dir.y * particle.startSpeed;
        particle.velocity.z = dir.z * particle.startSpeed;
        if (this.system.inheritVelocity?.enabled) {
            this.system.inheritVelocity.applyInitial(particle.velocity);
        }
        if (!particle[$startVelocity])
            particle[$startVelocity] = particle.velocity.clone();
        else
            particle[$startVelocity].copy(particle.velocity);
        const gravityFactor = this.system.main.gravityModifier.evaluate(Math.random(), Math.random());
        particle[$gravityFactor] = gravityFactor * simulationSpeed;
        particle[$gravitySpeed] = gravityFactor * simulationSpeed * .5;
        particle[$velocityLerpFactor] = Math.random();
        this.system.velocityOverLifetime?.init(particle);
        this._gravityDirection.set(0, -1, 0);
        if (this.system.main.simulationSpace === ParticleSystemSimulationSpace.Local)
            this._gravityDirection.applyQuaternion(this.system.worldQuaternionInverted).normalize();
    }
    update(particle, delta) {
        //////////////////////
        // calculate speed
        const baseVelocity = particle[$startVelocity];
        const gravityFactor = particle[$gravityFactor];
        if (gravityFactor !== 0) {
            const factor = gravityFactor * particle[$gravitySpeed];
            temp3.copy(this._gravityDirection).multiplyScalar(factor);
            particle[$gravitySpeed] += delta * .05;
            baseVelocity.add(temp3);
        }
        particle.velocity.copy(baseVelocity);
        const t01 = particle.age / particle.life;
        if (this.system.inheritVelocity?.enabled) {
            this.system.inheritVelocity.applyCurrent(particle.velocity, t01, particle[$velocityLerpFactor]);
        }
        const noise = this.system.noise;
        if (noise.enabled) {
            noise.apply(0, particle.position, particle.velocity, delta, particle.age, particle.life);
        }
        //////////////////////
        // evaluate by speed modules
        const sizeBySpeed = this.system.sizeBySpeed;
        if (sizeBySpeed?.enabled) {
            particle.size = sizeBySpeed.evaluate(particle.velocity, t01, particle[$velocityLerpFactor], particle.size);
        }
        const colorBySpeed = this.system.colorBySpeed;
        if (colorBySpeed?.enabled) {
            colorBySpeed.evaluate(particle.velocity, particle[$velocityLerpFactor], particle.color);
        }
        //////////////////////
        // limit or modify speed
        const velocity = this.system.velocityOverLifetime;
        if (velocity.enabled) {
            velocity.apply(particle, 0, particle.position, particle.velocity, delta, particle.age, particle.life);
        }
        const limitVelocityOverLifetime = this.system.limitVelocityOverLifetime;
        if (limitVelocityOverLifetime.enabled) {
            // const factor = this.system.worldScale.x;
            limitVelocityOverLifetime.apply(particle.position, baseVelocity, particle.velocity, particle.size, t01, delta, 1);
        }
        if (this.system.worldspace) {
            const ws = this.system.worldScale;
            particle.velocity.x *= ws.x;
            particle.velocity.y *= ws.y;
            particle.velocity.z *= ws.z;
        }
    }
}
const $colorLerpFactor = Symbol("colorLerpFactor");
const tempColor = new RGBAColor(1, 1, 1, 1);
const col = new RGBAColor(1, 1, 1, 1);
class ColorBehaviour extends ParticleSystemBaseBehaviour {
    type = "NeedleColor";
    initialize(_particle) {
    }
    _init(particle) {
        const materialColor = this.system.renderer.particleMaterial;
        col.copy(this.system.main.startColor.evaluate(Math.random()));
        if (materialColor?.color) {
            tempColor.copy(materialColor.color);
            col.multiply(tempColor);
        }
        col.convertLinearToSRGB();
        particle.startColor.set(col.r, col.g, col.b, col.alpha);
        particle.color.copy(particle.startColor);
        particle[$colorLerpFactor] = Math.random();
    }
    update(particle, _delta) {
        if (particle.age === 0)
            this._init(particle);
        if (this.system.colorOverLifetime.enabled) {
            const t = particle.age / particle.life;
            const col = this.system.colorOverLifetime.color.evaluate(t, particle[$colorLerpFactor]);
            particle.color.set(col.r, col.g, col.b, "alpha" in col ? col.alpha : 1).multiply(particle.startColor);
        }
        else {
            particle.color.copy(particle.startColor);
        }
    }
}
class ParticleSystemInterface {
    system;
    emission;
    get anim() {
        return this.system.textureSheetAnimation;
    }
    constructor(system) {
        this.system = system;
        this.emission = new ParticleSystemEmissionOverTime(this.system);
    }
    get prewarm() { return false; } // force disable three.quark prewarm, we have our own!
    get material() { return this.system.renderer.getMaterial(this.system.trails.enabled); }
    get layers() { return this.system.gameObject.layers; }
    update() {
        this.emission.update();
    }
    autoDestroy;
    get looping() { return this.system.main.loop; }
    get duration() { return this.system.duration; }
    get shape() { return this.system.shape; }
    get startLife() { return new MinMaxCurveFunction(this.system.main.startLifetime); }
    get startSpeed() { return new MinMaxCurveFunction(this.system.main.startSpeed); }
    get startRotation() { return new MinMaxCurveFunction(this.system.main.startRotation); }
    get startSize() { return new MinMaxCurveFunction(this.system.main.startSize); }
    startLength; /** start length is for trails */
    get startColor() { return new ConstantColor(new QVector4(1, 1, 1, 1)); }
    get emissionOverTime() { return this.emission; }
    /** this is not supported yet */
    get emissionOverDistance() { return new ParticleSystemEmissionOverDistance(this.system); }
    /** not used - burst is controled via emissionOverTime */
    emissionBursts;
    onlyUsedByOther;
    behaviors = [];
    get instancingGeometry() {
        return this.system.renderer.getMesh(this.system.renderer.renderMode).geometry;
    }
    get renderMode() {
        if (this.system.trails["enabled"] === true) {
            return RenderMode.Trail;
        }
        switch (this.system.renderer.renderMode) {
            case ParticleSystemRenderMode.Billboard: return RenderMode.BillBoard;
            case ParticleSystemRenderMode.Stretch: return RenderMode.StretchedBillBoard;
            case ParticleSystemRenderMode.HorizontalBillboard: return RenderMode.HorizontalBillBoard;
            case ParticleSystemRenderMode.VerticalBillboard: return RenderMode.VerticalBillBoard;
            case ParticleSystemRenderMode.Mesh: return RenderMode.Mesh;
        }
        return RenderMode.BillBoard;
    }
    rendererEmitterSettings = {
        startLength: new ConstantValue(220),
        followLocalOrigin: false,
    };
    get speedFactor() {
        let factor = this.system.main.simulationSpeed;
        if (this.system.renderer?.renderMode === ParticleSystemRenderMode.Stretch) {
            factor *= this.system.renderer.velocityScale ?? 1;
        }
        return factor;
    }
    flatWhiteTexture;
    clonedTexture = { original: undefined, clone: undefined };
    get texture() {
        const mat = this.material;
        if (mat && mat["map"]) {
            const original = mat["map"];
            // cache the last original one so we're not creating tons of clones
            if (this.clonedTexture.original !== original || !this.clonedTexture.clone) {
                const tex = original.clone();
                tex.premultiplyAlpha = false;
                tex.colorSpace = LinearSRGBColorSpace;
                this.clonedTexture.original = original;
                this.clonedTexture.clone = tex;
            }
            return this.clonedTexture.clone;
        }
        if (!this.flatWhiteTexture)
            this.flatWhiteTexture = createFlatTexture(new RGBAColor(1, 1, 1, 1), 1);
        return this.flatWhiteTexture;
    }
    get startTileIndex() { return new TextureSheetStartFrameGenerator(this.system); }
    get uTileCount() { return this.anim.enabled ? this.anim?.numTilesX : undefined; }
    get vTileCount() { return this.anim.enabled ? this.anim?.numTilesY : undefined; }
    get renderOrder() { return 1; }
    get blending() { return this.system.renderer.particleMaterial?.blending ?? NormalBlending; }
    get transparent() { return this.system.renderer.transparent; }
    get worldSpace() { return this.system.main.simulationSpace === ParticleSystemSimulationSpace.World; }
}
class ParticlesEmissionState {
    burstParticleIndex = 0;
    burstParticleCount = 0;
    isBursting = false;
    travelDistance = 0;
    previousWorldPos;
    burstIndex = 0;
    burstWaveIndex = 0;
    time = 0;
    waitEmiting = 0;
}
/**
 * ParticleSystem efficiently handles the motion and rendering of many individual particles.
 * Use it for visual effects like fire, smoke, sparks, rain, magic spells, and more.
 *
 * ![](https://cloud.needle.tools/-/media/qz5nO-raa7dNb_XCBNxHmA.gif)
 * ![](https://cloud.needle.tools/-/media/IKOrLhesy1dKTfQQxx_pLA.gif)
 *
 * **Modules:**
 * Configure particle behavior through modules like {@link EmissionModule}, {@link ShapeModule},
 * {@link ColorOverLifetimeModule}, {@link SizeOverLifetimeModule}, {@link VelocityOverLifetimeModule},
 * {@link NoiseModule}, and {@link TrailModule}.
 *
 * **Custom behaviors:**
 * Add custom particle behaviors by extending {@link ParticleSystemBaseBehaviour} and
 * calling `addBehaviour()`. This gives you full control over particle initialization and updates.
 *
 * **Performance:**
 * Particles are batched together for fast, performant rendering even on low-end devices.
 * Needle Engine uses [three.quarks](https://github.com/Alchemist0823/three.quarks) internally.
 *
 * @example Basic playback control
 * ```ts
 * const ps = myObject.getComponent(ParticleSystem);
 * ps.play();
 * ps.emit(10); // Emit 10 particles immediately
 * ps.pause();
 * ps.stop(true, true); // Stop and clear all particles
 * ```
 *
 * @example Custom particle behavior
 * ```ts
 * class GravityBehaviour extends ParticleSystemBaseBehaviour {
 *   update(particle: Particle, delta: number) {
 *     particle.velocity.y -= 9.8 * delta;
 *   }
 * }
 * particleSystem.addBehaviour(new GravityBehaviour());
 * ```
 *
 * - Example: https://engine.needle.tools/samples/particles
 * - Example: https://engine.needle.tools/samples/particle-bursts
 * - Example: https://engine.needle.tools/samples/particles-on-collision
 *
 * @summary Handles the motion and rendering of many individual particles
 * @category Rendering
 * @group Components
 * @see {@link ParticleSystemBaseBehaviour} for custom particle behaviors
 * @see {@link EmissionModule} for emission configuration
 * @see {@link ShapeModule} for emission shape control
 * @see {@link TrailModule} for particle trails
 * @link https://engine.needle.tools/docs/features/particles.html
 */
export class ParticleSystem extends Behaviour {
    play(includeChildren = false) {
        if (includeChildren) {
            GameObject.foreachComponent(this.gameObject, comp => {
                if (comp instanceof ParticleSystem && comp !== this) {
                    comp.play(false);
                }
            }, true);
        }
        this._isPlaying = true;
        // https://github.com/Alchemist0823/three.quarks/pull/35
        if (this._particleSystem) {
            this._particleSystem["emissionState"].time = 0;
            this._particleSystem["emitEnded"] = false;
        }
        this.emission?.reset();
    }
    pause(includeChildren = true) {
        if (includeChildren) {
            GameObject.foreachComponent(this.gameObject, comp => {
                if (comp instanceof ParticleSystem && comp !== this) {
                    comp.pause(false);
                }
            }, true);
        }
        this._isPlaying = false;
    }
    /** clear=true removes all emitted particles */
    stop(includeChildren = true, clear = false) {
        if (includeChildren) {
            GameObject.foreachComponent(this.gameObject, comp => {
                if (comp instanceof ParticleSystem && comp !== this) {
                    comp.stop(false, clear);
                }
            }, true);
        }
        this._isPlaying = false;
        this._time = 0;
        if (clear)
            this.reset();
    }
    /** remove emitted particles and reset time */
    reset() {
        this._time = 0;
        if (this._particleSystem) {
            this._particleSystem.particleNum = 0;
            this._particleSystem["emissionState"].time = 0;
            this._particleSystem["emitEnded"] = false;
            this.emission?.reset();
        }
    }
    _state;
    emit(count) {
        if (this._particleSystem) {
            // we need to call update the matrices etc e.g. if we call emit from a physics callback
            this.onUpdate();
            count = Math.min(count, this.maxParticles - this.currentParticles);
            if (!this._state)
                this._state = new ParticlesEmissionState();
            this._state.waitEmiting = count;
            this._state.time = 0;
            const emitEndedState = this._particleSystem["emitEnded"];
            this._particleSystem["emitEnded"] = false;
            this._particleSystem.emit(this.deltaTime, this._state, this._particleSystem.emitter.matrixWorld);
            this._particleSystem["emitEnded"] = emitEndedState;
        }
    }
    get playOnAwake() {
        return this.main.playOnAwake;
    }
    set playOnAwake(val) {
        this.main.playOnAwake = val;
    }
    colorOverLifetime;
    main;
    emission;
    sizeOverLifetime;
    shape;
    noise;
    trails;
    velocityOverLifetime;
    limitVelocityOverLifetime;
    inheritVelocity;
    colorBySpeed;
    textureSheetAnimation;
    rotationOverLifetime;
    rotationBySpeed;
    sizeBySpeed;
    get renderer() {
        return this._renderer;
    }
    get isPlaying() { return this._isPlaying; }
    get currentParticles() {
        return this._particleSystem?.particleNum ?? 0;
    }
    get maxParticles() {
        return this.main.maxParticles;
    }
    get time() {
        return this._time;
    }
    get duration() {
        return this.main.duration;
    }
    get deltaTime() {
        return this.context.time.deltaTime * this.main.simulationSpeed;
    }
    get scale() {
        return this.gameObject.scale.x;
    }
    get cameraScale() {
        return this._cameraScale;
    }
    _cameraScale = 1;
    get container() {
        return this._container;
    }
    get worldspace() {
        return this.main.simulationSpace === ParticleSystemSimulationSpace.World;
    }
    get localspace() {
        return this.main.simulationSpace === ParticleSystemSimulationSpace.Local;
    }
    __worldQuaternion = new Quaternion();
    get worldQuaternion() {
        return this.__worldQuaternion;
    }
    _worldQuaternionInverted = new Quaternion();
    get worldQuaternionInverted() {
        return this._worldQuaternionInverted;
    }
    _worldScale = new Vector3();
    get worldScale() {
        return this._worldScale;
    }
    _worldPositionFrame = -1;
    _worldPos = new Vector3();
    get worldPos() {
        if (this._worldPositionFrame !== this.context.time.frame) {
            this._worldPositionFrame = this.context.time.frame;
            getWorldPosition(this.gameObject, this._worldPos);
        }
        return this._worldPos;
    }
    get matrixWorld() {
        return this._container.matrixWorld;
    }
    get isSubsystem() {
        return this._isUsedAsSubsystem;
    }
    /** Add a custom quarks behaviour to the particle system.
     * You can add a quarks.Behaviour type or derive from {@link ParticleSystemBaseBehaviour}
     * @link https://github.com/Alchemist0823/three.quarks
     * @example
     * ```typescript
     * class MyBehaviour extends ParticleSystemBaseBehaviour {
     *    initialize(particle: Particle) {
     *       // initialize the particle
     *   }
     *    update(particle: Particle, delta: number) {
     *        // do something with the particle
     *   }
     * }
     *
     * const system = gameObject.getComponent(ParticleSystem);
     * system.addBehaviour(new MyBehaviour());
     * ```
    */
    addBehaviour(particleSystemBehaviour) {
        if (!this._particleSystem) {
            return false;
        }
        if (particleSystemBehaviour instanceof ParticleSystemBaseBehaviour) {
            particleSystemBehaviour.system = this;
        }
        if (debug)
            console.debug("Add custom ParticleSystem Behaviour", particleSystemBehaviour);
        this._particleSystem.addBehavior(particleSystemBehaviour);
        return true;
    }
    /** Remove a custom quarks behaviour from the particle system. **/
    removeBehaviour(particleSystemBehaviour) {
        if (!this._particleSystem) {
            return false;
        }
        const behaviours = this._particleSystem.behaviors;
        const index = behaviours.indexOf(particleSystemBehaviour);
        if (index !== -1) {
            if (isDevEnvironment() || debug)
                console.debug("Remove custom ParticleSystem Behaviour", index, particleSystemBehaviour);
            behaviours.splice(index, 1);
            return true;
        }
        return true;
    }
    /** Removes all behaviours from the particle system
     * **Note:** this will also remove the default behaviours like SizeBehaviour, ColorBehaviour etc.
     */
    removeAllBehaviours() {
        if (!this._particleSystem) {
            return false;
        }
        this._particleSystem.behaviors.length = 0;
        return true;
    }
    /** Get the underlying three.quarks particle system behaviours. This can be used to fully customize the behaviour of the particles. */
    get behaviours() {
        if (!this._particleSystem)
            return null;
        return this._particleSystem.behaviors;
    }
    /** Get access to the underlying quarks particle system if you need more control
     * @link https://github.com/Alchemist0823/three.quarks
     */
    get particleSystem() {
        return this._particleSystem ?? null;
    }
    _renderer;
    _batchSystem;
    _particleSystem;
    _interface;
    // private _system!: System;
    // private _emitter: Emitter;
    // private _size!: SizeBehaviour;
    _container;
    _time = 0;
    _isPlaying = true;
    _isUsedAsSubsystem = false;
    _didPreWarm = false;
    /** called from deserialization */
    set bursts(arr) {
        for (let i = 0; i < arr.length; i++) {
            const burst = arr[i];
            if ((burst instanceof ParticleBurst) === false) {
                const instance = new ParticleBurst();
                assign(instance, burst);
                arr[i] = instance;
            }
        }
        this._bursts = arr;
    }
    _bursts;
    /** called from deserialization */
    set subEmitterSystems(arr) {
        for (let i = 0; i < arr.length; i++) {
            const sub = arr[i];
            if ((sub instanceof SubEmitterSystem) === false) {
                const instance = new SubEmitterSystem();
                assign(instance, sub);
                arr[i] = instance;
            }
        }
        if (debug && arr.length > 0) {
            console.log("SubEmitters: ", arr, this);
        }
        this._subEmitterSystems = arr;
    }
    _subEmitterSystems;
    /** @internal */
    onAfterDeserialize(_) {
        // doing this here to get a chance to resolve the subemitter guid
        if (this._subEmitterSystems && Array.isArray(this._subEmitterSystems)) {
            for (const sub of this._subEmitterSystems) {
                sub._deserialize(this.context, this.gameObject);
            }
        }
    }
    /** @internal */
    awake() {
        this._worldPositionFrame = -1;
        this._renderer = this.gameObject.getComponent(ParticleSystemRenderer);
        if (!this.main) {
            throw new Error("Not Supported: ParticleSystem needs a serialized MainModule. Creating new particle systems at runtime is currently not supported.");
        }
        this._container = new Object3D();
        this._container.matrixAutoUpdate = false;
        // if (this.main.simulationSpace == ParticleSystemSimulationSpace.Local) {
        //     this.gameObject.add(this._container);
        // }
        // else 
        {
            this.context.scene.add(this._container);
        }
        // else this._container = this.context.scene;
        this._batchSystem = new BatchedParticleRenderer();
        this._batchSystem.name = this.gameObject.name;
        this._container.add(this._batchSystem);
        this._interface = new ParticleSystemInterface(this);
        this._particleSystem = new _ParticleSystem(this._interface);
        this._particleSystem.addBehavior(new SizeBehaviour(this));
        this._particleSystem.addBehavior(new ColorBehaviour(this));
        this._particleSystem.addBehavior(new TextureSheetAnimationBehaviour(this));
        this._particleSystem.addBehavior(new RotationBehaviour(this));
        this._particleSystem.addBehavior(new VelocityBehaviour(this));
        this._particleSystem.addBehavior(new TrailBehaviour(this));
        this._batchSystem.addSystem(this._particleSystem);
        const emitter = this._particleSystem.emitter;
        this.context.scene.add(emitter);
        if (this.inheritVelocity.system && this.inheritVelocity.system !== this) {
            this.inheritVelocity = this.inheritVelocity.clone();
        }
        this.inheritVelocity.awake(this);
        if (debug) {
            console.log(this);
            this.gameObject.add(new AxesHelper(1));
        }
    }
    /** @internal */
    start() {
        this.addSubParticleSystems();
        this.updateLayers();
        if (this.renderer.particleMesh instanceof Mesh && this._interface.renderMode == RenderMode.Mesh) {
            NEEDLE_progressive.assignMeshLOD(this.renderer.particleMesh, 0).then(geo => {
                if (geo && this.particleSystem && this._interface.renderMode == RenderMode.Mesh) {
                    this.particleSystem.instancingGeometry = geo;
                }
            });
        }
    }
    /** @internal */
    onDestroy() {
        this._container?.removeFromParent();
        this._batchSystem?.removeFromParent();
        this._particleSystem?.emitter.removeFromParent();
        this._particleSystem?.dispose();
    }
    /** @internal */
    onEnable() {
        if (!this.main)
            return;
        if (this.inheritVelocity)
            this.inheritVelocity.system = this;
        if (this._batchSystem)
            this._batchSystem.visible = true;
        if (this.playOnAwake)
            this.play();
        this._isPlaying = this.playOnAwake;
    }
    onDisable() {
        if (this._batchSystem)
            this._batchSystem.visible = false;
    }
    /** @internal */
    onBeforeRender() {
        if (!this.main)
            return;
        if (this._didPreWarm === false && this.main?.prewarm === true) {
            this._didPreWarm = true;
            this.preWarm();
        }
        this.onUpdate();
        this.onSimulate(this.deltaTime);
    }
    preWarm() {
        if (!this.emission?.enabled)
            return;
        const emission = this.emission.rateOverTime.getMax();
        if (emission <= 0)
            return;
        const dt = 1 / 60;
        const duration = this.main.duration;
        const lifetime = this.main.startLifetime.getMax();
        const maxDurationToPrewarm = 1000;
        const timeToSimulate = Math.min(Math.max(duration, lifetime) / Math.max(.01, this.main.simulationSpeed), maxDurationToPrewarm);
        const framesToSimulate = Math.ceil(timeToSimulate / dt);
        const startTime = Date.now();
        if (debug)
            console.log(`Particles ${this.name} - Prewarm for ${framesToSimulate} frames (${timeToSimulate} sec). Duration: ${duration}, Lifetime: ${lifetime}`);
        for (let i = 0; i < framesToSimulate; i++) {
            if (this.currentParticles >= this.maxParticles)
                break;
            const timePassed = Date.now() - startTime;
            if (timePassed > 2000) {
                console.warn(`Particles ${this.name} - Prewarm took too long. Aborting: ${timePassed}`);
                break;
            }
            this.onUpdate();
            this.onSimulate(dt);
        }
    }
    _lastBatchesCount = -1;
    onSimulate(dt) {
        if (this._batchSystem) {
            let needsUpdate = this.context.time.frameCount % 60 === 0;
            if (this._lastBatchesCount !== this._batchSystem.batches.length) {
                this._lastBatchesCount = this._batchSystem.batches.length;
                needsUpdate = true;
            }
            // Updating layers on batches
            // TODO: figure out a better way to do this
            // Issue: https://github.com/Alchemist0823/three.quarks/issues/49
            if (needsUpdate) {
                this.updateLayers();
            }
            this._batchSystem.update(dt);
        }
        this._time += dt;
        if (this._time > this.duration)
            this._time = 0;
    }
    updateLayers() {
        if (this._batchSystem) {
            for (let i = 0; i < this._batchSystem.batches.length; i++) {
                const batch = this._batchSystem.batches[i];
                batch.layers.disableAll();
                const layer = this.layer;
                batch.layers.mask = 1 << layer;
            }
        }
    }
    // private lastMaterialVersion: number = -1;
    onUpdate() {
        if (this._bursts) {
            this.emission.bursts = this._bursts;
            delete this._bursts;
        }
        if (!this._isPlaying)
            return;
        // sprite materials must be scaled in AR
        const cam = this.context.mainCamera;
        if (cam) {
            const scale = getWorldScale(cam);
            this._cameraScale = scale.x;
        }
        const isLocalSpace = !this.worldspace;
        const source = this.gameObject;
        getWorldQuaternion(source, this.__worldQuaternion);
        this._worldQuaternionInverted.copy(this.__worldQuaternion).invert();
        getWorldScale(this.gameObject, this._worldScale);
        // Handle LOCALSPACE
        if (isLocalSpace && this._container && this.gameObject?.parent) {
            const scale = getLocalSimulationScale(this, temp3);
            this._container.matrix.makeScale(scale.x, scale.y, scale.z);
            this._container.matrix.makeRotationFromQuaternion(this.__worldQuaternion);
            this._container.matrix.setPosition(this.worldPos);
            this._container.matrix.scale(this.gameObject.scale);
        }
        this.emission.system = this;
        this._interface.update();
        this.shape.onUpdate(this, this.context, this.main.simulationSpace, this.gameObject);
        this.noise.update(this.context);
        this.inheritVelocity?.update(this.context);
        this.velocityOverLifetime.update(this);
    }
    addSubParticleSystems() {
        if (this._subEmitterSystems && this._particleSystem) {
            for (const sys of this._subEmitterSystems) {
                // Make sure the particle system is created
                if (sys.particleSystem) {
                    if (sys.particleSystem.__internalAwake)
                        sys.particleSystem.__internalAwake();
                    else if (isDevEnvironment())
                        console.warn("SubParticleSystem serialization issue(?)", sys.particleSystem, sys);
                }
                const system = sys.particleSystem?._particleSystem;
                if (system) {
                    sys.particleSystem._isUsedAsSubsystem = true;
                    // sys.particleSystem!.main.simulationSpace = ParticleSystemSimulationSpace.World;
                    const sub = new ParticleSubEmitter(this, this._particleSystem, sys.particleSystem, system);
                    sub.emitterType = sys.type;
                    sub.emitterProbability = sys.emitProbability;
                    this._particleSystem.addBehavior(sub);
                }
                else if (debug)
                    console.warn("Could not add SubParticleSystem", sys, this);
            }
        }
    }
}
__decorate([
    serializable(ColorOverLifetimeModule)
], ParticleSystem.prototype, "colorOverLifetime", void 0);
__decorate([
    serializable(MainModule)
], ParticleSystem.prototype, "main", void 0);
__decorate([
    serializable(EmissionModule)
], ParticleSystem.prototype, "emission", void 0);
__decorate([
    serializable(SizeOverLifetimeModule)
], ParticleSystem.prototype, "sizeOverLifetime", void 0);
__decorate([
    serializable(ShapeModule)
], ParticleSystem.prototype, "shape", void 0);
__decorate([
    serializable(NoiseModule)
], ParticleSystem.prototype, "noise", void 0);
__decorate([
    serializable(TrailModule)
], ParticleSystem.prototype, "trails", void 0);
__decorate([
    serializable(VelocityOverLifetimeModule)
], ParticleSystem.prototype, "velocityOverLifetime", void 0);
__decorate([
    serializable(LimitVelocityOverLifetimeModule)
], ParticleSystem.prototype, "limitVelocityOverLifetime", void 0);
__decorate([
    serializable(InheritVelocityModule)
], ParticleSystem.prototype, "inheritVelocity", void 0);
__decorate([
    serializable(ColorBySpeedModule)
], ParticleSystem.prototype, "colorBySpeed", void 0);
__decorate([
    serializable(TextureSheetAnimationModule)
], ParticleSystem.prototype, "textureSheetAnimation", void 0);
__decorate([
    serializable(RotationOverLifetimeModule)
], ParticleSystem.prototype, "rotationOverLifetime", void 0);
__decorate([
    serializable(RotationBySpeedModule)
], ParticleSystem.prototype, "rotationBySpeed", void 0);
__decorate([
    serializable(SizeBySpeedModule)
], ParticleSystem.prototype, "sizeBySpeed", void 0);
/** @internal */
export class SubEmitterSystem {
    particleSystem;
    emitProbability = 1;
    properties;
    type;
    _deserialize(_context, gameObject) {
        const ps = this.particleSystem;
        if (ps instanceof ParticleSystem)
            return;
        let guid = "";
        if (ps && typeof ps["guid"] === "string") {
            guid = ps["guid"];
            // subemitter MUST be a child of the particle system
            this.particleSystem = GameObject.findByGuid(guid, gameObject);
        }
        if (debug && !(this.particleSystem instanceof ParticleSystem)) {
            console.warn("Could not find particle system for sub emitter", guid, gameObject, this);
        }
    }
}
function getLocalSimulationScale(system, vec) {
    vec.set(1, 1, 1);
    if (system.gameObject.parent && system.localspace) {
        switch (system.main.scalingMode) {
            case ParticleSystemScalingMode.Local:
                vec = getWorldScale(system.gameObject.parent, vec);
                vec.x = 1 / vec.x;
                vec.y = 1 / vec.y;
                vec.z = 1 / vec.z;
                break;
            default:
                if (!system["unsupported_scaling_mode"]) {
                    system["unsupported_scaling_mode"] = true;
                    const msg = "ParticleSystem scale mode " + ParticleSystemScalingMode[system.main.scalingMode] + " is not supported";
                    if (isDevEnvironment())
                        showBalloonWarning(msg);
                    console.warn(msg, system.name, system);
                }
                vec = getWorldScale(system.gameObject, vec);
                break;
        }
    }
    return vec;
}
//# sourceMappingURL=ParticleSystem.js.map