import { openDB } from 'idb';
import jsPDF from 'jspdf';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { USDZExporter } from 'three/examples/jsm/exporters/USDZExporter.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js';
import { renderUI } from './ui.js';

// --- Configuration & Globals ---
const config = customizerData.customizationOptions;
const modelUrl = customizerData.modelUrl;
const appSettings = customizerData.settings;

// Capture URL dprcu_cfg param immediately — before WordPress/WooCommerce scripts can modify the URL
const initialCfg = new URLSearchParams(window.location.search).get('dprcu_cfg');

// --- Central State ---
const state = {
    model: null,
    current: {}, // Stores current value for each part: { "partKey": { valueObject } }
    optIds: {},  // { stepKey: optId string } — tracked for variation matching
    texIds: {},  // { stepKey: textureId int | null } — tracked for variation matching
    history: [], // Undo stack
    historyIndex: -1,
    hiddenSteps: new Set(), // Step keys hidden by show/hide step rules
    stepUpdateOrder: [], // stepKeys in the order they were last changed (most recent at end)
    ui: {
        activeSection: null, // For Mobile Navigation
    },
    viewer_only_mode: customizerData.viewerOnly || false
};

// --- Three.js Globals ---
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
let canvas;
let renderer;
const textureLoader = new THREE.TextureLoader();
textureLoader.setCrossOrigin('anonymous'); // Critical for AR export
let controls;
let groundPlane;
let animReq; // Animation frame request ID
const textureCache = new Map();
const meshMap = new Map();

// --- Performance Helpers ---
const MAX_TEXTURE_CACHE = 50;
const MAX_IDB_SIZE = 100 * 1024 * 1024; // 100MB limit for IndexedDB
let textureDB = null;

// Tracks async texture loads so generateStepThumbnails waits for them all
let pendingTextureLoads = 0;
let onTexturesReady = null;
function _textureLoadDone() {
    if (--pendingTextureLoads === 0 && onTexturesReady) {
        const cb = onTexturesReady;
        onTexturesReady = null;
        cb();
    }
}
const textureVersions = customizerData.textureVersions || {};

// Benchmarking counters
const benchmarks = {
    textureLoads: {
        fromMemory: 0,
        fromIDB: 0,
        fromNetwork: 0,
    },
    timings: {},
    sizes: {
        totalCached: 0,
        totalMemory: 0,
    },
    startTime: Date.now(),
};



function deepEqual(a, b) {
    return JSON.stringify(a) === JSON.stringify(b);
}

/**
 * Initialize IndexedDB for persistent texture caching across sessions.
 * Invalidates outdated textures based on WordPress post_modified timestamps.
 */
function withTimeout(promise, ms) {
    return Promise.race([
        promise,
        new Promise((_, reject) => setTimeout(() => reject(new Error(`IDB timeout after ${ms}ms`)), ms))
    ]);
}

async function initTextureDB() {
    try {
        const startInit = performance.now();
        textureDB = await withTimeout(openDB('customizer-textures', 3, {
            upgrade(db) {
                if (!db.objectStoreNames.contains('textures')) {
                    db.createObjectStore('textures');
                }
                if (!db.objectStoreNames.contains('metadata')) {
                    db.createObjectStore('metadata');
                }
                if (!db.objectStoreNames.contains('config')) {
                    db.createObjectStore('config');
                }
                if (!db.objectStoreNames.contains('icons')) {
                    db.createObjectStore('icons');
                }
            },
            blocked() {
                showNotice('Another tab is blocking the configurator storage. Close other tabs and reload to fix saving/loading issues.');
            },
            blocking() {
                // Another tab needs to upgrade — release our connection so it can proceed
                if (textureDB) { textureDB.close(); textureDB = null; }
            },
            terminated() {
                textureDB = null;
            }
        }), 5000);
        
        // Get initial cache stats
        const allMetadata = await textureDB.getAll('metadata');
        let totalSize = allMetadata.reduce((sum, meta) => sum + (meta.size || 0), 0);
        let invalidatedCount = 0;
        
        // Check for outdated textures and invalidate them
        for (const meta of allMetadata) {
            const serverVersion = textureVersions[meta.url];
            // If server has a newer version or texture no longer exists in config, delete it
            if (serverVersion && meta.lastModified !== serverVersion) {
                await textureDB.delete('textures', meta.url);
                await textureDB.delete('metadata', meta.url);
                totalSize -= meta.size;
                invalidatedCount++;
            }
        }
        
        const initTime = performance.now() - startInit;
        console.log(`%c[Customizer] IndexedDB initialized in ${initTime.toFixed(2)}ms`, 'color: #4CAF50; font-weight: bold;');
        console.log(`%c  → ${allMetadata.length - invalidatedCount} textures cached (${((totalSize) / 1024 / 1024).toFixed(2)}MB)`, 'color: #81C784;');
        if (invalidatedCount > 0) {
            console.log(`%c  ↻ Invalidated ${invalidatedCount} outdated textures`, 'color: #FFA726;');
        }
    } catch (e) {
        console.warn('Could not init texture DB:', e.message);
        showNotice('Storage unavailable — your configuration will not be saved between sessions.');
    }
}

/**
 * Store texture blob in IndexedDB with metadata and version tracking.
 */
async function cacheTextureBlob(url, blob) {
    if (!textureDB) return;
    try {
        const metadata = {
            url,
            timestamp: Date.now(),
            size: blob.size,
            lastModified: textureVersions[url] || Date.now(),
        };
        await textureDB.put('textures', blob, url);
        await textureDB.put('metadata', metadata, url);
        benchmarks.sizes.totalCached += blob.size;
    } catch (e) {
        console.warn('Could not cache texture:', e.message);
    }
}

/**
 * Retrieve texture blob from IndexedDB.
 */
async function getTextureBlob(url) {
    if (!textureDB) return null;
    try {
        const blob = await textureDB.get('textures', url);
        return blob || null;
    } catch (e) {
        console.warn('Could not retrieve texture from cache:', e.message);
        return null;
    }
}

/**
 * Cleanup old cache entries if IDB quota exceeded.
 */
async function cleanupOldTextures() {
    if (!textureDB) return;
    try {
        const allMetadata = await textureDB.getAll('metadata');
        let totalSize = allMetadata.reduce((sum, meta) => sum + (meta.size || 0), 0);
        
        if (totalSize > MAX_IDB_SIZE) {
            // Sort by timestamp, delete oldest
            const sorted = allMetadata.sort((a, b) => a.timestamp - b.timestamp);
            for (const meta of sorted) {
                await textureDB.delete('textures', meta.url);
                await textureDB.delete('metadata', meta.url);
                totalSize -= meta.size;
                if (totalSize < MAX_IDB_SIZE * 0.8) break;
            }
        }
    } catch (e) {
        console.warn('Could not cleanup texture cache:', e.message);
    }
}

// ========================================================
// 2. ICON CACHE
// ========================================================

const iconCache = new Map();

async function loadCachedIcon(url) {
    if (!url) return '';
    if (iconCache.has(url)) return iconCache.get(url);

    if (textureDB) {
        try {
            const blob = await textureDB.get('icons', url);
            if (blob) {
                const blobUrl = URL.createObjectURL(blob);
                iconCache.set(url, blobUrl);
                return blobUrl;
            }
        } catch {}
    }

    try {
        const res = await fetch(url);
        if (!res.ok) return url;
        const blob = await res.blob();
        const blobUrl = URL.createObjectURL(blob);
        iconCache.set(url, blobUrl);
        if (textureDB) {
            textureDB.put('icons', blob, url).catch(() => {});
        }
        return blobUrl;
    } catch {
        return url;
    }
}

