import { createLoaders } from "@needle-tools/gltf-progressive";
import { BoxGeometry, BufferGeometry, Color, CylinderGeometry, DoubleSide, ExtrudeGeometry, Group, Mesh, MeshBasicMaterial, MeshStandardMaterial, PlaneGeometry, Shape, SphereGeometry, Sprite, SpriteMaterial, Vector2 } from "three";
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js";
import { Font, FontLoader } from "three/examples/jsm/loaders/FontLoader.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
export var PrimitiveType;
(function (PrimitiveType) {
    /**
     * A quad with a width and height of 1 facing the positive Z axis
     */
    PrimitiveType[PrimitiveType["Quad"] = 0] = "Quad";
    /**
     * A cube with a width, height, and depth of 1
     */
    PrimitiveType[PrimitiveType["Cube"] = 1] = "Cube";
    PrimitiveType[PrimitiveType["Sphere"] = 2] = "Sphere";
    PrimitiveType[PrimitiveType["Cylinder"] = 3] = "Cylinder";
    PrimitiveType[PrimitiveType["RoundedCube"] = 10] = "RoundedCube";
})(PrimitiveType || (PrimitiveType = {}));
/**
 * Utility class to create primitive objects
 * @example
 * ```typescript
 * const cube = ObjectUtils.createPrimitive("Cube", { name: "Cube", position: { x: 0, y: 0, z: 0 } });
 * ```
 */
