import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js';

const sceneSettings = window.dprcu_model_scanner || {};

class ModelPreviewer {
    constructor(containerId) {
        this.container = document.getElementById(containerId);
        if (!this.container) return;

        this.hiddenInput = document.getElementById('_dprcu_camera_position');
        this.hasHdr = !!sceneSettings.hdrFile;

        this.width = this.container.clientWidth;
        this.height = this.container.clientHeight;

        // --- Scene — mirrors customizer.js ---
        this.scene = new THREE.Scene();
        this.scene.background = new THREE.Color(sceneSettings.bgColor || '#f0f0f0');

        // --- Camera — mirrors customizer.js ---
        this.camera = new THREE.PerspectiveCamera(45, this.width / this.height, 0.1, 1000);
        this.camera.position.set(2, 2, 2);

        // --- Renderer — mirrors customizer.js initRenderer() ---
        this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        this.renderer.setSize(this.width, this.height);
        this.renderer.shadowMap.enabled = true;
        this.renderer.shadowMap.type = THREE.PCFShadowMap;
        this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
        this.renderer.toneMappingExposure = 0.8;
        this.renderer.outputColorSpace = THREE.SRGBColorSpace;

        const oldCanvas = this.container.querySelector('canvas');
        if (oldCanvas) oldCanvas.remove();

        this.container.appendChild(this.renderer.domElement);
        this.controls = new OrbitControls(this.camera, this.renderer.domElement);
        this.controls.enableDamping = false;

        // --- HDR Environment Map — mirrors customizer.js initScene() ---
        if (this.hasHdr) {
            new HDRLoader().load(sceneSettings.hdrFile, (hdrTexture) => {
                hdrTexture.mapping = THREE.EquirectangularReflectionMapping;
                this.scene.environment = hdrTexture;
            }, undefined, (err) => {
                console.error('Failed to load HDR:', err);
            });
        }

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

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

        const fillLight = new THREE.DirectionalLight(0xffffff, this.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;
        this.scene.add(fillLight.target);

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

        // --- Ground plane (shadow catcher) — mirrors customizer.js loadModel() ---
        const groundGeom = new THREE.PlaneGeometry(50, 50);
        const groundMat = new THREE.ShadowMaterial({ opacity: 0.5 });
        this.groundPlane = new THREE.Mesh(groundGeom, groundMat);
        this.groundPlane.rotation.x = -Math.PI / 2;
        this.groundPlane.receiveShadow = true;
        this.scene.add(this.groundPlane);

        this.resizeObserver = new ResizeObserver(() => {
            this.width = this.container.clientWidth;
            this.height = this.container.clientHeight;
            this.camera.aspect = this.width / this.height;
            this.camera.updateProjectionMatrix();
            this.renderer.setSize(this.width, this.height);
        });
        this.resizeObserver.observe(this.container);
        this.animate = this.animate.bind(this);
        this.animate();

        this._attachButtons();
    }
    _attachButtons() {
        const saveBtn = document.getElementById('cam-save-btn');
        const resetBtn = document.getElementById('cam-reset-btn');

        if (saveBtn) {
            saveBtn.onclick = () => {
                if (!this.currentModel) return;
                const state = JSON.stringify({
                    position: {
                        x: parseFloat(this.camera.position.x.toFixed(4)),
                        y: parseFloat(this.camera.position.y.toFixed(4)),
                        z: parseFloat(this.camera.position.z.toFixed(4)),
                    },
                    target: {
                        x: parseFloat(this.controls.target.x.toFixed(4)),
                        y: parseFloat(this.controls.target.y.toFixed(4)),
                        z: parseFloat(this.controls.target.z.toFixed(4)),
                    },
                });
                if (this.hiddenInput) this.hiddenInput.value = state;
                saveBtn.textContent = 'Saved!';
                setTimeout(() => { saveBtn.textContent = 'Save View'; }, 1500);
            };
        }

        if (resetBtn) {
            resetBtn.onclick = () => {
                if (this.hiddenInput) this.hiddenInput.value = '';
                resetBtn.textContent = 'Reset!';
                setTimeout(() => { resetBtn.textContent = 'Reset View'; }, 1500);
                if (!this.currentModel) return;
                const box = new THREE.Box3().setFromObject(this.currentModel);
                const size = new THREE.Vector3(); box.getSize(size);
                const maxDim = Math.max(size.x, size.y, size.z);
                const fov = this.camera.fov * (Math.PI / 180);
                const cameraDist = Math.abs(maxDim / 2 / Math.tan(fov / 2));
                this.camera.position.set(cameraDist, cameraDist * 0.6, cameraDist);
                this.camera.lookAt(0, 0, 0);
                this.controls.target.set(0, 0, 0);
                this.controls.update();
            };
        }
    }
    _loadSavedCameraState() {
        if (!this.hiddenInput || !this.hiddenInput.value) return null;
        try {
            const state = JSON.parse(this.hiddenInput.value);
            if (state?.position && state?.target) return state;
        } catch (e) { /* ignore invalid JSON */ }
        return null;
    }
    /**
     * Highlights a mesh (by name) by fading every other mesh in the model to
     * near-transparent, same technique customizer.js uses for step thumbnails
     * (generateStepThumbnails). Simpler and more reliable than an edge/outline
     * shader, and reads clearly regardless of viewing angle.
     */
    highlightPart(meshName) {
        this.clearHighlight();
        if (!this.currentModel) return;

        let target = null;
        this.currentModel.traverse((child) => {
            if (!target && child.isMesh && child.name === meshName) target = child;
        });
        if (!target) return;

        const dim = (m) => {
            const clone = m.clone();
            clone.transparent = true;
            clone.opacity = 0.03;
            clone.depthWrite = false;
            return clone;
        };

        this.dimmedMeshes = [];
        this.currentModel.traverse((child) => {
            if (!child.isMesh || child === target) return;
            this.dimmedMeshes.push({ mesh: child, original: child.material });
            child.material = Array.isArray(child.material) ? child.material.map(dim) : dim(child.material);
        });
    }
    clearHighlight() {
        if (!this.dimmedMeshes) return;
        this.dimmedMeshes.forEach(({ mesh, original }) => {
            const dispose = (m) => m.dispose();
            Array.isArray(mesh.material) ? mesh.material.forEach(dispose) : dispose(mesh.material);
            mesh.material = original;
        });
        this.dimmedMeshes = null;
    }
    load(url) {
        this.container.style.display = 'block';
        this.dimmedMeshes = null;
        if (this.currentModel) {
            this.scene.remove(this.currentModel);
            this.currentModel = null;
        }
        const loader = new GLTFLoader();
        loader.load(url, (gltf) => {
            this.currentModel = gltf.scene;
            this.scene.add(this.currentModel);
            this.currentModel.traverse((child) => {
                if (child.isMesh) {
                    child.castShadow = true;
                    child.receiveShadow = true;
                }
            });
            const box = new THREE.Box3().setFromObject(this.currentModel);
            const size = new THREE.Vector3(); box.getSize(size);
            const center = new THREE.Vector3(); box.getCenter(center);
            this.currentModel.position.x += (this.currentModel.position.x - center.x);
            this.currentModel.position.y += (this.currentModel.position.y - center.y);
            this.currentModel.position.z += (this.currentModel.position.z - center.z);

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

            const maxDim = Math.max(size.x, size.y, size.z);
            const fov = this.camera.fov * (Math.PI / 180);
            const cameraDist = Math.abs(maxDim / 2 / Math.tan(fov / 2));

            const saved = this._loadSavedCameraState();
            if (saved) {
                this.camera.position.set(saved.position.x, saved.position.y, saved.position.z);
                this.controls.target.set(saved.target.x, saved.target.y, saved.target.z);
            } else {
                this.camera.position.set(cameraDist, cameraDist * 0.6, cameraDist);
                this.camera.lookAt(0, 0, 0);
                this.controls.target.set(0, 0, 0);
            }
            this.controls.update();
        }, undefined, (err) => console.error('Preview Error:', err));
    }
    animate() {
        requestAnimationFrame(this.animate);
        this.controls.update();
        this.renderer.render(this.scene, this.camera);
    }
}

jQuery(document).ready(($) => {

    const $input = $('#_3d_model_url_field');
    const $status = $('#parser-status');
    let previewer = null;
    if (document.getElementById('admin-model-preview')) {
        previewer = new ModelPreviewer('admin-model-preview');
    }

    // Exposed so the mesh tags rendered by metabox-scripts.js can highlight
    // the corresponding part in the 3D preview on hover.
    window.dprcuHighlightMeshPart = (meshName) => previewer?.highlightPart(meshName);
    window.dprcuClearMeshHighlight = () => previewer?.clearHighlight();

    function performScan(url) {
        const loader = new GLTFLoader();

        $status.show().html('Scanning model... <span class="spinner is-active" style="float:none; margin:0;"></span>');

        loader.load(url, (gltf) => {
            const meshes = [];
            gltf.scene.traverse((child) => {
                if (child.isMesh && child.name) meshes.push(child.name);
            });

            const uniqueMeshes = [...new Set(meshes)].sort();

            if (typeof window.updateDetectedPartsDisplay === 'function') {
                window.updateDetectedPartsDisplay(uniqueMeshes);
            }

            $status.html('<span style="color:#00a32a">✔ Found ' + uniqueMeshes.length + ' parts.</span>');
            setTimeout(() => $status.fadeOut(), 3000);

        }, undefined, (err) => {
            console.error(err);
            $status.html('<span style="color:#d63638">✘ Error loading model.</span>');
        });
    }

    const initialUrl = $input.val();
    if (initialUrl && (initialUrl.endsWith('.glb') || initialUrl.endsWith('.gltf'))) {
        if (previewer) previewer.load(initialUrl);
        performScan(initialUrl);
    }

    $input.on('change input', function () {
        const url = $(this).val();
        if (!url || (!url.endsWith('.glb') && !url.endsWith('.gltf'))) return;

        if (previewer) previewer.load(url);

        performScan(url);
    });
});
