import { AxesHelper, BackSide, Blending, BufferGeometry, FrontSide, LinearSRGBColorSpace, Material, Matrix4, Mesh, MeshBasicMaterial, MeshStandardMaterial, NormalBlending, Object3D, PlaneGeometry, Quaternion, SpriteMaterial, Texture, Vector3, Vector4 } from "three";
import type { BatchedRenderer, Behavior, BurstParameters, EmissionState, EmitterShape, FunctionColorGenerator, FunctionJSON, FunctionValueGenerator, GeneratorMemory, Particle, ParticleSystemParameters, RecordState, TrailSettings, ValueGenerator } from "three.quarks";
import { BatchedParticleRenderer, ConstantColor, ConstantValue, ParticleSystem as _ParticleSystem, RenderMode, TrailParticle, Vector3 as QVector3, 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 { Context } from "../../engine/engine_setup.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, type IParticleSystem, LimitVelocityOverLifetimeModule, MainModule, MinMaxCurve, MinMaxGradient, 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");

export type { Particle as QParticle, Behavior as QParticleBehaviour, TrailParticle as QTrailParticle } from "three.quarks"

/**
 * 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 enum SubEmitterType {
    /** Sub-emitter triggers when the parent particle is born */
    Birth = 0,
    /** Sub-emitter triggers when the parent particle collides */
    Collision = 1,
    /** Sub-emitter triggers when the parent particle dies */
    Death = 2,
    /** Sub-emitter triggers when the parent particle enters a trigger zone */
    Trigger = 3,
    /** Sub-emitter is triggered manually via code */
    Manual = 4,
}

/** @internal */
export class ParticleSystemRenderer extends Behaviour {

    @serializable()
    renderMode?: ParticleSystemRenderMode;

    @serializable(Material)
    particleMaterial?: SpriteMaterial | MeshBasicMaterial;

    @serializable(Material)
    trailMaterial?: SpriteMaterial | MeshBasicMaterial;

    // @serializable(Mesh)
    particleMesh?: Mesh | string;

    @serializable()
    maxParticleSize!: number;

    @serializable()
    minParticleSize!: number;

    @serializable()
    velocityScale?: number;
    @serializable()
    cameraVelocityScale?: number;
    @serializable()
    lengthScale?: number;

    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(): boolean {
        const res = this.particleMaterial?.transparent ?? false;
        // console.log(res, this.particleMaterial);
        return res;
    }

    getMaterial(trailEnabled: boolean = 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?: ParticleSystemRenderMode) {
        let geo: BufferGeometry | null = 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;
    }
}

class MinMaxCurveFunction implements FunctionValueGenerator {

    private _curve: MinMaxCurve;
    private _factor: number;

    constructor(curve: MinMaxCurve, factor: number = 1) {
        this._curve = curve;
        this._factor = factor;
    }

    type: "function" = "function";

    startGen(_memory: GeneratorMemory): void {
        // ...
    }
    genValue(_memory: GeneratorMemory, t: number): number {
        return this._curve.evaluate(t, Math.random()) * this._factor;
    }
    toJSON(): FunctionJSON {
        throw new Error("Method not implemented.");
    }
    clone(): FunctionValueGenerator {
        throw new Error("Method not implemented.");
    }
}

class MinMaxGradientFunction implements FunctionColorGenerator {

    private _curve: MinMaxGradient;

    constructor(curve: MinMaxGradient) { this._curve = curve; }

    type: "function" = "function";

    startGen(_memory: GeneratorMemory): void {
        throw new Error("Method not implemented.");
    }
    genColor(_memory: GeneratorMemory, color: QVector4, t: number): QVector4 {
        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(): FunctionJSON {
        throw new Error("Method not implemented.");
    }
    clone(): FunctionColorGenerator {
        throw new Error("Method not implemented.");
    }

}

abstract class BaseValueGenerator implements ValueGenerator {

    type: "value" = "value";
    toJSON(): FunctionJSON {
        throw new Error("Method not implemented.");
    }
    clone(): ValueGenerator {
        throw new Error("Method not implemented.");
    }

    startGen(_memory: any): void { }
    abstract genValue(): number;

    readonly system: ParticleSystem;

    constructor(system: ParticleSystem) {
        this.system = system;
    }
}

class TextureSheetStartFrameGenerator extends BaseValueGenerator {
    genValue(): number {
        return this.system.textureSheetAnimation.getStartIndex();
    }

}

class ParticleSystemEmissionOverTime extends BaseValueGenerator {

    private _lastPosition: Vector3 = new Vector3();
    private _lastDistance: number = 0;

    update() {
        const currentPosition = getWorldPosition(this.system.gameObject);
        this._lastDistance = this._lastPosition.distanceTo(currentPosition)
        this._lastPosition.copy(currentPosition);
    }

