import { createNoise4D, type NoiseFunction4D } from 'simplex-noise';
import { BufferGeometry, Color, Euler, Matrix4, Mesh, Object3D, Quaternion, Triangle, Vector2, Vector3, Vector4 } from "three";
import type { EmitterShape, IParticleSystem as QParticleSystem, Particle, ShapeJSON, Vector3 as QVector3, Vector4 as QVector4 } from "three.quarks";

import { isDevEnvironment } from '../../engine/debug/index.js';
import { Gizmos } from "../../engine/engine_gizmos.js";
import { Mathf } from "../../engine/engine_math.js";
import { serializable } from "../../engine/engine_serialization.js";
import { Context } from "../../engine/engine_setup.js";
import { getTempVector, getWorldQuaternion } from '../../engine/engine_three_utils.js';
import type { Vec2, Vec3 } from "../../engine/engine_types.js";
import { getParam } from "../../engine/engine_utils.js";
import { RGBAColor } from "../../engine/js-extensions/index.js";
import { AnimationCurve } from "../AnimationCurve.js";
import { MeshRenderer } from '../Renderer.js';

const debug = getParam("debugparticles");

declare type Color4 = { r: number, g: number, b: number, a: number };
declare type ColorKey = { time: number, color: Color4 };
declare type AlphaKey = { time: number, alpha: number };

export interface IParticleSystem {
    get currentParticles(): number;
    get maxParticles(): number;
    get time(): number;
    get deltaTime(): number;
    get duration(): number;
    readonly main: MainModule;
    get container(): Object3D;
    get worldspace(): boolean;
    get worldPos(): Vector3;
    get worldQuaternion(): Quaternion;
    get worldQuaternionInverted(): Quaternion;
    get worldScale(): Vector3;
    get matrixWorld(): Matrix4;
}


export enum ParticleSystemRenderMode {
    Billboard = 0,
    Stretch = 1,
    HorizontalBillboard = 2,
    VerticalBillboard = 3,
    Mesh = 4,
    //   None = 5,
}


export class Gradient {
    @serializable()
    alphaKeys: Array<AlphaKey> = [];
    @serializable()
    colorKeys: Array<ColorKey> = [];

    get duration(): number {
        return 1;
    }

    evaluate(time: number, target: RGBAColor) {

        // target.r = this.colorKeys[0].color.r;
        // target.g = this.colorKeys[0].color.g;
        // target.b = this.colorKeys[0].color.b;
        // target.alpha = this.alphaKeys[0].alpha;
        // return;

        let closestAlpha: AlphaKey | undefined = undefined;
        let closestAlphaIndex = 0;
        let closestColor: ColorKey | null = null;
        let closestColorIndex = 0;
        for (let i = 0; i < this.alphaKeys.length; i++) {
            const key = this.alphaKeys[i];
            if (key.time < time || !closestAlpha) {
                closestAlpha = key;
                closestAlphaIndex = i;
            }
        }
        for (let i = 0; i < this.colorKeys.length; i++) {
            const key = this.colorKeys[i];
            if (key.time < time || !closestColor) {
                closestColor = key;
                closestColorIndex = i;
            }
        }
        if (closestColor) {
            const hasNextColor = closestColorIndex + 1 < this.colorKeys.length;
            if (hasNextColor) {
                const nextColor = this.colorKeys[closestColorIndex + 1];
                const t = Mathf.remap(time, closestColor.time, nextColor.time, 0, 1);
                target.r = Mathf.lerp(closestColor.color.r, nextColor.color.r, t);
                target.g = Mathf.lerp(closestColor.color.g, nextColor.color.g, t);
                target.b = Mathf.lerp(closestColor.color.b, nextColor.color.b, t);
            }
            else {
                target.r = closestColor.color.r;
                target.g = closestColor.color.g;
                target.b = closestColor.color.b;
            }
        }
        if (closestAlpha) {
            const hasNextAlpha = closestAlphaIndex + 1 < this.alphaKeys.length;
            if (hasNextAlpha) {
                const nextAlpha = this.alphaKeys[closestAlphaIndex + 1];
                const t = Mathf.remap(time, closestAlpha.time, nextAlpha.time, 0, 1);
                target.alpha = Mathf.lerp(closestAlpha.alpha, nextAlpha.alpha, t);
            }
            else {
                target.alpha = closestAlpha.alpha;
            }
        }
        return target;
    }
}

export enum ParticleSystemCurveMode {
    Constant = 0,
    Curve = 1,
    TwoCurves = 2,
    TwoConstants = 3
}
declare type ParticleSystemCurveModeKeys = keyof typeof ParticleSystemCurveMode;

export enum ParticleSystemGradientMode {
    Color = 0,
    Gradient = 1,
    TwoColors = 2,
    TwoGradients = 3,
    RandomColor = 4,
}
declare type ParticleSystemGradientModeKeys = keyof typeof ParticleSystemGradientMode;

export enum ParticleSystemSimulationSpace {
    Local = 0,
    World = 1,
    Custom = 2
}

export enum ParticleSystemShapeType {
    Sphere = 0,
    SphereShell = 1,
    Hemisphere = 2,
    HemisphereShell = 3,
    Cone = 4,
    Box = 5,
    Mesh = 6,
    ConeShell = 7,
    ConeVolume = 8,
    ConeVolumeShell = 9,
    Circle = 10,
    CircleEdge = 11,
    SingleSidedEdge = 12,
    MeshRenderer = 13,
    SkinnedMeshRenderer = 14,
    BoxShell = 15,
    BoxEdge = 16,
    Donut = 17,
    Rectangle = 18,
    Sprite = 19,
    SpriteRenderer = 20
}

export enum ParticleSystemShapeMultiModeValue {
    Random = 0,
    Loop = 1,
    PingPong = 2,
    BurstSpread = 3,
}

export class MinMaxCurve {

    static constant(val: number) {
        const obj = new MinMaxCurve();
        obj.setConstant(val);
        return obj;
    }
    static betweenTwoConstants(min: number, max: number) {
        const obj = new MinMaxCurve();
        obj.setMinMaxConstant(min, max);
        return obj;
    }
    static curve(curve: AnimationCurve, multiplier: number = 1) {
        const obj = new MinMaxCurve();
        obj.setCurve(curve, multiplier);
        return obj;
    }

    setConstant(val: number) {
        this.mode = ParticleSystemCurveMode.Constant;
        this.constant = val;
    }
    setMinMaxConstant(min: number, max: number) {
        this.mode = ParticleSystemCurveMode.TwoConstants;
        this.constantMin = min;
        this.constantMax = max;
    }
    setCurve(curve: AnimationCurve, multiplier: number = 1) {
        this.mode = ParticleSystemCurveMode.Curve;
        this.curve = curve;
        this.curveMultiplier = multiplier;
    }

    @serializable()
    mode: ParticleSystemCurveMode | ParticleSystemCurveModeKeys = "Constant";

    @serializable()
    constant!: number;

    @serializable()
    constantMin!: number;
    @serializable()
    constantMax!: number;

    @serializable(AnimationCurve)
    curve?: AnimationCurve;
    @serializable(AnimationCurve)
    curveMin?: AnimationCurve;
    @serializable(AnimationCurve)
    curveMax?: AnimationCurve;
    @serializable()
    curveMultiplier?: number;