function getCachedIconUrl(url) {
    if (!url) return '';
    return iconCache.get(url) || url;
}
window.dprcuGetCachedIconUrl = getCachedIconUrl;

async function preloadIcons() {
    const iconUrls = new Set();
    Object.values(config).forEach(section => {
        if (section.icon) iconUrls.add(section.icon);
        (section.options || []).forEach(opt => {
            if (opt.icon) iconUrls.add(opt.icon);
        });
    });
    if (iconUrls.size === 0) return;
    await Promise.allSettled([...iconUrls].map(loadCachedIcon));
}

const CONFIG_IDB_KEY = window.location.pathname.replace(/\/$/, '') || '/';

async function saveConfigToIDB() {
    if (!textureDB) return;
    try {
        await textureDB.put('config', buildMinifiedState(), CONFIG_IDB_KEY);
    } catch (e) {
        console.warn('Could not save config to IDB:', e.message);
    }
}

async function loadConfigFromIDB() {
    if (!textureDB) return null;
    try {
        return await textureDB.get('config', CONFIG_IDB_KEY) || null;
    } catch (e) {
        console.warn('Could not load config from IDB:', e.message);
        return null;
    }
}

/**
 * Clear all cached textures (useful for debugging or user preference).
 */
async function clearTextureCache() {
    if (!textureDB) return;
    try {
        await textureDB.clear('textures');
        await textureDB.clear('metadata');
        // Also delete saved config from IndexedDB
        await textureDB.delete('config', CONFIG_IDB_KEY);
        console.log('Texture cache cleared');
    } catch (e) {
        console.warn('Could not clear texture cache:', e.message);
    }
}

/**
 * Get cache statistics for debugging.
 */
async function getCacheStats() {
    if (!textureDB) return { enabled: false };
    try {
        const allMetadata = await textureDB.getAll('metadata');
        const totalSize = allMetadata.reduce((sum, meta) => sum + (meta.size || 0), 0);
        const memorySize = textureCache.size;
        const uptime = (Date.now() - benchmarks.startTime) / 1000;
        
        return {
            enabled: true,
            uptime: `${uptime.toFixed(1)}s`,
            loads: benchmarks.textureLoads,
            persistedTextures: allMetadata.length,
            persistedSizeMB: (totalSize / 1024 / 1024).toFixed(2),
            memoryTextures: memorySize,
            maxMemoryTextures: MAX_TEXTURE_CACHE,
            maxPersistentSizeMB: (MAX_IDB_SIZE / 1024 / 1024),
            totalCachedMB: (benchmarks.sizes.totalCached / 1024 / 1024).toFixed(2),
            hitRate: benchmarks.textureLoads.fromMemory + benchmarks.textureLoads.fromIDB,
        };
    } catch (e) {
        return { error: e.message };
    }
}



// Expose cache utilities to window for easy debugging/manual clearing
window.customizerCache = {
    clearAll: clearTextureCache,
    getStats: getCacheStats,
    benchmarks: benchmarks,
};


async function loadCachedTexture(url, onLoad) {
    // 1. Check in-memory cache first (synchronous — no pending count needed)
    if (textureCache.has(url)) {
        benchmarks.textureLoads.fromMemory++;
        onLoad(textureCache.get(url));
        return;
    }

    // This load is async — track it so thumbnail generation can wait
    pendingTextureLoads++;

    // 2. Try to load from IndexedDB (persistent cache)
    if (textureDB) {
        const cachedBlob = await getTextureBlob(url);
        if (cachedBlob) {
            try {
                const blobUrl = URL.createObjectURL(cachedBlob);
                textureLoader.load(blobUrl, (tex) => {
                    // Store in memory cache
                    if (textureCache.size >= MAX_TEXTURE_CACHE) {
                        const firstKey = textureCache.keys().next().value;
                        const oldTex = textureCache.get(firstKey);
                        oldTex.dispose();
                        textureCache.delete(firstKey);
                    }
                    textureCache.set(url, tex);
                    URL.revokeObjectURL(blobUrl);
                    benchmarks.textureLoads.fromIDB++;
                    onLoad(tex);
                    _textureLoadDone();
                });
                return;
            } catch (e) {
                console.warn('Could not load texture from IndexedDB:', e.message);
            }
        }
    }

    // 3. Fallback to fetch from network
    textureLoader.load(url, (tex) => {
        // Store in memory cache
        if (textureCache.size >= MAX_TEXTURE_CACHE) {
            const firstKey = textureCache.keys().next().value;
            const oldTex = textureCache.get(firstKey);
            oldTex.dispose();
            textureCache.delete(firstKey);
        }
        textureCache.set(url, tex);

        // Also cache to IndexedDB for next session
        fetch(url)
            .then(res => res.blob())
            .then(blob => {
                benchmarks.textureLoads.fromNetwork++;
                return cacheTextureBlob(url, blob);
            })
            .then(() => cleanupOldTextures())
            .catch(e => console.warn('Could not persist texture:', e.message));

        onLoad(tex);
        _textureLoadDone();
    }, undefined, (err) => {
        console.error('Texture load failed:', url, err);
        _textureLoadDone();
    });
}

function applyMapSettings(tex, repeat, offset, wrapS, wrapT) {
    if (!tex) return;
    tex.repeat.copy(repeat);
    tex.offset.copy(offset);
    tex.wrapS = wrapS;
    tex.wrapT = wrapT;
    tex.needsUpdate = true;
}

function initRenderer() {
    renderer = new THREE.WebGLRenderer({
        canvas,
        antialias: true,
        alpha: true,
        preserveDrawingBuffer: true,
        powerPreference: 'high-performance',
    });

    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFShadowMap;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 0.8;
    renderer.xr.enabled = true;
    renderer.xr.setReferenceSpaceType('local');
    renderer.outputColorSpace = THREE.SRGBColorSpace;
}

// --- Initialization ---
document.addEventListener('DOMContentLoaded', async () => {
    canvas = document.querySelector('#product-canvas');
    if (!canvas) return;

    // Initialize persistent texture cache (capped — never block model loading)
    await initTextureDB().catch(() => {});

    // Pre-cache icon images so they don't re-fetch every session
    preloadIcons().catch(() => {});

    // Pre-load saved config so the GLTF callback stays synchronous
    const savedConfig = await loadConfigFromIDB().catch(() => null);

    initRenderer();
    initScene();
    initUI();
    loadModel(savedConfig);
    animate();
});

// ========================================================
// 0. ANALYTICS  (premium only — stripped from free build)
// ========================================================



// ========================================================
// 1. CORE LOGIC: State Management
// ========================================================


function buildMinifiedState() {
    const minified = {};
    Object.entries(state.current).forEach(([key, val]) => {
        const step = config[key];
        if (step?.options) {
            const idx = step.options.findIndex(opt => deepEqual(opt.value, val));
            minified[key] = idx !== -1 ? idx : val;
        }
    });
    return minified;
}



/**
 * The single entry point for changing any option.
 * Updates State -> Models -> UI -> Cart -> History.
 */
