import { defineWidget, useNoteContext, useEffect, useRef } from "trilium:preact";

const THREE_JS = "api/notes/__THREE_ID__/download";
const ORBIT_JS = "api/notes/__THREE_ORBIT_ID__/download";
const STL_JS = "api/notes/__THREE_STL_ID__/download";
const OCCT_JS = "api/notes/__OCCT_JS_ID__/download";
const OCCT_WASM = "api/notes/__OCCT_WASM_ID__/download";
const WORKER_URL = "api/notes/__3D_WORKER_ID__/download";

const STEP_PARAMS = { linearDeflection: 0.4, angularDeflection: 0.2 };

const WORKER_ABS = new URL(WORKER_URL, location.href).href;
const OCCT_JS_ABS = new URL(OCCT_JS, location.href).href;
const OCCT_WASM_ABS = new URL(OCCT_WASM, location.href).href;

function formatBytes(n) {
    if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
    if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
    return n + " B";
}
function formatCount(n) {
    n = Math.round(n);
    if (n >= 1000000) return (n / 1000000).toFixed(2) + "M";
    if (n >= 1000) return (n / 1000).toFixed(1) + "k";
    return String(n);
}
function fixWinding(geo) {
    if (geo.index) return;
    var pos = geo.attributes.position;
    var count = pos.count;
    geo.computeBoundingBox();
    var center = geo.boundingBox.getCenter(new THREE.Vector3());
    var vA = new THREE.Vector3(), vB = new THREE.Vector3(), vC = new THREE.Vector3();
    var e1 = new THREE.Vector3(), e2 = new THREE.Vector3(), n = new THREE.Vector3(), toC = new THREE.Vector3();
    var p = pos.array;
    for (var i = 0; i < count; i += 3) {
        vA.fromBufferAttribute(pos, i); vB.fromBufferAttribute(pos, i + 1); vC.fromBufferAttribute(pos, i + 2);
        e1.subVectors(vB, vA); e2.subVectors(vC, vA); n.crossVectors(e1, e2);
        toC.subVectors(center, vA);
        if (n.dot(toC) > 0) {
            var x = (i + 1) * 3, y = (i + 2) * 3;
            var t;
            t = p[x]; p[x] = p[y]; p[y] = t;
            t = p[x + 1]; p[x + 1] = p[y + 1]; p[y + 1] = t;
            t = p[x + 2]; p[x + 2] = p[y + 2]; p[y + 2] = t;
        }
    }
    pos.needsUpdate = true;
}
function chunkGeometry(geo, group, mat) {
    var pos = geo.attributes.position;
    var idx = geo.index;
    var maxVerts = 65000;
    if (!idx || pos.count <= maxVerts) {
        geo.computeVertexNormals();
        group.add(new THREE.Mesh(geo, mat));
        return;
    }
    var triCount = idx.count / 3;
    var t = 0;
    while (t < triCount) {
        var vmap = new Map();
        var vpos = [];
        var iarr = [];
        while (t < triCount && vmap.size < maxVerts - 3) {
            var a = idx.getX(t * 3), b = idx.getX(t * 3 + 1), c = idx.getX(t * 3 + 2);
            var ids = [a, b, c];
            for (var k = 0; k < 3; k++) {
                var id = ids[k];
                var loc = vmap.get(id);
                if (loc === undefined) {
                    loc = vpos.length / 3;
                    vmap.set(id, loc);
                    vpos.push(pos.getX(id), pos.getY(id), pos.getZ(id));
                }
                iarr.push(loc);
            }
            t++;
        }
        var g = new THREE.BufferGeometry();
        g.setAttribute("position", new THREE.Float32BufferAttribute(vpos, 3));
        g.setIndex(new THREE.BufferAttribute(new Uint16Array(iarr), 1));
        g.computeVertexNormals();
        group.add(new THREE.Mesh(g, mat));
    }
}
function loadScript(src, cb) {
    var s = document.createElement("script");
    s.src = src;
    s.onload = cb;
    document.head.appendChild(s);
}
function fetchProgress(url, onProgress) {
    return fetch(url).then(function (r) {
        if (!r.ok) throw new Error("fetch " + r.status);
        var total = parseInt(r.headers.get("Content-Length") || "0", 10);
        var reader = r.body.getReader();
        var chunks = [];
        var received = 0;
        function pump() {
            return reader.read().then(function (res) {
                if (res.done) {
                    var buf = new Uint8Array(received);
                    var off = 0;
                    chunks.forEach(function (c) { buf.set(c, off); off += c.length; });
                    return buf.buffer;
                }
                chunks.push(res.value);
                received += res.value.length;
                if (total) onProgress(received / total);
                return pump();
            });
        }
        return pump();
    });
}