    clone() {
        const clone = new MinMaxCurve();
        clone.mode = this.mode;
        clone.constant = this.constant;
        clone.constantMin = this.constantMin;
        clone.constantMax = this.constantMax;
        clone.curve = this.curve?.clone();
        clone.curveMin = this.curveMin?.clone();
        clone.curveMax = this.curveMax?.clone();
        clone.curveMultiplier = this.curveMultiplier;
        return clone;
    }

    evaluate(t01: number, lerpFactor?: number): number {
        const t = lerpFactor === undefined ? Math.random() : lerpFactor;
        switch (this.mode) {
            case ParticleSystemCurveMode.Constant:
            case "Constant":
                return this.constant;
            case ParticleSystemCurveMode.Curve:
            case "Curve":
                t01 = Mathf.clamp01(t01);
                return this.curve!.evaluate(t01) * this.curveMultiplier!;
            case ParticleSystemCurveMode.TwoCurves:
            case "TwoCurves":
                const t1 = t01 * this.curveMin!.duration;
                const t2 = t01 * this.curveMax!.duration;
                return Mathf.lerp(this.curveMin!.evaluate(t1), this.curveMax!.evaluate(t2), t % 1) * this.curveMultiplier!;
            case ParticleSystemCurveMode.TwoConstants:
            case "TwoConstants":
                return Mathf.lerp(this.constantMin, this.constantMax, t % 1)
            default:
                this.curveMax!.evaluate(t01) * this.curveMultiplier!;
                break;
        }
        return 0;
    }

    getMax(): number {
        switch (this.mode) {
            case ParticleSystemCurveMode.Constant:
            case "Constant":
                return this.constant;
            case ParticleSystemCurveMode.Curve:
            case "Curve":
                return this.getMaxFromCurve(this.curve!) * this.curveMultiplier!;
            case ParticleSystemCurveMode.TwoCurves:
            case "TwoCurves":
                return Math.max(this.getMaxFromCurve(this.curveMin), this.getMaxFromCurve(this.curveMax)) * this.curveMultiplier!;
            case ParticleSystemCurveMode.TwoConstants:
            case "TwoConstants":
                return Math.max(this.constantMin, this.constantMax);
            default:
                return 0;
        }
    }

    private getMaxFromCurve(curve?: AnimationCurve) {
        if (!curve) return 0;
        let maxNumber = Number.MIN_VALUE;
        for (let i = 0; i < curve!.keys.length; i++) {
            const key = curve!.keys[i];
            if (key.value > maxNumber) {
                maxNumber = key.value;
            }
        }
        return maxNumber;
    }
}

export class MinMaxGradient {

    static constant(color: RGBAColor | Color) {
        const obj = new MinMaxGradient();
        obj.constant(color);
        return obj;
    }
    static betweenTwoColors(color1: RGBAColor | Color, color2: RGBAColor | Color) {
        const obj = new MinMaxGradient();
        obj.betweenTwoColors(color1, color2);
        return obj;
    }

    constant(color: RGBAColor | Color) {
        this.mode = ParticleSystemGradientMode.Color;
        this.color = color;
        return this;
    }
    betweenTwoColors(color1: RGBAColor | Color, color2: RGBAColor | Color) {
        this.mode = ParticleSystemGradientMode.TwoColors;
        this.colorMin = color1;
        this.colorMax = color2;
        return this;
    }


    /**
     * The mode of the gradient, which can be Color, Gradient, TwoColors or TwoGradients.
     */
    @serializable()
    mode: ParticleSystemGradientMode | ParticleSystemGradientModeKeys = ParticleSystemGradientMode.Color;

    @serializable(RGBAColor)
    color!: RGBAColor | Color;

    @serializable(RGBAColor)
    colorMin!: RGBAColor | Color;
    @serializable(RGBAColor)
    colorMax!: RGBAColor | Color;

    @serializable(Gradient)
    gradient!: Gradient;
    @serializable(Gradient)
    gradientMin!: Gradient;
    @serializable(Gradient)
    gradientMax!: Gradient;

    private static _temp: RGBAColor = new RGBAColor(0, 0, 0, 1);
    private static _temp2: RGBAColor = new RGBAColor(0, 0, 0, 1);

    evaluate(t01: number, lerpFactor?: number): RGBAColor | Color {
        const t = lerpFactor === undefined ? Math.random() : lerpFactor;
        switch (this.mode) {
            case ParticleSystemGradientMode.Color:
            case "Color":
                return this.color;
            case ParticleSystemGradientMode.Gradient:
            case "Gradient":
                this.gradient.evaluate(t01, MinMaxGradient._temp);
                return MinMaxGradient._temp
            case ParticleSystemGradientMode.TwoColors:
            case "TwoColors":
                const col1 = MinMaxGradient._temp.lerpColors(this.colorMin, this.colorMax, t);
                return col1;
            case ParticleSystemGradientMode.TwoGradients:
            case "TwoGradients":
                this.gradientMin.evaluate(t01, MinMaxGradient._temp);
                this.gradientMax.evaluate(t01, MinMaxGradient._temp2);
                return MinMaxGradient._temp.lerp(MinMaxGradient._temp2, t);

            case ParticleSystemGradientMode.RandomColor:
            case "RandomColor":
                const random_t = Math.random();
                this.gradientMin.evaluate(t01, MinMaxGradient._temp);
                this.gradientMax.evaluate(t01, MinMaxGradient._temp2);
                return MinMaxGradient._temp.lerp(MinMaxGradient._temp2, random_t);

        }
        // console.warn("Not implemented", ParticleSystemGradientMode[this.mode]);
        MinMaxGradient._temp.set(0xffffff)
        MinMaxGradient._temp.alpha = 1;
        return MinMaxGradient._temp;
    }
}

export enum ParticleSystemScalingMode {
    Hierarchy = 0,
    Local = 1,
    Shape = 2,
}

export class MainModule {
    cullingMode!: number;
    duration!: number;
    emitterVelocityMode!: number;
    flipRotation!: number;
    @serializable(MinMaxCurve)
    gravityModifier!: MinMaxCurve;
    gravityModifierMultiplier!: number;
    loop!: boolean;
    maxParticles!: number;
    playOnAwake!: boolean;
    prewarm!: boolean;
    ringBufferLoopRange!: { x: number, y: number };
    ringBufferMode!: boolean;
    scalingMode!: ParticleSystemScalingMode;
    simulationSpace!: ParticleSystemSimulationSpace;
    simulationSpeed!: number;
    @serializable(MinMaxGradient)
    startColor!: MinMaxGradient;
    @serializable(MinMaxCurve)
    startDelay!: MinMaxCurve;
    startDelayMultiplier!: number;
    @serializable(MinMaxCurve)
    startLifetime!: MinMaxCurve;
    startLifetimeMultiplier!: number;
    @serializable(MinMaxCurve)
    startRotation!: MinMaxCurve;
    startRotationMultiplier!: number;
    startRotation3D!: boolean;
    @serializable(MinMaxCurve)
    startRotationX!: MinMaxCurve;
    startRotationXMultiplier!: number;
    @serializable(MinMaxCurve)
    startRotationY!: MinMaxCurve;
    startRotationYMultiplier!: number;
    @serializable(MinMaxCurve)
    startRotationZ!: MinMaxCurve;
    startRotationZMultiplier!: number;
    @serializable(MinMaxCurve)
    startSize!: MinMaxCurve;
    startSize3D!: boolean;
    startSizeMultiplier!: number;
    @serializable(MinMaxCurve)
    startSizeX!: MinMaxCurve;
    startSizeXMultiplier!: number;
    @serializable(MinMaxCurve)
    startSizeY!: MinMaxCurve;
    startSizeYMultiplier!: number;
    @serializable(MinMaxCurve)
    startSizeZ!: MinMaxCurve;
    startSizeZMultiplier!: number;
    @serializable(MinMaxCurve)
    startSpeed!: MinMaxCurve;
    startSpeedMultiplier!: number;
    stopAction!: number;
    useUnscaledTime!: boolean;
}


