# src/explorer/ModelsExplorer.js

    import {math, XKTLoaderPlugin} from "@xeokit/xeokit-sdk/dist/xeokit-sdk.es.js";
    import {Controller} from "../Controller.js";
    import {ModelsContextMenu} from "../contextMenus/ModelsContextMenu.js";

    const tempVec3a = math.vec3();

    /**
     * Custom data access strategy for {@link XKTLoaderPlugin}.
     * @private
     */
    class BIMViewerDataSource {

        constructor(server) {
            this._server = server;
        }

        setProjectId(projectId) {
            this._projectId = projectId;
        }

        setModelId(modelId) {
            this._modelId = modelId;
        }

        getManifest(src, ok, error) {
            this._server.getSplitModelManifest(this._projectId, this._modelId, src, ok, error);
        }

        getMetaModel(src, ok, error) {
            this._server.getSplitModelMetadata(this._projectId, this._modelId, src, ok, error);
        }

        getXKT(src, ok, error) {
            this._server.getSplitModelGeometry(this._projectId, this._modelId, src, ok, error)
        }
    }

    /** @private */
    class ModelsExplorer extends Controller {

        constructor(parent, cfg) {

            super(parent, cfg);

            if (!cfg.modelsTabElement) {
                throw "Missing config: modelsTabElement";
            }

            if (!cfg.unloadModelsButtonElement) {
                throw "Missing config: unloadModelsButtonElement";
            }

            if (!cfg.modelsElement) {
                throw "Missing config: modelsElement";
            }

            this._enableAddModels = !!cfg.enableEditModels;
            this._modelsTabElement = cfg.modelsTabElement;
            this._loadModelsButtonElement = cfg.loadModelsButtonElement;
            this._unloadModelsButtonElement = cfg.unloadModelsButtonElement;
            this._addModelButtonElement = cfg.addModelButtonElement;
            this._modelsElement = cfg.modelsElement;
            this._modelsTabButtonElement = this._modelsTabElement.querySelector(".xeokit-tab-btn");

            if (!this._modelsTabButtonElement) {
                throw "Missing DOM element: ,xeokit-tab-btn";
            }

            this._dataSource = new BIMViewerDataSource(this.server);

            this._xktLoader = new XKTLoaderPlugin(this.viewer, {
                dataSource: this._dataSource
            });

            this._modelsContextMenu = new ModelsContextMenu({
                enableEditModels: cfg.enableEditModels,
                hideOnAction: true
            });

            this._modelsInfo = {};
            this._numModels = 0;
            this._numModelsLoaded = 0;
            this._projectId = null;
        }

        setObjectColors(objectColors) {
            this._xktLoader.objectDefaults = objectColors;
        }

        loadProject(projectId, done, error) {
            this.server.getProject(projectId, (projectInfo) => {
                this.unloadProject();
                this._projectId = projectId;
                this._modelsInfo = {};
                this._numModels = 0;
                this._parseProject(projectInfo, done);
                if (this._numModelsLoaded < this._numModels) {
                    this._loadModelsButtonElement.classList.remove("disabled");
                }
                if (this._numModelsLoaded > 0) {
                    this._unloadModelsButtonElement.classList.remove("disabled");
                }
                if (this._enableAddModels) {
                    this._addModelButtonElement.classList.remove("disabled");
                }
            }, (errMsg) => {
                this.error(errMsg);
                if (error) {
                    error(errMsg);
                }
            });
        }

        _parseProject(projectInfo, done) {
            this._buildModelsMenu(projectInfo);
            this._parseViewerConfigs(projectInfo);
            this._parseViewerContent(projectInfo, () => {
                this._parseViewerState(projectInfo, () => {
                    done();
                });
            });
        }

        _buildModelsMenu(projectInfo) {
            var html = "";
            const modelsInfo = projectInfo.models || [];
            this._modelsInfo = {};
            this._numModels = modelsInfo.length;
            for (let i = 0, len = modelsInfo.length; i < len; i++) {
                const modelInfo = modelsInfo[i];
                this._modelsInfo[modelInfo.id] = modelInfo;
                html += "<div class='xeokit-form-check'>";
                html += "<input id='" + modelInfo.id + "' type='checkbox' value=''><span id='span-" + modelInfo.id + "' class='disabled'>" + modelInfo.name + "</span>";
                html += "</div>";
            }
            this._modelsElement.innerHTML = html;
            for (let i = 0, len = modelsInfo.length; i < len; i++) {
                const modelInfo = modelsInfo[i];
                const modelId = modelInfo.id;
                const checkBox = document.getElementById("" + modelId);
                const span = document.getElementById("span-" + modelId);
                checkBox.addEventListener("click", () => {
                    if (checkBox.checked) {
                        this.loadModel(modelId);
                    } else {
                        this.unloadModel(modelInfo.id);
                    }
                });
                span.addEventListener("click", () => {
                    const model = this.viewer.scene.models[modelId];
                    const modelLoaded = (!!model);
                    if (!modelLoaded) {
                        this.loadModel(modelId);
                    } else {
                        this.unloadModel(modelInfo.id);
                    }
                });
                span.oncontextmenu = (e) => {
                    this._modelsContextMenu.context = {
                        bimViewer: this.bimViewer,
                        viewer: this.viewer,
                        modelId: modelId
                    };
                    this._modelsContextMenu.show(e.pageX, e.pageY);
                    e.preventDefault();
                };
            }
        }

        _parseViewerConfigs(projectInfo) {
            const viewerConfigs = projectInfo.viewerConfigs;
            if (viewerConfigs) {
                this.bimViewer.setConfigs(viewerConfigs);
            }
        }

        _parseViewerContent(projectInfo, done) {
            const viewerContent = projectInfo.viewerContent;
            if (!viewerContent) {
                done();
                return;
            }
            this._parseModelsLoaded(viewerContent, () => {
                done();
            });
        }

        _parseModelsLoaded(viewerContent, done) {
            const modelsLoaded = viewerContent.modelsLoaded;
            if (!modelsLoaded || (modelsLoaded.length === 0)) {
                done();
                return;
            }
            this._loadNextModel(modelsLoaded.slice(0), done);
        }

        _loadNextModel(modelsLoaded, done) {
            if (modelsLoaded.length === 0) {
                done();
                return;
            }
            const modelId = modelsLoaded.pop();
            this.loadModel(modelId,
                () => { // Done
                    this._loadNextModel(modelsLoaded, done);
                },
                () => { // Error - recover and attempt to load next model
                    this._loadNextModel(modelsLoaded, done);
                });
        }

        _parseViewerState(projectInfo, done) {
            const viewerState = projectInfo.viewerState;
            if (!viewerState) {
                done();
                return;
            }
            this.bimViewer.setViewerState(viewerState, done);
        }

        unloadProject() {
            if (!this._projectId) {
                return;
            }
            const models = this.viewer.scene.models;
            for (var modelId in models) {
                if (models.hasOwnProperty(modelId)) {
                    const model = models[modelId];
                    model.destroy();
                }
            }
            this._modelsElement.innerHTML = "";
            this._numModelsLoaded = 0;

            this._loadModelsButtonElement.classList.add("disabled");
            this._unloadModelsButtonElement.classList.add("disabled");
            if (this._enableAddModels) {
                this._addModelButtonElement.classList.add("disabled");
            }
            const lastProjectId = this._projectId;
            this._projectId = null;
            this.fire("projectUnloaded", {
                projectId: lastProjectId
            });
        }

        getLoadedProjectId() {
            return this._projectId;
        }

        getModelIds() {
            return Object.keys(this._modelsInfo);
        }

        loadModel(modelId, done, error) {
            if (!this._projectId) {
                const errMsg = "No project currently loaded";
                this.error(errMsg);
                if (error) {
                    error(errMsg);
                }
                return;
            }
            const modelInfo = this._modelsInfo[modelId];
            if (!modelInfo) {
                const errMsg = "Model not in currently loaded project";
                this.error(errMsg);
                if (error) {
                    error(errMsg);
                }
                return;
            }

            this.bimViewer._busyModal.show(`${this.viewer.localeService.translate("busyModal.loading") || "Loading"} ${modelInfo.name}`);

            const externalMetadata = this.bimViewer.getConfig("externalMetadata");

            if (externalMetadata && !modelInfo.manifest) {
                this.server.getMetadata(this._projectId, modelId, (json) => {
                        this._loadGeometry(modelId, modelInfo, json, done, error);
                    },
                    (errMsg) => {
                        this.bimViewer._busyModal.hide();
                        this.error(errMsg);
                        if (error) {
                            error(errMsg);
                        }
                    });
            } else {
                this._loadGeometry(modelId, modelInfo, null, done, error);
            }
        }

        _loadGeometry(modelId, modelInfo, json, done, error) {

            const modelLoaded = () => {
                const checkbox = document.getElementById("" + modelId);
                checkbox.checked = true;
                this._numModelsLoaded++;
                this._unloadModelsButtonElement.classList.remove("disabled");
                if (this._numModelsLoaded < this._numModels) {
                    this._loadModelsButtonElement.classList.remove("disabled");
                } else {
                    this._loadModelsButtonElement.classList.add("disabled");
                }
                if (this._numModelsLoaded === 1) { // Jump camera to view-fit first model loaded
                    this._jumpToInitialCamera();
                    this.fire("modelLoaded", modelId);
                    this.bimViewer._busyModal.hide();
                    if (done) {
                        done();
                    }
                } else {
                    this.fire("modelLoaded", modelId);
                    this.bimViewer._busyModal.hide();
                    if (done) {
                        done();
                    }
                }
            };

            const loadError = (errMsg) => {
                this.bimViewer._busyModal.hide();
                this.error(errMsg);
                if (error) {
                    error(errMsg);
                }
            };

            if (modelInfo.manifest) {

                // Load multi-part split model;
                // Uses the BIMViewerDataSource, which then uses the BIMViewer's Server strategy

                this._dataSource.setProjectId(this._projectId);
                this._dataSource.setModelId(modelId);

                const model = this._xktLoader.load({
                    id: modelId,
                    manifestSrc: modelInfo.manifest,
                    excludeUnclassifiedObjects: true,
                    origin: modelInfo.origin || modelInfo.position,
                    scale: modelInfo.scale,
                    rotation: modelInfo.rotation,
                    matrix: modelInfo.matrix,
                    edges: (modelInfo.edges !== false),
                    saoEnabled: modelInfo.saoEnabled,
                    pbrEnabled: modelInfo.pbrEnabled,
                    backfaces: modelInfo.backfaces,
                    globalizeObjectIds: modelInfo.globalizeObjectIds,
                    reuseGeometries: (modelInfo.reuseGeometries !== false)
                });

                model.on("loaded", modelLoaded);
                model.on("error", loadError);

            } else {

                // Load single XKT/Metamodel file model;
                // Uses the BIMViewer's Server strategy directly

                this.server.getGeometry(this._projectId, modelId, (arraybuffer) => {
                        const model = this._xktLoader.load({
                            id: modelId,
                            metaModelData: json,
                            xkt: arraybuffer,
                            excludeUnclassifiedObjects: true,
                            origin: modelInfo.origin || modelInfo.position,
                            scale: modelInfo.scale,
                            rotation: modelInfo.rotation,
                            matrix: modelInfo.matrix,
                            edges: (modelInfo.edges !== false),
                            saoEnabled: modelInfo.saoEnabled,
                            pbrEnabled: modelInfo.pbrEnabled,
                            backfaces: modelInfo.backfaces,
                            globalizeObjectIds: modelInfo.globalizeObjectIds,
                            reuseGeometries: (modelInfo.reuseGeometries !== false)
                        });
                        model.on("loaded", modelLoaded);
                        model.on("error", loadError);
                    }, loadError);
            }
        }

        _jumpToInitialCamera() {
            const viewer = this.viewer;
            const scene = viewer.scene;
            const aabb = scene.getAABB(scene.visibleObjectIds);
            const diag = math.getAABB3Diag(aabb);
            const center = math.getAABB3Center(aabb, tempVec3a);
            const camera = scene.camera;
            const fitFOV = camera.perspective.fov;
            const dist = Math.abs(diag / Math.tan(45 * math.DEGTORAD));
            const dir = math.normalizeVec3((camera.yUp) ? [-0.5, -0.7071, -0.5] : [-1, 1, -1]);
            const up = math.normalizeVec3((camera.yUp) ? [-0.5, 0.7071, -0.5] : [-1, 1, 1]);
            viewer.cameraControl.pivotPos = center;
            viewer.cameraControl.planView = false;
            viewer.cameraFlight.jumpTo({
                look: center,
                eye: [center[0] - (dist * dir[0]), center[1] - (dist * dir[1]), center[2] - (dist * dir[2])],
                up: up,
                orthoScale: diag * 1.1
            });
        }

        unloadModel(modelId) {
            const model = this.viewer.scene.models[modelId];
            if (!model) {
                this.error("Model not loaded: " + modelId);
                return;
            }
            model.destroy();
            const checkbox = document.getElementById("" + modelId);
            checkbox.checked = false;
            const span = document.getElementById("span-" + modelId);
            this._numModelsLoaded--;
            if (this._numModelsLoaded > 0) {
                this._unloadModelsButtonElement.classList.remove("disabled");
            } else {
                this._unloadModelsButtonElement.classList.add("disabled");
            }
            if (this._numModelsLoaded < this._numModels) {
                this._loadModelsButtonElement.classList.remove("disabled");
            } else {
                this._loadModelsButtonElement.classList.add("disabled");
            }
            this.fire("modelUnloaded", modelId);
        }

        unloadAllModels() {
            const models = this.viewer.scene.models;
            const modelIds = Object.keys(models);
            for (var i = 0, len = modelIds.length; i < len; i++) {
                const modelId = modelIds[i];
                this.unloadModel(modelId);
            }
        }

        getNumModelsLoaded() {
            return this._numModelsLoaded;
        }

        _getLoadedModelIds() {
            return Object.keys(this.viewer.scene.models);
        }

        isModelLoaded(modelId) {
            return (!!this.viewer.scene.models[modelId]);
        }

        getModelsInfo() {
            return this._modelsInfo;
        }

        getModelInfo(modelId) {
            return this._modelsInfo[modelId];
        }

        setEnabled(enabled) {
            if (!enabled) {
                this._modelsTabButtonElement.classList.add("disabled");
                this._unloadModelsButtonElement.classList.add("disabled");
            } else {
                this._modelsTabButtonElement.classList.remove("disabled");
                this._unloadModelsButtonElement.classList.remove("disabled");
            }
        }

        /** @private */
        destroy() {
            super.destroy();
            this._xktLoader.destroy();
        }
    }

    export {ModelsExplorer};