import { BVH } from "../../../../../core/bvh2/bvh3/BVH.js";
import List from "../../../../../core/collection/list/List.js";
import { ParticleEmitterFlag } from "./emitter/ParticleEmitterFlag.js";
import { ShaderManager } from "./shader/ShaderManager.js";
import { SimulationStepApplyForce } from "./simulator/SimulationStepApplyForce.js";
import { SimulationStepCurlNoiseAcceleration } from "./simulator/SimulationStepCurlNoiseAcceleration.js";
import { SimulationStepCurlNoiseVelocity } from "./simulator/SimulationStepCurlNoiseVelocity.js";
import { SimulationStepFixedPhysics } from "./simulator/SimulationStepFixedPhysics.js";


export class ParticularEngine {
    /**
     *
     * @type {THREE.Camera|null}
     */
    camera = null;

    /**
     * Managed emitters
     * @type {List.<ParticleEmitter>}
     */
    emitters = new List();

    /**
     * @readonly
     * @type {BVH}
     */
    bvh = new BVH();


    /**
     *
     * @type {AbstractSimulationStep[]}
     */
    steps = [
        new SimulationStepFixedPhysics(),
        new SimulationStepCurlNoiseAcceleration(),
        new SimulationStepCurlNoiseVelocity(),
        new SimulationStepApplyForce()
    ];

    /**
     *
     * @param {AssetManager} assetManager
     * @param {MaterialManager} material_manager
     * @constructor
     */
    constructor(assetManager, material_manager) {
        this.shaderManager = new ShaderManager(assetManager, material_manager);
    }

    /**
     *
     * @param {ParticleEmitter} emitter
     */
    add(emitter) {
        if (!emitter.getFlag(ParticleEmitterFlag.Built)) {
            emitter.build();
        }

        //mark sprites for update as their UVs might have changed since last usage
        emitter.setFlag(ParticleEmitterFlag.SpritesNeedUpdate);

        this.emitters.add(emitter);

        this.shaderManager.register(emitter);

        emitter.bvh_leaf.link(this.bvh, emitter.id);
    }

    /**
     *
     * @param {ParticleEmitter} emitter
     * @returns {boolean}
     */
    remove(emitter) {

        const removed = this.emitters.removeOneOf(emitter);

        if (!removed) {
            return false;
        }

        this.shaderManager.deregister(emitter);

        emitter.bvh_leaf.unlink();

        return true;
    }

    /**
     *
     * @param {THREE.Camera} camera
     */
    setCamera(camera) {
        this.camera = camera;
        this.shaderManager.setCamera(camera);
    }

    setDepthTexture(texture) {
        this.shaderManager.setDepthTexture(texture);
    }

    setViewportSize(x, y) {
        this.shaderManager.setViewportSize(x, y);
    }

    /**
     * @private
     * @param {ParticleEmitter} emitter
     * @param {number} timeDelta
     */
    advanceEmitter(emitter, timeDelta) {
        let i, j;


        /**
         *
         * @type {List<ParticleLayer>}
         */
        const layers = emitter.layers;

        const layer_count = layers.length;

        let step_mask = 0;

        const steps = this.steps;

        for (j = 0; j < layer_count; j++) {
            const particleLayer = layers.get(j);

            const simulationStepDefinitions = particleLayer.steps;

            const n = simulationStepDefinitions.length;

            for (i = 0; i < n; i++) {
                /**
                 *
                 * @type {SimulationStepDefinition}
                 */
                const stepDefinition = simulationStepDefinitions.get(i);

                const type = stepDefinition.type;

                step_mask |= 1 << type;

                const step = steps[type];

                if (step === undefined) {
                    // no step definition
                    continue;
                }

                step.emitter = emitter;
                step.timeDelta = timeDelta;
                step.particles = emitter.particles;
                step.layer_parameters[j] = stepDefinition.parameters;
                step.includeLayer(j);

            }
        }

        // apply simulation steps
        const step_index_limit = steps.length;

        for (i = 0; i < step_index_limit; i++) {
            const m = 1 << i;
            const is_used = (step_mask & m) !== 0;

            if (is_used) {
                const step = steps[i];

                step.layer_count = layer_count;

                step.execute();

                step.clear();
            }
        }

        emitter.advance(timeDelta);

    }

    /**
     * @private
     * @param {ParticleEmitter} emitter
     * @param {number} timeDelta
     */
    updateEmitter(emitter, timeDelta) {
        if (emitter.getFlag(ParticleEmitterFlag.Sleeping)) {
            emitter.sleepTime += timeDelta;
        } else {

            if (!emitter.getFlag(ParticleEmitterFlag.Initialized)) {
                emitter.initialize();
            }

            if (emitter.sleepTime > 0) {
                //emitter was sleeping, need to catch up the simulation
                const maxParticleLife = emitter.computeMaxEmittingParticleLife();

                let wakingTime = Math.min(emitter.sleepTime, maxParticleLife - timeDelta);

                const minWakingIncrement = 0.15;

                const maxWakingSteps = 10;

                const wakingIncrement = Math.max(wakingTime / maxWakingSteps, minWakingIncrement);

                while (wakingTime > 0) {
                    const wakingStep = Math.min(wakingIncrement, wakingTime);

                    this.advanceEmitter(emitter, wakingStep);

                    wakingTime -= wakingStep;
                }

                //consume the sleep time
                emitter.sleepTime = 0;
            }

            //advance simulation
            this.advanceEmitter(emitter, timeDelta);

            //update bounding box
            emitter.computeBoundingBox();
        }
    }

    /**
     *
     * @param {number} timeDelta
     */
    advance(timeDelta) {
        const emitters = this.emitters;

        const numEmitters = emitters.length;

        for (let i = 0; i < numEmitters; i++) {
            const emitter = emitters.get(i);
            this.updateEmitter(emitter, timeDelta);
        }
    }
}