export class ParticleBurst {
    cycleCount!: number;
    maxCount!: number;
    minCount!: number;
    probability!: number;
    repeatInterval!: number;
    time!: number;
    count!: {
        constant: number;
        constantMax: number;
        constantMin: number;
        curve?: AnimationCurve;
        curveMax?: AnimationCurve;
        curveMin?: AnimationCurve;
        curveMultiplier?: number;
        mode: ParticleSystemCurveMode;
    }


    private _performed: number = 0;


    reset() {
        this._performed = 0;
    }
    run(time: number): number {
        if (time <= this.time) {
            return 0;
        }
        let amount = 0;
        if (this.cycleCount === 0 || this._performed < this.cycleCount) {
            const nextTime = this.time + this.repeatInterval * this._performed;
            if (time >= nextTime) {
                this._performed += 1;
                if (Math.random() < this.probability) {
                    switch (this.count.mode) {
                        case ParticleSystemCurveMode.Constant:
                            amount = this.count.constant;
                            break;
                        case ParticleSystemCurveMode.TwoConstants:
                            amount = Mathf.lerp(this.count.constantMin, this.count.constantMax, Math.random());
                            break;
                        case ParticleSystemCurveMode.Curve:
                            amount = this.count.curve!.evaluate(Math.random());
                            break;
                        case ParticleSystemCurveMode.TwoCurves:
                            const t = Math.random();
                            amount = Mathf.lerp(this.count.curveMin!.evaluate(t), this.count.curveMax!.evaluate(t), Math.random());
                            break;
                    }
                }
            }
        }
        return amount;
    }
}

export class EmissionModule {

    @serializable()
    enabled!: boolean;


    get burstCount() {
        return this.bursts?.length ?? 0;
    }

    @serializable()
    bursts!: ParticleBurst[];

    @serializable(MinMaxCurve)
    rateOverTime!: MinMaxCurve;
    @serializable()
    rateOverTimeMultiplier!: number;

    @serializable(MinMaxCurve)
    rateOverDistance!: MinMaxCurve;
    @serializable()
    rateOverDistanceMultiplier!: number;


    /** set from system */
    system!: IParticleSystem;

    reset() {
        this.bursts?.forEach(b => b.reset());
    }

    getBurst() {
        let amount = 0;
        if (this.burstCount > 0) {
            for (let i = 0; i < this.burstCount; i++) {
                const burst = this.bursts[i];
                if (this.system.main.loop && burst.time >= this.system.time) {
                    burst.reset();
                }
                amount += Math.round(burst.run(this.system.time));
            }
        }
        return amount;
    }
}

export class ColorOverLifetimeModule {
    enabled!: boolean;
    @serializable(MinMaxGradient)
    color!: MinMaxGradient;
}

export class SizeOverLifetimeModule {
    enabled!: boolean;
    separateAxes!: boolean;
    @serializable(MinMaxCurve)
    size!: MinMaxCurve;
    sizeMultiplier!: number;
    @serializable(MinMaxCurve)
    x!: MinMaxCurve;
    xMultiplier!: number;
    @serializable(MinMaxCurve)
    y!: MinMaxCurve;
    yMultiplier!: number;
    @serializable(MinMaxCurve)
    z!: MinMaxCurve;
    zMultiplier!: number;

    private _time: number = 0;
    private _temp = new Vector3();

    evaluate(t01: number, target?: Vec3, lerpFactor?: number) {
        if (!target) target = this._temp;

        if (!this.enabled) {
            target.x = target.y = target.z = 1;
            return target;
        }

        if (!this.separateAxes) {
            const scale = this.size.evaluate(t01, lerpFactor) * this.sizeMultiplier;
            target.x = scale;
            // target.y = scale;
            // target.z = scale;
        }
        else {
            target.x = this.x.evaluate(t01, lerpFactor) * this.xMultiplier;
            target.y = this.y.evaluate(t01, lerpFactor) * this.yMultiplier;
            target.z = this.z.evaluate(t01, lerpFactor) * this.zMultiplier;
        }
        return target;
    }
}


export enum ParticleSystemMeshShapeType {
    Vertex = 0,
    Edge = 1,
    Triangle = 2,
}

export class ShapeModule implements EmitterShape {

    // Emittershape start
    get type(): string {
        return ParticleSystemShapeType[this.shapeType];
    }
    initialize(particle: Particle): void {
        this.onInitialize(particle);
        particle.position.x = this._vector.x;
        particle.position.y = this._vector.y;
        particle.position.z = this._vector.z;
    }
    toJSON(): ShapeJSON {
        return this;
    }
    clone(): EmitterShape {
        return new ShapeModule();
    }
    // EmitterShape end

    @serializable()
    shapeType: ParticleSystemShapeType = ParticleSystemShapeType.Box;
    @serializable()
    enabled: boolean = true;
    @serializable()
    alignToDirection: boolean = false;
    @serializable()
    angle: number = 0;
    @serializable()
    arc: number = 360;
    @serializable()
    arcSpread!: number;
    @serializable()
    arcSpeedMultiplier!: number;
    @serializable()
    arcMode!: ParticleSystemShapeMultiModeValue;


    @serializable(Vector3)
    boxThickness!: Vector3;
    @serializable(Vector3)
    position!: Vector3;
    @serializable(Vector3)
    rotation!: Vector3;
    private _rotation: Euler = new Euler();
    @serializable(Vector3)
    scale!: Vector3;

    @serializable()
    radius!: number;
    @serializable()
    radiusThickness!: number;
    @serializable()
    sphericalDirectionAmount!: number;
    @serializable()
    randomDirectionAmount!: number;
    @serializable()
    randomPositionAmount!: number;

    /** Controls if particles should spawn off vertices, faces or edges. `shapeType` must be set to `MeshRenderer` */
    @serializable()
    meshShapeType?: ParticleSystemMeshShapeType;
    /** When assigned and `shapeType` is set to `MeshRenderer` particles will spawn using a mesh in the scene.   
     * Use the `meshShapeType` to choose if particles should be spawned from vertices, faces or edges   
     * To re-assign use the `setMesh` function to cache the mesh and geometry
     * */
    @serializable(MeshRenderer)
    meshRenderer?: MeshRenderer;

    private _meshObj?: Mesh;
    private _meshGeometry?: BufferGeometry;
    setMesh(mesh: MeshRenderer) {
        this.meshRenderer = mesh;
        if (mesh) {
            this._meshObj = mesh.sharedMeshes[Math.floor(Math.random() * mesh.sharedMeshes.length)];
            this._meshGeometry = this._meshObj.geometry;
        }
        else {
            this._meshObj = undefined;
            this._meshGeometry = undefined;
        }
    }

