import type { SceneData } from "needle-bindings";
export type { SceneData };
import { isDevEnvironment } from "./debug/index.js";
import { getComponent } from "./engine_components.js";
import { ContextRegistry } from "./engine_context_registry.js";
import type { IContext } from "./engine_types.js";
import { TypeStore } from "./engine_typestore.js";

/**
 * Quick access to the current Needle Engine context from anywhere — no need to pass `ctx` around.
 * Use it in React/Svelte/Vue components, button handlers, or plain JavaScript.
 *
 * Safe to import at module level, including in SSR environments.
 * For pages with multiple `<needle-engine>` elements, use `ctx` directly instead.
 *
 * @experimental This API may change in future releases.
 *
 * @example
 * import { needle } from "@needle-tools/engine";
 * button.onclick = () => {
 *   needle.sceneData.Camera.OrbitControls.autoRotate = true;
 * };
 */
export const needle: IContext = new Proxy({} as IContext, {
    get(_target, prop: string) {
        if (prop === "then") return undefined; // not a Promise
        const ctx = ContextRegistry.Current;
        if (!ctx) {
            const fn = isDevEnvironment() ? console.error : console.warn;
            fn(`[needle] needle.${prop} was accessed before the scene started. Use "needle" inside event handlers or callbacks, not at module top-level. For setup code use: onStart(ctx => { ... })`);
            return makeErrorProxy(`needle not ready — scene hasn't started yet`);
        }
        const val = (ctx as any)[prop];
        return typeof val === "function" ? val.bind(ctx) : val;
    },
    set(_target, prop: string, value: unknown) {
        const ctx = ContextRegistry.Current;
        if (!ctx) {
            const fn = isDevEnvironment() ? console.error : console.warn;
            fn(`[needle] needle.${prop} was set before the scene started. Use "needle" inside event handlers or callbacks, not at module top-level. For setup code use: onStart(ctx => { ... })`);
            return true;
        }
        (ctx as any)[prop] = value;
        return true;
    },
});

const cache = new WeakMap<IContext, SceneData>();

/**
 * Returns a proxy that silently absorbs any property get/set and logs a
 * developer-friendly warning. Used when a node or component lookup fails so
 * that chained access like `ctx.sceneData.Foo.Bar.baz = 1` never throws.
 */
function makeErrorProxy(message: string): object {
    const handler: ProxyHandler<object> = {
        get(_t, prop: string) {
            if (prop === "then") return undefined; // not a Promise
            const fn = isDevEnvironment() ? console.error : console.warn;
            fn(`[SceneData] ${message}`);
            return makeErrorProxy(message);
        },
        set(_t, prop: string) {
            const fn = isDevEnvironment() ? console.error : console.warn;
            fn(`[SceneData] ${message} (tried to set "${prop}")`);
            return true; // suppress TypeError
        },
    };
    return new Proxy({}, handler);
}

/**
 * Returns a proxy for a scene node that exposes `$object`, `$components`,
 * and child nodes as nested properties.
 */
function makeNodeProxy(ctx: IContext, node: import("three").Object3D): object {
    return new Proxy({}, {
        get(_t, prop: string) {
            if (prop === "$object") return node;
            if (prop === "$components") {
                return new Proxy({}, {
                    get(_t2, compName: string) {
                        if (compName === "then") return undefined;
                        const ctor = TypeStore.get(compName);
                        if (!ctor) return makeErrorProxy(`Component type "${compName}" not registered (node "${node.name}")`);
                        const comp = getComponent(node, ctor);
                        if (!comp) return makeErrorProxy(`Component "${compName}" not found on node "${node.name}"`);
                        return comp;
                    }
                });
            }
            if (prop === "then") return undefined; // not a Promise
            // Child node lookup by name
            const child = node.children.find(c => c.name === prop) ?? null;
            if (!child) {
                const fn = isDevEnvironment() ? console.error : console.warn;
                fn(`[SceneData] "${prop}" is not a child of "${node.name}". Use .$object to get the Three.js object or .$components.Name to access a component.`);
                return makeErrorProxy(`"${prop}" not found on node "${node.name}"`);
            }
            return makeNodeProxy(ctx, child);
        }
    });
}

/**
 * Returns a lazily-resolved proxy typed as {@link SceneData}.
 * The proxy is cached per context — each context gets exactly one instance.
 *
 * Shape mirrors the generated `needle-bindings.gen.d.ts`:
 *   ctx.sceneData.MyGlb.Camera.$components.OrbitControls.autoRotate = true;
 *   ctx.sceneData.MyGlb.Camera.$object  // → THREE.Camera
 *   ctx.sceneData.MyGlb.UI.Button.$components.Button  // → Needle Button component
 *
 * GLB name is ignored at runtime (scene is already loaded).
 * Node lookup starts at the scene root.
 */
export function getSceneData(ctx: IContext): SceneData {
    let proxy = cache.get(ctx);
    if (!proxy) {
        proxy = new Proxy({} as SceneData, {
            get(_target, _glbName: string) {
                // GLB name level — ignored at runtime, return node-name proxy
                return new Proxy({}, {
                    get(_target, nodeName: string) {
                        if (nodeName === "then") return undefined;
                        const node = ctx.scene.getObjectByName(nodeName) ?? null;
                        if (!node) return makeErrorProxy(`Node "${nodeName}" not found in scene`);
                        return makeNodeProxy(ctx, node);
                    }
                });
            }
        });
        cache.set(ctx, proxy);
    }
    return proxy;
}