function updateState(partKey, newValue, type = 'material', saveHistory = true) {
    if (!state.model) return;

    const oldValue = state.current[partKey];
    if (deepEqual(oldValue, newValue)) return;

    // 1. Update Data
    state.current[partKey] = newValue;
    const _opt = config[partKey]?.options?.find(o => deepEqual(o.value, newValue));
    state.optIds[partKey] = _opt?.optId    ?? null;
    state.texIds[partKey] = _opt?.textureId ?? null;

    

    // 2. Apply changes to the 3D Model
    applyToModel(partKey, newValue, type);

    // 3. Update UI (highlight active swatch)
    refreshUI();

    

    // 4. Save to Undo History
    // Use deepEqual to detect changes without repeated JSON.stringify
    if (saveHistory && !deepEqual(oldValue, newValue)) {
        // If we are in the middle of the stack (undone state), remove future history
        if (state.historyIndex < state.history.length - 1) {
            state.history = state.history.slice(0, state.historyIndex + 1);
        }

        state.history.push({ partKey, oldValue, newValue, type });
        state.historyIndex++;
        updateUndoRedoButtons();
    }

    // 5. Update Hidden Cart Data
    updateCartData();
    saveConfigToIDB();
}

/**
 * Applies the configuration to the Three.js model.
 * @param {string} ruleKey - The rule identifier
 * @param {object} value - The option value (material textures or show/hide arrays)
 * @param {string} type - 'material' or 'object'
 */
function applyToModel(ruleKey, value, type) {
    const ruleConfig = config[ruleKey];
    const affectedParts = ruleConfig?.parts || [];

    // Apply visibility changes for both types
    

    if (type === 'object') {
        requestRender();
        return;
    }

    // Material change
    affectedParts.forEach(name => {
        const child = meshMap.get(name);
        if (!child) return;

        if (value === 'off') {
            child.visible = false;
            return;
        }

        child.userData.currentMaterial = value;

        const materials = Array.isArray(child.material)
            ? child.material
            : [child.material];

        for (let i = 0; i < materials.length; i++) applyMaterialProps(materials[i], value);
    });

    
}

/**
 * Helper to show/hide a list of mesh names
 */


/**
 * Helper to apply texture/color/maps to a material
 * Preserves UV Tiling (Repeat/Offset) from the original GLB.
 */
function applyMaterialProps(mat, value) {
    const setProps = (m) => {
        const oldRepeat = m.map?.repeat.clone() ?? new THREE.Vector2(1, 1);
        const oldOffset = m.map?.offset.clone() ?? new THREE.Vector2(0, 0);
        const oldWrapS = m.map?.wrapS ?? THREE.RepeatWrapping;
        const oldWrapT = m.map?.wrapT ?? THREE.RepeatWrapping;

        const maps = [
            { key: 'map', url: value.texture, colorSpace: THREE.SRGBColorSpace },
            { key: 'roughnessMap', url: value.roughnessMap, colorSpace: THREE.NoColorSpace },
            { key: 'normalMap', url: value.normalMap, colorSpace: THREE.NoColorSpace },
            { key: 'aoMap', url: value.aoMap, colorSpace: THREE.NoColorSpace },
            { key: 'metalnessMap', url: value.metalnessMap, colorSpace: THREE.NoColorSpace },
            { key: 'displacementMap', url: value.displacementMap, colorSpace: THREE.NoColorSpace },
        ];

        const pending = maps.filter(m => m.url);
        let loadedCount = 0;

        if (pending.length > 0) toggleLoader(true, customizerData.i18n.loadingTexture);

        maps.forEach(({ key, url, colorSpace }) => {
            if (!url) {
                m[key] = null;
                return;
            }

            loadCachedTexture(url, (tex) => {
                tex.colorSpace = colorSpace;
                applyMapSettings(tex, oldRepeat, oldOffset, oldWrapS, oldWrapT);
                m[key] = tex;

                loadedCount++;
                if (loadedCount === pending.length) {
                    toggleLoader(false);
                    m.needsUpdate = true;
                    requestRender();
                }
            });
        });

        // Batch scalar property updates
        Object.assign(m, {
            color: new THREE.Color(value.color || 0xffffff),
            roughness: value.roughness ?? 0.9,
            metalness: value.metalness ?? 0.1,
            envMapIntensity: value.envIntensity ?? 1.0,
            displacementScale: -(value.displacementScale ?? 0) / 2,
        });

        if (pending.length === 0) {
            m.needsUpdate = true;
            requestRender();
        }
    };

    if (Array.isArray(mat)) {
        mat.forEach(setProps);
    } else {
        setProps(mat);
    }
}


// ========================================================
// 2. INITIALIZATION & LOADING
// ========================================================

function updateSceneSize() {
    const container = document.getElementById('canvas-container');

    // 1. Get the actual display size in pixels
    const width = container.clientWidth;
    const height = container.clientHeight;

    // 2. Check if we actually need to resize (Optimization)
    const canvas = renderer.domElement;
    const pixelRatio = Math.min(window.devicePixelRatio, 2); // Cap at 2x for performance

    const needResize = canvas.width !== Math.round(width * pixelRatio) ||
        canvas.height !== Math.round(height * pixelRatio);

    if (needResize) {
        // 3. Update Camera Aspect Ratio
        camera.aspect = width / height;
        camera.updateProjectionMatrix();

        // 4. Update Renderer Size
        // IMPORTANT: passing 'false' as the 3rd arg prevents Three.js 
        // from changing the canvas CSS style, allowing CSS 100% to work.
        renderer.setPixelRatio(pixelRatio);
        renderer.setSize(width, height, false);

        requestRender();
    }
}

/**
 * Auto-frame the model in view by calculating optimal camera position.
 * Uses bounding box to ensure the entire model is visible with good framing.
 * Distance is multiplied by a user-configurable factor.
 */
function autoFrameModel(model) {
    if (!model) return;

    // 1. Calculate bounding box
    const box = new THREE.Box3().setFromObject(model);
    const size = new THREE.Vector3();
    box.getSize(size);

    const center = new THREE.Vector3();
    box.getCenter(center);

    // 2. Center the model at origin
    model.position.x += (model.position.x - center.x);
    model.position.y += (model.position.y - center.y);
    model.position.z += (model.position.z - center.z);

    // 3. Check for saved camera position
    const savedPos = customizerData.cameraPosition;
    if (savedPos?.position) {
        camera.position.set(
            savedPos.position.x,
            savedPos.position.y,
            savedPos.position.z
        );
        if (savedPos.target) {
            controls.target.set(
                savedPos.target.x,
                savedPos.target.y,
                savedPos.target.z
            );
        } else {
            controls.target.set(0, 0, 0);
        }
        camera.lookAt(controls.target);
        controls.update();
        return;
    }

    // 4. Calculate optimal camera distance based on model size
    const maxDim = Math.max(size.x, size.y, size.z);
    const fov = camera.fov * (Math.PI / 180); // Convert to radians
    let cameraDist = Math.abs(maxDim / 2 / Math.tan(fov / 2));

    // 5. Apply user-defined multiplier for fine-tuning (default 1.2)
    const distMultiplier = appSettings.cameraDistanceMultiplier || 1.2;
    cameraDist *= distMultiplier;

    // 6. Position camera isometrically (good for viewing 3D objects)
    camera.position.set(cameraDist, cameraDist * 0.6, cameraDist);
    camera.lookAt(0, 0, 0);

    // 7. Update controls target
    controls.target.set(0, 0, 0);
    controls.update();
}