    private system!: IParticleSystem;
    private _space?: ParticleSystemSimulationSpace;
    private readonly _worldSpaceMatrix: Matrix4 = new Matrix4();
    private readonly _worldSpaceMatrixInverse: Matrix4 = new Matrix4();

    constructor() {
        if (debug)
            console.log(this);
    }

    update(_system: QParticleSystem, _delta: number): void {
        /* this is called by quarks */
    }

    onUpdate(system: IParticleSystem, _context: Context, simulationSpace: ParticleSystemSimulationSpace, obj: Object3D) {
        this.system = system;
        this._space = simulationSpace;
        if (simulationSpace === ParticleSystemSimulationSpace.World) {
            this._worldSpaceMatrix.copy(obj.matrixWorld);
            // set scale to 1
            this._worldSpaceMatrix.elements[0] = 1;
            this._worldSpaceMatrix.elements[5] = 1;
            this._worldSpaceMatrix.elements[10] = 1;
            this._worldSpaceMatrixInverse.copy(this._worldSpaceMatrix).invert();
        }
    }

    private applyRotation(vector: Vector3) {
        const isRotated = this.rotation.x !== 0 || this.rotation.y !== 0 || this.rotation.z !== 0;
        if (isRotated) {
            // console.log(this._rotation);
            // TODO: we need to convert this to threejs euler
            this._rotation.x = Mathf.toRadians(this.rotation.x);
            this._rotation.y = Mathf.toRadians(this.rotation.y);
            this._rotation.z = Mathf.toRadians(this.rotation.z);
            this._rotation.order = 'ZYX';
            vector.applyEuler(this._rotation);
            // this._quat.setFromEuler(this._rotation);
            // // this._quat.invert();
            // this._quat.x *= -1;
            // // this._quat.y *= -1;
            // // this._quat.z *= -1;
            // this._quat.w *= -1;
            // vector.applyQuaternion(this._quat);

        }
        return isRotated;
    }

    /** nebula implementations: */

    /** initializer implementation */
    private _vector: Vector3 = new Vector3(0, 0, 0);
    private _temp: Vector3 = new Vector3(0, 0, 0);
    private _triangle: Triangle = new Triangle();

    onInitialize(particle: Particle): void {
        this._vector.set(0, 0, 0);
        // remove mesh from particle in case it got destroyed (we dont want to keep a reference to a destroyed mesh in the particle system)
        particle["mesh"] = undefined;
        particle["mesh_geometry"] = undefined;

        const pos = this._temp.copy(this.position);
        const isWorldSpace = this._space === ParticleSystemSimulationSpace.World;
        if (isWorldSpace) {
            pos.applyQuaternion(this.system.worldQuaternion);
        }
        let radius = this.radius;
        if (isWorldSpace) radius *= this.system.worldScale.x;
        if (this.enabled) {
            switch (this.shapeType) {
                case ParticleSystemShapeType.Box:
                    if (debug) Gizmos.DrawWireBox(this.position, this.scale, 0xdddddd, 1);
                    this._vector.x = Math.random() * this.scale.x - this.scale.x / 2;
                    this._vector.y = Math.random() * this.scale.y - this.scale.y / 2;
                    this._vector.z = Math.random() * this.scale.z - this.scale.z / 2;
                    this._vector.add(pos);
                    break;
                case ParticleSystemShapeType.Cone:
                    this.randomConePoint(this.position, this.angle, radius, this.radiusThickness, this.arc, this.arcMode, this._vector);
                    break;
                case ParticleSystemShapeType.Sphere:
                    this.randomSpherePoint(this.position, radius, this.radiusThickness, this.arc, this._vector);
                    break;
                case ParticleSystemShapeType.Circle:
                    this.randomCirclePoint(this.position, radius, this.radiusThickness, this.arc, this._vector);
                    break;
                case ParticleSystemShapeType.MeshRenderer:
                    const renderer = this.meshRenderer;
                    if (renderer?.destroyed == false) this.setMesh(renderer);
                    const mesh = particle["mesh"] = this._meshObj;
                    const geometry = particle["mesh_geometry"] = this._meshGeometry;
                    if (mesh && geometry) {
                        switch (this.meshShapeType) {
                            case ParticleSystemMeshShapeType.Vertex:
                                {
                                    const vertices = geometry.getAttribute("position");
                                    const index = Math.floor(Math.random() * vertices.count);
                                    this._vector.fromBufferAttribute(vertices, index);
                                    this._vector.applyMatrix4(mesh.matrixWorld);
                                    particle["mesh_normal"] = index;
                                }
                                break;
                            case ParticleSystemMeshShapeType.Edge:
                                break;
                            case ParticleSystemMeshShapeType.Triangle:
                                {
                                    const faces = geometry.index;
                                    if (faces) {
                                        let u = Math.random();
                                        let v = Math.random();
                                        if (u + v > 1) {
                                            u = 1 - u;
                                            v = 1 - v;
                                        }
                                        const faceIndex = Math.floor(Math.random() * (faces.count / 3));
                                        let i0 = faceIndex * 3;
                                        let i1 = faceIndex * 3 + 1;
                                        let i2 = faceIndex * 3 + 2;
                                        i0 = faces.getX(i0);
                                        i1 = faces.getX(i1);
                                        i2 = faces.getX(i2);
                                        const positionAttribute = geometry.getAttribute("position");
                                        this._triangle.a.fromBufferAttribute(positionAttribute, i0);
                                        this._triangle.b.fromBufferAttribute(positionAttribute, i1);
                                        this._triangle.c.fromBufferAttribute(positionAttribute, i2);
                                        this._vector
                                            .set(0, 0, 0)
                                            .addScaledVector(this._triangle.a, u)
                                            .addScaledVector(this._triangle.b, v)
                                            .addScaledVector(this._triangle.c, 1 - (u + v));
                                        this._vector.applyMatrix4(mesh.matrixWorld);
                                        particle["mesh_normal"] = faceIndex;
                                    }
                                }
                                break;
                        }
                    }
                    break;
                default:
                    this._vector.set(0, 0, 0);
                    if (isDevEnvironment() && !globalThis["__particlesystem_shapetype_unsupported"]) {
                        console.warn("ParticleSystem ShapeType is not supported:", ParticleSystemShapeType[this.shapeType]);
                        globalThis["__particlesystem_shapetype_unsupported"] = true;
                    }
                    break;
                // case ParticleSystemShapeType.Hemisphere:
                //     randomSpherePoint(this.position.x, this.position.y, this.position.z, this.radius, this.radiusThickness, 180, this._vector);
                //     break;
            }

            this.randomizePosition(this._vector, this.randomPositionAmount);
        }

        this.applyRotation(this._vector);

        if (isWorldSpace) {
            this._vector.applyQuaternion(this.system.worldQuaternion);
            this._vector.add(this.system.worldPos);
        }

        if (debug) {
            Gizmos.DrawSphere(this._vector, .03, 0xff0000, .5, true);
        }
    }



    private _dir: Vector3 = new Vector3();

