import { ParticleEmitterFlag } from "../../emitter/ParticleEmitterFlag.js";
import { PARTICLE_ATTRIBUTE_DEATH_AGE, PARTICLE_ATTRIBUTE_POSITION } from "../../emitter/PARTICLE_ATTRIBUTES.js";
import { Matrix4 } from "three";
import { composeMatrix4 } from "../../../../../util/composeMatrix4.js";
import Quaternion from "../../../../../../../core/geom/Quaternion.js";
import { ParticleEmitter } from "../../emitter/ParticleEmitter.js";
import { Transform } from "../../../../../../ecs/transform/Transform.js";
import Entity from "../../../../../../ecs/Entity.js";
import { vec3 } from "gl-matrix";
import { AttributeValue } from "./AttributeValue.js";
import { PARTICULAR_PARTICLE_SPECIFICATION } from "../../emitter/PARTICULAR_PARTICLE_SPECIFICATION.js";
import { SerializationMetadata } from "../../../../../../ecs/components/SerializationMetadata.js";
import { UnitCubeShape3D } from "../../../../../../../core/geom/3d/shape/UnitCubeShape3D.js";
import { shape_from_json } from "../../../../../../../core/geom/3d/shape/json/shape_from_json.js";
import { min2 } from "../../../../../../../core/math/min2.js";
import { max2 } from "../../../../../../../core/math/max2.js";
import { SamplingFunctionKind } from "./SamplingFunctionKind.js";
import { assert } from "../../../../../../../core/assert.js";

const DEFAULT_ATTRIBUTE_VALUES = {
    color: [1, 1, 1, 1],
    age: [0],
    velocity: [0, 0, 0],
    deathAge: [99999999],
    layerPosition: [0],
    rotation: [0],
    rotationSpeed: [0],
    blending: [0],
    size: [1],
    initial_size: [1]
};

const EXPOSED_ATTRIBUTES = [
    "color",
    "size",
    "blending"
];

/**
 * Maximum allowed number of particles per emitter, used as a sanity constraint to prevent renderer from crashing
 * @type {number}
 */
const LIMIT_PARTICLE_COUNT = 1000000;

export class ParticleVolume {

    constructor() {
        this.__emitter = new ParticleEmitter();

        this.__emitter.fromJSON({
            "parameters": [
                {
                    "name": "scale",
                    "itemSize": 1,
                    "defaultTrackValue": {
                        "itemSize": 1,
                        "data": [
                            1
                        ],
                        "positions": [
                            0
                        ]
                    }
                },
                {
                    "name": "color",
                    "itemSize": 4,
                    "defaultTrackValue": {
                        "itemSize": 4,
                        "data": [
                            1,
                            1,
                            1,
                            1
                        ],
                        "positions": [
                            0
                        ]
                    }
                }
            ],
            "preWarm": false,
            "readDepth": true,
            "softDepth": true,
            "depthSort": false,
            "velocityAlign": false,
            "receiveLight": true,
            "blendingMode": 0,
            "layers": [
                {
                    "imageURL": "data/textures/particle/smokeparticle.png",
                    "particleLife": {
                        "min": 6,
                        "max": 8
                    },
                    "particleSize": {
                        "min": 0.4,
                        "max": 0.4
                    },
                    "particleRotation": {
                        "min": 0,
                        "max": 0
                    },
                    "particleRotationSpeed": {
                        "min": 0,
                        "max": 0
                    },
                    "emissionShape": 1,
                    "emissionFrom": 1,
                    "emissionRate": 0,
                    "emissionImmediate": 0,
                    "parameterTracks": [
                        {
                            "name": "color",
                            "track": {
                                "itemSize": 4,
                                "data": [
                                    1,
                                    1,
                                    1,
                                    1,
                                ],
                                "positions": [
                                    0
                                ]
                            }
                        },
                        {
                            "name": "scale",
                            "track": {
                                "itemSize": 1,
                                "data": [
                                    1
                                ],
                                "positions": [
                                    0
                                ]
                            }
                        }
                    ],
                    "position": {
                        "x": 0,
                        "y": 0,
                        "z": 0
                    },
                    "scale": {
                        "x": 1,
                        "y": 1,
                        "z": 1
                    },
                    "particleVelocityDirection": {
                        "direction": {
                            "x": 0,
                            "y": 1,
                            "z": 0
                        },
                        "angle": 0.1
                    },
                    "particleSpeed": {
                        "min": 0,
                        "max": 0
                    },
                    "steps": []
                }
            ]
        });

        /**
         *
         * @type {Transform}
         * @private
         */
        this.__transform = new Transform();

        /**
         * Particle count per unit of volume
         * @type {number}
         * @private
         */
        this.__density = 1;

        /**
         *
         * @type {AbstractShape3D}
         * @private
         */
        this.__shape = new UnitCubeShape3D();

        /**
         *
         * @type {SamplingFunctionKind|number}
         * @private
         */
        this.__sampling_function = SamplingFunctionKind.Random;

        const eb = new Entity();

        eb.add(this.__emitter)
            .add(SerializationMetadata.Transient)
            .add(this.__transform);

        /**
         *
         * @type {Entity}
         */
        this.entity = eb;

        /**
         *
         * @type {Object<AttributeValue>}
         */
        this.attributes = {};

        this.init_attributes();
    }