/**
 * Generates 128×128 thumbnail images for each step by rendering just that
 * step's parts in isolation, with the rest of the model dimmed.
 *
 * Camera direction: from model centre → part centre, so the part is always
 * viewed "from outside". Camera distance is calculated to fit the part's
 * bounding box exactly in frame (with 30 % padding).
 *
 * Skips steps that already have a manually assigned icon.
 */
function generateStepThumbnails() {
    if (!state.model) return;

    const stepsToThumb = Object.entries(config).filter(
        ([, step]) => !step.icon && Array.isArray(step.parts) && step.parts.length
    );
    if (!stepsToThumb.length) return;

    // Derive scene scale for near/far planes from model bounding box
    const modelBox = new THREE.Box3().setFromObject(state.model);
    const modelCenter = new THREE.Vector3();
    modelBox.getCenter(modelCenter);
    const modelSize = new THREE.Vector3();
    modelBox.getSize(modelSize);
    const modelMaxDim = Math.max(modelSize.x, modelSize.y, modelSize.z);

    // Decompose the default camera into spherical coordinates so we can
    // keep its elevation (phi) but orbit its azimuth (theta) per-part.
    const camOffset = new THREE.Vector3().subVectors(camera.position, controls.target);
    const camSpherical = new THREE.Spherical().setFromVector3(camOffset);

    // One shared off-screen renderer for all steps
    const offCanvas = document.createElement('canvas');
    offCanvas.width = 128;
    offCanvas.height = 128;
    const offRenderer = new THREE.WebGLRenderer({ canvas: offCanvas, alpha: true, antialias: true });
    offRenderer.setPixelRatio(1);
    offRenderer.setSize(128, 128);
    offRenderer.setClearColor(0x000000, 0);

    const offCamera = new THREE.PerspectiveCamera(45, 1, modelMaxDim * 0.001, modelMaxDim * 10);

    // Distance to fit the whole model in the offscreen camera's frame
    const offFov = offCamera.fov * (Math.PI / 180);
    const modelFitDist = Math.abs(modelMaxDim / 2 / Math.tan(offFov / 2));

    for (const [key, step] of stepsToThumb) {
        // Collect meshes belonging to this step
        const partMeshes = [];
        state.model.traverse(child => {
            if (child.isMesh && step.parts.includes(child.name)) partMeshes.push(child);
        });
        if (!partMeshes.length) continue;

        // Bounding box of the part — centre for camera target, size for partial zoom
        const partBox = new THREE.Box3();
        partMeshes.forEach(m => partBox.expandByObject(m));
        const partCenter = new THREE.Vector3();
        partBox.getCenter(partCenter);
        const partSize = new THREE.Vector3();
        partBox.getSize(partSize);
        const partMaxDim = Math.max(partSize.x, partSize.y, partSize.z);
        const partFitDist = Math.abs(partMaxDim / 2 / Math.tan(offFov / 2));

        // Halfway between whole-model distance and part-only distance
        const dist = (modelFitDist + partFitDist) / 2;

        // Orbit camera to face the part: keep elevation (phi) from default camera,
        // rotate azimuth (theta) so the camera points from model centre toward part centre.
        const toPartFlat = new THREE.Vector3()
            .subVectors(partCenter, modelCenter)
            .setY(0); // flatten to XZ plane for pure horizontal orbit
        const theta = toPartFlat.lengthSq() > 1e-8
            ? Math.atan2(toPartFlat.x, toPartFlat.z)
            : camSpherical.theta; // fallback: part is on the vertical axis, keep current azimuth

        const orbitDir = new THREE.Vector3()
            .setFromSpherical(new THREE.Spherical(1, camSpherical.phi, theta));

        offCamera.position.copy(partCenter).addScaledVector(orbitDir, dist);
        offCamera.lookAt(partCenter);
        offCamera.updateProjectionMatrix();

        // Dim every mesh that is NOT part of this step
        const savedMats = [];
        state.model.traverse(child => {
            if (!child.isMesh || step.parts.includes(child.name)) return;
            savedMats.push({ mesh: child, original: child.material });
            const dim = child.material.clone();
            dim.transparent = true;
            dim.opacity = 0.08;
            child.material = dim;
        });

        offRenderer.render(scene, offCamera);
        step.icon = offCanvas.toDataURL('image/png');

        // Restore original materials
        savedMats.forEach(({ mesh, original }) => { mesh.material = original; });
    }

    offRenderer.dispose();
    refreshUI();
}

function initScene() {

    if (appSettings.hdrFile) {
        new HDRLoader().load(appSettings.hdrFile, (hdrTexture) => {
            hdrTexture.mapping = THREE.EquirectangularReflectionMapping;
            scene.environment = hdrTexture;
            //scene.background = hdrTexture;
        }, undefined, (err) => {
            console.error("Failed to load HDR:", err);
        });
    }

    // --- Camera Setup ---
    // Camera position is automatically calculated via autoFrameModel() when model loads
    camera.position.set(0, 0, 5); // Temporary default until model loads

    // --- Controls ---
    controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = false;

    // Camera target is automatically set via autoFrameModel() when model loads
    controls.target.set(0, 0, 0);

    // Apply zoom limits from settings
    const zoomLimits = appSettings.zoomLimits || [1, 10];
    controls.minDistance = zoomLimits[0];
    controls.maxDistance = zoomLimits[1];

    controls.addEventListener('change', requestRender);

    controls.addEventListener('start', () => {
        canvas.classList.add('is-dragging');
        requestRender(); // Ensure loop is active
        
    });

    controls.addEventListener('end', () => {
        canvas.classList.remove('is-dragging');
        requestRender();
    });

    // --- Lights ---
    // When no HDR is present the environment provides no illumination, so use
    // stronger lights to keep the scene readable.
    const hasHdr = !! appSettings.hdrFile;
    const ambient = new THREE.AmbientLight(0xffffff, hasHdr ? 0.1 : 0.8);
    const hemi = new THREE.HemisphereLight(0xffffff, 0x444444, hasHdr ? 0.4 : 1.2);
    hemi.position.set(0, 20, 0);

    const keyLight = new THREE.DirectionalLight(0xfff7ea, hasHdr ? 0.3 : 2.0);
    keyLight.position.set(-6, 10, -6);
    keyLight.target.position.set(0, 0, 0);
    keyLight.castShadow = false;
    scene.add(keyLight.target);

    const fillLight = new THREE.DirectionalLight(0xffffff, hasHdr ? 0.3 : 1.2);
    fillLight.position.set(4, 3, 2);
    fillLight.target.position.set(0, 0, 0);
    fillLight.castShadow = true;
    fillLight.shadow.mapSize.set(4096, 4096);
    fillLight.shadow.bias = -0.0005;
    fillLight.shadow.normalBias = 0.02;
    fillLight.shadow.camera.near = 0.1;
    fillLight.shadow.camera.far = 30;
    fillLight.shadow.camera.left = -8;
    fillLight.shadow.camera.right = 8;
    fillLight.shadow.camera.top = 8;
    fillLight.shadow.camera.bottom = -8;
    scene.add(fillLight.target);

    scene.add(ambient, hemi, keyLight, fillLight);

    // --- Background ---
    const bgs = appSettings.bgColors || ['#f0f0f0'];

    renderer.setClearColor(new THREE.Color(bgs[0]), 1);

    // --- Resize Handler ---
    // Use the robust update function
    window.addEventListener('resize', updateSceneSize);

    // Call once immediately to set initial size correctly
    updateSceneSize();
}
function applySavedMap(savedMap) {
    Object.entries(savedMap).forEach(([partKey, savedValue]) => {
        try {
            const stepConfig = config[partKey];
            if (!stepConfig) return;

            let valueToApply = null;
            let type = 'material';

            if (typeof savedValue === 'number' && stepConfig.options[savedValue]) {
                const opt = stepConfig.options[savedValue];
                valueToApply = opt.value;
                type = opt.type || 'material';
            } else if (typeof savedValue === 'object' && savedValue !== null) {
                valueToApply = savedValue;
                if (stepConfig.options[0]) type = stepConfig.options[0].type;
            }

            if (valueToApply) {
                state.current[partKey] = valueToApply;
                applyToModel(partKey, valueToApply, type);
            }
        } catch (e) {
            console.warn(`Config load error for key "${partKey}":`, e);
        }
    });
}