    getDirection(particle: Particle, pos: Vec3): Vector3 {
        if (!this.enabled) {
            this._dir.set(0, 0, 1);
            return this._dir;
        }
        switch (this.shapeType) {
            case ParticleSystemShapeType.Box:
                this._dir.set(0, 0, 1);
                break;
            case ParticleSystemShapeType.Cone:
                this._dir.set(0, 0, 1);
                // apply cone angle
                // this._dir.applyAxisAngle(new Vector3(0, 1, 0), Mathf.toRadians(this.angle));
                break;
            case ParticleSystemShapeType.Circle:
            case ParticleSystemShapeType.Sphere:
                const rx = pos.x;
                const ry = pos.y;
                const rz = pos.z;
                this._dir.set(rx, ry, rz)
                if (this.system?.worldspace)
                    this._dir.sub(this.system.worldPos)
                else
                    this._dir.sub(this.position)
                break;
            case ParticleSystemShapeType.MeshRenderer:
                const mesh = particle["mesh"];
                const geometry = particle["mesh_geometry"];
                if (mesh && geometry) {
                    switch (this.meshShapeType) {
                        case ParticleSystemMeshShapeType.Vertex:
                            {
                                const normal = geometry.getAttribute("normal");
                                const index = particle["mesh_normal"];
                                this._dir.fromBufferAttribute(normal, index);
                            }
                            break;
                        case ParticleSystemMeshShapeType.Edge:
                            break;
                        case ParticleSystemMeshShapeType.Triangle:
                            {
                                const faces = geometry.index;
                                if (faces) {
                                    const index = particle["mesh_normal"];
                                    const i0 = faces.getX(index * 3);
                                    const i1 = faces.getX(index * 3 + 1);
                                    const i2 = faces.getX(index * 3 + 2);
                                    const positionAttribute = geometry.getAttribute("position");
                                    const a = getTempVector();
                                    const b = getTempVector();
                                    const c = getTempVector();
                                    a.fromBufferAttribute(positionAttribute, i0);
                                    b.fromBufferAttribute(positionAttribute, i1);
                                    c.fromBufferAttribute(positionAttribute, i2);
                                    a.sub(b);
                                    c.sub(b);
                                    a.cross(c);
                                    this._dir.copy(a).multiplyScalar(-1);
                                    const rot = getWorldQuaternion(mesh);
                                    this._dir.applyQuaternion(rot)
                                }
                            }
                            break;
                    }
                }
                break;
            default:
                this._dir.set(0, 0, 1);
                break;
        }
        if (this._space === ParticleSystemSimulationSpace.World) {
            this._dir.applyQuaternion(this.system.worldQuaternion);
        }
        this.applyRotation(this._dir);
        this._dir.normalize();
        this.spherizeDirection(this._dir, this.sphericalDirectionAmount);
        this.randomizeDirection(this._dir, this.randomDirectionAmount);
        if (debug) {
            Gizmos.DrawSphere(pos, .01, 0x883300, .5, true);
            Gizmos.DrawDirection(pos, this._dir, 0x883300, .5, true);
        }
        return this._dir;
    }

    private static _randomQuat = new Quaternion();
    private static _tempVec = new Vector3();

    private randomizePosition(pos: Vector3, amount: number) {
        if (amount <= 0) return;
        const rp = ShapeModule._tempVec;
        rp.set(Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1);
        rp.x *= amount * this.scale.x;
        rp.y *= amount * this.scale.y;
        rp.z *= amount * this.scale.z;
        pos.add(rp);
    }

    private randomizeDirection(direction: Vector3, amount: number) {
        if (amount === 0) return;
        const randomQuat = ShapeModule._randomQuat;
        const tempVec = ShapeModule._tempVec;
        tempVec.set(Math.random() - .5, Math.random() - .5, Math.random() - .5).normalize();
        randomQuat.setFromAxisAngle(tempVec, amount * Math.random() * Math.PI);
        direction.applyQuaternion(randomQuat);
    }

    private spherizeDirection(dir: Vector3, amount: number) {
        if (amount === 0) return;
        const theta = Math.random() * Math.PI * 2;
        const phi = Math.acos(1 - Math.random() * 2);
        const x = Math.sin(phi) * Math.cos(theta);
        const y = Math.sin(phi) * Math.sin(theta);
        const z = Math.cos(phi);
        const v = new Vector3(x, y, z);
        dir.lerp(v, amount);
    }

    private randomSpherePoint(pos: Vec3, radius: number, thickness: number, arc: number, vec: Vec3) {
        const u = Math.random();
        const v = Math.random();
        const theta = 2 * Math.PI * u * (arc / 360);
        const phi = Math.acos(2 * v - 1);
        const r = Mathf.lerp(1, 1 - (Math.pow(1 - Math.random(), Math.PI)), thickness) * (radius);
        const x = pos.x + this.scale.x * (-r * Math.sin(phi) * Math.cos(theta));
        const y = pos.y + this.scale.y * (r * Math.sin(phi) * Math.sin(theta));
        const z = pos.z + this.scale.z * (r * Math.cos(phi));
        vec.x = x;
        vec.y = y;
        vec.z = z;
    }

    private randomCirclePoint(pos: Vec3, radius: number, thickness: number, arg: number, vec: Vec3) {
        const u = Math.random();
        const theta = 2 * Math.PI * u * (arg / 360);
        const r = Mathf.lerp(1, 1 - (Math.pow(1 - Math.random(), Math.PI)), thickness) * (radius);
        const x = pos.x + this.scale.x * r * Math.cos(theta);
        const y = pos.y + this.scale.y * r * Math.sin(theta);
        const z = pos.z;
        vec.x = x;
        vec.y = y;
        vec.z = z;
    }

    private _loopTime: number = 0;
    private _loopDirection: number = 1;

    private randomConePoint(pos: Vec3, _angle: number, radius: number, thickness: number, arc: number, arcMode: ParticleSystemShapeMultiModeValue, vec: Vec3) {
        let u = 0;
        let v = 0;
        switch (arcMode) {
            case ParticleSystemShapeMultiModeValue.Random:
                u = Math.random();
                v = Math.random();
                break;
            case ParticleSystemShapeMultiModeValue.PingPong:
                if (this._loopTime > 1) this._loopDirection = -1;
                if (this._loopTime < 0) this._loopDirection = 1;
            // continue with loop 

            case ParticleSystemShapeMultiModeValue.Loop:
                u = .5;
                v = Math.random()
                this._loopTime += this.system.deltaTime * this._loopDirection;
                break;
        }

        let theta = 2 * Math.PI * u * (arc / 360);
        switch (arcMode) {
            case ParticleSystemShapeMultiModeValue.PingPong:
            case ParticleSystemShapeMultiModeValue.Loop:
                theta += Math.PI + .5;
                theta += this._loopTime * Math.PI * 2;
                theta %= Mathf.toRadians(arc);
                break;
        }

        const phi = Math.acos(2 * v - 1);
        const r = Mathf.lerp(1, 1 - (Math.pow(1 - Math.random(), Math.PI)), thickness) * radius;
        const x = pos.x + (-r * Math.sin(phi) * Math.cos(theta));
        const y = pos.y + (r * Math.sin(phi) * Math.sin(theta));
        const z = pos.z;
        vec.x = x * this.scale.x;
        vec.y = y * this.scale.y;
        vec.z = z * this.scale.z;
    }
}