    get shape() {
        return this.__shape;
    }

    /**
     * @private
     */
    init_attributes() {
        const spec = PARTICULAR_PARTICLE_SPECIFICATION;

        const attributeSpecs = spec.attributes;
        const attribute_count = attributeSpecs.length;
        for (let i = 0; i < attribute_count; i++) {
            const attributeSpec = attributeSpecs[i];

            const av = new AttributeValue();
            av.spec = attributeSpec;

            const attribute_name = attributeSpec.name;

            if (!EXPOSED_ATTRIBUTES.includes(attribute_name)) {
                // don't expose attribute
                continue;
            }

            const default_value = DEFAULT_ATTRIBUTE_VALUES[attribute_name];

            if (default_value !== undefined) {
                av.write_value(default_value);
            }

            this.attributes[attribute_name] = av;

            const attribute_index = i;

            av.onValueChange.add(() => {
                this.__write_attribute(attribute_index);
            });
        }
    }

    get emitter() {
        return this.__emitter;
    }

    get transform() {
        return this.__transform;
    }

    /**
     *
     * @param {number} [density]
     * @param {number} [particle_size]
     * @param {AbstractShape3D} [shape]
     * @param {boolean} sort
     * @param {boolean} lighting
     * @param {string} [sprite]
     * @param {SamplingFunctionKind|number} [sampling_function]
     */
    from({
             density = 1,
             particle_size = 1,
             shape = new UnitCubeShape3D(),
             sort = false,
             lighting = true,
             sprite = "data/textures/particle/smokeparticle.png",
             sampling_function = SamplingFunctionKind.Random
         } = {}) {

        assert.enum(sampling_function, SamplingFunctionKind, 'sampling_function');

        this.__density = density;
        this.__shape = shape;

        this.__sampling_function = sampling_function;

        const emitter = this.__emitter;

        emitter.writeFlag(ParticleEmitterFlag.DepthSorting, sort);
        emitter.writeFlag(ParticleEmitterFlag.Lit, lighting);

        emitter.layers.get(0).imageURL = sprite;

        const attributes = this.attributes;

        attributes.size.write_value_silent([particle_size]);
    }