function loadModel(savedConfig) {
    toggleLoader(true, customizerData.i18n.loadingModel);
    const loader = new GLTFLoader();

    loader.load(modelUrl, (gltf) => {
        if (state.model) scene.remove(state.model);
        state.model = gltf.scene;
        scene.add(state.model);

        // Auto-frame the model for optimal viewing
        autoFrameModel(state.model);

        // Create ground plane positioned at the bottom of the model
        if (!groundPlane) {
            const groundGeom = new THREE.PlaneGeometry(50, 50);
            const groundMat = new THREE.ShadowMaterial({ opacity: 0.5 });
            groundPlane = new THREE.Mesh(groundGeom, groundMat);
            groundPlane.rotation.x = -Math.PI / 2; // Rotate to be horizontal
            groundPlane.receiveShadow = true;
            scene.add(groundPlane);
        }

        // Position ground plane at the bottom of the model's bounding box
        const box = new THREE.Box3().setFromObject(state.model);
        groundPlane.position.y = box.min.y;

        state.model.traverse((child) => {
            if (child.isMesh) {
                meshMap.set(child.name, child);
                child.castShadow = true;
                child.receiveShadow = true;

                const mat = Array.isArray(child.material) ? child.material[0].clone() : child.material.clone();

                if (mat.name.toLowerCase().includes('glass')) {
                    const glassMat = new THREE.MeshPhysicalMaterial({

                        name: mat.name,
                        color: mat.color,
                        map: mat.map,

                        // Transmission
                        transmission: 1.0,
                        opacity: 1.0,
                        transparent: true,

                        //  Surface Props
                        roughness: 0.05,
                        metalness: 0.0,
                        ior: 1.5,

                        // Volume
                        thickness: 0.001,     // CRITICAL: Adjust based on your model scale! (0.1 meters usually)
                        attenuationColor: new THREE.Color(0xffffff),
                        attenuationDistance: 0.25,

                        // Render Settings
                        side: THREE.DoubleSide, // Render both inside and outside faces
                        envMapIntensity: 1.0
                    });

                    child.material = glassMat;
                } else {
                    child.material = mat;
                }
            }
        });
        let loaded = false;

        if (initialCfg) {
            try {
                applySavedMap(JSON.parse(atob(initialCfg.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(initialCfg.length / 4) * 4, '='))));
                loaded = true;
            } catch (e) {
                console.error("URL config parse failed:", e);
            }
            const url = new URL(window.location);
            url.searchParams.delete('dprcu_cfg');
            window.history.replaceState({}, '', url);
        }

        if (!loaded && savedConfig) {
            applySavedMap(savedConfig);
            loaded = true;
        }

        if (!loaded) {
            applyDefaults();
        } else {
            
            refreshUI();
            updateCartData();
        }

        if (appSettings.stepThumbnailsEnabled !== false) {
            if (pendingTextureLoads === 0) {
                generateStepThumbnails();
            } else {
                onTexturesReady = generateStepThumbnails;
            }
        }

        
        requestRender();
        toggleLoader(false);

    }, undefined, (err) => {
        console.error('Error loading model:', err);
        toggleLoader(false);
    });
}


function applyDefaults() {
    Object.entries(config).forEach(([key, section]) => {
        if (section.options && section.options.length > 0) {
            const defOpt = section.options[0];
            const type = defOpt.type || 'material';
            updateState(key, defOpt.value, type, false);
        }
    });
}

/**
 * Renders the UI using the helper from ui.js
 */
function refreshUI() {
    const container = document.getElementById('sidebar-content');
    renderUI(container, config, state, {
        // Callback when a swatch is clicked
        onOptionSelect: (partKey, value, type) => updateState(partKey, value, type, true),
        // Callback for mobile menu navigation
        onNavChange: (sectionKey) => {
            state.ui.activeSection = sectionKey;
            refreshUI();
        }
    });
}

const shareUrlCache = { config: null, url: null };

function buildShareUrl() {
    const configStr = JSON.stringify(state.current);
    if (shareUrlCache.config === configStr) return shareUrlCache.url;

    shareUrlCache.url = `${window.location.origin}${window.location.pathname}?dprcu_cfg=${btoa(JSON.stringify(buildMinifiedState())).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')}`;
    shareUrlCache.config = configStr;
    return shareUrlCache.url;
}

function openShareModal() {
    const modal = document.getElementById('share-modal');
    const previewImg = document.getElementById('share-preview-img');
    const linkInput = document.getElementById('share-link-input');

    previewImg.src = canvas.toDataURL('image/png');
    linkInput.value = buildShareUrl();

    modal.classList.add('active');
    document.body.style.overflow = 'hidden';
}

function closeShareModal() {
    document.getElementById('share-modal').classList.remove('active');
    document.body.style.overflow = '';
}

function shareToSocial(platform) {
    const url = encodeURIComponent(document.getElementById('share-link-input').value);
    const img = encodeURIComponent(document.getElementById('share-preview-img').src);
    const title = encodeURIComponent(document.querySelector('h2.customizer-title')?.textContent.trim() || customizerData.i18n.shareDefaultMsg);
    const socialUrls = {
        facebook: `https://www.facebook.com/sharer/sharer.php?u=${url}`,
        twitter: `https://twitter.com/intent/tweet?url=${url}&text=${title}`,
        linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${url}`,
        pinterest: `https://www.pinterest.com/pin/create/button/?url=${url}&media=${img}&description=${title}`,
    };
    if (socialUrls[platform]) {
        window.open(socialUrls[platform], `share-${platform}`, 'width=600,height=400');
    }
}

function openHelpModal() {
    const modal = document.getElementById('help-modal');
    if (modal) {
        modal.classList.add('active');
        document.body.style.overflow = 'hidden';
    }
}

function closeHelpModal() {
    const modal = document.getElementById('help-modal');
    if (modal) {
        modal.classList.remove('active');
        document.body.style.overflow = '';
    }
}

function isInputFocused() {
    const activeElement = document.activeElement;
    return activeElement && (
        activeElement.tagName === 'INPUT' ||
        activeElement.tagName === 'TEXTAREA' ||
        activeElement.contentEditable === 'true'
    );
}

function doReset() {
    const url = new URL(window.location);
    url.searchParams.delete('dprcu_cfg');
    window.history.replaceState({}, '', url);
    // delete saved config from IndexedDB as well to prevent auto-reload of old config
    if (textureDB) {
        textureDB.delete('config', CONFIG_IDB_KEY).catch(() => {});
    }
    location.reload();
}