export class ObjectUtils {
    /**
     * Creates a 3D text object
     * @param text The text to display
     * @param opts Options to create the object
     */
    static createText(text, opts) {
        let geometry = null;
        const font = opts?.font || loadFont(opts?.familyFamily || null);
        if (font instanceof Font) {
            geometry = this.#createTextGeometry(text, font, opts);
        }
        else if (geometry == null) {
            geometry = new BufferGeometry();
        }
        const color = opts?.color || 0xffffff;
        const mesh = new Mesh(geometry, opts?.material ?? new MeshStandardMaterial({ color: color }));
        this.applyDefaultObjectOptions(mesh, opts);
        if (font instanceof Promise) {
            font.then(res => {
                mesh.geometry = this.#createTextGeometry(text, res, opts);
                if (opts?.onGeometry)
                    opts.onGeometry(mesh);
            });
        }
        else {
            if (opts?.onGeometry)
                opts.onGeometry(mesh);
        }
        return mesh;
    }
    static #createTextGeometry(text, font, opts) {
        const depth = opts?.depth || .1;
        const geo = new TextGeometry(text, {
            font,
            size: 1,
            depth: depth,
            height: depth,
            bevelEnabled: opts?.bevel || false,
            bevelThickness: .01,
            bevelOffset: .01,
            bevelSize: .01,
        });
        return geo;
    }
    /**
     * Creates an occluder object that only render depth but not color
     * @param type The type of primitive to create
     * @returns The created object
     */
    static createOccluder(type) {
        const occluderMaterial = new MeshBasicMaterial({ colorWrite: false, depthWrite: true, side: DoubleSide });
        return this.createPrimitive(type, { material: occluderMaterial });
    }
    static createPrimitive(type, opts) {
        let obj;
        const color = opts?.color || 0xffffff;
        switch (type) {
            case "Quad":
            case PrimitiveType.Quad:
                {
                    const quadGeo = new PlaneGeometry(1, 1, 1, 1);
                    const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
                    if (opts?.texture && "map" in mat)
                        mat.map = opts.texture;
                    obj = new Mesh(quadGeo, mat);
                    obj.name = "Quad";
                }
                break;
            case "Cube":
            case PrimitiveType.Cube:
                {
                    const boxGeo = new BoxGeometry(1, 1, 1);
                    const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
                    if (opts?.texture && "map" in mat)
                        mat.map = opts.texture;
                    obj = new Mesh(boxGeo, mat);
                    obj.name = "Cube";
                }
                break;
            case PrimitiveType.RoundedCube:
            case "RoundedCube":
                {
                    const boxGeo = createBoxWithRoundedEdges(1, 1, 1, .1, 2);
                    const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
                    if (opts?.texture && "map" in mat)
                        mat.map = opts.texture;
                    obj = new Mesh(boxGeo, mat);
                    obj.name = "RoundedCube";
                }
                break;
            case "Sphere":
            case PrimitiveType.Sphere:
                {
                    const sphereGeo = new SphereGeometry(.5, 16, 16);
                    const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
                    if (opts?.texture && "map" in mat)
                        mat.map = opts.texture;
                    obj = new Mesh(sphereGeo, mat);
                    obj.name = "Sphere";
                }
                break;
            case "Cylinder":
            case PrimitiveType.Cylinder:
                {
                    const geo = new CylinderGeometry(.5, .5, 1, 32);
                    const mat = opts?.material ?? new MeshStandardMaterial({ color: color });
                    if (opts?.texture && "map" in mat)
                        mat.map = opts.texture;
                    obj = new Mesh(geo, mat);
                    obj.name = "Cylinder";
                }
                break;
            case "ShaderBall":
                {
                    obj = new Group();
                    obj.name = "ShaderBall";
                    loadShaderball(obj, opts);
                }
                break;
        }
        this.applyDefaultObjectOptions(obj, opts);
        return obj;
    }
    /**
     * Creates a Sprite object
     * @param opts Options to create the object
     * @returns The created object
     */
    static createSprite(opts) {
        const color = 0xffffff;
        const mat = new SpriteMaterial({ color: color });
        if (opts?.texture && "map" in mat)
            mat.map = opts.texture;
        const sprite = new Sprite(mat);
        this.applyDefaultObjectOptions(sprite, opts);
        return sprite;
    }
    static applyDefaultObjectOptions(obj, opts) {
        obj.receiveShadow = true;
        obj.castShadow = true;
        if (opts?.name)
            obj.name = opts.name;
        if (opts?.position) {
            if (Array.isArray(opts.position))
                obj.position.set(opts.position[0], opts.position[1], opts.position[2]);
            else {
                obj.position.set(opts.position.x || 0, opts.position.y || 0, opts.position.z || 0);
            }
        }
        if (opts?.rotation) {
            if (Array.isArray(opts.rotation))
                obj.rotation.set(opts.rotation[0], opts.rotation[1], opts.rotation[2]);
            else
                obj.rotation.set(opts.rotation.x || 0, opts.rotation.y || 0, opts.rotation.z || 0);
        }
        if (opts?.scale) {
            if (typeof opts.scale === "number")
                obj.scale.set(opts.scale, opts.scale, opts.scale);
            else if (Array.isArray(opts.scale)) {
                obj.scale.set(opts.scale[0], opts.scale[1], opts.scale[2]);
            }
            else {
                obj.scale.set(opts.scale.x || 1, opts.scale.y || 1, opts.scale.z || 1);
            }
        }
        if (opts?.receiveShadow != undefined) {
            obj.receiveShadow = opts.receiveShadow;
        }
        if (opts?.castShadow != undefined) {
            obj.castShadow = opts.castShadow;
        }
        if (opts?.parent) {
            opts.parent.add(obj);
        }
    }
}
function createBoxWithRoundedEdges(width, height, _depth, radius0, smoothness) {
    const shape = new Shape();
    const eps = 0.00001;
    const radius = radius0 - eps;
    shape.absarc(eps, eps, eps, -Math.PI / 2, -Math.PI, true);
    shape.absarc(eps, height - radius * 2, eps, Math.PI, Math.PI / 2, true);
    shape.absarc(width - radius * 2, height - radius * 2, eps, Math.PI / 2, 0, true);
    shape.absarc(width - radius * 2, eps, eps, 0, -Math.PI / 2, true);
    const geometry = new ExtrudeGeometry(shape, {
        bevelEnabled: true,
        bevelSegments: smoothness * 2,
        steps: 1,
        bevelSize: radius,
        bevelThickness: radius0,
        curveSegments: smoothness,
        UVGenerator: {
            generateTopUV: (_, vertices) => {
                const uvs = [];
                for (let i = 0; i < vertices.length; i += 3) {
                    uvs.push(new Vector2(vertices[i] / width, vertices[i + 1] / height));
                }
                return uvs;
            },
            generateSideWallUV: (_, vertices, indexA, indexB, indexC, indexD) => {
                const uvs = [];
                uvs.push(new Vector2(vertices[indexA] / width, vertices[indexA + 1] / height));
                uvs.push(new Vector2(vertices[indexB] / width, vertices[indexB + 1] / height));
                uvs.push(new Vector2(vertices[indexC] / width, vertices[indexC + 1] / height));
                uvs.push(new Vector2(vertices[indexD] / width, vertices[indexD + 1] / height));
                return uvs;
            }
        },
    });
    geometry.scale(1, 1, 1 - radius0);
    geometry.center();
    // Ensure we have an index buffer
    if (!geometry.index)
        geometry.setIndex(Array.from({ length: geometry.attributes.position.count }, (_, i) => i));
    geometry.computeVertexNormals();
    return geometry;
}
const fontsDict = new Map();
function loadFont(family) {
    let url = "";
    switch (family) {
        default:
        case "OpenSans":
            url = "https://cdn.needle.tools/static/fonts/facetype/Open Sans_Regular_ascii.json";
            break;
        case "Helvetiker":
            url = "https://cdn.needle.tools/static/fonts/facetype/Helvetiker_Regular_ascii.json";
            break;
    }
    if (fontsDict.has(url)) {
        const res = fontsDict.get(url);
        if (res)
            return res;
    }
    const loader = new FontLoader();
    const promise = new Promise((resolve, reject) => {
        loader.load(url, res => {
            fontsDict.set(url, res);
            resolve(res);
        }, undefined, reject);
    });
    fontsDict.set(url, promise);
    return promise;
}
let __shaderballIsLoading = false;
let __shaderballPromise = null;
/** Loads the shaderball mesh in the background and assigns it to the provided mesh */
function loadShaderball(group, opts) {
    // invoke the loading process ONCE
    if (__shaderballPromise === null) {
        // to make the autofit work we need to insert a placeholder the first time this is invoked
        const url = "https://cdn.needle.tools/static/models/shaderball.glb";
        const loader = new GLTFLoader();
        const loaders = createLoaders(null);
        loader.setDRACOLoader(loaders.dracoLoader);
        loader.setKTX2Loader(loaders.ktx2Loader);
        __shaderballIsLoading = true;
        __shaderballPromise = loader.loadAsync(url)
            .then(res => {
            const scene = res.scene;
            // the shaderball pivot is on the bottom and has a size of 1x1x1
            // to match the behaviour from all other object's we move it down by 0.5
            scene.position.y -= .5;
            return scene;
        })
            .catch(err => {
            // Handle case if the shaderball mesh fails to load or is unavailable for any reason
            console.warn("Failed to load shaderball mesh: " + err.message);
            return createShaderballPlaceholder();
        })
            .finally(() => {
            __shaderballIsLoading = false;
        });
    }
    if (__shaderballIsLoading) {
        // if the shaderball is still loading, insert a placeholder
        // this is mainly so that the autofit works correctly
        const placeholder = createShaderballPlaceholder();
        placeholder.name = "ShaderBall-Placeholder";
        const mesh = placeholder.children[0];
        if (mesh?.type === "Mesh")
            updateShaderballMaterial(mesh, opts);
        group.add(placeholder);
    }
    // and for every object that needs it, clone the loaded mesh
    __shaderballPromise.then(res => {
        // remove placeholders
        group.children.forEach(c => {
            if (c.name === "ShaderBall-Placeholder")
                group.remove(c);
        });
        // clone the loaded mesh
        const instance = res.clone();
        const mesh = instance.children[0];
        if (mesh?.type === "Mesh") {
            // Ensure we have tangents for the Shaderball mesh
            if (!mesh.geometry.attributes.tangent)
                mesh.geometry.computeTangents();
            updateShaderballMaterial(mesh, opts);
        }
        group.add(instance);
    });
}
function updateShaderballMaterial(mesh, opts) {
    const needCustomMaterial = opts?.color || opts?.material || opts?.texture;
    if (needCustomMaterial) {
        const mat = opts?.material ?? mesh.material?.clone() ?? new MeshStandardMaterial();
        if (opts.color && "color" in mat && mat.color instanceof Color)
            mat.color.set(opts.color);
        if (opts?.texture && "map" in mat)
            mat.map = opts.texture;
        mesh.material = mat;
    }
}
function createShaderballPlaceholder() {
    return new Group().add(ObjectUtils.createPrimitive("Sphere", { material: new MeshBasicMaterial({ transparent: true, opacity: .1 }) }));
}
//# sourceMappingURL=engine_create_objects.js.map