import { assert } from "../../../../core/assert.js";
import { BVH } from "../../../../core/bvh2/bvh3/BVH.js";
import { bvh_query_depth_range_in_frustum } from "../../../../core/bvh2/bvh3/query/bvh_query_depth_range_in_frustum.js";
import Vector3 from '../../../../core/geom/Vector3.js';
import { clamp } from "../../../../core/math/clamp.js";
import { max2 } from "../../../../core/math/max2.js";
import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
import { System } from '../../../ecs/System.js';
import { Transform } from '../../../ecs/transform/Transform.js';
import { Reference } from "../../../reference/v2/Reference.js";
import { GraphicsEngine } from "../../GraphicsEngine.js";
import { make_bvh_visibility_builder } from "../../render/make_bvh_visibility_builder.js";
import {
    RIBBON_ATTRIBUTE_ADDRESS_AGE,
    RIBBON_ATTRIBUTE_ADDRESS_UV_OFFSET
} from "../../trail/x/ribbon_attributes_spec.js";
import { RibbonX } from "../../trail/x/RibbonX.js";
import { RibbonXPlugin } from "../../trail/x/RibbonXPlugin.js";
import { RibbonXFixedPhysicsSimulator } from "../../trail/x/simulator/RibbonXFixedPhysicsSimulator.js";

import Trail2D from './Trail2D.js';
import { Trail2DFlags } from "./Trail2DFlags.js";

const v3Temp1 = new Vector3();

class Trail2DSystem extends System {
    dependencies = [Trail2D, Transform];

    components_used = [
        ResourceAccessSpecification.from(Trail2D, ResourceAccessKind.Write)
    ];

    /**
     *
     * @type {RibbonXFixedPhysicsSimulator}
     */
    simulator = new RibbonXFixedPhysicsSimulator();

    /**
     *
     * @type {Reference<RibbonXPlugin>}
     * @private
     */
    __ribbon_plugin = Reference.NULL;


    /**
     *
     * @type {number}
     * @private
     */
    __timeDelta = 0;

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

    /**
     * @param {Engine} engine
     * @constructor
     */
    constructor(engine) {
        super();

        assert.defined(engine, 'engine');
        assert.notNull(engine, 'engine');
        assert.equal(engine.isEngine, true, 'engine.isEngine !== true');


        /**
         *
         * @type {Engine}
         * @private
         */
        this.__engine = engine;

        /**
         *
         * @type {GraphicsEngine}
         */
        this.graphics = engine.graphics;

        /**
         *
         * @type {RenderLayer|null}
         */
        this.renderLayer = null;


    }

    async startup(entityManager) {
        this.entityManager = entityManager;

        this.renderLayer = this.graphics.layers.create('trail-2d-system');

        this.renderLayer.buildVisibleSet = make_bvh_visibility_builder(
            this.entityManager, this.bvh,
            (destination, offset, entity, ecd) => {
                destination[offset] = ecd.getComponent(entity, Trail2D).mesh
                return 1;
            }
        );

        const bvh = this.bvh;
        this.renderLayer.compute_depth_range = (result, frustum, nx, ny, nz, c) => {
            bvh_query_depth_range_in_frustum(result, bvh, frustum, nx, ny, nz, c);
        };

        this.__ribbon_plugin = await this.__engine.plugins.acquire(RibbonXPlugin);
    }

    async shutdown(entityManager) {
        this.graphics.layers.remove(this.renderLayer);

        this.__ribbon_plugin.release();
    }

    /**
     *
     * @param {Trail2D} trail
     * @param {Transform} transform
     * @private
     */
    __build_trail(trail, transform) {
        const segmentsPerSecond = 60;
        const maxSegments = 1024;
        //instantiation

        //make a mesh
        const segment_count = Math.ceil(clamp(trail.maxAge * segmentsPerSecond, 2, maxSegments));

        trail.build(segment_count);

        const ribbon = trail.ribbon;

        const ribbons = this.__ribbon_plugin.getValue();

        const material = ribbons.obtain_material_x(trail.material);

        trail.mesh.material = material;

        trail.time = 0;
        trail.trailingIndex = 0;
        trail.ribbon = ribbon;
        trail.timeSinceLastUpdate = 0;


        const trail_offset = trail.offset;
        const transform_position = transform.position;

        const position_x = trail_offset.x + transform_position.x;
        const position_y = trail_offset.y + transform_position.y;
        const position_z = trail_offset.z + transform_position.z;

        const color = trail.color;

        //initialize segments
        for (let i = 0; i < segment_count; i++) {
            const f = i / (segment_count - 1);

            ribbon.setPointColor(i, color.x * 255, color.y * 255, color.z * 255);
            ribbon.setPointPosition(i, position_x, position_y, position_z);
            ribbon.setPointThickness(i, trail.width);
            ribbon.setPointAttribute_Scalar(i, RIBBON_ATTRIBUTE_ADDRESS_UV_OFFSET, f);
            ribbon.setPointAlpha(i, color.w);
            ribbon.setPointAttribute_Scalar(i, RIBBON_ATTRIBUTE_ADDRESS_AGE, trail.maxAge);

        }

        trail.bvh.resize(
            position_x, position_y, position_z,
            position_x, position_y, position_z
        );

        trail.setFlag(Trail2DFlags.Built);
    }

    /**
     *
     * @param {Transform} transform
     * @param {Trail2D} trail
     * @param {number} entityId
     */
    link(trail, transform, entityId) {
        if (!trail.getFlag(Trail2DFlags.Built)) {
            this.__build_trail(trail, transform);
        } else {

            const material = this.__ribbon_plugin.getValue().obtain_material_x(trail.material);
            trail.mesh.material = material;
        }


        trail.bvh.link(this.bvh, entityId);
    }

    /**
     *
     * @param {Trail2D} component
     * @param {Transform} transform
     * @param {number} entity
     */
    unlink(component, transform, entity) {

        component.bvh.unlink();

        // release resources
        component.dispose();

    }

    /**
     *
     * @param {Trail2D} trail
     * @param {Transform} transform
     */
    updateTrailEntity(trail, transform) {
        const timeDelta = this.__timeDelta;

        /**
         *
         * @type {RibbonX|null}
         */
        const ribbon = trail.ribbon;

        const newPosition = v3Temp1;

        newPosition.copy(trail.offset);
        newPosition.applyMatrix4(transform.matrix);

        trail.timeSinceLastUpdate += timeDelta;
        trail.time += timeDelta;

        const ageOffset = max2(0, trail.maxAge - trail.time);

        this.simulator.update(ribbon, trail.maxAge, timeDelta);

        if (trail.getFlag(Trail2DFlags.Spawning)) {
            trail.updateHead(newPosition.x, newPosition.y, newPosition.z, ageOffset);
        }

        if (trail.getFlag(Trail2DFlags.BoundsNeedUpdate)) {
            const bvh = trail.bvh;

            ribbon.computeBoundingBox(bvh.bounds);
            bvh.write_bounds();

            trail.clearFlag(Trail2DFlags.BoundsNeedUpdate);
        }

    }

    update(timeDelta) {

        const em = this.entityManager;

        const dataset = em.dataset;

        this.__timeDelta = timeDelta;

        if (dataset !== null) {
            dataset.traverseEntities([Trail2D, Transform], this.updateTrailEntity, this);
        }
    }
}

export default Trail2DSystem;