function setupXRDiagnostics() {
    renderer.getContext().canvas.addEventListener('webglcontextlost', (e) => {
        console.error('WebGL context lost during XR:', e);
        console.error('This indicates GPU memory pressure or device limits reached.');
        if (inXR) {
            alert('AR session ended due to GPU context loss. Try closing other apps.');
        }
    });

    window.addEventListener('error', (e) => {
        if (inXR) {
            console.error('Error during XR session:', e.error, e.message);
            console.error('Stack:', e.error?.stack);
        }
    });

    window.addEventListener('unhandledrejection', (e) => {
        if (inXR) {
            console.error('Unhandled rejection during XR:', e.reason);
        }
    });
}

function initUI() {
    refreshUI(); // Initial Render
    setupXRDiagnostics(); // Setup XR error handlers once during init

    // Wire up Static Buttons
    const bind = (id, fn) => { const el = document.getElementById(id); if (el) el.onclick = fn; };

    bind('btn-undo', doUndo);
    bind('btn-redo', doRedo);
    bind('btn-reset', doReset);
    const adjustZoom = (factor) => {
        const dir = new THREE.Vector3().copy(camera.position).sub(controls.target);
        const dist = dir.length();
        const newDist = THREE.MathUtils.clamp(dist * factor, controls.minDistance, controls.maxDistance);
        dir.normalize().multiplyScalar(newDist);
        camera.position.copy(controls.target).add(dir);
        controls.update();
        requestRender();
    };
    bind('btn-zoom-in', () => adjustZoom(1 / 1.1));
    bind('btn-zoom-out', () => adjustZoom(1.1));
    bind('btn-fullscreen', () => {
        const el = document.getElementById('canvas-container');
        document.fullscreenElement ? document.exitFullscreen() : el.requestFullscreen().catch(() => { });
    });
    bind('btn-download-preview', downloadPDF);
    bind('btn-view-ar', startAR);
    // Fire analytics when the WC cart form is submitted (standard button or express payments).
    const expressForm = document.getElementById('dprcu-express-form');
    if (expressForm) {
        expressForm.addEventListener('submit', () => {
            
        });
    }
    bind('btn-bg', toggleBackground);
    
    bind('btn-share-config', openShareModal);

    // Share Modal
    bind('share-modal-close', closeShareModal);
    const backdrop = document.querySelector('.share-modal-backdrop');
    if (backdrop) backdrop.onclick = closeShareModal;
    bind('share-link-copy', () => {
        const input = document.getElementById('share-link-input');
        navigator.clipboard.writeText(input.value).then(() => {
            const btn = document.getElementById('share-link-copy');
            const prev = btn.innerText;
            btn.innerText = customizerData.i18n.copied;
            setTimeout(() => { btn.innerText = prev; }, 2000);
        });
    });
    bind('share-facebook', () => shareToSocial('facebook'));
    bind('share-twitter', () => shareToSocial('twitter'));
    bind('share-linkedin', () => shareToSocial('linkedin'));
    bind('share-pinterest', () => shareToSocial('pinterest'));
    bind('share-download-img', () => {
        const src = document.getElementById('share-preview-img').src;
        const a = document.createElement('a');
        a.href = src;
        a.download = `design-${Date.now()}.png`;
        a.click();
    });
    document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
            closeShareModal();
            closeHelpModal();
        }
    });

    // Help Modal
    bind('btn-help', openHelpModal);
    bind('help-modal-close', closeHelpModal);
    const helpBackdrop = document.querySelector('.help-modal-backdrop');
    if (helpBackdrop) {
        helpBackdrop.onclick = closeHelpModal;
        helpBackdrop.ontouchend = (e) => { e.preventDefault(); closeHelpModal(); };
    }

    // Detect OS and update shortcut display for Mac compatibility
    const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform);
    if (isMac) {
        document.querySelectorAll('.mod-key').forEach(el => {
            el.textContent = 'Cmd';
        });
    }

    // Keyboard Shortcut Bindings
    document.addEventListener('keydown', (e) => {
        const modKey = isMac ? e.metaKey : e.ctrlKey;

        if (modKey && e.key === 'z' && !e.shiftKey) {
            e.preventDefault();
            doUndo();
        }
        if ((modKey && e.key === 'y') || (modKey && e.shiftKey && e.key === 'z')) {
            e.preventDefault();
            doRedo();
        }
        if ((e.key === 'r' || e.key === 'R') && !modKey) {
            if (!isInputFocused()) doReset();
        }
        if (e.key === '+' || e.key === '=') {
            e.preventDefault();
            adjustZoom(1 / 1.1);
        }
        if (e.key === '-' || e.key === '_') {
            e.preventDefault();
            adjustZoom(1.1);
        }
        if (e.key === 'f' || e.key === 'F') {
            if (!isInputFocused()) {
                e.preventDefault();
                const container = document.getElementById('canvas-container');
                document.fullscreenElement ? document.exitFullscreen() : container.requestFullscreen().catch(() => { });
            }
        }
        if (e.key === 'b' || e.key === 'B') {
            if (!isInputFocused()) {
                e.preventDefault();
                toggleBackground();
            }
        }
        if (e.key === '?') {
            e.preventDefault();
            openHelpModal();
        }
    });

    // Handle Window Resize for Responsive UI
    let resizeTimer;
    window.addEventListener('resize', () => {
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(refreshUI, 100);
    });

}

// ========================================================
// 3. FEATURES (Cart, AR, PDF, Undo)
// ========================================================

function formatPrice(amount) {
    const sym = customizerData.currencySymbol || '$';
    const pos = customizerData.currencyPosition || 'left';
    const val = amount.toFixed(2);
    switch (pos) {
        case 'left': return sym + val;
        case 'right': return val + sym;
        case 'left_space': return sym + ' ' + val;
        case 'right_space': return val + ' ' + sym;
        default: return sym + val;
    }
}

function updatePriceDisplay() {
    const priceEl = document.getElementById('dprcu-total-price');
    if (!priceEl) {
        // Base price wasn't set, so the server didn't render the price span at
        // all — keep the cart button disabled regardless of what other listeners
        // (e.g. the variations addon) do with it.
        const cartBtn = document.querySelector('#dprcu-express-form .single_add_to_cart_button');
        if (cartBtn) cartBtn.disabled = true;
        return;
    }

    const basePrice = parseFloat(priceEl.dataset.basePrice) || 0;
    let surcharge = 0;
    for (const [key, val] of Object.entries(state.current)) {
        const step = config[key];
        if (!step) continue;
        const opt = step.options.find(o => deepEqual(o.value, val));
        surcharge += opt ? (parseFloat(opt.price) || 0) : 0;
    }

    const total = basePrice + surcharge;
    priceEl.textContent = formatPrice(total);
}