let styleInjected = false;
function injectStyle() {
    if (styleInjected) return;
    styleInjected = true;
    var st = document.createElement("style");
    st.textContent = [
        ".model-viewer { position: relative; box-sizing: border-box; width: 100%; margin: 1em 0; background: #1d2021; border: 1px solid #504945; border-radius: 8px; overflow: hidden; }",
        ".model-viewer-data { position: absolute; top: 8px; right: 10px; z-index: 3; display: none; font-size: 0.75em; color: #a89984; background: rgba(40,40,40,0.8); border: 1px solid #504945; border-radius: 6px; padding: 2px 10px; font-variant-numeric: tabular-nums; pointer-events: none; }",
        ".model-viewer-canvas { position: relative; width: 100%; height: min(70vh, 900px); min-height: 420px; }",
        ".model-viewer-status { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; text-align: center; padding: 48px; box-sizing: border-box; color: #a89984; font-size: 0.9em; }",
        ".model-viewer-status.error { color: #fb4934; }",
        ".model-viewer-progress { position: absolute; left: 0; right: 0; bottom: 40px; height: 6px; width: min(60%, 480px); margin: 0 auto; background: #3c3836; border-radius: 3px; overflow: hidden; }",
        ".model-viewer-progress-fill { height: 100%; width: 0; background: #b8bb26; border-radius: 3px; transition: width 150ms ease; }",
        ".model-viewer-progress-fill.indeterminate { width: 40%; animation: model-progress 1.1s ease-in-out infinite; }",
        "@keyframes model-progress { 0% { transform: translateX(-100%); } 100% { transform: translateX(250%); } }",
        ".model-viewer-hint { position: absolute; top: 8px; left: 10px; font-size: 0.75em; color: #a89984; z-index: 2; pointer-events: none; }",
        ".model-viewer-caption { font-size: 0.85em; font-style: italic; color: #a89984; text-align: center; padding: 4px 12px; background: #1d2021; border-top: 1px solid #504945; }",
        ".model-viewer canvas { display: block; width: 100%; height: 100%; }"
    ].join("\n");
    document.head.appendChild(st);
}

const cleaners = new Map();

function makeViewer(name) {
    var box = document.createElement("div");
    box.className = "model-viewer";
    var wrap = document.createElement("div");
    wrap.className = "model-viewer-canvas";
    var status = document.createElement("div");
    status.className = "model-viewer-status";
    status.textContent = "Loading 3D model\u2026";
    var progress = document.createElement("div");
    progress.className = "model-viewer-progress";
    var fill = document.createElement("div");
    fill.className = "model-viewer-progress-fill";
    progress.appendChild(fill);
    var data = document.createElement("div");
    data.className = "model-viewer-data";
    wrap.appendChild(status);
    wrap.appendChild(progress);
    box.appendChild(data);
    box.appendChild(wrap);
    var cap = document.createElement("div");
    cap.className = "model-viewer-caption";
    cap.textContent = name;
    box.appendChild(cap);
    return box;
}

