import { Host, h, getAssetPath, } from "@stencil/core";
import * as THREE from "three";
import { ColladaLoader } from "three/examples/jsm/loaders/ColladaLoader.js";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import "dayjs/locale/nl";
import { getStore } from "../../store";
import { isNil } from "lodash-es";
import { Easing, Tween } from "@tweenjs/tween.js";
import { getSafeRelativeTime } from "../../utils/safe-relative-time";
import { applyErrorMessage, clearErrorMessage, } from "../../utils/error-message";
import { watchStoreLocale } from "../../utils/locale-state";
import { getStoreUnavailableError } from "../../utils/store-error";
export class Ls3dScene {
    constructor() {
        /**
         * Centralized error state for tracking component errors
         */
        this.errorState = {
            hasError: false,
            messageKey: undefined,
            type: "",
        };
        this.hasBackDoorTimeout = null;
        this.isComponentMounted = false;
        this.store = null;
        this.storeUnsubscribe = null;
        this.animateTheFloor = false;
        this.isAnimating = false;
        this.isEasingOut = false;
        this.isEasingIn = false;
        this.animationFrameId = null;
        this.backDoorLeft = new THREE.Object3D();
        this.backDoorRight = new THREE.Object3D();
        this.doorWidth = 12.5;
        this.floorAnimationTween = null;
        this.doorAnimationTween = null;
        this.floorHeight = 500;
        this.floorSeparatorIndicator = new THREE.Group();
        this.frontDoorLeft = new THREE.Object3D();
        this.frontDoorRight = new THREE.Object3D();
        this.movingDirection = "down";
        this.pulseInterval = null;
        this.speed = 3000;
        this.DEFAULT_SCENE_SIZE = 500;
        this.resizeObserver = null;
        this.resizeFrameId = null;
        this.lastCanvasSize = { width: 0, height: 0 };
        this.positions = {
            frontDoor: {
                left: {
                    open: 0,
                    closed: 0,
                },
                right: {
                    open: 0,
                    closed: 0,
                },
            },
            backDoor: {
                left: {
                    open: 0,
                    closed: 0,
                },
                right: {
                    open: 0,
                    closed: 0,
                },
            },
            floor: {
                down: 0,
                up: 0,
            },
        };
        this.materials = {
            active: new THREE.MeshStandardMaterial({ color: 0xff0000, opacity: 1 }),
            normal: new THREE.MeshStandardMaterial({
                color: 0xa3a3a3,
                opacity: 1,
                transparent: false,
            }),
            pulseOn: new THREE.MeshStandardMaterial({
                color: 0x024970,
                opacity: 0.5,
                transparent: true,
            }),
            pulseOff: new THREE.MeshStandardMaterial({
                color: 0x024970,
                opacity: 1,
                transparent: false,
            }),
            transparent: new THREE.MeshStandardMaterial({
                color: 0xffffff,
                opacity: 0,
                transparent: true,
            }),
            floor: new THREE.MeshStandardMaterial({
                color: 0x85b19b,
                opacity: 0.15,
                transparent: true,
            }),
            door: new THREE.MeshStandardMaterial({
                color: 0xff0000,
                emissive: 0x0,
                roughness: 0,
                metalness: 1,
            }),
        };
        this.initializeScene = () => {
            // Clean up existing scene if it exists
            this.cleanupScene();
            // Re-initialize materials after cleaning
            this.materials = {
                active: new THREE.MeshStandardMaterial({ color: 0xff0000, opacity: 1 }),
                normal: new THREE.MeshStandardMaterial({
                    color: 0xa3a3a3,
                    opacity: 1,
                    transparent: false,
                }),
                pulseOn: new THREE.MeshStandardMaterial({
                    color: 0x024970,
                    opacity: 0.5,
                    transparent: true,
                }),
                pulseOff: new THREE.MeshStandardMaterial({
                    color: 0x024970,
                    opacity: 1,
                    transparent: false,
                }),
                transparent: new THREE.MeshStandardMaterial({
                    color: 0xffffff,
                    opacity: 0,
                    transparent: true,
                }),
                floor: new THREE.MeshStandardMaterial({
                    color: 0x85b19b,
                    opacity: 0.15,
                    transparent: true,
                }),
                door: new THREE.MeshStandardMaterial({
                    color: 0xff0000,
                    emissive: 0x0,
                    roughness: 0,
                    metalness: 1,
                }),
            };
            const { width, height } = this.getCanvasSize();
            // Setup the camera
            this.camera = new THREE.PerspectiveCamera(45, width / height, 1, 1000);
            this.camera.position.set(7, 4, 3.5);
            this.camera.zoom = 1.7;
            this.camera.updateProjectionMatrix();
            this.camera.lookAt(0, 0, 0);
            // Create the scene
            this.scene = new THREE.Scene();
            // Initialize the loading manager
            const loadingManager = new THREE.LoadingManager(() => {
                // If 3D scene file failed to load
                if (this.elevator === undefined) {
                    const error = document.createElement("div");
                    error.textContent = this.store.t("ui:scene.failed_to_load");
                    error.classList.add("failed-to-load-error");
                    // On reload: replace the current scene, instead of creating a new one
                    if (this.canvas.children.length > 2) {
                        this.canvas.replaceChild(error, this.canvas.children[2]);
                    }
                    else {
                        this.canvas.appendChild(error);
                    }
                }
                else {
                    this.scene.add(this.elevator);
                }
            });
            // Load the elevator.dae file and initialize the objects
            const loader = new ColladaLoader(loadingManager);
            loader.load(getAssetPath("./assets/elevator-001.dae"), (collada) => {
                var _a, _b, _c;
                this.elevator = collada.scene;
                this.arrowUp = this.elevator.getObjectByName("arrow_up");
                this.arrowDown = this.elevator.getObjectByName("arrow_down");
                this.elevatorBackWall =
                    this.elevator.getObjectByName("elevator_back_wall");
                this.elevatorBackWallBeam =
                    this.elevator.getObjectByName("achter_wand_beam");
                this.frontDoorLeft = this.elevator.getObjectByName("front_door_left");
                this.frontDoorRight = this.elevator.getObjectByName("front_door_right");
                this.backDoor = this.elevator.getObjectByName("back_door");
                this.backDoorLeft = this.elevator.getObjectByName("back_door_left");
                this.backDoorRight = this.elevator.getObjectByName("back_door_right");
                this.floorSeparatorIndicator = this.elevator.getObjectByName("floor_separator_indicator");
                // Hide outlines
                this.elevator.children[0].traverse((childNode) => {
                    if (childNode.type === "LineSegments") {
                        childNode.visible = false;
                    }
                });
                // Change the material for the arrows
                for (let i = 0; i < this.arrowDown.children.length; i++) {
                    this.changeMaterial(this.arrowDown.children[i], this.materials.pulseOff);
                    this.changeMaterial(this.arrowUp.children[i], this.materials.pulseOff);
                }
                // Hide arrows
                this.stopMoving();
                // Assign positions
                this.positions = {
                    frontDoor: {
                        left: {
                            open: this.frontDoorLeft.position.y + this.doorWidth,
                            closed: this.frontDoorLeft.position.y,
                        },
                        right: {
                            open: this.frontDoorRight.position.y - this.doorWidth,
                            closed: this.frontDoorRight.position.y,
                        },
                    },
                    backDoor: {
                        left: {
                            open: this.backDoorLeft.position.y + this.doorWidth,
                            closed: this.backDoorLeft.position.y,
                        },
                        right: {
                            open: this.backDoorRight.position.y - this.doorWidth,
                            closed: this.backDoorRight.position.y,
                        },
                    },
                    floor: {
                        down: this.floorSeparatorIndicator.position.z,
                        up: this.floorSeparatorIndicator.position.z + this.floorHeight,
                    },
                };
                const snapshot = (_b = (_a = this.store) === null || _a === void 0 ? void 0 : _a.state) === null || _b === void 0 ? void 0 : _b.currentSensorData;
                if (snapshot !== undefined && snapshot !== null) {
                    this.processSensorUpdate(snapshot, true);
                    // Ensure initial movement state is visually applied once the model is ready.
                    const initialMovement = (_c = snapshot.values) === null || _c === void 0 ? void 0 : _c[500];
                    if (initialMovement === "U") {
                        this.moveUp();
                    }
                    else if (initialMovement === "D") {
                        this.moveDown();
                    }
                    else {
                        this.stopMoving();
                    }
                }
            });
            // Setup lighting
            const ambientLight = new THREE.AmbientLight(0xcccccc, 1);
            this.scene.add(ambientLight);
            const directionalLight = new THREE.DirectionalLight(0xeeeeee, 2);
            directionalLight.position.set(1, 1, 0).normalize();
            this.scene.add(directionalLight);
            // Setup renderer
            this.renderer = new THREE.WebGLRenderer({
                antialias: true,
                alpha: true,
            });
            this.renderer.setPixelRatio(window.devicePixelRatio);
            this.renderer.setClearColor(0x000000, 0);
            // On reload: replace the current scene, instead of creating a new one
            if (this.canvas.children.length > 3) {
                this.canvas.replaceChild(this.renderer.domElement, this.canvas.children[3]);
            }
            else {
                this.canvas.appendChild(this.renderer.domElement);
            }
            this.resizeScene();
        };
        this.scheduleResize = () => {
            if (this.resizeFrameId !== null) {
                return;
            }
            this.resizeFrameId = requestAnimationFrame(() => {
                this.resizeFrameId = null;
                this.resizeScene();
            });
        };
        this.observeResize = () => {
            const ResizeObserverConstructor = window.ResizeObserver;
            if (isNil(this.canvas) || ResizeObserverConstructor === undefined) {
                return;
            }
            this.resizeObserver = new ResizeObserverConstructor(() => {
                if (!this.isComponentMounted) {
                    return;
                }
                this.scheduleResize();
            });
            this.resizeObserver.observe(this.canvas);
        };
        this.getCanvasSize = () => {
            const fallbackSize = this.DEFAULT_SCENE_SIZE;
            if (isNil(this.canvas)) {
                return { width: fallbackSize, height: fallbackSize };
            }
            const rect = this.canvas.getBoundingClientRect();
            const width = Math.max(1, Math.round(rect.width || fallbackSize));
            const height = Math.max(1, Math.round(rect.height || width));
            return { width, height };
        };
        this.resizeScene = () => {
            if (isNil(this.renderer) ||
                isNil(this.camera) ||
                isNil(this.canvas)) {
                return;
            }
            const { width, height } = this.getCanvasSize();
            if (width === this.lastCanvasSize.width &&
                height === this.lastCanvasSize.height) {
                return;
            }
            this.lastCanvasSize = { width, height };
            this.renderer.setPixelRatio(window.devicePixelRatio);
            this.renderer.setSize(width, height, false);
            this.camera.aspect = width / height;
            this.camera.updateProjectionMatrix();
            this.updateIndicatorScale(width);
            this.renderScene();
        };
        this.updateIndicatorScale = (canvasWidth) => {
            if (isNil(this.canvas)) {
                return;
            }
            const baseSize = this.DEFAULT_SCENE_SIZE;
            const scale = Math.min(1, canvasWidth / baseSize);
            const fontSizePx = Number.parseFloat(getComputedStyle(this.canvas).fontSize) || 16;
            const toPx = (value, min = 1) => `${Math.max(min, Math.round(value))}px`;
            this.canvas.style.setProperty("--ls-indicator-width", toPx(150 * scale));
            this.canvas.style.setProperty("--ls-indicator-height", toPx(96 * scale));
            this.canvas.style.setProperty("--ls-indicator-label-size", toPx(fontSizePx * scale));
            this.canvas.style.setProperty("--ls-indicator-label-line-height", toPx(fontSizePx * 1.5 * scale));
            this.canvas.style.setProperty("--ls-indicator-digit-size", toPx(fontSizePx * 5 * scale));
            this.canvas.style.setProperty("--ls-indicator-digit-line-height", toPx(80 * scale));
            this.canvas.style.setProperty("--ls-indicator-digit-width", toPx(75 * scale));
            this.canvas.style.setProperty("--ls-indicator-letter-spacing", toPx(fontSizePx * 0.05 * scale, 0));
            this.canvas.style.setProperty("--ls-3d-scene-last-update-size", toPx(fontSizePx * scale));
            this.canvas.style.setProperty("--ls-3d-scene-last-update-line-height", toPx(fontSizePx * 1.5 * scale));
            this.canvas.style.setProperty("--ls-3d-scene-last-update-padding", `${toPx(fontSizePx * 0.25 * scale)} ${toPx(fontSizePx * 0.5 * scale)}`);
            this.canvas.style.setProperty("--ls-3d-scene-last-update-radius", toPx(20 * scale));
        };
        this.animateScene = () => {
            if (!this.isAnimating || !this.isComponentMounted) {
                this.animationFrameId = null;
                return;
            }
            // Update tweens only if they exist
            if (this.doorAnimationTween !== null) {
                this.doorAnimationTween.update();
            }
            if (this.floorAnimationTween !== null) {
                this.floorAnimationTween.update();
            }
            this.renderScene();
            // Schedule next frame
            this.animationFrameId = requestAnimationFrame(this.animateScene);
        };
        this.startAnimation = () => {
            if (!this.isAnimating) {
                this.isAnimating = true;
                this.animateScene();
            }
        };
        this.stopAnimation = () => {
            this.isAnimating = false;
            if (this.animationFrameId !== null) {
                cancelAnimationFrame(this.animationFrameId);
                this.animationFrameId = null;
            }
        };
        this.renderScene = () => {
            var _a;
            (_a = this.renderer) === null || _a === void 0 ? void 0 : _a.render(this.scene, this.camera);
        };
        this.stopDoorAnimation = () => {
            if (this.doorAnimationTween !== null) {
                if (typeof this.doorAnimationTween.stop === "function") {
                    this.doorAnimationTween.stop();
                }
                this.doorAnimationTween = null;
            }
        };
        this.changeMaterial = (node, material) => {
            if (typeof node.traverse !== "function" ||
                !this.isComponentMounted) {
                return;
            }
            node.traverse((childNode) => {
                if (childNode.type === "LineSegments") {
                    childNode.visible = false;
                }
                else if (childNode instanceof THREE.Mesh) {
                    childNode.material = material;
                }
            });
        };
        this.openFrontDoor = (skipAnimation = false) => {
            if (isNil(this.frontDoorLeft) || isNil(this.frontDoorRight)) {
                return;
            }
            if (this.frontDoorLeft.position.y === this.positions.frontDoor.left.open) {
                return;
            }
            if (skipAnimation) {
                this.frontDoorLeft.position.y = this.positions.frontDoor.left.open;
                this.frontDoorRight.position.y = this.positions.frontDoor.right.open;
                return;
            }
            // Stop and clean up any existing door animation
            this.stopDoorAnimation();
            // Ensure main animation loop is running for tween updates
            this.startAnimation();
            this.doorAnimationTween = new Tween({
                leftDoor: this.frontDoorLeft.position.y,
                rightDoor: this.frontDoorRight.position.y,
            })
                .to({
                leftDoor: this.positions.frontDoor.left.open,
                rightDoor: this.positions.frontDoor.right.open,
            }, this.speed)
                .easing(Easing.Cubic.InOut)
                .onUpdate(({ leftDoor, rightDoor }) => {
                this.frontDoorLeft.position.y = leftDoor;
                this.frontDoorRight.position.y = rightDoor;
            })
                .start();
        };
        this.closeFrontDoor = (skipAnimation = false) => {
            if (isNil(this.frontDoorLeft) || isNil(this.frontDoorRight)) {
                return;
            }
            if (this.frontDoorLeft.position.y === this.positions.frontDoor.left.closed) {
                return;
            }
            if (skipAnimation) {
                this.frontDoorLeft.position.y = this.positions.frontDoor.left.closed;
                this.frontDoorRight.position.y = this.positions.frontDoor.right.closed;
                return;
            }
            // Stop and clean up any existing door animation
            this.stopDoorAnimation();
            // Ensure main animation loop is running for tween updates
            this.startAnimation();
            this.doorAnimationTween = new Tween({
                leftDoor: this.frontDoorLeft.position.y,
                rightDoor: this.frontDoorRight.position.y,
            })
                .to({
                leftDoor: this.positions.frontDoor.left.closed,
                rightDoor: this.positions.frontDoor.right.closed,
            }, this.speed)
                .easing(Easing.Cubic.InOut)
                .onUpdate(({ leftDoor, rightDoor }) => {
                this.frontDoorLeft.position.y = leftDoor;
                this.frontDoorRight.position.y = rightDoor;
            })
                .start();
        };
        this.openBackDoor = (skipAnimation = false) => {
            if (isNil(this.backDoorLeft) || isNil(this.backDoorRight)) {
                return;
            }
            if (this.backDoorLeft.position.y === this.positions.backDoor.left.open) {
                return;
            }
            if (skipAnimation) {
                this.backDoorLeft.position.y = this.positions.backDoor.left.open;
                this.backDoorRight.position.y = this.positions.backDoor.right.open;
                return;
            }
            // Stop and clean up any existing door animation
            this.stopDoorAnimation();
            // Ensure main animation loop is running for tween updates
            this.startAnimation();
            this.doorAnimationTween = new Tween({
                leftDoor: this.backDoorLeft.position.y,
                rightDoor: this.backDoorRight.position.y,
            })
                .to({
                leftDoor: this.positions.backDoor.left.open,
                rightDoor: this.positions.backDoor.right.open,
            }, this.speed)
                .easing(Easing.Cubic.InOut)
                .onUpdate(({ leftDoor, rightDoor }) => {
                this.backDoorLeft.position.y = leftDoor;
                this.backDoorRight.position.y = rightDoor;
            })
                .start();
        };
        this.closeBackDoor = (skipAnimation = false) => {
            if (isNil(this.backDoorLeft) || isNil(this.backDoorRight)) {
                return;
            }
            if (this.backDoorLeft.position.y === this.positions.backDoor.left.closed) {
                return;
            }
            if (skipAnimation) {
                this.backDoorLeft.position.y = this.positions.backDoor.left.closed;
                this.backDoorRight.position.y = this.positions.backDoor.right.closed;
                return;
            }
            // Stop and clean up any existing door animation
            this.stopDoorAnimation();
            // Ensure main animation loop is running for tween updates
            this.startAnimation();
            this.doorAnimationTween = new Tween({
                leftDoor: this.backDoorLeft.position.y,
                rightDoor: this.backDoorRight.position.y,
            })
                .to({
                leftDoor: this.positions.backDoor.left.closed,
                rightDoor: this.positions.backDoor.right.closed,
            }, this.speed)
                .easing(Easing.Cubic.InOut)
                .onUpdate(({ leftDoor, rightDoor }) => {
                this.backDoorLeft.position.y = leftDoor;
                this.backDoorRight.position.y = rightDoor;
            })
                .start();
        };
        this.startPulseArrowAnimation = (arrow) => {
            if (!arrow)
                return;
            // Clear any existing pulse interval
            this.stopPulseArrowAnimation();
            let counter = 0;
            this.pulseInterval = setInterval(() => {
                // Check if component is still mounted
                if (!this.isComponentMounted || !arrow.children) {
                    this.stopPulseArrowAnimation();
                    return;
                }
                const selectedIndex = counter % 10;
                const childrenLength = arrow.children.length;
                for (let i = 0; i < childrenLength; i++) {
                    const child = arrow.children[i];
                    if (child) {
                        this.changeMaterial(child, i === selectedIndex
                            ? this.materials.pulseOn
                            : this.materials.pulseOff);
                    }
                }
                counter++;
            }, 180);
        };
        this.stopPulseArrowAnimation = () => {
            var _a, _b;
            if (this.pulseInterval !== null) {
                clearInterval(this.pulseInterval);
                this.pulseInterval = null;
            }
            if (((_b = (_a = this.arrowDown) === null || _a === void 0 ? void 0 : _a.children) === null || _b === void 0 ? void 0 : _b.length) > 0) {
                for (let i = 0; i < this.arrowDown.children.length; i++) {
                    this.changeMaterial(this.arrowDown.children[i], this.materials.pulseOff);
                    this.changeMaterial(this.arrowUp.children[i], this.materials.pulseOff);
                }
            }
        };
        this.moveUp = () => {
            if (isNil(this.arrowUp) || isNil(this.arrowDown)) {
                return;
            }
            this.stopPulseArrowAnimation();
            this.hideNode(this.arrowDown);
            this.showNode(this.arrowUp);
            this.startPulseArrowAnimation(this.arrowUp);
            this.startAnimateFloorsIndicator("down");
            this.movingDirection = "down";
        };
        this.moveDown = () => {
            if (isNil(this.arrowUp) || isNil(this.arrowDown)) {
                return;
            }
            this.stopPulseArrowAnimation();
            this.hideNode(this.arrowUp);
            this.showNode(this.arrowDown);
            this.startPulseArrowAnimation(this.arrowDown);
            this.startAnimateFloorsIndicator("up");
            this.movingDirection = "up";
        };
        this.stopMoving = () => {
            if (!isNil(this.arrowUp)) {
                this.hideNode(this.arrowUp);
            }
            if (!isNil(this.arrowDown)) {
                this.hideNode(this.arrowDown);
            }
            this.stopPulseArrowAnimation();
            this.stopAnimateFloorsIndicator(this.movingDirection);
        };
        this.startAnimateFloorsIndicator = (direction) => {
            if (isNil(this.floorSeparatorIndicator)) {
                return;
            }
            // If already easing in, don't start a new easing animation
            if (this.isEasingIn) {
                return;
            }
            // Stop any existing animation and reset flags
            if (this.floorAnimationTween !== null) {
                this.floorAnimationTween.stop();
                this.floorAnimationTween = null;
            }
            // Reset any ongoing easing out state
            if (this.isEasingOut) {
                this.isEasingOut = false;
            }
            // Reset continuous animation flag
            this.animateTheFloor = false;
            this.isEasingIn = true;
            let easingAmount = 50;
            if (direction === "down") {
                easingAmount = -easingAmount;
            }
            // Easy in the floor animation before starting continuous movement
            this.floorAnimationTween = new Tween({
                topPos: this.floorSeparatorIndicator.position.z,
            })
                .to({
                topPos: this.floorSeparatorIndicator.position.z + easingAmount,
            })
                .easing(Easing.Cubic.In)
                .onUpdate(({ topPos }) => {
                this.floorSeparatorIndicator.position.z = topPos;
            })
                .onComplete(() => {
                this.isEasingIn = false;
                this.animateTheFloor = true;
                this.animateFloorsIndicator(direction);
            })
                .start();
        };
        this.stopAnimateFloorsIndicator = (elevatorDirection) => {
            if (isNil(this.floorSeparatorIndicator)) {
                return;
            }
            // If already easing out, don't start a new easing animation
            if (this.isEasingOut) {
                return;
            }
            if (this.floorAnimationTween !== null) {
                this.animateTheFloor = false;
                this.floorAnimationTween.stop();
                // Reset easing in flag if we were in that state
                if (this.isEasingIn) {
                    this.isEasingIn = false;
                }
                this.isEasingOut = true;
                let easingAmount = 50;
                if (elevatorDirection === "down") {
                    easingAmount = -easingAmount;
                }
                // Easy out the floor animation
                this.floorAnimationTween = new Tween({
                    topPos: this.floorSeparatorIndicator.position.z,
                })
                    .to({
                    topPos: this.floorSeparatorIndicator.position.z + easingAmount,
                })
                    .easing(Easing.Cubic.Out)
                    .onUpdate(({ topPos }) => {
                    this.floorSeparatorIndicator.position.z = topPos;
                })
                    .onComplete(() => {
                    this.floorAnimationTween = null;
                    this.isEasingOut = false;
                })
                    .start();
            }
        };
        this.animateFloorsIndicator = (elevatorDirection) => {
            if (isNil(this.floorSeparatorIndicator)) {
                return;
            }
            // Don't start continuous animation if we're easing out or no longer supposed to animate or component unmounted
            if (!this.animateTheFloor || !this.isComponentMounted) {
                return;
            }
            let startPos;
            let endPos;
            let movementSpeed = this.floorHeight * 10;
            if (elevatorDirection === "up") {
                if (this.floorSeparatorIndicator.position.z >= this.positions.floor.up) {
                    startPos = this.positions.floor.down;
                }
                // If the floor separator indicator is in view, continue from there
                else {
                    startPos = this.floorSeparatorIndicator.position.z;
                    // Adjust speed based on distance
                    movementSpeed =
                        Math.abs(this.floorSeparatorIndicator.position.z - this.positions.floor.up) * 10;
                }
                endPos = this.positions.floor.up;
            }
            else if (elevatorDirection === "down") {
                if (this.floorSeparatorIndicator.position.z <= this.positions.floor.down) {
                    startPos = this.positions.floor.up;
                }
                // If the floor separator indicator is in view, continue from there
                else {
                    startPos = this.floorSeparatorIndicator.position.z;
                    // Adjust speed based on distance
                    movementSpeed =
                        (this.floorSeparatorIndicator.position.z -
                            this.positions.floor.down) *
                            10;
                }
                endPos = this.positions.floor.down;
            }
            if (this.floorAnimationTween !== null) {
                this.floorAnimationTween.stop();
                this.floorAnimationTween = null;
            }
            this.floorAnimationTween = new Tween({
                topPos: startPos,
            })
                .to({
                topPos: endPos,
            }, movementSpeed)
                .onUpdate(({ topPos }) => {
                this.floorSeparatorIndicator.position.z = topPos;
            })
                .onComplete(() => {
                // Restart the animation for continuous effect only if component is still mounted
                if (this.animateTheFloor === true && this.isComponentMounted) {
                    this.animateFloorsIndicator(elevatorDirection);
                }
            })
                .start();
        };
        this.hideNode = (node) => {
            if (!this.isComponentMounted) {
                return;
            }
            node.traverse((childNode) => {
                childNode.visible = false;
            });
        };
        this.showNode = (node) => {
            if (!this.isComponentMounted) {
                return;
            }
            node.traverse((childNode) => {
                if (childNode.type === "LineSegments") {
                    childNode.visible = false;
                }
                else {
                    childNode.visible = true;
                }
            });
        };
        this.processSensorUpdate = (sensorData, initial = false) => {
            // Prevent processing if component is not mounted, except for initial snapshot
            if (!this.isComponentMounted && !initial) {
                return;
            }
            const sensorValues = sensorData.values;
            // Only update error state if not initial load to avoid excessive DOM operations
            if (!initial) {
                this.updateErrorState();
            }
            if (this.errorState.hasError) {
                return;
            }
            let lastUpdate = 0;
            const sensors = [500, 502, 503, 514, 515];
            sensors.forEach((value) => {
                lastUpdate = dayjs(sensorData.updated[value]).isAfter(dayjs(lastUpdate))
                    ? sensorData.updated[value]
                    : lastUpdate;
            });
            this.lastUpdate = lastUpdate;
            for (const sensorId in sensorValues) {
                // Elevator state (movement)
                switch (sensorId) {
                    case "500":
                        switch (sensorValues[sensorId]) {
                            case "D":
                                if (this.movementState !== "D") {
                                    this.moveDown();
                                }
                                break;
                            case "U":
                                if (this.movementState !== "U") {
                                    this.moveUp();
                                }
                                break;
                            default:
                                this.stopMoving();
                        }
                        this.movementState = sensorValues[sensorId];
                        break;
                    // Current stop place
                    case "515":
                        this.currentStop = sensorValues[sensorId];
                        break;
                    // Front door
                    case "502":
                        switch (sensorValues[sensorId]) {
                            case "C":
                            case "X":
                                this.closeFrontDoor(true);
                                break;
                            case "-":
                                this.closeFrontDoor(false);
                                break;
                            case "O":
                                this.openFrontDoor(true);
                                break;
                            case "+":
                                this.openFrontDoor(false);
                                break;
                            default:
                                this.closeFrontDoor(true);
                        }
                        break;
                    // Rear door
                    case "503":
                        switch (sensorValues[sensorId]) {
                            case "C":
                            case "X":
                                this.closeBackDoor(true);
                                break;
                            case "-":
                                this.closeBackDoor(false);
                                break;
                            case "O":
                                this.openBackDoor(true);
                                break;
                            case "+":
                                this.openBackDoor(false);
                                break;
                            default:
                                this.closeBackDoor(true);
                        }
                        break;
                    // Last passed
                    case "514":
                        this.lastPassedStop = sensorValues[sensorId];
                        break;
                }
            }
        };
        this.hasBackDoor = (hasBackDoor) => {
            if (hasBackDoor)
                return;
            // Clear any existing timeout
            if (this.hasBackDoorTimeout !== null) {
                clearTimeout(this.hasBackDoorTimeout);
                this.hasBackDoorTimeout = null;
            }
            // Wait until the scene is loaded
            if (this.backDoor === undefined ||
                this.elevatorBackWall === undefined ||
                this.elevatorBackWallBeam === undefined) {
                // Only retry if component is still mounted
                if (this.isComponentMounted) {
                    // Use arrow function to minimize closure scope
                    this.hasBackDoorTimeout = setTimeout(() => {
                        // Re-check mounted state inside timeout
                        if (this.isComponentMounted) {
                            this.hasBackDoor(hasBackDoor);
                        }
                    }, 100);
                }
                return;
            }
            this.hideNode(this.backDoor);
            this.showNode(this.elevatorBackWall);
            this.showNode(this.elevatorBackWallBeam);
        };
    }
    /*
     * Functions
     */
    scheduleSceneResizeOnWindowResize() {
        if (!this.isComponentMounted) {
            return;
        }
        if (this.resizeObserver === null) {
            this.scheduleResize();
        }
    }
    /**
     * Updates the error state based on current component conditions
     */
    updateErrorState() {
        var _a;
        if (this.store === null) {
            this.errorState = getStoreUnavailableError((_a = this.store) === null || _a === void 0 ? void 0 : _a.state.locale);
            return;
        }
        if (this.store.state.error.hasError) {
            if (this.errorState.hasError) {
                this.errorState = {
                    hasError: true,
                    messageKey: this.store.state.error.messageKey,
                    type: this.store.state.error.type,
                };
                this.updateErrorMessageElement();
                return;
            }
            this.errorState = {
                hasError: true,
                messageKey: this.store.state.error.messageKey,
                type: this.store.state.error.type,
            };
            if (!isNil(this.canvas)) {
                applyErrorMessage(this.canvas, this.getErrorMessageForDisplay());
            }
            this.clearTimer();
            return;
        }
        // No error detected
        if (this.errorState.hasError) {
            this.errorState = {
                hasError: false,
                messageKey: undefined,
                type: "",
            };
        }
        if (!isNil(this.canvas)) {
            clearErrorMessage(this.canvas);
        }
        this.setTimer();
    }
    getErrorMessageForDisplay() {
        var _a;
        const errorKey = (_a = this.errorState.messageKey) !== null && _a !== void 0 ? _a : "CONSOLE_INFO";
        return this.store.t(`errors:${errorKey}`);
    }
    updateErrorMessageElement() {
        if (isNil(this.canvas))
            return;
        const existingError = this.canvas.querySelector(".error-message");
        if (existingError !== null) {
            existingError.textContent = this.getErrorMessageForDisplay();
        }
    }
    setTimer() {
        // Clear any existing timer first
        this.clearTimer();
        // The timer is used to keep the relative timestamps and movement state fresh
        // Use a weak reference to avoid closure memory leaks
        this.timer = window.setInterval(() => {
            var _a, _b;
            // Only process if component is still mounted
            if (!this.isComponentMounted) {
                this.clearTimer();
                return;
            }
            // Access store data only when needed to minimize closure scope
            const sensorData = (_b = (_a = this.store) === null || _a === void 0 ? void 0 : _a.state) === null || _b === void 0 ? void 0 : _b.currentSensorData;
            if (sensorData !== undefined) {
                this.processSensorUpdate(sensorData);
            }
        }, 1000);
    }
    clearTimer() {
        window.clearInterval(this.timer);
        this.timer = null;
    }
    connectedCallback() {
        this.isComponentMounted = true;
        this.setTimer();
        this.startAnimation();
    }
    cleanupScene() {
        // Dispose of existing renderer
        if (!isNil(this.renderer)) {
            this.renderer.dispose();
            this.renderer = null;
        }
        // Dispose of existing scene resources
        if (!isNil(this.scene)) {
            // Dispose geometries and materials
            this.scene.traverse((object) => {
                if (object instanceof THREE.Mesh) {
                    if (object.geometry) {
                        object.geometry.dispose();
                    }
                    if (object.material) {
                        if (Array.isArray(object.material)) {
                            object.material.forEach((material) => material.dispose());
                        }
                        else {
                            object.material.dispose();
                        }
                    }
                }
            });
            this.scene.clear();
            this.scene = null;
        }
        // Dispose of materials
        Object.values(this.materials).forEach((material) => {
            if (material !== undefined && typeof material.dispose === "function") {
                material.dispose();
            }
        });
        // Clear references to Three.js objects
        this.camera = null;
        this.elevator = null;
        this.arrowDown = null;
        this.arrowUp = null;
        this.backDoor = null;
        this.backDoorLeft = null;
        this.backDoorRight = null;
        this.elevatorBackWall = null;
        this.elevatorBackWallBeam = null;
        this.frontDoorLeft = null;
        this.frontDoorRight = null;
        this.floorSeparatorIndicator = null;
    }
    disconnectedCallback() {
        this.isComponentMounted = false;
        // Stop all animations and timers
        this.stopAnimation();
        this.clearTimer();
        // Clear hasBackDoor timeout
        if (this.hasBackDoorTimeout !== null) {
            clearTimeout(this.hasBackDoorTimeout);
            this.hasBackDoorTimeout = null;
        }
        // Unsubscribe from store changes
        if (this.storeUnsubscribe !== null) {
            this.storeUnsubscribe();
            this.storeUnsubscribe = null;
        }
        // Stop and cleanup tweens
        if (this.floorAnimationTween !== null) {
            this.floorAnimationTween.stop();
            this.floorAnimationTween = null;
        }
        this.stopDoorAnimation();
        // Reset animation flags
        this.isEasingOut = false;
        this.isEasingIn = false;
        this.animateTheFloor = false;
        // Clear pulse interval
        if (this.pulseInterval !== null) {
            clearInterval(this.pulseInterval);
            this.pulseInterval = null;
        }
        if (this.resizeObserver !== null) {
            this.resizeObserver.disconnect();
            this.resizeObserver = null;
        }
        if (this.resizeFrameId !== null) {
            cancelAnimationFrame(this.resizeFrameId);
            this.resizeFrameId = null;
        }
        // Clean up Three.js resources
        this.cleanupScene();
    }
    async componentWillLoad() {
        dayjs.extend(relativeTime);
        this.store = await getStore(this.idKey);
        watchStoreLocale(this, this.store, () => {
            var _a;
            this.updateErrorMessageElement();
            const failedToLoad = (_a = this.canvas) === null || _a === void 0 ? void 0 : _a.querySelector(".failed-to-load-error");
            if (failedToLoad !== null) {
                failedToLoad.textContent = this.store.t("ui:scene.failed_to_load");
            }
        });
        // Initialize lastUpdate directly from the aggregate store data so the UI
        // does not have to wait for the 20s timer on first load.
        const initialSensorData = this.store.state.currentSensorData;
        if (initialSensorData !== undefined && initialSensorData !== null) {
            const aggregateLastUpdate = initialSensorData.lastUpdate;
            if (typeof aggregateLastUpdate === "number" && aggregateLastUpdate > 0) {
                this.lastUpdate = aggregateLastUpdate;
            }
        }
    }
    componentDidLoad() {
        this.initializeScene();
        this.startAnimation();
        this.observeResize();
        this.hasBackDoor(this.store.state.hasBackDoor);
        this.updateErrorState();
        this.processSensorUpdate(this.store.state.currentSensorData, true);
        const sensorDataUnsubscribe = this.store.onChange("currentSensorData", (sensorData) => {
            this.processSensorUpdate(sensorData);
        });
        const errorUnsubscribe = this.store.onChange("error", () => {
            this.updateErrorState();
        });
        this.storeUnsubscribe = () => {
            sensorDataUnsubscribe();
            errorUnsubscribe();
        };
    }
    componentDidUpdate() {
        // Only start animation if not already running
        if (!this.isAnimating) {
            this.startAnimation();
        }
    }
    render() {
        var _a, _b, _c;
        const effectiveLastUpdate = this.lastUpdate ||
            ((_b = (_a = this.store) === null || _a === void 0 ? void 0 : _a.state.currentSensorData) === null || _b === void 0 ? void 0 : _b.lastUpdate) ||
            0;
        return (h(Host, { key: 'da2e9578da250d40e0daec81a6c8a8cca16d0b79' }, h("div", { key: 'c90627d387a8314eb2bc5a7f57268b5d16aeb09a', class: "canvas", ref: (el) => {
                this.canvas = el;
            } }, h("ls-indicator", { key: 'd673827a9141943be8da59b9fc5aa9a45142f747', class: "current-floor", "sensor-id": "515", "id-key": this.idKey }), h("ls-indicator", { key: '23f14f63421c7d92e2549afcc80cb85cbee1619a', class: "last-passed-floor", "sensor-id": "514", "id-key": this.idKey }), h("div", { key: '70b9cd3f352ec034b1f62381fdc95f3c32eafbea', class: "last-update" }, getSafeRelativeTime(effectiveLastUpdate, (_c = this.store) === null || _c === void 0 ? void 0 : _c.state.locale))), h("slot", { key: 'e0ad61b9345253f45ac8e016134d67c357088e3e' })));
    }
    static get is() { return "ls-3d-scene"; }
    static get encapsulation() { return "shadow"; }
    static get originalStyleUrls() {
        return {
            "$": ["ls-3d-scene.css"]
        };
    }
    static get styleUrls() {
        return {
            "$": ["ls-3d-scene.css"]
        };
    }
    static get assetsDirs() { return ["assets"]; }
    static get properties() {
        return {
            "idKey": {
                "type": "string",
                "attribute": "id-key",
                "mutable": false,
                "complexType": {
                    "original": "string",
                    "resolved": "string",
                    "references": {}
                },
                "required": false,
                "optional": false,
                "docs": {
                    "tags": [],
                    "text": "The unique key that is used to identify store data."
                },
                "getter": false,
                "setter": false,
                "reflect": false
            }
        };
    }
    static get states() {
        return {
            "movementState": {},
            "currentStop": {},
            "lastPassedStop": {},
            "lastUpdate": {},
            "errorState": {}
        };
    }
    static get listeners() {
        return [{
                "name": "resize",
                "method": "scheduleSceneResizeOnWindowResize",
                "target": "window",
                "capture": false,
                "passive": true
            }];
    }
}
//# sourceMappingURL=ls-3d-scene.js.map