export class NoiseModule {
    @serializable()
    damping!: boolean;
    @serializable()
    enabled!: boolean;
    @serializable()
    frequency!: number;
    @serializable()
    octaveCount!: number;
    @serializable()
    octaveMultiplier!: number;
    @serializable()
    octaveScale!: number;
    @serializable(MinMaxCurve)
    positionAmount!: MinMaxCurve;
    @serializable()
    quality!: number;

    @serializable(MinMaxCurve)
    remap!: MinMaxCurve;
    @serializable()
    remapEnabled!: boolean;
    @serializable()
    remapMultiplier!: number;
    @serializable(MinMaxCurve)
    remapX!: MinMaxCurve;
    @serializable()
    remapXMultiplier!: number;
    @serializable(MinMaxCurve)
    remapY!: MinMaxCurve;
    @serializable()
    remapYMultiplier!: number;
    @serializable(MinMaxCurve)
    remapZ!: MinMaxCurve;
    @serializable()
    remapZMultiplier!: number;

    @serializable()
    scrollSpeedMultiplier!: number;
    @serializable()
    separateAxes!: boolean;
    @serializable()
    strengthMultiplier!: number;
    @serializable(MinMaxCurve)
    strengthX!: MinMaxCurve;
    @serializable()
    strengthXMultiplier!: number;
    @serializable(MinMaxCurve)
    strengthY!: MinMaxCurve;
    @serializable()
    strengthYMultiplier!: number;
    @serializable(MinMaxCurve)
    strengthZ!: MinMaxCurve;
    @serializable()
    strengthZMultiplier!: number;


    private _noise?: NoiseFunction4D;
    private _time: number = 0;

    update(context: Context) {
        this._time += context.time.deltaTime * this.scrollSpeedMultiplier;
    }

    /** nebula implementations: */
    private _temp: Vector3 = new Vector3();
    apply(_index: number, pos: Vec3, vel: Vec3, _deltaTime: number, age: number, life: number) {
        if (!this.enabled) return;
        if (!this._noise) {
            this._noise = createNoise4D(() => 0);
        }
        const temp = this._temp.set(pos.x, pos.y, pos.z).multiplyScalar(this.frequency);
        const nx = this._noise(temp.x, temp.y, temp.z, this._time);
        const ny = this._noise(temp.x, temp.y, temp.z, this._time + 1000 * this.frequency);
        const nz = this._noise(temp.x, temp.y, temp.z, this._time + 2000 * this.frequency);
        this._temp.set(nx, ny, nz).normalize()

        const t = age / life;
        let strengthFactor = this.positionAmount.evaluate(t);
        if (!this.separateAxes) {
            if (this.strengthX) {
                strengthFactor *= this.strengthX.evaluate(t) * 1.5;
            }
            // strengthFactor *= this.strengthMultiplier;
            // strengthFactor *= deltaTime;
            this._temp.multiplyScalar(strengthFactor);
        }
        else {
            this._temp.x *= strengthFactor * this.strengthXMultiplier
            this._temp.y *= strengthFactor * this.strengthYMultiplier;
            this._temp.z *= strengthFactor * this.strengthZMultiplier;
        }
        // this._temp.setLength(strengthFactor * deltaTime);
        vel.x += this._temp.x;
        vel.y += this._temp.y;
        vel.z += this._temp.z;
    }
}

export enum ParticleSystemTrailMode {
    PerParticle,
    Ribbon,
}

export enum ParticleSystemTrailTextureMode {
    Stretch = 0,
    Tile = 1,
    DistributePerSegment = 2,
    RepeatPerSegment = 3,
}

export class TrailModule {

    @serializable()
    enabled!: boolean;

    @serializable()
    attachRibbonToTransform = false;

    @serializable(MinMaxGradient)
    colorOverLifetime!: MinMaxGradient;

    @serializable(MinMaxGradient)
    colorOverTrail!: MinMaxGradient;

    @serializable()
    dieWithParticles: boolean = true;

    @serializable()
    inheritParticleColor: boolean = true;

    @serializable(MinMaxCurve)
    lifetime!: MinMaxCurve;
    @serializable()
    lifetimeMultiplier!: number;

    @serializable()
    minVertexDistance: number = .2;

    @serializable()
    mode: ParticleSystemTrailMode = ParticleSystemTrailMode.PerParticle;

    @serializable()
    ratio: number = 1;

    @serializable()
    ribbonCount: number = 1;

    @serializable()
    shadowBias: number = 0;

    @serializable()
    sizeAffectsLifetime: boolean = false;

    @serializable()
    sizeAffectsWidth: boolean = false;

    @serializable()
    splitSubEmitterRibbons: boolean = false;

    @serializable()
    textureMode: ParticleSystemTrailTextureMode = ParticleSystemTrailTextureMode.Stretch;

    @serializable(MinMaxCurve)
    widthOverTrail!: MinMaxCurve;
    @serializable()
    widthOverTrailMultiplier!: number;

    @serializable()
    worldSpace: boolean = false;

    getWidth(size: number, _life01: number, pos01: number, t: number) {
        const res = this.widthOverTrail.evaluate(pos01, t);
        size *= res;
        return size;
    }

    getColor(color: Vector4 | QVector4, life01: number, pos01: number) {
        const overTrail = this.colorOverTrail.evaluate(pos01);
        const overLife = this.colorOverLifetime.evaluate(life01);
        color.x *= overTrail.r * overLife.r;
        color.y *= overTrail.g * overLife.g;
        color.z *= overTrail.b * overLife.b;
        if ("alpha" in overTrail && "alpha" in overLife)
            color.w *= overTrail.alpha * overLife.alpha;
    }
}

export class VelocityOverLifetimeModule {
    @serializable()
    enabled!: boolean;

    @serializable()
    space: ParticleSystemSimulationSpace = ParticleSystemSimulationSpace.Local;

    @serializable(MinMaxCurve)
    orbitalX!: MinMaxCurve;
    @serializable(MinMaxCurve)
    orbitalY!: MinMaxCurve;
    @serializable(MinMaxCurve)
    orbitalZ!: MinMaxCurve;

    @serializable()
    orbitalXMultiplier!: number;
    @serializable()
    orbitalYMultiplier!: number;
    @serializable()
    orbitalZMultiplier!: number;

    @serializable()
    orbitalOffsetX!: number;
    @serializable()
    orbitalOffsetY!: number;
    @serializable()
    orbitalOffsetZ!: number;

    @serializable(MinMaxCurve)
    speedModifier!: MinMaxCurve;
    @serializable()
    speedModifierMultiplier!: number;
    @serializable(MinMaxCurve)
    x!: MinMaxCurve;
    @serializable()
    xMultiplier!: number;
    @serializable(MinMaxCurve)
    y!: MinMaxCurve;
    @serializable()
    yMultiplier!: number;
    @serializable(MinMaxCurve)
    z!: MinMaxCurve;
    @serializable()
    zMultiplier!: number;

    private _system?: IParticleSystem;
    // private _worldRotation: Quaternion = new Quaternion();

    update(system: IParticleSystem) {
        this._system = system;
    }

    private _temp: Vector3 = new Vector3();
    private _temp2: Vector3 = new Vector3();
    private _temp3: Vector3 = new Vector3();
    private _hasOrbital = false;
    private _index = 0;
    private _orbitalMatrix: Matrix4 = new Matrix4();

