import { min2 } from "../../../../../core/math/min2.js";
import { ResourceAccessKind } from "../../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../../core/model/ResourceAccessSpecification.js";
import { STATIC_ASSET_CACHE } from "../../../../asset/STATIC_ASSET_CACHE.js";
import { EntityNode } from "../../../../ecs/parent/EntityNode.js";
import { ParentEntity } from "../../../../ecs/parent/ParentEntity.js";
import { System } from "../../../../ecs/System.js";
import { TransformAttachment } from "../../../../ecs/transform-attachment/TransformAttachment.js";
import { TransformAttachmentSystem } from "../../../../ecs/transform-attachment/TransformAttachmentSystem.js";
import { Transform } from "../../../../ecs/transform/Transform.js";
import { assetTypeByPath } from "../../mesh/assetTypeByPath.js";
import { ShadedGeometry } from "../ShadedGeometry.js";
import { ShadedGeometryFlags } from "../ShadedGeometryFlags.js";
import { three_object_to_entity_composition } from "../three_object_to_entity_composition.js";
import { SGMesh } from "./SGMesh.js";
import { SGMeshEvents } from "./SGMeshEvents.js";


/**
 * @readonly
 * @type {number}
 */
const MAX_CYCLE_COUNT = 128;

/**
 * In milliseconds
 * @readonly
 * @type {number}
 */
const MAX_CYCLE_TIME = 40;


/**
 *
 * @param {string} url
 * @param {AssetManager} assetManager
 * @returns {Promise<THREE.Object3D>}
 */
async function getAsset(url, assetManager) {

    const assetType = assetTypeByPath(url);

    const asset = await assetManager.promise(url, assetType);
}


export class SGMeshSystem extends System {
    /**
     *
     * @param {Engine} engine
     */
    constructor(engine) {
        super();

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

        /**
         *
         * @type {AssetManager}
         * @private
         */
        this.__assetManager = engine.assetManager;

        this.dependencies = [SGMesh, Transform];
        this.components_used = [
            ResourceAccessSpecification.from(SGMesh, ResourceAccessKind.Read | ResourceAccessKind.Write),
            ResourceAccessSpecification.from(Transform, ResourceAccessKind.Read),
            ResourceAccessSpecification.from(ShadedGeometry, ResourceAccessKind.Create),
            ResourceAccessSpecification.from(TransformAttachment, ResourceAccessKind.Create),
        ];

        /**
         *
         * @type {number[]}
         * @private
         */
        this.__wait_queue = [];
        /**
         *
         * @type {number}
         * @private
         */
        this.__wait_queue_process_index = 0;

    }

    async startup(em) {

        if (!em.hasSystem(TransformAttachmentSystem)) {
            await em.addSystem(new TransformAttachmentSystem());
        }

    }

    /**
     *
     * @param {number} entity
     * @param {SGMesh} mesh
     * @param {Transform} transform
     */
    async process(entity, mesh, transform) {
        const am = this.__assetManager;
        const ecd = this.entityManager.dataset;

        const assetType = assetTypeByPath(mesh.url);

        let asset;

        try {
            asset = await am.promise(mesh.url, assetType);
        } catch (e) {
            ecd.sendEvent(entity, SGMeshEvents.AssetLoadFailed);
            return;
        }

        if (!ecd.entityExists(entity)) {
            // entity no longer exists
            return;
        }

        if (ecd.getComponent(entity, SGMesh) !== mesh) {
            // mesh has changed
            return;
        }

        if (asset.boundingBox !== undefined) {
            mesh.__initial_bounds.copy(asset.boundingBox);

            if (asset.has_root_transform) {
                // TODO potentially move this to the GLTF Asset Loader to skip matrix operations here
                mesh.__initial_bounds.applyMatrix4(asset.root_transform.matrix);
            }

        } else {
            console.error(`No bounds for asset: ${asset.description}`);
        }

        let object;
        const cached_object = STATIC_ASSET_CACHE.get(asset);

        if (cached_object !== null) {
            object = cached_object;
        } else {
            object = asset.create();

            STATIC_ASSET_CACHE.set(asset, object);
        }

        let entity_node = three_object_to_entity_composition(object);

        if (asset.has_root_transform) {
            // apply root transform

            const new_root = new EntityNode();

            new_root.entity.add(new Transform());

            entity_node.transform.copy(asset.root_transform);

            new_root.addChild(entity_node);

            entity_node = new_root;
        }

        // attach parent entity to link hierarchy to this entity
        entity_node.entity.add(ParentEntity.from(entity));

        mesh.__node = entity_node;

        function copy_transform() {
            entity_node.transform.copy(transform);
        }

        copy_transform();

        entity_node.build(ecd);

        transform.subscribe(copy_transform);

        entity_node.on.destroyed.addOne(() => {

            transform.unsubscribe(copy_transform);

        });

        const material_override = mesh.materialOverride;

        // apply flags to the hierarchy
        entity_node.traverse(node => {
            /**
             *
             * @type {ShadedGeometry|null}
             */
            const sg = node.entity.getComponent(ShadedGeometry);
            if (sg === null) {
                return;
            }

            sg.writeFlag(ShadedGeometryFlags.CastShadow, mesh.castShadow);
            sg.writeFlag(ShadedGeometryFlags.ReceiveShadow, mesh.receiveShadow);

            if (material_override !== null) {
                sg.material = material_override;
            }
        });


        ecd.sendEvent(entity, SGMeshEvents.AssetLoaded);
    }

    /**
     *
     * @param {SGMesh} mesh
     * @param {Transform} transform
     * @param {number} entity
     */
    link(mesh, transform, entity) {
        const p = transform.position;

        // set initial bounds to single point
        mesh.__initial_bounds.setBounds(
            p.x, p.y, p.z,
            p.x, p.y, p.z,
        );

        if (mesh.url === null) {
            this.__wait_queue.push(entity);
            return;
        }

        this.process(entity, mesh, transform);
    }

    /**
     *
     * @param {SGMesh} mesh
     * @param {Transform} transform
     * @param {number} entity
     */
    unlink(mesh, transform, entity) {
        if (mesh.__node !== null) {
            mesh.__node.destroy();

            mesh.__node = null;
        }

        // check the queue
        const i = this.__wait_queue.indexOf(entity);

        if (i !== -1) {
            this.__wait_queue.splice(i, 1);
        }
    }

    update(time_delta) {

        const entityManager = this.entityManager;
        const dataset = entityManager.dataset;

        if (dataset === null) {
            return;
        }

        const waiting = this.__wait_queue;

        let l = waiting.length;

        if (l > 0) {

            const time_0 = performance.now();

            let pointer = this.__wait_queue_process_index;

            const pointer_limit = pointer + min2(l, MAX_CYCLE_COUNT);

            while (pointer < pointer_limit) {

                const index = pointer % l;
                pointer++;

                const entity = waiting[index];

                const mesh = dataset.getComponent(entity, SGMesh);

                if (mesh.url === null) {
                    // no URL yet
                    continue;
                }

                waiting.splice(index, 1);

                const transform = dataset.getComponent(entity, Transform);

                this.process(entity, mesh, transform);

                const time_1 = performance.now();

                if ((time_1 - time_0) > MAX_CYCLE_TIME) {
                    // too much time has passed
                    break;
                }

            }

            this.__wait_queue_process_index = pointer;

        }
    }
}