    /**
     *
     * @param {number} density
     * @param {boolean} [is_density_size_adjusted]
     * @param {number} [particle_size]
     * @param {Object} shape
     * @param {boolean} [is_sorted]
     * @param {boolean} [is_lit]
     * @param {string} sprite
     * @param {Object<Object>} [attributes]
     * @param {string} [sampling_function]
     */
    fromJSON({
                 density,
                 is_density_size_adjusted = true,
                 particle_size = 1,
                 shape,
                 is_sorted = true,
                 is_lit = true,
                 sprite,
                 attributes = {},
                 sampling_function = 'Random'
             }) {

        const shape_object = shape_from_json(shape);

        const actual_density = is_density_size_adjusted ? density / particle_size : density;

        const sf_type = SamplingFunctionKind[sampling_function];

        if (sf_type === undefined) {
            throw new Error(`Unsupported sampling function type '${sampling_function}', valid values:[${Object.keys(SamplingFunctionKind).join(', ')}]`);
        }

        this.from({
            density: actual_density,
            shape: shape_object,
            particle_size,
            sort: is_sorted,
            lighting: is_lit,
            sprite,
            sampling_function: sf_type
        });

        // parse attributes
        for (const attributesKey in attributes) {
            const attribute_object = this.attributes[attributesKey];

            if (attribute_object === undefined) {
                console.warn(`No '${attributesKey}' attribute, skipping`);
            }

            const attribute_json = attributes[attributesKey];
            attribute_object.fromJSON(attribute_json);
        }
    }

    /**
     *
     * @private
     */
    __ensure_initialized() {

        const emitter = this.__emitter;

        if (!emitter.getFlag(ParticleEmitterFlag.Built)) {
            //particle pool needs to be set up first
            emitter.build();
        }

        if (!emitter.getFlag(ParticleEmitterFlag.Initialized)) {
            //particles are not initialized, do that
            emitter.initialize();
        }

    }

    __distribute_grid() {
        const emitter = this.__emitter;
        const particles = emitter.particles;

        const layer = emitter.layers.get(0);

        // build transform matrix
        const m4_transform = new Matrix4();

        const t = this.__transform;

        composeMatrix4(m4_transform, t.position, t.rotation, t.scale);

        const m4_layer = new Matrix4();

        composeMatrix4(m4_layer, layer.position, new Quaternion(), layer.scale);

        const v3_position = [];

        const m4_final = new Matrix4();

        m4_final.multiplyMatrices(m4_transform, m4_layer);


        // cache variables to save a bit on de-referencing
        const matrix_elements = m4_final.elements;
        const shape = this.__shape;

        const ts = shape;

        const bounding_box = [];

        ts.compute_bounding_box(bounding_box);

        const density_step = 1 / Math.pow(this.__density, 1 / 3);

        let particle_index = 0;

        // apply margin to the space, this will shrink the grid spacing to align particles more evenly within the bounding volume
        const density_margin = density_step * 0.5;

        const mid_x = (bounding_box[3] + bounding_box[0]) * 0.5;
        const mid_y = (bounding_box[4] + bounding_box[1]) * 0.5;
        const mid_z = (bounding_box[5] + bounding_box[2]) * 0.5;

        const x0 = min2(mid_x, bounding_box[0] + density_margin);
        const x1 = max2(mid_x, bounding_box[3] - density_margin);

        const y0 = min2(mid_y, bounding_box[1] + density_margin);
        const y1 = max2(mid_y, bounding_box[4] - density_margin);

        const z0 = min2(mid_z, bounding_box[2] + density_margin);
        const z1 = max2(mid_z, bounding_box[5] - density_margin);

        main_loop:for (let x = x0; x <= x1; x += density_step) {
            for (let y = y0; y <= y1; y += density_step) {
                for (let z = z0; z <= z1; z += density_step) {

                    v3_position[0] = x;
                    v3_position[1] = y;
                    v3_position[2] = z;

                    if (ts.contains_point(v3_position)) {

                        vec3.transformMat4(v3_position, v3_position, matrix_elements);

                        particles.writeAttributeVector3(particle_index, PARTICLE_ATTRIBUTE_POSITION, v3_position[0], v3_position[1], v3_position[2]);

                        particle_index++;

                        if (particle_index >= particles.capacity) {
                            break main_loop;
                        }
                    }
                }
            }
        }

        if (particle_index < particles.capacity) {
            // crop out remaining particles in the pool
            particles.occupancy.clearRange(particle_index, particles.capacity);
            particles.update();
        }

        // request bounds update
        emitter.setFlag(ParticleEmitterFlag.ParticleBoundsNeedUpdate);
        emitter.computeBoundingBox();
    }

