/// <reference path="./types/global.d.ts" />
import { Emitter } from '@wonderlandengine/api';
import { mat4, quat, vec3 } from 'gl-matrix';
import { ARProvider, TrackingType, } from '@wonderlandengine/ar-tracking';
import { loadZappar, setOptions as zapparSetOptions } from './zappar-module.js';
import { WorldTracking_Zappar } from './world-tracking-mode-zappar.js';
import { FaceTracking_Zappar } from './face-tracking-mode-zappar.js';
import { ImageTracking_Zappar } from './image-tracking-mode-zappar.js';
/**
 * ARProvider implementation backed by the Zappar Universal AR JavaScript SDK.
 */
export class ZapparProvider extends ARProvider {
    static _cvWorkerConfigured = false;
    static _cvWorker = null;
    /** Mirror Zappar THREE.js `CameraPoseMode` for SLAM pose output. */
    slamPoseMode = 'anchor-origin';
    _xrSession = null;
    _gl = null;
    _zappar = null;
    pipeline = null;
    cameraSource = null;
    instantTracker = null;
    _faceTracker = null;
    _faceMesh = null;
    _faceResourcesPromise = null;
    _imageTracker = null;
    _preferredCameraUserFacing = null;
    _cameraSourceUserFacing = null;
    _imageTargetDescriptors = [];
    _imageTargetsChanged = new Emitter();
    _worldTracker = null;
    /** Set to `true` when a component requests world-tracker before the session starts. */
    _worldTrackerRequested = false;
    cameraStarted = false;
    hasInitializedAnchor = false;
    _anchorWarmupFramesRemaining = 0;
    /**
     * When `true`, {@link updateTracking} keeps calling
     * `setAnchorPoseFromCameraOffset` every frame so the world anchor
     * continuously follows the camera centre. Set via
     * {@link startUserPlacement}; cleared by {@link placeInstantAnchor}.
     */
    _userPlacementMode = false;
    preRenderRegistered = false;
    _slamStateValid = false;
    _slamProjectionMatrix = new Float32Array(16);
    _slamAnchorMatrix = mat4.create();
    _slamFrameNumber = 0;
    _videoTextureUnit = null;
    _videoTextureProgram = null;
    _videoTextureUniform = null;
    _videoTextureTransformUniform = null;
    _videoTextureBindErrorLogged = false;
    _missingCameraTextureFrames = 0;
    _missingCameraTextureWarningLogged = false;
    _slamCameraMatrix = mat4.create();
    _slamCameraPosition = vec3.create();
    _slamCameraRotation = quat.create();
    _debugCameraPosition = vec3.create();
    _debugAnchorPosition = vec3.create();
    _debugCameraPositionDelta = vec3.create();
    _debugAnchorPositionDelta = vec3.create();
    _debugLastSampleFrameNumber = null;
    _debugLastSampleCameraPosition = vec3.create();
    _debugLastSampleAnchorPosition = vec3.create();
    _zapparDebugLogIntervalId = null;
    _preRenderErrorLogged = false;
    get isVerboseDebugLoggingEnabled() {
        if (typeof window === 'undefined') {
            return false;
        }
        const debugWindow = window;
        return (debugWindow.__WLE_ZAPPAR_DEBUG__ === true ||
            debugWindow.__ZAPPAR_WORKER_DEBUG__ === true);
    }
    static Name = 'Zappar';
    get name() {
        return ZapparProvider.Name;
    }
    get supportsInstantTracking() {
        return true;
    }
    get onImageTargetsChanged() {
        return this._imageTargetsChanged;
    }
    get xrSession() {
        return this._xrSession;
    }
    /**
     * Hint camera facing preference for the next/active session.
     * - `true`: user/front camera
     * - `false`: rear/back camera
     * - `null`: Zappar default
     */
    setPreferredCameraUserFacing(userFacing) {
        this._preferredCameraUserFacing = userFacing;
    }
    static registerTrackingProviderWithARSession(arSession) {
        const provider = new ZapparProvider(arSession.engine);
        arSession.registerTrackingProvider(provider);
        return provider;
    }
    constructor(engine) {
        super(engine);
        if (typeof document === 'undefined') {
            return;
        }
        engine.onXRSessionStart.add((session) => {
            this._xrSession = session;
        });
        engine.onXRSessionEnd.add(() => {
            this._xrSession = null;
        });
    }
    async startSession() {
        this._slamStateValid = false;
        this._missingCameraTextureFrames = 0;
        this._missingCameraTextureWarningLogged = false;
        // Warm-up for the InstantWorldTracker anchor (~2 seconds at 60fps).
        this._anchorWarmupFramesRemaining = 120;
        await this.ensureZapparLoaded();
        await this._zappar.loadedPromise();
        this.ensurePipeline();
        this.ensureCameraSourcePreference();
        await this.ensureCameraRunning();
        this.ensurePreRenderRegistered();
        this.startZapparDebugLogging();
        if (this._faceTracker) {
            this._faceTracker.enabled = true;
        }
        if (this._imageTracker) {
            this._imageTracker.enabled = true;
        }
        // For instant tracking providers, we should emit session start here.
        // (Unlike WebXR, there is no XRSessionStart event.)
        this.onSessionStart.notify(this);
        if (this.instantTracker) {
            this.instantTracker.enabled = true;
        }
        // Create the WorldTracker if it was requested before the session started.
        if (this._worldTrackerRequested && !this._worldTracker) {
            this._createWorldTracker();
            this._worldTrackerRequested = false;
        }
        else if (this._worldTracker) {
            // Re-enable a pre-existing WorldTracker from a previous session.
            this._worldTracker.enabled = true;
        }
    }
    ensurePreRenderRegistered() {
        if (!this.preRenderRegistered) {
            this.engine.scene.onPreRender.add(this.onPreRender);
            this.preRenderRegistered = true;
        }
    }
    startZapparDebugLogging() {
        if (this._zapparDebugLogIntervalId !== null)
            return;
        if (!this.isVerboseDebugLoggingEnabled) {
            return;
        }
        const canAccessWindow = typeof window !== 'undefined' &&
            typeof window.setInterval ===
                'function';
        if (canAccessWindow) {
            this._zapparDebugLogIntervalId = window.setInterval(() => {
                const debug = window.ZapparDebug;
                // Keep this intentionally simple: user asked to log ZapparDebug.
                console.log('[ZapparDebug]', debug ?? null);
            }, 2000);
            return;
        }
        // WL Editor / non-browser fallback
        const canAccessGlobalIntervals = typeof globalThis.setInterval === 'function';
        if (canAccessGlobalIntervals) {
            const globalAny = globalThis;
            this._zapparDebugLogIntervalId = globalAny.setInterval(() => {
                console.log('[ZapparDebug]', globalAny.ZapparDebug ?? null);
            }, 2000);
        }
    }
    stopZapparDebugLogging() {
        if (this._zapparDebugLogIntervalId === null)
            return;
        const id = this._zapparDebugLogIntervalId;
        this._zapparDebugLogIntervalId = null;
        if (typeof window !== 'undefined' && typeof window.clearInterval === 'function') {
            window.clearInterval(id);
            return;
        }
        if (typeof globalThis.clearInterval === 'function') {
            globalThis.clearInterval(id);
        }
    }
    async ensureZapparLoaded() {
        if (this._zappar)
            return;
        // Must be called before Zappar initializes, otherwise the CV worker option
        // may be ignored and Zappar CV will fall back to requesting `./worker`.
        await this.configureCvWorkerIfNeeded();
        this._zappar = await loadZappar();
    }
    /** Used by tracking modes that need the Zappar namespace. */
    async ensureZapparNamespace() {
        await this.ensureZapparLoaded();
        return this._zappar;
    }
    async configureCvWorkerIfNeeded() {
        if (ZapparProvider._cvWorkerConfigured)
            return;
        // Hard-coded paths for local development
        const workerUrl = './zappar-cv.worker.js';
        const wasmUrl = './zappar-cv.wasm';
        const debugWorker = this.isVerboseDebugLoggingEnabled;
        if (debugWorker) {
            console.log('[ZapparProvider] configureCvWorkerIfNeeded()', {
                workerUrl,
                wasmUrl,
                hasWorkerGlobal: typeof Worker !== 'undefined',
            });
        }
        if (typeof Worker === 'undefined')
            return;
        try {
            if (debugWorker) {
                console.log('[ZapparProvider] Creating CV worker:', workerUrl);
            }
            const worker = new Worker(workerUrl, { type: 'module' });
            // Keep a reference for debugging / inspection.
            ZapparProvider._cvWorker = worker;
            if (typeof window !== 'undefined') {
                window.__ZapparCvWorker =
                    worker;
            }
            // Diagnostics: if the worker fails to load/respond, the pipeline can get stuck with
            // `frameNumber() === 0` and no camera texture.
            const resolvedWorkerUrl = typeof window !== 'undefined'
                ? new URL(workerUrl, window.location.href).toString()
                : workerUrl;
            const resolvedWasmUrl = typeof window !== 'undefined'
                ? new URL(wasmUrl, window.location.href).toString()
                : wasmUrl;
            if (debugWorker) {
                console.log('[ZapparProvider] CV worker created', {
                    workerUrl,
                    resolvedWorkerUrl,
                    wasmUrl,
                    resolvedWasmUrl,
                });
            }
            let workerMessagesSeen = 0;
            worker.addEventListener('message', (event) => {
                workerMessagesSeen++;
                if (workerMessagesSeen === 1 && debugWorker) {
                    console.log('[ZapparProvider] CV worker first message received', event.data);
                    return;
                }
                // Keep a couple more messages for context, but avoid spamming.
                if (debugWorker && workerMessagesSeen <= 5) {
                    console.log('[ZapparProvider] CV worker message', {
                        index: workerMessagesSeen,
                        data: event.data,
                    });
                }
            });
            worker.addEventListener('error', (event) => {
                console.warn('[ZapparProvider] CV worker error', event);
            });
            worker.addEventListener('messageerror', (event) => {
                console.warn('[ZapparProvider] CV worker messageerror', event);
            });
            // If the worker never sends anything (not even the initial "loaded"), call it out.
            // This usually means the worker script or wasm URL is 404, blocked, or CSP/COEP issues.
            if (typeof window !== 'undefined' && typeof window.setTimeout === 'function') {
                window.setTimeout(() => {
                    if (workerMessagesSeen > 0)
                        return;
                    console.warn('[ZapparProvider] CV worker produced no messages (timeout)', {
                        workerUrl,
                        resolvedWorkerUrl,
                        wasmUrl,
                        resolvedWasmUrl,
                    });
                }, 5000);
            }
            // Register the worker with Zappar BEFORE any async work so that
            // setOptions is called prior to Zappar JS initialization.
            await zapparSetOptions({ worker });
            if (wasmUrl) {
                const resolvedWasmUrl = new URL(wasmUrl, window.location.href).toString();
                if (debugWorker) {
                    console.log('[ZapparProvider] Compiling WASM for worker:', resolvedWasmUrl);
                }
                // The worker's launchHandler expects a pre-compiled WebAssembly.Module.
                // Compile async and post without blocking Zappar initialization.
                WebAssembly.compileStreaming(fetch(resolvedWasmUrl))
                    .then((compiledModule) => {
                    worker.postMessage({
                        t: 'wasm',
                        url: resolvedWasmUrl,
                        module: compiledModule,
                    });
                })
                    .catch((e) => {
                    console.warn('[ZapparProvider] Failed to compile WASM for worker:', e);
                });
            }
            ZapparProvider._cvWorkerConfigured = true;
            if (debugWorker) {
                console.log('[ZapparProvider] CV worker configured');
            }
        }
        catch (e) {
            console.warn('[ZapparProvider] Failed to create CV worker:', e);
        }
    }
    ensurePipeline() {
        if (this.pipeline) {
            return;
        }
        if (!this._zappar) {
            throw new Error('Zappar is not loaded yet. Call startSession() first.');
        }
        const Zappar = this._zappar;
        const gl = this.engine.canvas.getContext('webgl2');
        if (!gl) {
            throw new Error('Zappar requires a WebGL2 context.');
        }
        this._gl = gl;
        const pipeline = new Zappar.Pipeline();
        pipeline.glContextSet(gl);
        this.pipeline = pipeline;
        if (typeof window !== 'undefined') {
            window.ZapparPipeline = pipeline;
        }
        else if (typeof WL_EDITOR !== 'undefined' && WL_EDITOR) {
            globalThis.ZapparPipeline = pipeline;
        }
        if (typeof window !== 'undefined') {
            if (this.isVerboseDebugLoggingEnabled) {
                console.log('[ZapparProvider] Using device camera source');
            }
        }
        this.ensureCameraSourcePreference();
        this.instantTracker = new Zappar.InstantWorldTracker(pipeline);
    }
    ensureCameraSourcePreference() {
        if (!this.pipeline || !this._zappar) {
            return;
        }
        const desiredUserFacing = this._preferredCameraUserFacing;
        if (this.cameraSource && this._cameraSourceUserFacing === desiredUserFacing) {
            return;
        }
        const deviceId = desiredUserFacing === null
            ? this._zappar.cameraDefaultDeviceID()
            : this._zappar.cameraDefaultDeviceID(desiredUserFacing);
        this.cameraSource = new this._zappar.CameraSource(this.pipeline, deviceId);
        this._cameraSourceUserFacing = desiredUserFacing;
    }
    getPipeline() {
        if (!this.pipeline) {
            throw new Error('Zappar pipeline not initialized. Call startSession() first.');
        }
        return this.pipeline;
    }
    async ensureFaceResources() {
        if (this._faceResourcesPromise) {
            await this._faceResourcesPromise;
            return;
        }
        await this.ensureZapparLoaded();
        const Zappar = this._zappar;
        await Zappar.loadedPromise();
        this.ensurePipeline();
        this._faceResourcesPromise = (async () => {
            const faceTracker = new Zappar.FaceTracker(this.pipeline);
            // Zappar's default loaders fetch model files relative to `import.meta.url` of the
            // module that contains them. After bundling, that usually points at the app bundle
            // in the deploy root, which causes 404s.
            //
            // We stage these assets into `static/zappar-cv/` via this package's postinstall.
            // Load explicitly from there, and fall back to the SDK defaults for flexibility.
            try {
                await faceTracker.loadModel('./zappar-cv/face_tracking_model.zbin');
            }
            catch (e) {
                console.warn('[ZapparProvider] Failed to load face tracking model from ./zappar-cv; falling back to Zappar defaults.', e);
                await faceTracker.loadDefaultModel();
            }
            const faceMesh = new Zappar.FaceMesh();
            try {
                await faceMesh.load('./zappar-cv/face_mesh_face_model.zbin', true, true, true, false);
            }
            catch (e) {
                console.warn('[ZapparProvider] Failed to load face mesh model from ./zappar-cv; falling back to Zappar defaults.', e);
                await faceMesh.loadDefaultFace(true, true, true);
            }
            this._faceTracker = faceTracker;
            this._faceMesh = faceMesh;
        })();
        await this._faceResourcesPromise;
    }
    getFaceTracker() {
        if (!this._faceTracker) {
            throw new Error('Face tracker not initialized. Call ensureFaceResources() first.');
        }
        this._faceTracker.enabled = true;
        return this._faceTracker;
    }
    getFaceMesh() {
        if (!this._faceMesh) {
            throw new Error('Face mesh not initialized. Call ensureFaceResources() first.');
        }
        return this._faceMesh;
    }
    ensureImageTracker() {
        this.ensurePipeline();
        if (!this._zappar) {
            throw new Error('Zappar is not loaded yet. Call startSession() first.');
        }
        const Zappar = this._zappar;
        if (!this._imageTracker) {
            this._imageTracker = new Zappar.ImageTracker(this.pipeline);
        }
        this._imageTracker.enabled = true;
        return this._imageTracker;
    }
    async registerImageTarget(source, options) {
        if (!options.name) {
            throw new Error('Image target registration requires a name.');
        }
        // Deduplicate: if a descriptor with the same name is already registered,
        // skip loading to avoid double-loading the same .zpt file when multiple
        // scene components share the same imageId target.
        if (this._imageTargetDescriptors.some((d) => d.name === options.name)) {
            return;
        }
        // Allow registering targets before an AR session starts.
        // This is useful so image targets are known for ImageScanningEvent
        // and so apps can prefetch/prepare targets without requesting camera access.
        await this.ensureZapparLoaded();
        await this._zappar.loadedPromise();
        this.ensurePipeline();
        const Zappar = this._zappar;
        if (!this._imageTracker) {
            this._imageTracker = new Zappar.ImageTracker(this.pipeline);
        }
        this._imageTracker.enabled = true;
        await this._imageTracker.loadTarget(source);
        const targets = this._imageTracker.targets;
        const targetIndex = targets.length - 1;
        const target = targets[targetIndex];
        const inferredType = options.type ?? this._inferImageTargetType(target);
        const geometry = this._buildImageTargetGeometry(target, options.physicalWidthInMeters);
        const descriptor = {
            name: options.name,
            type: inferredType,
            geometry,
            properties: null,
            metadata: options.metadata ?? null,
        };
        this._imageTargetDescriptors[targetIndex] = descriptor;
        this._imageTargetsChanged.notify();
    }
    _inferImageTargetType(target) {
        if (target.topRadius !== undefined || target.bottomRadius !== undefined) {
            if (target.topRadius !== undefined &&
                target.bottomRadius !== undefined &&
                Math.abs(target.topRadius - target.bottomRadius) > 1e-5) {
                return 'conical';
            }
            return 'cylindrical';
        }
        return 'flat';
    }
    _buildImageTargetGeometry(target, physicalWidth) {
        const geometry = {};
        const hasPhysicalScale = target.physicalScaleFactor !== undefined;
        const planarScale = hasPhysicalScale
            ? target.physicalScaleFactor
            : physicalWidth !== undefined
                ? physicalWidth
                : undefined;
        if (planarScale !== undefined) {
            geometry.scaleWidth = planarScale;
            geometry.scaledHeight = planarScale;
        }
        const radiusFactor = hasPhysicalScale ? (target.physicalScaleFactor ?? 1) : 1;
        if (target.topRadius !== undefined) {
            geometry.radiusTop = target.topRadius * radiusFactor;
        }
        if (target.bottomRadius !== undefined) {
            geometry.radiusBottom = target.bottomRadius * radiusFactor;
        }
        if (target.sideLength !== undefined) {
            geometry.height = target.sideLength * radiusFactor;
        }
        return geometry;
    }
    getImageScanningEvent() {
        return {
            imageTargets: this._imageTargetDescriptors.map((descriptor) => ({
                name: descriptor.name,
                type: descriptor.type,
                metadata: descriptor.metadata ?? null,
                geometry: descriptor.geometry,
                properties: descriptor.properties,
            })),
        };
    }
    getImageTargetDescriptors() {
        return [...this._imageTargetDescriptors];
    }
    getImageTargetDescriptor(index) {
        return this._imageTargetDescriptors[index];
    }
    // -----------------------------------------------------------------------
    // World tracking / plane anchors (Zappar SDK >= 4.x)
    // -----------------------------------------------------------------------
    /**
     * Access the active {@link ZapparWorldTracker} instance, or `null` when
     * plane tracking has not been requested or is unavailable.
     *
     * The instance is only non-null once {@link enableWorldTracker} has been
     * called **and** an AR session is running.
     */
    get worldTracker() {
        return this._worldTracker;
    }
    /**
     * Opt in to Zappar plane tracking via the `WorldTracker` API introduced in
     * `@zappar/zappar >= 4.x`.
     *
     * Call this once (e.g. from a component's `start()` hook) before or after
     * a session starts.  If the installed SDK does not expose `WorldTracker`
     * (i.e. < 4.x) a warning is logged and the method returns `null`.
     *
     * The tracker is disabled again when the session ends and re-enabled on
     * the next `startSession()` call.
     *
     * @returns The {@link ZapparWorldTracker} instance, or `null` on failure.
     */
    enableWorldTracker() {
        if (this._worldTracker) {
            this._worldTracker.enabled = true;
            return this._worldTracker;
        }
        if (this.pipeline && this._zappar) {
            return this._createWorldTracker();
        }
        // Session not yet started – remember the request.
        this._worldTrackerRequested = true;
        return null;
    }
    _createWorldTracker() {
        if (!this.pipeline || !this._zappar)
            return null;
        // WorldTracker is only available in @zappar/zappar >= 4.x.
        const ZapparAny = this._zappar;
        if (typeof ZapparAny['WorldTracker'] !== 'function') {
            console.warn('[ZapparProvider] WorldTracker not available in the installed' +
                ' version of @zappar/zappar. Upgrade to >= 4.x to enable plane tracking.');
            return null;
        }
        const TrackerCtor = ZapparAny['WorldTracker'];
        this._worldTracker = new TrackerCtor(this.pipeline);
        this._worldTracker.enabled = true;
        this._worldTracker.horizontalPlaneDetectionEnabled = true;
        return this._worldTracker;
    }
    async ensureCameraRunning() {
        if (!this.cameraSource || this.cameraStarted)
            return;
        await this.ensureZapparLoaded();
        const Zappar = this._zappar;
        await Zappar.loadedPromise();
        const granted = await Zappar.permissionRequestUI();
        if (!granted) {
            Zappar.permissionDeniedUI();
            return;
        }
        this.cameraSource.start();
        if (typeof document !== 'undefined') {
            document.addEventListener('visibilitychange', this.onVisibilityChange);
        }
        this.cameraStarted = true;
    }
    onVisibilityChange = () => {
        if (!this.cameraSource || typeof document === 'undefined')
            return;
        switch (document.visibilityState) {
            case 'hidden':
                this.cameraSource.pause();
                break;
            case 'visible':
                this.cameraSource.start();
                break;
        }
    };
    onPreRender = () => {
        if (!this.pipeline)
            return;
        const gl = this._gl;
        if (!gl) {
            this.pipeline.processGL();
            this.pipeline.cameraFrameUploadGL();
            this.pipeline.frameUpdate();
            return;
        }
        const previousPackBuffer = gl.getParameter(gl.PIXEL_PACK_BUFFER_BINDING);
        const previousUnpackBuffer = gl.getParameter(gl.PIXEL_UNPACK_BUFFER_BINDING);
        if (previousPackBuffer)
            gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
        if (previousUnpackBuffer)
            gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);
        try {
            // Keep GL update order consistent with the non-GL path:
            // process tracking input, upload camera frame texture, then update frame state.
            this.pipeline.processGL();
            this.pipeline.cameraFrameUploadGL();
            this.pipeline.frameUpdate();
            // Update SLAM state every frame (one loop in the provider).
            this.updateTracking();
            // Bind the Zappar camera texture for Wonderland's sky material.
            // Wonderland Engine will handle drawing; we only ensure that:
            // - `videoTexture` sampler uniform points to the last texture unit
            // - that texture unit is bound to the Zappar camera texture
            this.bindVideoTextureForSkyMaterial();
        }
        catch (error) {
            // Ensure exceptions aren't swallowed by the engine render loop.
            // Log once per session to avoid spamming every frame.
            if (!this._preRenderErrorLogged) {
                this._preRenderErrorLogged = true;
                console.error('[ZapparProvider] onPreRender pipeline error', error);
            }
            // Avoid consumers using stale matrices.
            this._slamStateValid = false;
        }
        finally {
            if (previousPackBuffer) {
                gl.bindBuffer(gl.PIXEL_PACK_BUFFER, previousPackBuffer);
            }
            if (previousUnpackBuffer) {
                gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, previousUnpackBuffer);
            }
        }
    };
    /** Latest projection matrix for the SLAM camera (valid only if {@link hasSlamTrackingState} is true). */
    get slamProjectionMatrix() {
        return this._slamStateValid ? this._slamProjectionMatrix : null;
    }
    /** Latest camera pose matrix for the SLAM camera (valid only if {@link hasSlamTrackingState} is true). */
    get slamCameraPoseMatrix() {
        return this._slamStateValid ? this._slamCameraMatrix : null;
    }
    /** Latest anchor pose matrix (valid only if {@link hasSlamTrackingState} is true). */
    get slamAnchorPoseMatrix() {
        return this._slamStateValid ? this._slamAnchorMatrix : null;
    }
    get hasSlamTrackingState() {
        return this._slamStateValid;
    }
    /**
     * `true` while the instant world anchor is being continuously repositioned
     * in front of the camera (either during warmup or during user-placement
     * mode). `false` once the anchor has been locked.
     */
    get isPlacingInstantAnchor() {
        return this._userPlacementMode || this._anchorWarmupFramesRemaining > 0;
    }
    /**
     * Enter user-placement mode: the instant world anchor tracks 5 m in front
     * of the camera every frame until {@link placeInstantAnchor} is called.
     *
     * Typically called by a placement-UI component immediately after session
     * start so the user can choose where to lock the world origin.
     */
    startUserPlacement() {
        this._userPlacementMode = true;
    }
    /**
     * Lock the instant world anchor at its current position and exit placement
     * mode. After this call {@link isPlacingInstantAnchor} returns `false` and
     * the anchor no longer follows the camera.
     */
    placeInstantAnchor() {
        this._userPlacementMode = false;
        this._anchorWarmupFramesRemaining = 0;
    }
    get slamFrameNumber() {
        return this._slamFrameNumber;
    }
    bindVideoTextureForSkyMaterial() {
        try {
            const pipeline = this.pipeline;
            const gl = this._gl;
            if (!pipeline || !gl)
                return;
            const cameraTexture = pipeline.cameraFrameTextureGL();
            if (!cameraTexture) {
                this._missingCameraTextureFrames++;
                if (!this._missingCameraTextureWarningLogged &&
                    this._missingCameraTextureFrames > 30) {
                    this._missingCameraTextureWarningLogged = true;
                    console.warn('[ZapparProvider] No camera texture available yet');
                }
                return;
            }
            // Got a texture: reset missing-texture diagnostics.
            this._missingCameraTextureFrames = 0;
            this._missingCameraTextureWarningLogged = false;
            // Choose the last texture unit to avoid colliding with engine bindings.
            if (this._videoTextureUnit === null) {
                const maxFragmentUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
                const maxCombinedUnits = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
                const maxUnits = Math.min(maxFragmentUnits | 0, maxCombinedUnits | 0);
                this._videoTextureUnit = Math.max(0, maxUnits - 1);
            }
            // Grab the engine's sky material pipeline program (provided by WLE).
            const skyMaterial = this.engine.scene.skyMaterial;
            if (!skyMaterial)
                return;
            const pipelineName = skyMaterial.pipeline;
            const enginePipeline = this.engine.pipelines.findByName(pipelineName);
            const program = enginePipeline?.webglProgram;
            if (!program)
                return;
            // Ensure the sampler uniform points at our dedicated unit.
            if (this._videoTextureProgram !== program) {
                this._videoTextureProgram = program;
                this._videoTextureUniform = gl.getUniformLocation(program, 'videoTexture');
                this._videoTextureTransformUniform = gl.getUniformLocation(program, 'videoTextureTransform');
            }
            if (this._videoTextureUniform || this._videoTextureTransformUniform) {
                const prevProgram = gl.getParameter(gl.CURRENT_PROGRAM);
                gl.useProgram(program);
                if (this._videoTextureUniform) {
                    gl.uniform1i(this._videoTextureUniform, this._videoTextureUnit);
                }
                if (this._videoTextureTransformUniform) {
                    const mirror = pipeline.cameraFrameUserFacing();
                    const m = pipeline.cameraFrameTextureMatrix(this.engine.canvas.width, this.engine.canvas.height, mirror);
                    gl.uniformMatrix4fv(this._videoTextureTransformUniform, false, m);
                }
                gl.useProgram(prevProgram);
            }
            // Bind the texture for the upcoming render. Do not unbind it, we need it
            // to still be bound when the sky material draws.
            const prevActiveTex = gl.getParameter(gl.ACTIVE_TEXTURE);
            gl.activeTexture(gl.TEXTURE0 + this._videoTextureUnit);
            gl.bindTexture(gl.TEXTURE_2D, cameraTexture);
            gl.activeTexture(prevActiveTex);
        }
        catch (error) {
            if (!this._videoTextureBindErrorLogged) {
                this._videoTextureBindErrorLogged = true;
                const skyMaterial = this.engine.scene.skyMaterial;
                console.error('[ZapparProvider] bindVideoTextureForSkyMaterial exception', {
                    error,
                    pipelineName: skyMaterial?.pipeline,
                    hasGl: !!this._gl,
                    hasZapparPipeline: !!this.pipeline,
                    videoTextureUnit: this._videoTextureUnit,
                });
            }
        }
    }
    updateTracking() {
        if (!this.pipeline || !this.instantTracker)
            return;
        if (!this._zappar)
            return;
        const Zappar = this._zappar;
        const mirrorPoses = this.pipeline.cameraFrameUserFacing();
        // Let Zappar continuously refine a stable surface point briefly, then lock.
        // If we lock immediately at startup, we can end up with effectively 3DoF behavior.
        // If we *never* lock, the origin follows the camera and the camera appears frozen.
        // _userPlacementMode keeps the anchor updating beyond the warmup window until the
        // user explicitly confirms placement via placeInstantAnchor().
        if (this._anchorWarmupFramesRemaining > 0 ||
            this._userPlacementMode ||
            !this.hasInitializedAnchor) {
            this.instantTracker.setAnchorPoseFromCameraOffset(0, 0, -5);
            if (this._anchorWarmupFramesRemaining > 0)
                this._anchorWarmupFramesRemaining--;
            this.hasInitializedAnchor = true;
        }
        // Use the active view's near/far when available; incorrect near/far can make tracking feel
        // visually "unrooted" even if the pose is correct.
        const activeView = this.engine.scene.activeViews[0];
        if (!activeView)
            return;
        const zNear = activeView.near;
        const zFar = activeView.far;
        const [cameraDataWidth, cameraDataHeight] = this.pipeline.cameraDataSize();
        const projectionMatrix = Zappar.projectionMatrixFromCameraModelAndSize(this.pipeline.cameraModel(), cameraDataWidth, cameraDataHeight, this.engine.canvas.width, this.engine.canvas.height, zNear, zFar);
        let origin;
        let cameraPoseMatrix;
        // Match zappar-threejs `CameraPoseMode` behavior.
        switch (this.slamPoseMode) {
            case 'default':
                cameraPoseMatrix = this.pipeline.cameraPoseDefault();
                break;
            case 'attitude':
                cameraPoseMatrix = this.pipeline.cameraPoseWithAttitude(mirrorPoses);
                break;
            case 'anchor-origin':
            default:
                // For SLAM camera movement, we treat the InstantWorld anchor as the world origin.
                origin = this.instantTracker.anchor.poseCameraRelative(mirrorPoses);
                cameraPoseMatrix = this.pipeline.cameraPoseWithOrigin(origin);
                break;
        }
        const anchorPoseMatrix = this.instantTracker.anchor.pose(cameraPoseMatrix, mirrorPoses);
        // Diagnostics: track whether translation is meaningfully changing over time.
        // Zappar matrices are column-major; translation lives at indices 12..14.
        this._debugCameraPosition[0] = cameraPoseMatrix[12];
        this._debugCameraPosition[1] = cameraPoseMatrix[13];
        this._debugCameraPosition[2] = cameraPoseMatrix[14];
        this._debugAnchorPosition[0] = anchorPoseMatrix[12];
        this._debugAnchorPosition[1] = anchorPoseMatrix[13];
        this._debugAnchorPosition[2] = anchorPoseMatrix[14];
        const currentFrameNumber = this.pipeline.frameNumber();
        if (this._debugLastSampleFrameNumber === null) {
            this._debugLastSampleFrameNumber = currentFrameNumber;
            vec3.copy(this._debugLastSampleCameraPosition, this._debugCameraPosition);
            vec3.copy(this._debugLastSampleAnchorPosition, this._debugAnchorPosition);
            vec3.zero(this._debugCameraPositionDelta);
            vec3.zero(this._debugAnchorPositionDelta);
        }
        else {
            vec3.sub(this._debugCameraPositionDelta, this._debugCameraPosition, this._debugLastSampleCameraPosition);
            vec3.sub(this._debugAnchorPositionDelta, this._debugAnchorPosition, this._debugLastSampleAnchorPosition);
            // Refresh the baseline every ~2 seconds worth of frames at 60fps.
            // This matches the ZapparDebug interval logger, so deltas are easy to interpret.
            if (currentFrameNumber - this._debugLastSampleFrameNumber >= 120) {
                this._debugLastSampleFrameNumber = currentFrameNumber;
                vec3.copy(this._debugLastSampleCameraPosition, this._debugCameraPosition);
                vec3.copy(this._debugLastSampleAnchorPosition, this._debugAnchorPosition);
            }
        }
        this._slamProjectionMatrix.set(projectionMatrix);
        mat4.copy(this._slamCameraMatrix, cameraPoseMatrix);
        mat4.copy(this._slamAnchorMatrix, anchorPoseMatrix);
        this._slamFrameNumber = currentFrameNumber;
        this._slamStateValid = true;
        const motionPermissionGranted = typeof Zappar.permissionGranted === 'function' &&
            Zappar.Permission?.MOTION !== undefined
            ? Zappar.permissionGranted(Zappar.Permission.MOTION)
            : undefined;
        if (typeof window !== 'undefined') {
            window.ZapparDebug = {
                projectionMatrix,
                cameraPoseMatrix,
                anchorPoseMatrix,
                originMatrix: origin,
                frameNumber: this._slamFrameNumber,
                instantTrackerEnabled: this.instantTracker.enabled,
                slamPoseMode: this.slamPoseMode,
                mirrorPoses,
                motionPermissionGranted,
                cameraPosition: [
                    this._debugCameraPosition[0],
                    this._debugCameraPosition[1],
                    this._debugCameraPosition[2],
                ],
                anchorPosition: [
                    this._debugAnchorPosition[0],
                    this._debugAnchorPosition[1],
                    this._debugAnchorPosition[2],
                ],
                cameraPositionDelta: [
                    this._debugCameraPositionDelta[0],
                    this._debugCameraPositionDelta[1],
                    this._debugCameraPositionDelta[2],
                ],
                anchorPositionDelta: [
                    this._debugAnchorPositionDelta[0],
                    this._debugAnchorPositionDelta[1],
                    this._debugAnchorPositionDelta[2],
                ],
                cameraPositionDeltaLength: vec3.length(this._debugCameraPositionDelta),
                anchorPositionDeltaLength: vec3.length(this._debugAnchorPositionDelta),
            };
        }
        else if (typeof WL_EDITOR !== 'undefined' && WL_EDITOR) {
            globalThis.ZapparDebug = {
                projectionMatrix,
                cameraPoseMatrix,
                anchorPoseMatrix,
                originMatrix: origin,
                frameNumber: this._slamFrameNumber,
                instantTrackerEnabled: this.instantTracker.enabled,
                slamPoseMode: this.slamPoseMode,
                mirrorPoses,
                motionPermissionGranted,
                cameraPosition: [
                    this._debugCameraPosition[0],
                    this._debugCameraPosition[1],
                    this._debugCameraPosition[2],
                ],
                anchorPosition: [
                    this._debugAnchorPosition[0],
                    this._debugAnchorPosition[1],
                    this._debugAnchorPosition[2],
                ],
                cameraPositionDelta: [
                    this._debugCameraPositionDelta[0],
                    this._debugCameraPositionDelta[1],
                    this._debugCameraPositionDelta[2],
                ],
                anchorPositionDelta: [
                    this._debugAnchorPositionDelta[0],
                    this._debugAnchorPositionDelta[1],
                    this._debugAnchorPositionDelta[2],
                ],
                cameraPositionDeltaLength: vec3.length(this._debugCameraPositionDelta),
                anchorPositionDeltaLength: vec3.length(this._debugAnchorPositionDelta),
            };
        }
    }
    async endSession() {
        this.stopZapparDebugLogging();
        this._preRenderErrorLogged = false;
        if (this.cameraSource && this.cameraStarted) {
            this.cameraSource.pause();
            if (typeof document !== 'undefined') {
                document.removeEventListener('visibilitychange', this.onVisibilityChange);
            }
            this.cameraStarted = false;
        }
        if (this.preRenderRegistered) {
            this.engine.scene.onPreRender.remove(this.onPreRender);
            this.preRenderRegistered = false;
        }
        this._videoTextureProgram = null;
        this._videoTextureUniform = null;
        this._videoTextureTransformUniform = null;
        this._videoTextureUnit = null;
        if (this._faceTracker) {
            this._faceTracker.enabled = false;
        }
        if (this._imageTracker) {
            this._imageTracker.enabled = false;
        }
        if (this.instantTracker) {
            this.instantTracker.enabled = false;
        }
        if (this._worldTracker) {
            this._worldTracker.enabled = false;
        }
        if (this._xrSession) {
            try {
                await this._xrSession.end();
            }
            catch {
                /* session already closed */
            }
            this._xrSession = null;
        }
        this.hasInitializedAnchor = false;
        this._anchorWarmupFramesRemaining = 0;
        this._userPlacementMode = false;
        this._slamStateValid = false;
        this.onSessionEnd.notify(this);
    }
    async load() {
        this.loaded = true;
    }
    /** Whether this provider supports given tracking type */
    supports(type) {
        switch (type) {
            case TrackingType.SLAM:
            case TrackingType.Face:
            case TrackingType.Image:
                return true;
            default:
                return false;
        }
    }
    /** Create a tracking implementation */
    createTracking(type, component) {
        switch (type) {
            case TrackingType.SLAM:
                return new WorldTracking_Zappar(this, component);
            case TrackingType.Face:
                return new FaceTracking_Zappar(this, component);
            case TrackingType.Image:
                return new ImageTracking_Zappar(this, component);
            default:
                throw new Error('Tracking mode ' + type + ' not supported.');
        }
    }
}