    init(particle: object) {
        if (this._index == 0) particle["debug"] = true;
        this._index += 1;
        particle["orbitx"] = this.orbitalX.evaluate(Math.random());
        particle["orbity"] = this.orbitalY.evaluate(Math.random());
        particle["orbitz"] = this.orbitalZ.evaluate(Math.random());
        // console.log(particle["orbitx"], particle["orbity"], particle["orbitz"])
        this._hasOrbital = particle["orbitx"] != 0 || particle["orbity"] != 0 || particle["orbitz"] != 0;
    }

    apply(_particle: object, _index: number, _pos: Vec3, vel: Vec3, _dt: number, age: number, life: number) {
        if (!this.enabled) return;
        const t = age / life;

        const speed = this.speedModifier.evaluate(t) * this.speedModifierMultiplier;
        const x = this.x.evaluate(t);
        const y = this.y.evaluate(t);
        const z = this.z.evaluate(t);
        this._temp.set(-x, y, z);
        if (this._system) {
            // if (this.space === ParticleSystemSimulationSpace.World) {
            //     this._temp.applyQuaternion(this._system.worldQuaternionInverted);
            // }
            if (this._system.main.simulationSpace === ParticleSystemSimulationSpace.World) {
                this._temp.applyQuaternion(this._system.worldQuaternion);
            }
        }

        if (this._hasOrbital) {
            const position = this._system?.worldPos;
            if (position) {

                // TODO: we absolutely need to fix this, this is a hack for a specific usecase and doesnt work yet correctly
                // https://github.com/needle-tools/needle-tiny/issues/710

                const pos = this._temp2.set(_pos.x, _pos.y, _pos.z);

                const ox = this.orbitalXMultiplier;// particle["orbitx"];
                const oy = this.orbitalYMultiplier;// particle["orbity"];
                const oz = this.orbitalZMultiplier;// particle["orbitz"];
                const angle = speed * Math.PI * 2 * 10; // < Oh god

                const cosX = Math.cos(angle * ox);
                const sinX = Math.sin(angle * ox);
                const cosY = Math.cos(angle * oy);
                const sinY = Math.sin(angle * oy);
                const cosZ = Math.cos(angle * oz);
                const sinZ = Math.sin(angle * oz);

                const newX = pos.x * (cosY * cosZ) + pos.y * (cosY * sinZ) + pos.z * (-sinY);
                const newY = pos.x * (sinX * sinY * cosZ - cosX * sinZ) + pos.y * (sinX * sinY * sinZ + cosX * cosZ) + pos.z * (sinX * cosY);
                const newZ = pos.x * (cosX * sinY * cosZ + sinX * sinZ) + pos.y * (cosX * sinY * sinZ - sinX * cosZ) + pos.z * (cosX * cosY);

                // pos.x += this.orbitalOffsetX;
                // pos.y += this.orbitalOffsetY;
                // pos.z += this.orbitalOffsetZ;
                const v = this._temp3.set(pos.x - newX, pos.y - newY, pos.z - newZ);
                v.normalize();
                v.multiplyScalar(.2 / _dt * (Math.max(this.orbitalXMultiplier, this.orbitalYMultiplier, this.orbitalZMultiplier)));
                vel.x += v.x;
                vel.y += v.y;
                vel.z += v.z;
            }
        }

        vel.x += this._temp.x;
        vel.y += this._temp.y;
        vel.z += this._temp.z;
        vel.x *= speed;
        vel.y *= speed;
        vel.z *= speed;
    }
}



enum ParticleSystemAnimationTimeMode {
    Lifetime,
    Speed,
    FPS,
}

enum ParticleSystemAnimationMode {
    Grid,
    Sprites,
}

enum ParticleSystemAnimationRowMode {
    Custom,
    Random,
    MeshIndex,
}

enum ParticleSystemAnimationType {
    WholeSheet,
    SingleRow,
}

export class TextureSheetAnimationModule {

    @serializable()
    animation!: ParticleSystemAnimationType;

    @serializable()
    enabled!: boolean;

    @serializable()
    cycleCount!: number;

    @serializable(MinMaxCurve)
    frameOverTime!: MinMaxCurve;
    @serializable()
    frameOverTimeMultiplier!: number;

    @serializable()
    numTilesX!: number;
    @serializable()
    numTilesY!: number;

    @serializable(MinMaxCurve)
    startFrame!: MinMaxCurve;
    @serializable()
    startFrameMultiplier!: number;

    @serializable()
    rowMode!: ParticleSystemAnimationRowMode;
    @serializable()
    rowIndex!: number;

    @serializable()
    spriteCount!: number;

    @serializable()
    timeMode!: ParticleSystemAnimationTimeMode;

    private sampleOnceAtStart(): boolean {
        if (this.timeMode === ParticleSystemAnimationTimeMode.Lifetime) {
            switch (this.frameOverTime.mode) {
                case ParticleSystemCurveMode.Constant:
                case ParticleSystemCurveMode.TwoConstants:
                case ParticleSystemCurveMode.TwoCurves:
                case ParticleSystemCurveMode.Curve:
                    return true;
            }
        }
        return false;
    }

    getStartIndex(): number {
        if (this.sampleOnceAtStart()) {
            const start = Math.random();
            return start * (this.numTilesX * this.numTilesY);
        }
        return 0;
    }

    evaluate(t01: number): number | undefined {
        if (this.sampleOnceAtStart()) {
            return undefined;
        }
        return this.getIndex(t01);
    }

    private getIndex(t01: number): number {
        const tiles = this.numTilesX * this.numTilesY;
        t01 = t01 * this.cycleCount;
        let index = this.frameOverTime.evaluate(t01 % 1);
        index *= this.frameOverTimeMultiplier;
        index *= tiles;
        index = index % tiles;
        index = Math.floor(index);
        return index;
    }
}


export class RotationOverLifetimeModule {
    @serializable()
    enabled!: boolean;

    @serializable()
    separateAxes!: boolean;

    @serializable(MinMaxCurve)
    x!: MinMaxCurve;
    @serializable()
    xMultiplier!: number;
    @serializable(MinMaxCurve)
    y!: MinMaxCurve;
    @serializable()
    yMultiplier!: number;
    @serializable(MinMaxCurve)
    z!: MinMaxCurve;
    @serializable()
    zMultiplier!: number;

    evaluate(t01: number, t: number): number {
        if (!this.enabled) return 0;
        if (!this.separateAxes) {
            const rot = this.z.evaluate(t01, t) * -1;
            return rot;
        }
        return 0;
    }
}

export class RotationBySpeedModule {
    @serializable()
    enabled!: boolean;

    @serializable()
    range!: Vec2;

    @serializable()
    separateAxes!: boolean;

    @serializable(MinMaxCurve)
    x!: MinMaxCurve;
    @serializable()
    xMultiplier!: number;
    @serializable(MinMaxCurve)
    y!: MinMaxCurve;
    @serializable()
    yMultiplier!: number;
    @serializable(MinMaxCurve)
    z!: MinMaxCurve;
    @serializable()
    zMultiplier!: number;

    evaluate(_t01: number, speed: number): number {
        if (!this.enabled) return 0;
        if (!this.separateAxes) {
            const t = Mathf.lerp(this.range.x, this.range.y, speed);
            const rot = this.z.evaluate(t) * -1;
            return rot;
        }
        return 0;
    }
}


export class LimitVelocityOverLifetimeModule {
    @serializable()
    enabled!: boolean;