    /**
     *
     * @param {function} random
     * @private
     */
    __distribute(random) {
        const sf = this.__sampling_function;

        if (sf === SamplingFunctionKind.Random) {
            this.__distribute_random(random);
        } else if (sf === SamplingFunctionKind.Grid) {
            this.__distribute_grid();
        } else {
            throw new Error(`Unsupported sampling function '${sf}'`);
        }
    }

    /**
     * Distribute particles in the volume
     * @param {function():number} random
     * @private
     */
    __distribute_random(random) {
        const emitter = this.__emitter;
        const particles = emitter.particles;
        const particle_count = particles.size();

        const layer = emitter.layers.get(0);

        // build transform matrix
        const m4_transform = new Matrix4();

        const t = this.__transform;

        composeMatrix4(m4_transform, t.position, t.rotation, t.scale);

        const m4_layer = new Matrix4();

        composeMatrix4(m4_layer, layer.position, new Quaternion(), layer.scale);

        const v3_position = [];

        const m4_final = new Matrix4();

        m4_final.multiplyMatrices(m4_transform, m4_layer);

        // cache variables to save a bit on de-referencing
        const matrix_elements = m4_final.elements;
        const shape = this.__shape;

        for (let i = 0; i < particle_count; i++) {
            shape.sample_random_point_in_volume(v3_position, 0, random);

            vec3.transformMat4(v3_position, v3_position, matrix_elements);

            particles.writeAttributeVector3(i, PARTICLE_ATTRIBUTE_POSITION, v3_position[0], v3_position[1], v3_position[2]);
        }

        // request bounds update
        emitter.setFlag(ParticleEmitterFlag.ParticleBoundsNeedUpdate);
        emitter.computeBoundingBox();
    }

    /**
     *
     * @private
     */
    __write_attributes() {
        this.__ensure_initialized();

        const emitter = this.__emitter;
        const particles = emitter.particles;

        const attributes = particles.spec.attributes;

        for (let i = 0; i < attributes.length; i++) {
            const name = attributes[i].name;

            if (this.attributes.hasOwnProperty(name)) {
                this.__write_attribute(i);
            }
        }
    }

    /**
     *
     * @param {number} index
     * @private
     */
    __write_attribute(index) {
        this.__ensure_initialized();

        const emitter = this.__emitter;
        const particles = emitter.particles;

        const spec = particles.spec.attributes[index];

        const av = this.attributes[spec.name];

        particles.writeAttributeForAllArray(index, av.__value);
    }

    build() {
        const volume_value = this.__shape.volume;

        // total particle count
        let particle_count = Math.max(1, Math.ceil(volume_value * this.__density));

        if (particle_count > LIMIT_PARTICLE_COUNT) {
            console.warn(`ParticleVolume exceeds maximum allowed particle count (limit=${LIMIT_PARTICLE_COUNT}, requested=${particle_count}), clamping to limit. Consider reducing density (current = ${this.__density} particles per unit of volume)`);

            particle_count = LIMIT_PARTICLE_COUNT;
        }

        this.__ensure_initialized();

        const particles = this.__emitter.particles;

        // reserve particles
        particles.setCapacity(particle_count);

        // "spawn" particles to occupy the entire pool
        particles.occupancy.setRange(0, particle_count - 1);

        // set certain defaults
        particles.writeAttributeForAllArray(PARTICLE_ATTRIBUTE_DEATH_AGE, [99999999]);

        // fill out volume
        this.__distribute(Math.random);

        // apply attributes
        this.__write_attributes();

        // trigger sprite update
        this.__emitter.setFlag(ParticleEmitterFlag.SpritesNeedUpdate);
    }

    /**
     *
     * @param {number} r
     * @param {number} g
     * @param {number} b
     * @param {number} a
     */
    setColor(r, g, b, a) {
        this.attributes.color.write_value([r, g, b, a]);
    }
}


/**
 * @readonly
 * @type {string}
 */
ParticleVolume.typeName = "ParticleVolume";