    genValue(): number {
        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(): number {
        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 abstract class ParticleSystemBaseBehaviour implements Behavior {

    /** Reference to the particle system this behavior belongs to */
    system!: ParticleSystem;
    /** Access to the engine context for timing, input, etc. */
    get context() { return this.system.context; }

    constructor(ps?: ParticleSystem) {
        this.type = Object.getPrototypeOf(this).constructor.name || "ParticleSystemBaseBehaviour";
        if (ps)
            this.system = ps;
    }

    /** Behavior type identifier used by three.quarks */
    type: string;

    /** Called once when a particle is spawned. Use to initialize per-particle state. */
    initialize(_particle: Particle): void { }
    /** Called every frame for each active particle. Use to update particle properties. */
    update(_particle: Particle, _delta: number): void { }
    /** Called once per frame before particle updates. Use for shared calculations. */
    frameUpdate(_delta: number): void { }
    toJSON() { throw new Error("Method not implemented."); }
    clone(): Behavior { throw new Error("Method not implemented."); }
    /** Called when the particle system is reset. */
    reset() { }
}

const $startFrame = Symbol("startFrame")
class TextureSheetAnimationBehaviour extends ParticleSystemBaseBehaviour {
    type: string = "NeedleTextureSheet"

    // initialize(_particle: Particle): void {
    //     _particle[$startFrame] = this.system.textureSheetAnimation.getStartIndex();
    // }

    update(particle: Particle, _delta: number) {
        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: string = "NeedleRotation"

    initialize(particle: Particle) {
        particle[$particleRotation] = Math.random();
    }

    update(particle: Particle, delta: number) {
        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: string = "NeedleSize";

    private _minSize = 0;
    private _maxSize = 1;

    initialize(particle: Particle) {
        particle[$sizeLerpFactor] = Math.random();
        this._minSize = this.system.renderer.minParticleSize;
        this._maxSize = this.system.renderer.maxParticleSize;
    }

    update(particle: Particle, _delta: number): void {
        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: string = "NeedleTrail";

    initialize(particle: 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: Particle) {
        if (this.system.trails?.enabled && particle instanceof TrailParticle) {
            const trailParticle = particle as TrailParticle;
            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 as RecordState;
                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: string = "NeedleVelocity";

    private _gravityDirection = new Vector3();

    initialize(particle: Particle): void {
        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: Particle, delta: number): void {

        //////////////////////
        // 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: string = "NeedleColor";

    initialize(_particle: Particle): void {
    }

    private _init(particle: 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: Particle, _delta: number): void {
        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 implements ParticleSystemParameters {

    private readonly system: ParticleSystem;
    private readonly emission: ParticleSystemEmissionOverTime;
    private get anim(): TextureSheetAnimationModule {
        return this.system.textureSheetAnimation;
    }

    constructor(system: ParticleSystem) {
        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) as Material; }
    get layers() { return this.system.gameObject.layers; }

    update() {
        this.emission.update();
    }

    autoDestroy?: boolean | undefined;
    get looping() { return this.system.main.loop; }
    get duration() { return this.system.duration; }
    get shape(): EmitterShape { return this.system.shape as unknown as EmitterShape; }
    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?: ValueGenerator | FunctionValueGenerator | undefined; /** 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?: BurstParameters[] | undefined;
    onlyUsedByOther?: boolean | undefined;
    readonly behaviors: Behavior[] = [];
    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: TrailSettings = {
        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;
    }
    private flatWhiteTexture?: Texture;
    private clonedTexture: { original?: Texture, clone?: Texture } = { original: undefined, clone: undefined };
    get texture(): Texture {
        const mat = this.material;
        if (mat && mat["map"]) {
            const original = mat["map"]! as Texture;
            // 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(): 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 implements EmissionState {
    burstParticleIndex: number = 0;
    burstParticleCount: number = 0;
    isBursting: boolean = false;
    travelDistance: number = 0;
    previousWorldPos?: QVector3 | undefined;

    burstIndex: number = 0;
    burstWaveIndex: number = 0;
    time: number = 0;
    waitEmiting: number = 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 implements IParticleSystem {

    play(includeChildren: boolean = 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: boolean = 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();
        }
    }

    private _state?: ParticlesEmissionState;
    emit(count: number) {
        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 as any);
            this._particleSystem["emitEnded"] = emitEndedState;
        }
    }

    get playOnAwake(): boolean {
        return this.main.playOnAwake;
    }
    set playOnAwake(val: boolean) {
        this.main.playOnAwake = val;
    }

    @serializable(ColorOverLifetimeModule)
    readonly colorOverLifetime!: ColorOverLifetimeModule;

    @serializable(MainModule)
    readonly main!: MainModule;

    @serializable(EmissionModule)
    readonly emission!: EmissionModule;

    @serializable(SizeOverLifetimeModule)
    readonly sizeOverLifetime!: SizeOverLifetimeModule;

    @serializable(ShapeModule)
    readonly shape!: ShapeModule;

    @serializable(NoiseModule)
    readonly noise!: NoiseModule;

    @serializable(TrailModule)
    readonly trails!: TrailModule;

    @serializable(VelocityOverLifetimeModule)
    readonly velocityOverLifetime!: VelocityOverLifetimeModule;

    @serializable(LimitVelocityOverLifetimeModule)
    readonly limitVelocityOverLifetime!: LimitVelocityOverLifetimeModule;

    @serializable(InheritVelocityModule)
    inheritVelocity!: InheritVelocityModule;

    @serializable(ColorBySpeedModule)
    readonly colorBySpeed!: ColorBySpeedModule;

    @serializable(TextureSheetAnimationModule)
    readonly textureSheetAnimation!: TextureSheetAnimationModule;

    @serializable(RotationOverLifetimeModule)
    readonly rotationOverLifetime!: RotationOverLifetimeModule;

    @serializable(RotationBySpeedModule)
    readonly rotationBySpeed!: RotationBySpeedModule;

    @serializable(SizeBySpeedModule)
    readonly sizeBySpeed!: SizeBySpeedModule;

    get renderer(): ParticleSystemRenderer {
        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(): number {
        return this._cameraScale;
    }
    private _cameraScale: number = 1;

    get container(): Object3D {
        return this._container!;
    }

    get worldspace() {
        return this.main.simulationSpace === ParticleSystemSimulationSpace.World;
    }
    get localspace() {
        return this.main.simulationSpace === ParticleSystemSimulationSpace.Local;
    }

    private __worldQuaternion = new Quaternion();
    get worldQuaternion(): Quaternion {
        return this.__worldQuaternion;
    }
    private _worldQuaternionInverted = new Quaternion();
    get worldQuaternionInverted(): Quaternion {
        return this._worldQuaternionInverted;
    }
    private _worldScale = new Vector3();
    get worldScale(): Vector3 {
        return this._worldScale;
    }

    private _worldPositionFrame: number = -1;
    private _worldPos: Vector3 = new Vector3();
    get worldPos(): Vector3 {
        if (this._worldPositionFrame !== this.context.time.frame) {
            this._worldPositionFrame = this.context.time.frame;
            getWorldPosition(this.gameObject, this._worldPos);
        }
        return this._worldPos;
    }

    get matrixWorld(): Matrix4 {
        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: Behavior | ParticleSystemBaseBehaviour): boolean {
        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: Behavior | ParticleSystemBaseBehaviour): boolean {
        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(): Behavior[] | null {
        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(): _ParticleSystem | null {
        return this._particleSystem ?? null;
    }

    private _renderer!: ParticleSystemRenderer;
    private _batchSystem?: BatchedRenderer;
    private _particleSystem?: _ParticleSystem;
    private _interface!: ParticleSystemInterface;

    // private _system!: System;
    // private _emitter: Emitter;
    // private _size!: SizeBehaviour;
    private _container!: Object3D;
    private _time: number = 0;
    private _isPlaying: boolean = true;
    private _isUsedAsSubsystem: boolean = false;
    private _didPreWarm: boolean = false;

    /** called from deserialization */
    private set bursts(arr: ParticleBurst[]) {
        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;
    }
    private _bursts?: ParticleBurst[];

    /** called from deserialization */
    private set subEmitterSystems(arr: SubEmitterSystem[]) {
        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;
    }
    private _subEmitterSystems?: SubEmitterSystem[];

    /** @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(): void {
        this._worldPositionFrame = -1;

        this._renderer = this.gameObject.getComponent(ParticleSystemRenderer) as 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(): void {
        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);
    }

    private 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);
        }
    }

    private _lastBatchesCount = -1;
    private onSimulate(dt: number) {
        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;
    }

    private 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;
    private 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);
    }

    private 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);
            }
        }
    }
}


/** @internal */
export class SubEmitterSystem {

    particleSystem?: ParticleSystem;
    emitProbability: number = 1;
    properties?: number;
    type?: SubEmitterType;

    _deserialize(_context: Context, gameObject: 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) as ParticleSystem;
        }

        if (debug && !(this.particleSystem instanceof ParticleSystem)) {
            console.warn("Could not find particle system for sub emitter", guid, gameObject, this);
        }
    }
}


function getLocalSimulationScale(system: ParticleSystem, vec: Vector3) {
    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;
}