    @serializable()
    dampen!: number;

    @serializable(MinMaxCurve)
    drag!: MinMaxCurve;
    @serializable()
    dragMultiplier!: number;

    @serializable(MinMaxCurve)
    limit!: MinMaxCurve;
    @serializable()
    limitMultiplier!: number;

    @serializable()
    separateAxes!: boolean;

    @serializable(MinMaxCurve)
    limitX!: MinMaxCurve;
    @serializable()
    limitXMultiplier!: number;
    @serializable(MinMaxCurve)
    limitY!: MinMaxCurve;
    @serializable()
    limitYMultiplier!: number;
    @serializable(MinMaxCurve)
    limitZ!: MinMaxCurve;
    @serializable()
    limitZMultiplier!: number;

    @serializable()
    multiplyDragByParticleSize: boolean = false;
    @serializable()
    multiplyDragByParticleVelocity: boolean = false;

    @serializable()
    space!: ParticleSystemSimulationSpace;

    private _temp: Vector3 = new Vector3();
    private _temp2: Vector3 = new Vector3();

    apply(_position: Vec3, baseVelocity: Vector3, currentVelocity: Vector3 | QVector3, _size: QVector3, t01: number, _dt: number, _scale: number) {
        if (!this.enabled) return;
        // if (this.separateAxes) {
        //     // const maxX = this.limitX.evaluate(t01) * this.limitXMultiplier;
        //     // const maxY = this.limitY.evaluate(t01) * this.limitYMultiplier;
        //     // const maxZ = this.limitZ.evaluate(t01) * this.limitZMultiplier;

        // }
        // else 
        {
            const max = this.limit.evaluate(t01) * this.limitMultiplier;
            const speed = baseVelocity.length();
            if (speed > max) {
                this._temp.copy(baseVelocity).normalize().multiplyScalar(max);
                const t = this.dampen * .5;
                // t *= scale;
                baseVelocity.x = Mathf.lerp(baseVelocity.x, this._temp.x, t);
                baseVelocity.y = Mathf.lerp(baseVelocity.y, this._temp.y, t);
                baseVelocity.z = Mathf.lerp(baseVelocity.z, this._temp.z, t);

                // this._temp2.set(0, 0, 0);
                currentVelocity.x = Mathf.lerp(currentVelocity.x, this._temp.x, t);
                currentVelocity.y = Mathf.lerp(currentVelocity.y, this._temp.y, t);
                currentVelocity.z = Mathf.lerp(currentVelocity.z, this._temp.z, t);
            }
            // vel.multiplyScalar(dragFactor);
        }
        // vel.x *= 0.3;
        // vel.y *= 0.3;
        // vel.z *= 0.3;
    }
}


export enum ParticleSystemInheritVelocityMode {
    Initial,
    Current,
}

export class InheritVelocityModule {

    @serializable()
    enabled!: boolean;

    @serializable(MinMaxCurve)
    curve!: MinMaxCurve;
    @serializable()
    curveMultiplier!: number;

    @serializable()
    mode!: ParticleSystemInheritVelocityMode;

    clone() {
        const ni = new InheritVelocityModule();
        ni.enabled = this.enabled;
        ni.curve = this.curve?.clone();
        ni.curveMultiplier = this.curveMultiplier;
        ni.mode = this.mode;
        return ni;
    }

    system!: IParticleSystem;

    private get _lastWorldPosition() {
        if (!this.system['_iv_lastWorldPosition']) {
            this.system['_iv_lastWorldPosition'] = new Vector3();
        }
        return this.system['_iv_lastWorldPosition'];
    }
    private get _velocity() {
        if (!this.system['_iv_velocity']) {
            this.system['_iv_velocity'] = new Vector3();
        }
        return this.system['_iv_velocity'];
    }

    private readonly _temp: Vector3 = new Vector3();
    private _firstUpdate: boolean = true;

    awake(system: IParticleSystem) {
        this.system = system;
        this.reset();
    }

    reset() {
        this._firstUpdate = true;
    }

    update(_context: Context) {
        if (!this.enabled) return;
        if (this.system.worldspace === false) return;
        if (this._firstUpdate) {
            this._firstUpdate = false;
            this._velocity.set(0, 0, 0);
            this._lastWorldPosition.copy(this.system.worldPos);
        }
        else if (this._lastWorldPosition) {
            this._velocity.copy(this.system.worldPos).sub(this._lastWorldPosition).multiplyScalar(1 / this.system.deltaTime);
            this._lastWorldPosition.copy(this.system.worldPos);
        }
    }

    // TODO: make work for subsystems
    applyInitial(vel: Vector3 | QVector3) {
        if (!this.enabled) return;
        if (this.system.worldspace === false) return;
        if (this.mode === ParticleSystemInheritVelocityMode.Initial) {
            const factor = this.curve.evaluate(Math.random(), Math.random());
            this._temp.copy(this._velocity).multiplyScalar(factor);
            vel.x += this._temp.x;
            vel.y += this._temp.y;
            vel.z += this._temp.z;
        }
    }

    private _frames = 0;
    applyCurrent(vel: Vector3 | QVector3, t01: number, lerpFactor: number) {
        if (!this.enabled) return;
        if (!this.system) return;
        if (this.system.worldspace === false) return;
        if (this.mode === ParticleSystemInheritVelocityMode.Current) {
            const factor = this.curve.evaluate(t01, lerpFactor);
            this._temp.copy(this._velocity).multiplyScalar(factor);
            vel.x += this._temp.x;
            vel.y += this._temp.y;
            vel.z += this._temp.z;
        }
    }
}


export class SizeBySpeedModule {
    @serializable()
    enabled!: boolean;

    @serializable(Vector2)
    range!: Vector2;
    @serializable()
    separateAxes!: boolean;

    @serializable(MinMaxCurve)
    size!: MinMaxCurve;
    @serializable()
    sizeMultiplier!: number;

    @serializable(MinMaxCurve)
    x!: MinMaxCurve;
    @serializable()
    xMultiplier!: number;
    @serializable(MinMaxCurve)
    y!: MinMaxCurve;
    @serializable()
    yMultiplier!: number;
    @serializable(MinMaxCurve)
    z!: MinMaxCurve;
    @serializable()
    zMultiplier!: number;

    evaluate<T extends Vector3 | QVector3>(vel: T, _t01: number, lerpFactor: number, size: T): T {

        const speed = vel.length();
        const x = Mathf.remap(speed, this.range.x, this.range.y, 0, 1);
        const factor = this.size.evaluate(x, lerpFactor);
        // return size;
        size.x *= factor;
        size.y *= factor;
        size.z *= factor;
        return size;
    }
}

export class ColorBySpeedModule {
    @serializable()
    enabled!: boolean;
    @serializable(Vector2)
    range!: Vector2;
    @serializable(MinMaxGradient)
    color!: MinMaxGradient;

    evaluate<T extends Vector3 | QVector3>(vel: T, lerpFactor: number, color: Vector4 | QVector4) {
        const speed = vel.length();
        const x = Mathf.remap(speed, this.range.x, this.range.y, 0, 1);
        const res = this.color.evaluate(x, lerpFactor);
        color.x *= res.r;
        color.y *= res.g;
        color.z *= res.b;
        if ("alpha" in res)
            color.w *= res.alpha;
    }
}