function initViewer(el, url, ext) {
    var isStep = /\.(step|stp)$/i.test(ext);
    var fileSize = null;
    var raf = 0;
    var renderer = null;
    var resizeObs = null;

    function setStatus(text) {
        var s = el.querySelector(".model-viewer-status");
        if (s) s.textContent = text;
    }
    function setProgress(percent) {
        var bar = el.querySelector(".model-viewer-progress-fill");
        if (!bar) return;
        if (typeof percent === "number") {
            bar.classList.remove("indeterminate");
            bar.style.width = Math.round(percent * 100) + "%";
        } else {
            bar.classList.add("indeterminate");
            bar.style.width = "";
        }
    }
    function onError(msg) {
        var s = el.querySelector(".model-viewer-status");
        if (s) { s.textContent = msg || "Could not load 3D model."; s.classList.add("error"); }
        setProgress(0);
    }
    function showData(extra) {
        var d = el.querySelector(".model-viewer-data");
        if (d) {
            var parts = [];
            if (fileSize) parts.push(formatBytes(fileSize));
            if (extra) parts.push(extra);
            if (parts.length) { d.textContent = parts.join(" \u00b7 "); d.style.display = "block"; }
        }
        var cap = el.querySelector(".model-viewer-caption");
        if (cap && extra) {
            var stats = [];
            if (fileSize) stats.push(formatBytes(fileSize));
            stats.push(extra);
            var name = cap.getAttribute("data-model-name");
            if (!name) { name = (cap.textContent || "").trim(); cap.setAttribute("data-model-name", name); }
            cap.textContent = (name ? name + " \u00b7 " : "") + stats.join(" \u00b7 ");
        }
    }

    function loadThree(cb) {
        if (window.THREE && window.THREE.OrbitControls) { cb(); return; }
        loadScript(THREE_JS, function () {
            loadScript(ORBIT_JS, function () {
                if (isStep) { cb(); return; }
                if (window.THREE.STLLoader) { cb(); return; }
                loadScript(STL_JS, cb);
            });
        });
    }

    setProgress(null);
    setStatus("Loading 3D libraries\u2026");

    loadThree(function () {
        var wrap = el.querySelector(".model-viewer-canvas") || el;
        function canvasSize() {
            return { w: wrap.clientWidth || el.clientWidth || 400, h: wrap.clientHeight || 420 };
        }
        var initSize = canvasSize();
        renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(initSize.w, initSize.h);
        renderer.setClearColor(0x1d2021, 1);
        var scene = new THREE.Scene();
        var camera = new THREE.PerspectiveCamera(45, initSize.w / initSize.h, 0.1, 1000000);
        camera.position.set(300, 220, 320);
        scene.add(new THREE.AmbientLight(0xffffff, 0.55));
        var dir = new THREE.DirectionalLight(0xffffff, 1.0); dir.position.set(1, 1.5, 1); scene.add(dir);
        var back = new THREE.DirectionalLight(0xffffff, 0.35); back.position.set(-1, -0.5, -1); scene.add(back);
        var controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true;
        var group = new THREE.Group();
        scene.add(group);
        resizeObs = new ResizeObserver(function () {
            var s = canvasSize();
            if (!s.w || !s.h) return;
            renderer.setSize(s.w, s.h);
            camera.aspect = s.w / s.h;
            camera.updateProjectionMatrix();
        });
        resizeObs.observe(wrap);
        function animate() { raf = requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); }
        animate();
        function fit() {
            var box = new THREE.Box3().setFromObject(group);
            if (box.isEmpty()) return;
            var size = box.getSize(new THREE.Vector3()).length() || 1;
            var center = box.getCenter(new THREE.Vector3());
            var d = size * 2.4;
            camera.position.set(center.x + d * 0.9, center.y + d * 0.6, center.z + d);
            controls.target.copy(center);
            controls.update();
            camera.near = d * 0.05;
            camera.far = d * 10;
            camera.updateProjectionMatrix();
        }
        function showCanvas() {
            wrap.innerHTML = "";
            wrap.appendChild(renderer.domElement);
            var hint = document.createElement("div");
            hint.className = "model-viewer-hint";
            hint.textContent = "Drag to rotate \u00b7 scroll to zoom";
            wrap.appendChild(hint);
        }

        setStatus("Downloading model\u2026");
        fetchProgress(url, function (p) { setProgress(p); }).then(function (buf) {
            fileSize = buf.byteLength;
            showData();
            var data = new Uint8Array(buf);
            if (isStep) {
                setStatus("Downloading STEP parser\u2026");
                setProgress(null);
                fetchProgress(OCCT_WASM_ABS, function () {}).then(function (wasmBuf) {
                    setStatus("Parsing STEP\u2026");
                    setProgress(null);
                    var t0 = Date.now();
                    var parseTimer = setInterval(function () {
                        var el = Date.now() - t0;
                        setStatus("Parsing STEP\u2026 (" + Math.round(el / 1000) + "s)");
                        if (el > 240000 && !settled) { worker.terminate(); clearInterval(parseTimer); onError("STEP parse timed out."); }
                    }, 1000);
                    var worker = new Worker(WORKER_ABS, { type: "module" });
                    var settled = false;
                    worker.onmessage = function (e) {
                        if (e.data.type === "done") {
                            settled = true; worker.terminate(); clearInterval(parseTimer);
                            var geo = new THREE.BufferGeometry();
                            geo.setAttribute("position", new THREE.Float32BufferAttribute(e.data.positions, 3));
                            if (e.data.indices) geo.setIndex(new THREE.BufferAttribute(e.data.indices, 1));
                            fixWinding(geo);
                            var mat = new THREE.MeshStandardMaterial({ color: 0xb8bb26, metalness: 0.2, roughness: 0.6, side: THREE.DoubleSide, flatShading: true });
                            chunkGeometry(geo, group, mat);
                            var tris = (e.data.indices && e.data.indices.length) ? e.data.indices.length / 3 : e.data.positions.length / 9;
                            fit();
                            showCanvas();
                            showData(formatCount(tris) + " triangles");
                        } else if (e.data.type === "error") {
                            settled = true; worker.terminate(); clearInterval(parseTimer);
                            onError(e.data.message || "Could not parse STEP file.");
                        }
                    };
                    worker.onerror = function (e) {
                        if (!settled) { settled = true; worker.terminate(); clearInterval(parseTimer); onError("STEP worker error: " + (e && e.message || "unknown")); }
                    };
                    worker.postMessage({ type: "parse", data: data, params: STEP_PARAMS, wasm: new Uint8Array(wasmBuf) }, [data.buffer, wasmBuf]);
                }).catch(function () { onError("Could not download STEP parser."); });
            } else {
                var geo = new THREE.STLLoader().parse(data.buffer);
                geo.computeVertexNormals();
                var mat = new THREE.MeshStandardMaterial({ color: 0xb8bb26, metalness: 0.25, roughness: 0.55, flatShading: true });
                group.add(new THREE.Mesh(geo, mat));
                fit();
                showCanvas();
                showData(formatCount(geo.index ? geo.index.count / 3 : geo.attributes.position.count / 3) + " triangles");
            }
        }).catch(function () { onError("Could not load 3D model."); });
    });

    return function cleanup() {
        cancelAnimationFrame(raf);
        try { if (resizeObs) resizeObs.disconnect(); } catch (e) {}
        try { if (renderer) renderer.dispose(); } catch (e) {}
    };
}

function ModelViewer({ note }) {
    const ref = useRef(null);
    useEffect(() => {
        const el = ref.current;
        if (!el) return;
        injectStyle();
        if (cleaners.has(el)) { cleaners.get(el)(); cleaners.delete(el); }
        el.innerHTML = "";
        const m = (note.title || "").match(/\.(stl|step|stp)$/i);
        if (!m) return;
        const box = makeViewer(note.title.trim());
        el.appendChild(box);
        cleaners.set(el, initViewer(box, "api/notes/" + note.noteId + "/download", m[0]));
    }, [note.noteId]);
    return <div ref={ref} />;
}

export default defineWidget({
    parent: "note-detail-pane",
    position: 10,
    render: () => {
        const { note } = useNoteContext();
        const title = note ? note.title : null;
        if (!note || note.type !== "file" || !title || !/\.(stl|step|stp)$/i.test(title)) return null;
        return <ModelViewer note={note} />;
    }
});