// Simplified GLTFExporter for local use
// Based on three.js examples

if (!window.THREE) throw new Error('THREE is not defined');

THREE.GLTFExporter = class GLTFExporter {
    constructor() {}

    parse(scene, onComplete, onError, options = {}) {
        const binary = options.binary !== false;
        const exportOnlyVisible = options.onlyVisible === true;
        
        try {
            // Create a minimal glTF structure
            const gltf = {
                asset: { version: "2.0" },
                scene: 0,
                scenes: [{ nodes: [0] }],
                nodes: [],
                meshes: [],
                geometries: [],
                materials: [],
                images: [],
                textures: [],
                accessors: [],
                bufferViews: [],
                buffers: [{ byteLength: 0 }]
            };

            let nodeIndex = 0;
            let meshIndex = 0;
            let materialIndex = 0;
            let accessorIndex = 0;
            let bufferViewIndex = 0;
            let buffers = [];
            let bufferByteOffset = 0;
            const processedObjects = new Set(); // Track processed objects to avoid circular refs
            const imageCache = new Map();
            const textureCache = new Map();

            const getTextureDataUri = (texture) => {
                if (!texture || !texture.image) {
                    return null;
                }

                try {
                    const image = texture.image;
                    if (typeof image.toDataURL === 'function') {
                        return image.toDataURL('image/png');
                    }

                    if (typeof image.src === 'string' && image.src) {
                        return image.src;
                    }
                } catch (err) {
                    console.warn('Failed to serialize texture image:', err);
                }

                return null;
            };

            const getTextureIndex = (texture) => {
                if (!texture) {
                    return null;
                }

                const cacheKey = texture.uuid || texture.id || texture.name || '';
                if (cacheKey && textureCache.has(cacheKey)) {
                    return textureCache.get(cacheKey);
                }

                const uri = getTextureDataUri(texture);
                if (!uri) {
                    return null;
                }

                let imageIndex = imageCache.get(uri);
                if (imageIndex === undefined) {
                    imageIndex = gltf.images.length;
                    gltf.images.push({
                        uri: uri,
                        name: texture.name || `Image_${imageIndex}`
                    });
                    imageCache.set(uri, imageIndex);
                }

                const textureIndex = gltf.textures.length;
                gltf.textures.push({
                    source: imageIndex
                });

                if (cacheKey) {
                    textureCache.set(cacheKey, textureIndex);
                }

                return textureIndex;
            };

            // Recursively process scene objects
            const processObject = (obj, parentNodeIndex, depth = 0) => {
                // Prevent infinite recursion
                if (depth > 50) {
                    console.warn('Max recursion depth reached, stopping object processing');
                    return -1;
                }

                // Prevent circular references and stack overflow
                if (!obj || processedObjects.has(obj)) {
                    return -1;
                }
                if (exportOnlyVisible && obj.visible === false) {
                    return -1;
                }
                processedObjects.add(obj);

                if (obj.isMesh || obj.isGroup) {
                    const nodeData = {
                        name: obj.name || `Node_${nodeIndex}`
                    };

                    if (obj.position && !(obj.position.x === 0 && obj.position.y === 0 && obj.position.z === 0)) {
                        nodeData.translation = [obj.position.x, obj.position.y, obj.position.z];
                    }

                    if (obj.rotation && !(obj.rotation.x === 0 && obj.rotation.y === 0 && obj.rotation.z === 0)) {
                        const quat = new THREE.Quaternion();
                        quat.setFromEuler(obj.rotation);
                        nodeData.rotation = [quat.x, quat.y, quat.z, quat.w];
                    }

                    if (obj.scale && !(obj.scale.x === 1 && obj.scale.y === 1 && obj.scale.z === 1)) {
                        nodeData.scale = [obj.scale.x, obj.scale.y, obj.scale.z];
                    }

                    const currentNodeIndex = gltf.nodes.length;
                    gltf.nodes.push(nodeData);

                    if (obj.isMesh && obj.geometry && obj.material) {
                        const geometry = obj.geometry;
                        const materials = Array.isArray(obj.material) ? obj.material : [obj.material];
                        
                        const meshData = {
                            primitives: []
                        };

                        // Get position data
                        const positionAttr = geometry.getAttribute('position');
                        const normalAttr = geometry.getAttribute('normal');
                        const indexData = geometry.index;

                        if (positionAttr) {
                            // Push position data to buffer
                            const positions = new Float32Array(positionAttr.array);
                            buffers.push(positions);

                            // Compute bounds without spreading large arrays
                            let minX = Infinity;
                            let minY = Infinity;
                            let minZ = Infinity;
                            let maxX = -Infinity;
                            let maxY = -Infinity;
                            let maxZ = -Infinity;

                            for (let i = 0; i < positions.length; i += 3) {
                                const x = positions[i];
                                const y = positions[i + 1];
                                const z = positions[i + 2];

                                if (x < minX) minX = x;
                                if (y < minY) minY = y;
                                if (z < minZ) minZ = z;
                                if (x > maxX) maxX = x;
                                if (y > maxY) maxY = y;
                                if (z > maxZ) maxZ = z;
                            }

                            // Position accessor
                            const posAccessor = {
                                bufferView: bufferViewIndex,
                                componentType: 5126, // FLOAT
                                count: positions.length / 3,
                                type: 'VEC3',
                                min: [minX, minY, minZ],
                                max: [maxX, maxY, maxZ]
                            };
                            gltf.accessors.push(posAccessor);
                            const posAccessorIndex = accessorIndex++;

                            // BufferView for positions
                            gltf.bufferViews.push({
                                buffer: 0,
                                byteOffset: bufferByteOffset,
                                byteLength: positions.byteLength,
                                componentType: 5126,
                                target: 34962 // ARRAY_BUFFER
                            });
                            bufferByteOffset += positions.byteLength;
                            bufferViewIndex++;

                            const primitive = {
                                attributes: {
                                    POSITION: posAccessorIndex
                                }
                            };

                            // Add normals if available
                            if (normalAttr) {
                                const normals = new Float32Array(normalAttr.array);
                                buffers.push(normals);

                                const normAccessor = {
                                    bufferView: bufferViewIndex,
                                    componentType: 5126,
                                    count: normals.length / 3,
                                    type: 'VEC3'
                                };
                                gltf.accessors.push(normAccessor);
                                primitive.attributes.NORMAL = accessorIndex++;

                                gltf.bufferViews.push({
                                    buffer: 0,
                                    byteOffset: bufferByteOffset,
                                    byteLength: normals.byteLength,
                                    componentType: 5126,
                                    target: 34962
                                });
                                bufferByteOffset += normals.byteLength;
                                bufferViewIndex++;
                            }

                            // Add indices if available
                            if (indexData) {
                                const indices = new Uint32Array(indexData.array);
                                buffers.push(indices);

                                const indexAccessor = {
                                    bufferView: bufferViewIndex,
                                    componentType: 5125, // UNSIGNED_INT
                                    count: indices.length,
                                    type: 'SCALAR'
                                };
                                gltf.accessors.push(indexAccessor);
                                primitive.indices = accessorIndex++;

                                gltf.bufferViews.push({
                                    buffer: 0,
                                    byteOffset: bufferByteOffset,
                                    byteLength: indices.byteLength,
                                    componentType: 5125,
                                    target: 34963 // ELEMENT_ARRAY_BUFFER
                                });
                                bufferByteOffset += indices.byteLength;
                                bufferViewIndex++;
                            }

                            // Add material
                            primitive.material = materialIndex;
                            meshData.primitives.push(primitive);

                            // Store material data
                            const material = materials.find(m => m && m.isMaterial && m.map) || materials.find(m => m && m.isMaterial) || null;
                            if (material) {
                                const materialData = {
                                    name: material.name || `Material_${materialIndex}`,
                                    pbrMetallicRoughness: {
                                        baseColorFactor: material.color ? [
                                            material.color.r,
                                            material.color.g,
                                            material.color.b,
                                            material.transparent ? material.opacity : 1.0
                                        ] : [1, 1, 1, 1],
                                        metallicFactor: material.metalness || 0,
                                        roughnessFactor: material.roughness || 0.5
                                    }
                                };

                                if (material.map) {
                                    const textureIndex = getTextureIndex(material.map);
                                    if (textureIndex !== null) {
                                        materialData.pbrMetallicRoughness.baseColorTexture = { index: textureIndex };
                                    }
                                }

                                if (material.side === THREE.DoubleSide) {
                                    materialData.doubleSided = true;
                                }

                                gltf.materials.push(materialData);
                            }
                            materialIndex++;
                        }

                        gltf.meshes.push(meshData);
                        nodeData.mesh = meshIndex++;
                    }

                    // Process children
                    if (obj.children && obj.children.length > 0) {
                        nodeData.children = [];
                        obj.children.forEach(child => {
                            const childIndex = processObject(child, currentNodeIndex, depth + 1);
                            if (childIndex !== -1) {
                                nodeData.children.push(childIndex);
                            }
                        });
                    }

                    return currentNodeIndex;
                }
                return -1;
            };

            processObject(scene, 0);

            // Create binary data from all buffers
            let totalBufferLength = 0;
            buffers.forEach(buf => {
                totalBufferLength += buf.byteLength;
            });

            // Create a single ArrayBuffer for all geometry data
            const geometryBuffer = new ArrayBuffer(totalBufferLength);
            const geometryView = new Uint8Array(geometryBuffer);
            let geometryOffset = 0;

            buffers.forEach(buf => {
                const view = new Uint8Array(buf.buffer || buf);
                geometryView.set(view, geometryOffset);
                geometryOffset += view.byteLength;
            });

            // Update gltf to reference the buffer
            gltf.buffers = [{
                byteLength: totalBufferLength
            }];

            // Create a simple binary glTF with json header
            const jsonString = JSON.stringify(gltf);
            const jsonBuffer = new TextEncoder().encode(jsonString);
            const jsonLength = jsonBuffer.length;
            
            // Align to 4-byte boundary
            const paddingLength = (4 - (jsonLength % 4)) % 4;
            const paddedJsonLength = jsonLength + paddingLength;
            
            // GLB Header: magic, version, length
            // Structure: Header(12) + JSON header(8) + JSON data(paddedJsonLength) + BIN header(8) + BIN data(totalBufferLength)
            const fileLength = 12 + 8 + paddedJsonLength + 8 + totalBufferLength;
            const header = new ArrayBuffer(12);
            const headerView = new DataView(header);
            headerView.setUint32(0, 0x46546C67, true); // 'glTF'
            headerView.setUint32(4, 2, true); // version
            headerView.setUint32(8, fileLength, true); // file length
            
            // JSON chunk header
            const jsonChunkHeader = new ArrayBuffer(8);
            const jsonChunkView = new DataView(jsonChunkHeader);
            jsonChunkView.setUint32(0, paddedJsonLength, true);
            jsonChunkView.setUint32(4, 0x4E4F534A, true); // 'JSON'
            
            // Pad JSON chunk
            const jsonChunkData = new Uint8Array(paddedJsonLength);
            jsonChunkData.set(jsonBuffer);
            for (let i = 0; i < paddingLength; i++) {
                jsonChunkData[jsonLength + i] = 0x20; // space padding
            }

            // Binary chunk header
            const binaryChunkHeader = new ArrayBuffer(8);
            const binaryChunkView = new DataView(binaryChunkHeader);
            binaryChunkView.setUint32(0, totalBufferLength, true);
            binaryChunkView.setUint32(4, 0x004E4942, true); // 'BIN'
            
            // Concatenate all parts
            const glbData = new Uint8Array(fileLength);
            let offset = 0;

            glbData.set(new Uint8Array(header), offset);
            offset += 12;

            glbData.set(new Uint8Array(jsonChunkHeader), offset);
            offset += 8;

            glbData.set(jsonChunkData, offset);
            offset += paddedJsonLength;

            glbData.set(new Uint8Array(binaryChunkHeader), offset);
            offset += 8;

            glbData.set(new Uint8Array(geometryBuffer), offset);
            offset += totalBufferLength;
            
            // Call complete callback with the binary data
            if (typeof onComplete === 'function') {
                onComplete(glbData.buffer);
            }
        } catch (error) {
            console.error('GLTFExporter error:', error);
            if (typeof onError === 'function') {
                onError(error);
            } else {
                throw error;
            }
        }
    }
};