function updateCartData() {
    const cartList = [];
    let totalSurcharge = 0;
    for (const [key, val] of Object.entries(state.current)) {
        const step = config[key];
        if (!step) continue;

        const opt = step.options.find(o => deepEqual(o.value, val));
        const optName = opt ? opt.name : 'Custom';
        const optPrice = opt ? (parseFloat(opt.price) || 0) : 0;

        totalSurcharge += optPrice;
        cartList.push({ part: step.name, option: optName, optId: opt?.optId ?? null, price: optPrice });
    }

    const expressInput = document.getElementById('dprcu-express-shoe-config');
    if (expressInput) expressInput.value = JSON.stringify(cartList);

    document.dispatchEvent(new CustomEvent('dprcu:cartUpdated', {
        detail: {
            cartList,
            optIds:      state.optIds,
            texIds:      state.texIds,
            config:      customizerData,
            expressForm: document.getElementById('dprcu-express-form'),
        },
    }));
    updatePriceDisplay();
}

// --- AR Logic ---
async function startAR() {
    if (!state.model) return;
    state.model.updateMatrixWorld(true);

    const platform = detectARPlatform();

    try {
        if (platform === 'ios') await launchQuickLook();
        else await launchWebXR();
    } catch (e) {
        console.error('AR Error:', e);
        alert(`AR failed: ${e.message}`);
    }
}

async function launchWebXR() {
    if (!navigator.xr) throw new Error('WebXR unavailable');
    const ok = await navigator.xr.isSessionSupported('immersive-ar');
    if (!ok) throw new Error('Device does not support AR');

    const overlay = document.getElementById('dprcu-ar-overlay');
    overlay.hidden = false;
    setOverlayState('searching');

    const session = await navigator.xr.requestSession('immersive-ar', {
        requiredFeatures: ['hit-test'],
        optionalFeatures: ['dom-overlay', 'light-estimation', 'local-floor'],
        domOverlay: { root: overlay },
    });

    wireOverlayButtons(session);
    setupReticleAndPlacement(session);
    await renderer.xr.setSession(session);
}

function setOverlayState(overlayState) {
    const overlay = document.getElementById('dprcu-ar-overlay');
    const els = overlay.querySelectorAll('[data-state]');
    for (let i = 0; i < els.length; i++) {
        els[i].hidden = els[i].dataset.state !== overlayState;
    }
}

function wireOverlayButtons(session) {
    const overlay = document.getElementById('dprcu-ar-overlay');
    overlay.querySelector('.dprcu-ar-exit').onclick = () => session.end();
}

async function launchQuickLook() {
    const ab = await new USDZExporter().parseAsync(state.model, { quickLookCompatible: true });
    if (!ab || ab.byteLength < 100) throw new Error('USDZ empty');

    const url = URL.createObjectURL(new Blob([ab], { type: 'model/vnd.usdz+zip' }));
    const a = document.createElement('a');
    a.rel = 'ar';
    a.href = url;
    const img = document.createElement('img');
    img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
    a.appendChild(img);
    document.body.appendChild(a);
    a.click();
    setTimeout(() => { a.remove(); URL.revokeObjectURL(url); }, 5000);
}


function detectARPlatform() {
    const ua = navigator.userAgent;
    const uaData = navigator.userAgentData;

    // iOS / iPadOS / visionOS Safari → AR Quick Look
    const isIPhone = /iPad|iPhone|iPod/.test(ua);
    const isIPadOS = (
        (uaData?.platform === 'macOS' || navigator.platform === 'MacIntel') &&
        navigator.maxTouchPoints > 1
    );
    const isVisionOS = /Macintosh.*Safari/.test(ua) && navigator.maxTouchPoints > 0 && !('ontouchstart' in window) === false;
    if (isIPhone || isIPadOS || isVisionOS) return 'ios';

    // Android Chrome / Samsung Internet → Scene Viewer
    // Firefox on Android does not handle the intent:// URL the same way; treat as desktop fallback
    const isAndroid = /android/i.test(ua);
    const isAndroidChromeLike = isAndroid && /Chrome|SamsungBrowser/.test(ua) && !/Firefox/.test(ua);
    if (isAndroidChromeLike) return 'android';

    return 'android';
}

let hitTestSource = null;
let hitTestSourceRequested = false;
let reticle = null;
let arRoot = null;
let arGroup = null;
let arOriginalState = null;
let arController = null;
let arPlaced = false;

/**
 * Swap model materials to simpler Lambert shaders for AR mode.
 * Lambert is ~10x cheaper than MeshStandardMaterial and looks good under camera light.
 */
function swapToARMaterials() {
    arRoot.traverse((o) => {
        if (!o.isMesh || !o.material) return;

        // Save original for restoration
        o.userData.originalMaterial = o.material;

        // Create Lambert material with base texture/color
        const originalMat = Array.isArray(o.material) ? o.material[0] : o.material;
        o.material = new THREE.MeshLambertMaterial({
            map: originalMat.map,
            color: originalMat.color?.clone() ?? new THREE.Color(0xffffff),
        });
    });
    console.log('AR materials swapped to Lambert');
}

/**
 * Restore original PBR materials after exiting AR.
 * Disposes the temporary Lambert materials to free GPU memory.
 */
function restoreOriginalMaterials() {
    arRoot.traverse((o) => {
        if (o.userData.originalMaterial) {
            if (o.material?.dispose) o.material.dispose();
            o.material = o.userData.originalMaterial;
            delete o.userData.originalMaterial;
        }
    });
    console.log('Original materials restored');
}

function setupReticleAndPlacement(session) {
    if (reticle) {
        scene.remove(reticle);
        reticle.geometry.dispose();
        reticle.material.dispose();
    }

    reticle = new THREE.Mesh(
        new THREE.RingGeometry(0.07, 0.09, 32).rotateX(-Math.PI / 2),
        new THREE.MeshBasicMaterial({ color: 0xffffff, toneMapped: false })
    );
    reticle.matrixAutoUpdate = false;
    reticle.visible = false;
    scene.add(reticle);

    // 2. Clone model into a dedicated group so original is never mutated
    arGroup = new THREE.Group();
    arGroup.visible = false;
    const modelClone = state.model.clone(true);
    modelClone.traverse((child) => {
        if (child.isMesh && child.material) {
            child.material = child.material.clone();
        }
    });
    arGroup.add(modelClone);
    scene.add(arGroup);
    arRoot = arGroup;

    arOriginalState = {
        background: scene.background,
        clearAlpha: renderer.getClearAlpha(),
        cameraPosition: camera.position.clone(),
        cameraQuaternion: camera.quaternion.clone(),
        cameraFov: camera.fov,
        cameraAspect: camera.aspect,
        cameraNear: camera.near,
        cameraFar: camera.far,
        rendererSize: renderer.getSize(new THREE.Vector2()),
        pixelRatio: renderer.getPixelRatio(),
        controlsTarget: controls.target.clone(),
        shadowsEnabled: renderer.shadowMap.enabled,
        toneMapping: renderer.toneMapping,
        modelVisible: state.model.visible,
    };

    state.model.visible = false;
    scene.background = null;
    renderer.setClearAlpha(0);
    renderer.shadowMap.enabled = false;
    renderer.toneMapping = THREE.NoToneMapping;

    swapToARMaterials();

    // 3. Controller-based select (official Three.js pattern)
    arController = renderer.xr.getController(0);
    scene.add(arController);

    arPlaced = false;

    const onSelect = () => {
        if (!reticle.visible || !arRoot) return;

        arGroup.position.setFromMatrixPosition(reticle.matrix);

        if (!arPlaced) {
            arRoot.visible = true;
            arPlaced = true;
            setOverlayState('placed');
            // overlay stays visible so user sees Move/Rotate controls
        }
        // subsequent selects reposition the model (no state change needed)
    };
    arController.addEventListener('select', onSelect);

    // 4. Hand render loop to Three.js's XR driver
    inXR = true;
    if (typeof animReq !== 'undefined' && animReq) {
        cancelAnimationFrame(animReq);
        animReq = null;
    }
    renderer.setAnimationLoop(animate);

    // 5. Tear-down on session end
    session.addEventListener('end', () => {
        renderer.setAnimationLoop(null);
        inXR = false;

        if (arController) {
            arController.removeEventListener('select', onSelect);
            scene.remove(arController);
            arController = null;
        }

        if (hitTestSource) {
            try { hitTestSource.cancel?.(); } catch { }
            hitTestSource = null;
        }

        if (reticle) {
            scene.remove(reticle);
            reticle.geometry.dispose();
            reticle.material.dispose();
            reticle = null;
        }

        if (arGroup) {
            restoreOriginalMaterials();
            scene.remove(arGroup);
            arGroup = null;
            arRoot = null;
        }

        if (arOriginalState) {
            scene.background = arOriginalState.background;
            renderer.setClearAlpha(arOriginalState.clearAlpha);

            camera.position.copy(arOriginalState.cameraPosition);
            camera.quaternion.copy(arOriginalState.cameraQuaternion);
            camera.fov = arOriginalState.cameraFov;
            camera.aspect = arOriginalState.cameraAspect;
            camera.near = arOriginalState.cameraNear;
            camera.far = arOriginalState.cameraFar;
            camera.updateProjectionMatrix();
            camera.updateMatrixWorld(true);

            renderer.setPixelRatio(arOriginalState.pixelRatio);
            renderer.setSize(arOriginalState.rendererSize.x, arOriginalState.rendererSize.y, false);

            renderer.shadowMap.enabled = arOriginalState.shadowsEnabled;
            renderer.toneMapping = arOriginalState.toneMapping;

            if (state.model) state.model.visible = arOriginalState.modelVisible;

            controls.target.copy(arOriginalState.controlsTarget);
            controls.update();
        }

        arOriginalState = null;
        document.getElementById('dprcu-ar-overlay').hidden = true;

        requestRender();
    });
}

