import { Layers, Material, Matrix3, Matrix4, Object3D, PerspectiveCamera, Quaternion, Texture, Vector2, Vector3, Vector4 } from "three";
import ThreeMeshUI from "three-mesh-ui";
import { ContextRegistry } from "../engine_context_registry.js";
import { OneEuroFilterXYZ } from "../engine_math.js";
import { lookAtObject } from "../engine_three_utils.js";
import { isDevEnvironment } from "./debug.js";
import { onError } from "./debug_overlay.js";
let _isActive = false;
// enable the spatial console if we receive an error while in dev session and in XR
onError((...args) => {
    if (isDevEnvironment() && ContextRegistry.Current?.isInXR) {
        enableSpatialConsole(true);
        onLog("error", ...args);
    }
});
/** Enable a spatial debug console that follows the camera */
export function enableSpatialConsole(active) {
    if (active) {
        if (_isActive)
            return;
        _isActive = true;
        onEnable();
    }
    else {
        if (!_isActive)
            return;
        _isActive = false;
        onDisable();
    }
}
const originalConsoleMethods = {
    "log": undefined,
    "warn": undefined,
    "error": undefined,
};
class SpatialMessagesHandler {
    familyName = "needle-xr";
    root = null;
    context = null;
    defaultFontSize = .06;
    constructor() {
        this.ensureFont();
    }
    onEnable() {
        this.context = ContextRegistry.Current || ContextRegistry.All[0];
        this.context.pre_render_callbacks.push(this.onBeforeRender);
    }
    onDisable() {
        this.context?.pre_render_callbacks.splice(this.context?.pre_render_callbacks.indexOf(this.onBeforeRender), 1);
        this.root?.removeFromParent();
    }
    targetObject = new Object3D();
    /** this is a point in forward view of the user */
    userForwardViewPoint = new Vector3();
    oneEuroFilter = new OneEuroFilterXYZ(90, .8);
    _lastElementRemoveTime = 0;
    onBeforeRender = () => {
        const cam = this.context?.mainCamera;
        if (this.context && cam instanceof PerspectiveCamera) {
            const root = this.getRoot();
            // TODO: need to figure out why this happens when entering VR (in the simulator at least)
            if (Number.isNaN(root.position.x))
                root.position.set(0, 0, 0);
            if (Number.isNaN(root.quaternion.x))
                root.quaternion.set(0, 0, 0, 1);
            this.context.scene.add(this.targetObject);
            const rigScale = this.context.xr?.rigScale ?? 1;
            const dist = 3.5 * rigScale;
            const forward = cam.worldForward;
            forward.y = 0;
            forward.normalize().multiplyScalar(dist);
            this.userForwardViewPoint.copy(cam.worldPosition).sub(forward);
            const distFromForwardView = this.targetObject.position.distanceTo(this.userForwardViewPoint);
            if (distFromForwardView > 2 * rigScale) {
                this.targetObject.position.copy(this.userForwardViewPoint);
                lookAtObject(this.targetObject, cam, true, true);
                this.targetObject.rotateY(Math.PI);
            }
            this.oneEuroFilter.filter(this.targetObject.position, root.position, this.context.time.time);
            const step = this.context.time.deltaTime;
            root.quaternion.slerp(this.targetObject.quaternion, step * 5);
            root.scale.setScalar(rigScale);
            this.targetObject.removeFromParent();
            this.context.scene.add(root);
            if (this.context.time.time - this._lastElementRemoveTime > .1) {
                this._lastElementRemoveTime = this.context.time.time;
                const now = Date.now();
                for (let i = 0; i < this._activeTexts.length; i++) {
                    const el = this._activeTexts[i];
                    if (el instanceof ThreeMeshUI.Text && now - el["_activatedTime"] > 20000) {
                        el.removeFromParent();
                        this._textBuffer.push(el);
                        this._activeTexts.splice(i, 1);
                        break;
                    }
                }
            }
        }
    };
    addLog(type, message) {
        const root = this.getRoot();
        const text = this.getText();
        let backgroundColor = 0xffffff;
        let fontColor = 0x000000;
        switch (type) {
            case "log":
                backgroundColor = 0xffffff;
                fontColor = 0x000000;
                break;
            case "warn":
                backgroundColor = 0xffee99;
                fontColor = 0x442200;
                break;
            case "error":
                backgroundColor = 0xffaaaa;
                fontColor = 0x770000;
                break;
        }
        if (message.length > 1000)
            message = message.substring(0, 1000) + "...";
        const minuteSecondMilliSecond = new Date().toISOString().split("T")[1].split(".")[0];
        text.textContent = "[" + minuteSecondMilliSecond + "] " + message;
        text.visible = true;
        text["_activatedTime"] = Date.now();
        root.add(text);
        this._activeTexts.push(text);
        if (this.context)
            this.context.scene.add(root);
        text.set({
            backgroundColor: backgroundColor,
            color: fontColor,
        });
        ThreeMeshUI.update();
    }
    ensureFont() {
        let fontFamily = ThreeMeshUI.FontLibrary.getFontFamily(this.familyName);
        if (!fontFamily) {
            fontFamily = ThreeMeshUI.FontLibrary.addFontFamily(this.familyName);
            const variant = fontFamily.addVariant("normal", "normal", "https://cdn.needle.tools/static/fonts/msdf/arial/arial-msdf.json", "https://cdn.needle.tools/static/fonts/msdf/arial/arial.png");
            /** @ts-ignore */
            variant?.addEventListener('ready', () => {
                ThreeMeshUI.update();
            });
        }
    }
    textOptions = {
        fontSize: this.defaultFontSize,
        fontFamily: this.familyName,
        padding: .03,
        margin: .005,
        color: 0x000000,
        backgroundColor: 0xffffff,
        backgroundOpacity: .4,
        borderRadius: .03,
        offset: .025,
    };
    _textBuffer = [];
    _activeTexts = [];
    getText() {
        const root = this.getRoot();
        if (this._textBuffer.length > 0) {
            const text = this._textBuffer.pop();
            text.visible = true;
            setTimeout(() => this.disableDepthTestRecursive(text), 100);
            return text;
        }
        if (root.children.length > 20 && this._activeTexts.length > 0) {
            const active = this._activeTexts.shift();
            return active;
        }
        const newText = new ThreeMeshUI.Text(this.textOptions);
        setTimeout(() => this.disableDepthTestRecursive(newText), 500);
        setTimeout(() => this.disableDepthTestRecursive(newText), 1500);
        return newText;
    }
    disableDepthTestRecursive(obj, level = 0) {
        for (let i = 0; i < obj.children.length; i++) {
            const child = obj.children[i];
            if (child instanceof Object3D) {
                this.disableDepthTestRecursive(child, level + 1);
            }
        }
        obj.renderOrder = 10 * level;
        obj.layers.set(2);
        // obj.position.z = .01 * level;
        const mat = obj.material;
        if (mat) {
            mat.depthWrite = false;
            mat.depthTest = false;
            mat.transparent = true;
        }
        if (level === 0)
            ThreeMeshUI.update();
    }
    getRoot() {
        if (this.root) {
            return this.root;
        }
        const fontSize = this.defaultFontSize;
        const defaultOptions = {
            boxSizing: 'border-box',
            fontFamily: this.familyName,
            width: "2.6",
            fontSize: fontSize,
            color: 0x000000,
            lineHeight: 1,
            backgroundColor: 0xffffff,
            backgroundOpacity: 0,
            // borderColor: 0xffffff,
            // borderOpacity: .5,
            // borderWidth: 0.01,
            // padding: 0.01,
            whiteSpace: 'pre-wrap',
            flexDirection: 'column-reverse',
        };
        this.root = new ThreeMeshUI.Block(defaultOptions);
        return this.root;
    }
}
let messagesHandler = null;
function onEnable() {
    // create messages handler
    if (!messagesHandler)
        messagesHandler = new SpatialMessagesHandler();
    messagesHandler.onEnable();
    // save original console methods
    for (const key in originalConsoleMethods) {
        originalConsoleMethods[key] = console[key];
        let isLogging = false;
        console[key] = function () {
            // call original console method
            originalConsoleMethods[key]?.apply(console, arguments);
            // prevent circular calls
            if (isLogging)
                return;
            try {
                isLogging = true;
                onLog(key, ...arguments);
            }
            finally {
                isLogging = false;
            }
        };
    }
}
function onDisable() {
    messagesHandler?.onDisable();
    for (const key in originalConsoleMethods) {
        console[key] = originalConsoleMethods[key];
    }
}
const seen = new Map();
function onLog(key, ...args) {
    try {
        seen.clear();
        switch (key) {
            case "log":
                messagesHandler?.addLog("log", getLogString());
                break;
            case "warn":
                messagesHandler?.addLog("warn", getLogString());
                break;
            case "error":
                messagesHandler?.addLog("error", getLogString());
                break;
        }
    }
    catch (err) {
        console.error("Error in spatial console", err);
    }
    finally {
        seen.clear();
    }
    function getLogString() {
        let str = "";
        for (let i = 0; i < args.length; i++) {
            const el = args[i];
            str += serialize(el);
            if (i < args.length - 1)
                str += ", ";
        }
        return str;
    }
    function serialize(value, level = 0) {
        if (typeof value === "string") {
            return "\"" + value + "\"";
        }
        else if (typeof value === "number") {
            const hasDecimal = value % 1 !== 0;
            if (hasDecimal) {
                const str = value.toFixed(5);
                const dotIndex = str.indexOf(".");
                let i = str.length - 1;
                while (i > dotIndex && str[i] === "0")
                    i--;
                return str.substring(0, i + 1);
            }
            return value.toString();
        }
        else if (Array.isArray(value)) {
            let res = "[";
            for (let i = 0; i < value.length; i++) {
                const val = value[i];
                res += serialize(val, level + 1);
                if (i < value.length - 1)
                    res += ", ";
            }
            res += "]";
            return res;
        }
        else if (value === null) {
            return "null";
        }
        else if (value === undefined) {
            return "undefined";
        }
        else if (typeof value === "function") {
            return value.name + "()";
        }
        // if (value instanceof Object3D) {
        //     const name = value.name?.length > 0 ? value.name : ("object@" + (value["guid"] ?? value.uuid));;
        //     return name;
        // }
        if (value instanceof Vector2)
            return `(${serialize(value.x)}, ${serialize(value.y)})`;
        if (value instanceof Vector3)
            return `(${serialize(value.x)}, ${serialize(value.y)}, ${serialize(value.z)})`;
        if (value instanceof Vector4)
            return `(${serialize(value.x)}, ${serialize(value.y)}, ${serialize(value.z)}, ${serialize(value.w)})`;
        if (value instanceof Quaternion)
            return `(${serialize(value.x)}, ${serialize(value.y)}, ${serialize(value.z)}, ${serialize(value.w)})`;
        if (value instanceof Material)
            return value.name;
        if (value instanceof Texture)
            return value.name;
        if (value instanceof Matrix3)
            return `[${value.elements.join(", ")}]`;
        if (value instanceof Matrix4)
            return `[${value.elements.join(", ")}]`;
        if (value instanceof Layers)
            return value.mask.toString();
        if (typeof value === "object") {
            if (seen.has(value))
                return "*";
            let res = "{\n";
            res += pad(level);
            const keys = Object.keys(value);
            let line = "";
            for (let i = 0; i < keys.length; i++) {
                const key = keys[i];
                const val = value[key];
                if (seen.has(val)) {
                    line += "";
                    continue;
                }
                seen.set(val, true);
                line += key + ":" + serialize(val, level + 1);
                if (i < keys.length - 1)
                    line += ", ";
                if (line.length >= 60) {
                    line += "\n";
                    line += pad(level);
                    res += line;
                    line = "";
                }
            }
            res += line;
            res += "\n}";
            return res;
            // return JSON.stringify(value, (_key, value) => {
            //     if (seen.has(value)) return seen.get(value);
            //     seen.set(value, "-");
            //     const res = serialize(value);
            //     seen.set(value, res);
            //     return _key;
            // }, 1);
        }
        return value;
    }
    function pad(spaces) {
        let res = "";
        for (let i = 0; i < spaces; i++) {
            res += " ";
        }
        return res;
    }
}
// // this is just a hack - the spatial console should be enabled from the user or the NeedleXRSession
// if (getParam("debugwebxr") || getParam("console"))
//     setTimeout(() => enableSpatialConsole(true), 1000);
//# sourceMappingURL=debug_spatial_console.js.map