import { BoxBufferGeometry, MeshLambertMaterial } from "three";
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 { SignalBinding } from "../../../../core/events/signal/SignalBinding.js";

import Vector3 from "../../../../core/geom/Vector3.js";
import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
import { GLTFAssetLoader } from "../../../asset/loaders/GLTFAssetLoader.js";
import { System } from "../../../ecs/System.js";
import { Transform } from "../../../ecs/transform/Transform.js";
import { make_bvh_visibility_builder } from "../../render/make_bvh_visibility_builder.js";

import checkerTexture from "../../texture/CheckersTexture.js";

import ThreeFactory from "../../three/ThreeFactory.js";
import { assetTypeByPath } from "./assetTypeByPath.js";
import Mesh, { MeshFlags } from "./Mesh.js";
import { MeshEvents } from "./MeshEvents.js";
import { setMesh } from "./setMesh.js";

const PLACEHOLDER_GEOMETRY = new BoxBufferGeometry(1, 1, 1);

PLACEHOLDER_GEOMETRY.computeBoundingSphere();

const PLACEHOLDER_TEXTURE = checkerTexture.create();

const PLACEHOLDER_MATERIAL = new MeshLambertMaterial({ map: PLACEHOLDER_TEXTURE });

export class MeshSystem extends System {
    /**
     *
     * @param {Engine} engine
     * @constructor
     * @extends {System}
     */
    constructor(engine) {
        super();
        assert.equal(engine.isEngine, true, 'engine.isEngine !== true');

        const graphics = engine.graphics;
        const assetManager = engine.assetManager;

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

        this.dependencies = [Mesh, Transform];

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


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

        this.waitingForMesh = [];

        /**
         *
         * @type {Array.<Array.<SignalBinding>>}
         */
        this.entityData = [];

        this.graphics = graphics;

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

        /**
         *
         * @type {boolean}
         * @private
         */
        this.__use_placeholder_mesh = false;

        /**
         *
         * @type {BVH}
         * @private
         */
        this.__bvh_binary = new BVH();
    }

    /**
     *
     * @param {number} entity
     * @param {EntityComponentDataset} dataset
     * @param {Asset} asset
     * @param {Mesh} component
     */
    __set_mesh_from_asset(entity, dataset, asset, component) {


        const mesh = asset.create(); // remember the asset

        component.asset = asset;

        if (asset.boundingBox !== undefined) {
            component.boundingBox.copy(asset.boundingBox);
        } else if (mesh.isMesh) {
            const geometry = mesh.geometry;
            const bb = geometry.boundingBox;
            component.boundingBox.setBounds(
                bb.min.x,
                bb.min.y,
                bb.min.z,
                bb.max.x,
                bb.max.y,
                bb.max.z
            );
        }

        component.setFlag(MeshFlags.Loaded);

        setMesh(dataset, entity, component, mesh);


        // send event about asset being attached
        dataset.sendEvent(entity, MeshEvents.AssetLoaded);
    }

    async startup(entityManager) {
        const am = this.assetManager;

        this.entityManager = entityManager;

        if (!am.hasLoaderForType('model/gltf+json')) {
            const gltf_asset_loader = new GLTFAssetLoader();
            await am.registerLoader('model/gltf+json', gltf_asset_loader);
        }

        const graphics = this.graphics;
        this.renderLayer = graphics.layers.create("mesh-system");

        this.renderLayer.buildVisibleSet = make_bvh_visibility_builder(
            this.entityManager, this.__bvh_binary,
            (destination, offset, entity, ecd) => {
                const component = ecd.getComponent(entity, Mesh);

                if (component.mesh === null) {
                    return 0;
                }

                destination[offset] = component.mesh;
                return 1;
            }
        )

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

        const visibleSet = this.renderLayer.visibleSet;

        visibleSet.onAdded.add(m => {
            /**
             *
             * @type {Mesh}
             */
            const component = m.__meep_ecs_component;


            component.setFlag(MeshFlags.InView);
        });

        visibleSet.onRemoved.add(m => {
            /**
             *
             * @type {Mesh}
             */
            const component = m.__meep_ecs_component;

            if (m !== component.mesh) {
                //object does not match current component mesh, skip this event
                return;
            }

            component.clearFlag(MeshFlags.InView);
        });

    }

    /**
     *
     * @param {function(Mesh, number)} visitor
     * @param {*} [thisArg]
     */
    traverseVisible(visitor, thisArg) {
        const em = this.entityManager;

        const ecd = em.dataset;

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

        const visibleSet = this.renderLayer.visibleSet;
        const visibleElements = visibleSet.elements;

        const n = visibleSet.size;

        for (let i = 0; i < n; i++) {

            /**
             *
             * @type {Object3D}
             */
            const m = visibleElements[i];

            /**
             *
             * @type {Mesh}
             */
            const component = m.__meep_ecs_component;

            /**
             * @type {number}
             */
            const entity = m.__meep_ecs_entity;

            if (!ecd.entityExists(entity)) {
                //entity was destroyed at some point, visibility set updates asynchronously from ECS dataset updates
                //skip
                return;
            }

            if (component !== undefined) {
                visitor.call(thisArg, component, entity);
            }
        }
    }

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