// --- Undo/Redo Logic ---

function doUndo() {
    // We can only undo if the pointer is at 0 or higher
    if (state.historyIndex < 0) return;

    // 1. Get the command we are undoing
    const entry = state.history[state.historyIndex];

    // 2. Revert to the OLD value
    // (saveHistory = false prevents infinite loops)
    updateState(entry.partKey, entry.oldValue, entry.type, false);

    // 3. Move pointer back
    state.historyIndex--;

    updateUndoRedoButtons();
}

function doRedo() {
    // We can only redo if the pointer is not at the end of the stack
    if (state.historyIndex >= state.history.length - 1) return;

    // 1. Move pointer forward to the next command
    state.historyIndex++;

    // 2. Get the command we are re-applying
    const entry = state.history[state.historyIndex];

    // 3. Apply the NEW value
    updateState(entry.partKey, entry.newValue, entry.type, false);

    updateUndoRedoButtons();
}

function updateUndoRedoButtons() {
    const undoBtn = document.getElementById('btn-undo');
    const redoBtn = document.getElementById('btn-redo');

    if (undoBtn) {
        // Disabled if index is -1 (start of history)
        undoBtn.disabled = state.historyIndex < 0;
        // Optional: Change opacity for visual feedback
        undoBtn.style.opacity = undoBtn.disabled ? '0.5' : '1';
    }

    if (redoBtn) {
        // Disabled if index is at the very end
        redoBtn.disabled = state.historyIndex >= state.history.length - 1;
        redoBtn.style.opacity = redoBtn.disabled ? '0.5' : '1';
    }
}



// --- Background Toggle ---
function toggleBackground() {
    const colors = appSettings.bgColors?.length > 0
        ? appSettings.bgColors
        : ['#f0f0f0', '#ffffff', '#222222'];

    // Read current color from renderer instead of mesh
    const currentHex = `#${renderer.getClearColor(new THREE.Color()).getHexString()}`;

    let idx = colors.findIndex(c => c.toLowerCase() === currentHex.toLowerCase());
    if (idx < 0) idx = 0;
    idx = (idx + 1) % colors.length;

    const newHex = colors[idx];
    renderer.setClearColor(new THREE.Color(newHex), 1);

    const isDark = new THREE.Color(newHex).getHSL({}).l < 0.5;
    if (groundPlane) groundPlane.material.opacity = isDark ? 0.2 : 0.5;

    requestRender();
}

// --- Loader Helper ---
function toggleLoader(show, text = 'Loading...') {
    const loader = document.getElementById('customizer-loader');
    const label = document.getElementById('loader-text');
    if (!loader) return;

    if (show) {
        if (label) label.innerText = text;
        loader.classList.remove('hidden');
    } else {
        loader.classList.add('hidden');
    }
}

function showNotice(message, type = 'warning') {
    const existing = document.getElementById('customizer-notice');
    if (existing) existing.remove();

    const notice = document.createElement('div');
    notice.id = 'customizer-notice';
    notice.textContent = message;
    Object.assign(notice.style, {
        position: 'absolute',
        top: '12px',
        left: '50%',
        transform: 'translateX(-50%)',
        zIndex: '9999',
        padding: '8px 16px',
        borderRadius: '6px',
        fontSize: '13px',
        fontWeight: '500',
        pointerEvents: 'none',
        background: type === 'warning' ? '#fff3cd' : '#f8d7da',
        color: type === 'warning' ? '#856404' : '#721c24',
        border: `1px solid ${type === 'warning' ? '#ffc107' : '#f5c6cb'}`,
        boxShadow: '0 2px 8px rgba(0,0,0,0.12)',
    });

    const container = document.getElementById('canvas-container') || document.body;
    container.appendChild(notice);

    setTimeout(() => notice.remove(), 6000);
}

// --- Hotspot / Annotation System ---

let inXR = false;

// --- Animation Loop ---
function requestRender() {
    if (inXR) return; // setAnimationLoop is driving frames
    if (!animReq) animReq = requestAnimationFrame(animate);
}


function animate(_time, frame) {
    animReq = null;

    if (frame) {
        const session = renderer.xr.getSession();

        // Lazy-init hit test source on first XR frame (official Three.js pattern)
        if (inXR && !hitTestSourceRequested) {
            hitTestSourceRequested = true;
            session.requestReferenceSpace('viewer')
                .then((space) => session.requestHitTestSource({ space }))
                .then((src) => { hitTestSource = src; })
                .catch((e) => {
                    console.error('hit-test setup failed:', e);
                    hitTestSourceRequested = false;
                });
            session.addEventListener('end', () => {
                hitTestSourceRequested = false;
                hitTestSource = null;
            }, { once: true });
        }

        if (hitTestSource) {
            const refSpace = renderer.xr.getReferenceSpace();
            try {
                const hits = frame.getHitTestResults(hitTestSource);
                if (hits.length) {
                    const pose = hits[0].getPose(refSpace);
                    reticle.visible = true;
                    reticle.matrix.fromArray(pose.transform.matrix);
                    if (arRoot && !arRoot.visible) setOverlayState('ready');
                } else {
                    reticle.visible = false;
                }
            } catch (e) {
                console.warn('hit-test error:', e);
            }
        }
    }

    if (!inXR) controls.update();

    renderer.render(scene, camera);
    
}
