<!DOCTYPE html>
<html>
<head>
    <title>Three.js Infinite World</title>
    <style>
        body { margin: 0; overflow: hidden; }
        canvas { display: block; }
    </style>
    <!-- New: Polling function for game state -->
    <script>
        // Poll the shared game state every 5 seconds (for demonstration)
        function pollGameState() {
            console.log("Polling updated game state:", window.GAME_STATE);
            // Here you could update the scene based on the new state.
        }
        setInterval(pollGameState, 5000);
    </script>
</head>
<body>
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.163.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.163.0/examples/jsm/"
            }
        }
    </script>

    <script type="module">
        import * as THREE from 'three';

        let scene, camera, renderer, playerMesh;
        let raycaster, mouse;
        const keysPressed = {};
        const playerSpeed = 0.15;
        let newlyPlacedObjects = []; // Track objects added THIS session for saving
        const placeholderPlots = new Set();
        const groundMeshes = {}; // Store ground mesh references

        // --- Session Storage Key ---
        const SESSION_STORAGE_KEY = 'unsavedInfiniteWorldState';

        // --- Injected State from Streamlit ---
        const allInitialObjects = window.ALL_INITIAL_OBJECTS || [];
        const plotsMetadata = window.PLOTS_METADATA || [];
        const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
        const plotWidth = window.PLOT_WIDTH || 50.0;
        const plotDepth = window.PLOT_DEPTH || 50.0;

        const groundMaterial = new THREE.MeshStandardMaterial({
            color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide
        });
        const placeholderGroundMaterial = new THREE.MeshStandardMaterial({
            color: 0x448844, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide
        });

        function init() {
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0xabcdef);
            const aspect = window.innerWidth / window.innerHeight;
            camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 4000);
            camera.position.set(0, 15, 20);
            camera.lookAt(0, 0, 0);
            scene.add(camera);

            setupLighting();
            setupInitialGround();
            setupPlayer();

            raycaster = new THREE.Raycaster();
            mouse = new THREE.Vector2();

            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.shadowMap.enabled = true;
            renderer.shadowMap.type = THREE.PCFSoftShadowMap;
            document.body.appendChild(renderer.domElement);

            loadInitialObjects();
            restoreUnsavedState();

            // Event Listeners
            document.addEventListener('mousemove', onMouseMove, false);
            document.addEventListener('click', onDocumentClick, false);
            window.addEventListener('resize', onWindowResize, false);
            document.addEventListener('keydown', onKeyDown);
            document.addEventListener('keyup', onKeyUp);

            // Define functions callable by Streamlit
            window.teleportPlayer = teleportPlayer;
            window.getSaveDataAndPosition = getSaveDataAndPosition;
            window.resetNewlyPlacedObjects = resetNewlyPlacedObjects;

            console.log("Three.js Initialized. World ready.");
            animate();
        }

        function setupLighting() {
            const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
            scene.add(ambientLight);
            const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
            directionalLight.position.set(50, 150, 100);
            directionalLight.castShadow = true;
            directionalLight.shadow.mapSize.width = 4096;
            directionalLight.shadow.mapSize.height = 4096;
            directionalLight.shadow.camera.near = 0.5;
            directionalLight.shadow.camera.far = 500;
            directionalLight.shadow.camera.left = -150;
            directionalLight.shadow.camera.right = 150;
            directionalLight.shadow.camera.top = 150;
            directionalLight.shadow.camera.bottom = -150;
            directionalLight.shadow.bias = -0.001;
            scene.add(directionalLight);
        }

        function setupInitialGround() {
            console.log(`Setting up initial ground for ${plotsMetadata.length} plots.`);
            plotsMetadata.forEach(plot => {
                createGroundPlane(plot.grid_x, plot.grid_z, false);
            });
            if (plotsMetadata.length === 0) {
                createGroundPlane(0, 0, false);
            }
        }

        function createGroundPlane(gridX, gridZ, isPlaceholder) {
            const gridKey = `${gridX}_${gridZ}`;
            if (groundMeshes[gridKey]) return;
            console.log(`Creating ${isPlaceholder ? 'placeholder' : 'initial'} ground at ${gridX}, ${gridZ}`);
            const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth);
            const material = isPlaceholder ? placeholderGroundMaterial : groundMaterial;
            const groundMesh = new THREE.Mesh(groundGeometry, material);
            groundMesh.rotation.x = -Math.PI / 2;
            groundMesh.position.y = -0.05;
            groundMesh.position.x = gridX * plotWidth + plotWidth / 2.0;
            groundMesh.position.z = gridZ * plotDepth + plotDepth / 2.0;
            groundMesh.receiveShadow = true;
            groundMesh.userData.gridKey = gridKey;
            scene.add(groundMesh);
            groundMeshes[gridKey] = groundMesh;
            if (isPlaceholder) placeholderPlots.add(gridKey);
        }

        function setupPlayer() {
            const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
            const playerMat = new THREE.MeshStandardMaterial({ color: 0x0000ff, roughness: 0.6 });
            playerMesh = new THREE.Mesh(playerGeo, playerMat);
            playerMesh.position.set(plotWidth / 2, 0.4 + 0.8/2, plotDepth/2);
            playerMesh.castShadow = true;
            playerMesh.receiveShadow = true;
            scene.add(playerMesh);
        }

        function loadInitialObjects() {
            console.log(`Loading ${allInitialObjects.length} initial objects from Python.`);
            allInitialObjects.forEach(objData => { createAndPlaceObject(objData, false); });
            console.log("Finished loading initial objects.");
        }

        function createAndPlaceObject(objData, isNewObject) {
            let loadedObject = null;
            switch (objData.type) {
                case "Simple House": loadedObject = createSimpleHouse(); break;
                case "Tree": loadedObject = createTree(); break;
                case "Rock": loadedObject = createRock(); break;
                case "Fence Post": loadedObject = createFencePost(); break;
                default: console.warn("Unknown object type in data:", objData.type); break;
            }
            if (loadedObject) {
                if (objData.position && objData.position.x !== undefined) {
                    loadedObject.position.set(objData.position.x, objData.position.y, objData.position.z);
                } else if (objData.pos_x !== undefined) {
                    loadedObject.position.set(objData.pos_x, objData.pos_y, objData.pos_z);
                }
                if (objData.rotation) {
                    loadedObject.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
                } else if (objData.rot_x !== undefined) {
                    loadedObject.rotation.set(objData.rot_x, objData.rot_y, objData.rot_z, objData.rot_order || 'XYZ');
                }
                loadedObject.userData.obj_id = objData.obj_id || loadedObject.userData.obj_id;
                scene.add(loadedObject);
                if (isNewObject) newlyPlacedObjects.push(loadedObject);
                return loadedObject;
            }
            return null;
        }

        function saveUnsavedState() {
            try {
                const stateToSave = newlyPlacedObjects.map(obj => ({
                    obj_id: obj.userData.obj_id,
                    type: obj.userData.type,
                    position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
                    rotation: { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order }
                }));
                sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(stateToSave));
                console.log(`Saved ${stateToSave.length} unsaved objects to sessionStorage.`);
            } catch (e) {
                console.error("Error saving state to sessionStorage:", e);
            }
        }

        function restoreUnsavedState() {
            try {
                const savedState = sessionStorage.getItem(SESSION_STORAGE_KEY);
                if (savedState) {
                    console.log("Found unsaved state in sessionStorage. Restoring...");
                    const objectsToRestore = JSON.parse(savedState);
                    if (Array.isArray(objectsToRestore)) {
                        newlyPlacedObjects = [];
                        let count = 0;
                        objectsToRestore.forEach(objData => {
                            if(createAndPlaceObject(objData, true)) { count++; }
                        });
                        console.log(`Restored ${count} objects.`);
                    }
                } else {
                    console.log("No unsaved state found in sessionStorage.");
                }
            } catch (e) {
                console.error("Error restoring state from sessionStorage:", e);
                sessionStorage.removeItem(SESSION_STORAGE_KEY);
            }
        }

        function clearUnsavedState() {
            sessionStorage.removeItem(SESSION_STORAGE_KEY);
            newlyPlacedObjects = [];
            console.log("Cleared unsaved state from memory and sessionStorage.");
        }

        function createObjectBase(type) {
            return { userData: { type: type, obj_id: THREE.MathUtils.generateUUID() } };
        }

        function createSimpleHouse() {
            const base = createObjectBase("Simple House");
            const group = new THREE.Group();
            Object.assign(group, base);
            const mat1 = new THREE.MeshStandardMaterial({color:0xffccaa, roughness:0.8});
            const mat2 = new THREE.MeshStandardMaterial({color:0xaa5533, roughness:0.7});
            const m1 = new THREE.Mesh(new THREE.BoxGeometry(2,1.5,2.5), mat1);
            m1.position.y = 1.5/2;
            m1.castShadow = true;
            m1.receiveShadow = true;
            group.add(m1);
            const m2 = new THREE.Mesh(new THREE.ConeGeometry(1.8,1,4), mat2);
            m2.position.y = 1.5+1/2;
            m2.rotation.y = Math.PI/4;
            m2.castShadow = true;
            m2.receiveShadow = true;
            group.add(m2);
            return group;
        }

        function createTree() {
            const base = createObjectBase("Tree");
            const group = new THREE.Group();
            Object.assign(group, base);
            const mat1 = new THREE.MeshStandardMaterial({color:0x8B4513, roughness:0.9});
            const mat2 = new THREE.MeshStandardMaterial({color:0x228B22, roughness:0.8});
            const m1 = new THREE.Mesh(new THREE.CylinderGeometry(0.3,0.4,2,8), mat1);
            m1.position.y = 1;
            m1.castShadow = true;
            m1.receiveShadow = true;
            group.add(m1);
            const m2 = new THREE.Mesh(new THREE.IcosahedronGeometry(1.2,0), mat2);
            m2.position.y = 2.8;
            m2.castShadow = true;
            m2.receiveShadow = true;
            group.add(m2);
            return group;
        }

        function createRock() {
            const base = createObjectBase("Rock");
            const mat = new THREE.MeshStandardMaterial({color:0xaaaaaa, roughness:0.8, metalness:0.1});
            const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(0.7,0), mat);
            Object.assign(rock, base);
            rock.position.y = 0.35;
            rock.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, 0);
            rock.castShadow = true;
            rock.receiveShadow = true;
            return rock;
        }

        function createFencePost() {
            const base = createObjectBase("Fence Post");
            const mat = new THREE.MeshStandardMaterial({color:0xdeb887, roughness:0.9});
            const post = new THREE.Mesh(new THREE.BoxGeometry(0.2,1.5,0.2), mat);
            Object.assign(post, base);
            post.position.y = 0.75;
            post.castShadow = true;
            post.receiveShadow = true;
            return post;
        }

        function onMouseMove(event) {
            mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
            mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
        }

        function onDocumentClick(event) {
            if (selectedObjectType === "None") return;
            const groundCandidates = Object.values(groundMeshes);
            if (groundCandidates.length === 0) return;
            raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObjects(groundCandidates);
            if (intersects.length > 0) {
                const intersectPoint = intersects[0].point;
                let newObjectToPlace = null;
                switch (selectedObjectType) {
                    case "Simple House": newObjectToPlace = createSimpleHouse(); break;
                    case "Tree": newObjectToPlace = createTree(); break;
                    case "Rock": newObjectToPlace = createRock(); break;
                    case "Fence Post": newObjectToPlace = createFencePost(); break;
                    default: return;
                }
                if (newObjectToPlace) {
                    newObjectToPlace.position.copy(intersectPoint);
                    newObjectToPlace.position.y += 0.01;
                    scene.add(newObjectToPlace);
                    newlyPlacedObjects.push(newObjectToPlace);
                    saveUnsavedState();
                    console.log(`Placed new ${selectedObjectType}. Total unsaved: ${newlyPlacedObjects.length}`);
                }
            }
        }

        function onKeyDown(event) { keysPressed[event.code] = true; }
        function onKeyUp(event) { keysPressed[event.code] = false; }

        function teleportPlayer(targetX, targetZ) {
            console.log(`JS teleportPlayer called with targetX: ${targetX}, targetZ: ${targetZ}`);
            if (playerMesh) {
                playerMesh.position.x = targetX;
                playerMesh.position.z = targetZ;
                const offset = new THREE.Vector3(0, 15, 20);
                const targetPosition = playerMesh.position.clone().add(offset);
                camera.position.copy(targetPosition);
                camera.lookAt(playerMesh.position);
                console.log("Player teleported to:", playerMesh.position);
            } else {
                console.error("Player mesh not found for teleport.");
            }
        }

        function getSaveDataAndPosition() {
            console.log(`JS getSaveDataAndPosition called. Found ${newlyPlacedObjects.length} new objects.`);
            const objectsToSave = newlyPlacedObjects.map(obj => {
                if (!obj.userData || !obj.userData.type) { return null; }
                const rotation = { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order };
                return {
                    obj_id: obj.userData.obj_id, type: obj.userData.type,
                    position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
                    rotation: rotation
                };
            }).filter(obj => obj !== null);
            const playerPos = playerMesh ? { x: playerMesh.position.x, y: playerMesh.position.y, z: playerMesh.position.z } : {x:0, y:0, z:0};
            const payload = {
                playerPosition: playerPos,
                objectsToSave: objectsToSave
            };
            console.log("Prepared payload for saving:", payload);
            return JSON.stringify(payload);
        }

        function resetNewlyPlacedObjects() {
            console.log(`JS resetNewlyPlacedObjects called.`);
            clearUnsavedState();
        }

        function updatePlayerMovement() {
            if (!playerMesh) return;
            const moveDirection = new THREE.Vector3(0, 0, 0);
            if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1;
            if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1;
            if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1;
            if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;
            if (moveDirection.lengthSq() > 0) {
                moveDirection.normalize().multiplyScalar(playerSpeed);
                const forward = new THREE.Vector3();
                camera.getWorldDirection(forward);
                forward.y = 0;
                forward.normalize();
                const right = new THREE.Vector3().crossVectors(camera.up, forward).normalize();
                const worldMove = new THREE.Vector3();
                worldMove.add(forward.multiplyScalar(-moveDirection.z));
                worldMove.add(right.multiplyScalar(-moveDirection.x));
                worldMove.normalize().multiplyScalar(playerSpeed);
                playerMesh.position.add(worldMove);
                playerMesh.position.y = Math.max(playerMesh.position.y, 0.4 + 0.8/2);
                checkAndExpandGround();
            }
        }

        function checkAndExpandGround() {
            if (!playerMesh) return;
            const currentGridX = Math.floor(playerMesh.position.x / plotWidth);
            const currentGridZ = Math.floor(playerMesh.position.z / plotDepth);
            for (let dx = -1; dx <= 1; dx++) {
                for (let dz = -1; dz <= 1; dz++) {
                    if (dx === 0 && dz === 0) continue;
                    const checkX = currentGridX + dx;
                    const checkZ = currentGridZ + dz;
                    const gridKey = `${checkX}_${checkZ}`;
                    if (!groundMeshes[gridKey]) {
                        const isSavedPlot = plotsMetadata.some(plot => plot.grid_x === checkX && plot.grid_z === checkZ);
                        if (!isSavedPlot) {
                            createGroundPlane(checkX, checkZ, true);
                        }
                    }
                }
            }
        }

        function updateCamera() {
            if (!playerMesh) return;
            const offset = new THREE.Vector3(0, 15, 20);
            const targetPosition = playerMesh.position.clone().add(offset);
            camera.position.lerp(targetPosition, 0.08);
            camera.lookAt(playerMesh.position);
        }

        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        function animate() {
            requestAnimationFrame(animate);
            updatePlayerMovement();
            updateCamera();
            renderer.render(scene, camera);
        }

        init();
    </script>
</body>
</html>