    /**
     *
     * @param {Transform} transform
     * @param {Mesh} model
     * @param {number} entity
     */
    link(model, transform, entity) {
        //remember entity for fast lookup
        this.process(entity, model);

        function apply_transform() {
            model.internalApplyTransform(transform);
        }

        const bPosition = new SignalBinding(
            transform.position.onChanged,
            apply_transform
        );
        const bRotation = new SignalBinding(
            transform.rotation.onChanged,
            apply_transform
        );
        const bScale = new SignalBinding(
            transform.scale.onChanged,
            apply_transform
        );

        bPosition.link();
        bRotation.link();
        bScale.link();

        this.entityData[entity] = [
            bPosition,
            bRotation,
            bScale
        ];

        model.__bvh_leaf.link(this.__bvh_binary, entity);
    }

    /**
     *
     * @param {Transform} transform
     * @param {Mesh} model
     * @param {string} entityId
     */
    unlink(model, transform, entityId) {
        const list = this.entityData[entityId];

        if (list !== undefined) {

            for (let i = 0; i < list.length; i++) {
                const binding = list[i];
                binding.unlink();
            }

            delete this.entityData[entityId];

        }

        //remove from 'waiting for mesh' set
        const waitingForMesh = this.waitingForMesh;

        let n = waitingForMesh.length;

        for (let i = 0; i < n; i++) {
            const element = waitingForMesh[i];

            if (element.entity === entityId) {
                waitingForMesh.splice(i, 1);
                i--;
                n--;
            }
        }

        model.__bvh_leaf.unlink();

        // cleanup resources
        model.dispose();
    }

    /**
     *
     * @param {int} entity
     * @param {Mesh} component
     */
    process(entity, component) {
        const em = this.entityManager;
        const am = this.assetManager;

        const dataset = em.dataset;

        if (component.hasMesh()) {
            //do not re-run if mesh is set
            setMesh(dataset, entity, component, component.mesh);
            return;
        }

        if (component.url === null) {
            this.waitingForMesh.push({ component, entity });
            return;
        }

        const assetType = assetTypeByPath(component.url);

        const asset = am.tryGet(component.url, assetType);

        if (asset !== null) {
            // asset is already loaded, use it
            this.__set_mesh_from_asset(entity, dataset, asset, component);
            return;
        }

        // mesh was not found, use temp mesh and submit a request

        if (this.__use_placeholder_mesh) {
            // set placeholder mesh
            const mesh = ThreeFactory.createMesh(PLACEHOLDER_GEOMETRY, PLACEHOLDER_MATERIAL);

            setMesh(dataset, entity, component, mesh);
        }

        function assetFailure(error) {
            console.error("failed to load model " + component.url, error);

            dataset.sendEvent(entity, MeshEvents.AssetLoadFailed, {
                reason: error
            });
        }

        if (assetType === null) {
            // couldn't figure out what the asset type is
            assetFailure("no asset type deduced");
            return;
        }


        /**
         *
         * @param {Asset<Object3D>} asset
         */
        const assetLoaded = (asset) => {
            if (!dataset.entityExists(entity)) {
                //entity no longer exists, probably dataset has been switched, abort
                return;
            }

            //check that component is still actual
            const actualComponent = dataset.getComponent(entity, Mesh);

            if (actualComponent === component) {
                // scene.remove(component.mesh);
                this.__set_mesh_from_asset(entity, dataset, asset, component);

            } else {
                //component is no longer in the manager. do nothing.
                //console.warn("component is no longer in the manager");
            }
        }

        // load the asset
        am.get({ path: component.url, type: assetType, callback: assetLoaded, failure: assetFailure });

    }

    update(timeDelta) {
        const waiting = this.waitingForMesh;

        let i = 0;
        let l = waiting.length;

        for (; i < l; i++) {
            const element = waiting[i];
            const component = element.component;
            if (component.url !== null) {
                const entity = element.entity;

                this.process(entity, component);

                waiting.splice(i, 1);

                i--;
                l--;
            }
        }
    }

    /**
     *
     * @param {Mesh} component
     * @param {HumanoidBoneType} boneType
     * @param {Vector3} [result=new Vector3()]
     * @returns {Vector3}
     * @throws {Error} if no such bone exists
     */
    static getBonePosition(component, boneType, result = new Vector3()) {

        component.getBonePositionByType(result, boneType);

        return result;
    }
}

