import { APP_CONFIG } from "../../app/config.js";
import { EVENTS } from "../../app/events.js";
import { clamp01 } from "./state.js";
import { computeProcessorScale } from "./processorUtils.js";
import { drawMediaError } from "./imageProcessor.js";
import { createWebGLVideoProcessor, isWebGLAvailable } from "./videoProcessorWebGL.js";

export function createVideoProcessor({
    canvas,
    videoEl,
    disableWebGL,
    pauseDuringExecution = null,
    getGradeParams,
    isDefaultGrade,
    _tonemap,
    maxProcPixelsVideo,
    throttleFps,
    safeAddListener,
    safeCall,
    onReady,
}: Record<string, any> = {}): Record<string, any> {
    const shouldPauseDuringExecution =
        pauseDuringExecution == null
            ? !!APP_CONFIG?.VIEWER_PAUSE_DURING_EXECUTION
            : !!pauseDuringExecution;
    // Attempt WebGL first
    let glProc: any = null;
    if (!disableWebGL && isWebGLAvailable()) {
        try {
            glProc = createWebGLVideoProcessor({
                canvas,
                videoEl,
                getGradeParams,
                isDefaultGrade,
                maxProcPixelsVideo,
            });
        } catch (e: any) {
            console.warn("WebGL Init failed, falling back to 2D", e);
            glProc = null;
        }
    }

    // alpha: true so the canvas stays transparent until a frame actually paints,
    // instead of flashing/sticking opaque black if rendering stalls (e.g. the WebGL
    // fallback path kicking in under context pressure from rapid open/close cycles).
    const ctx = glProc
        ? null
        : (() => {
              try {
                  return canvas.getContext("2d", { willReadFrequently: true, alpha: true });
              } catch {
                  return null;
              }
          })();

    const srcCanvas = document.createElement("canvas");
    const srcCtx = (() => {
        try {
            return srcCanvas.getContext("2d", { willReadFrequently: true, alpha: false });
        } catch {
            return null;
        }
    })();

    const sampleCanvas = document.createElement("canvas");
    sampleCanvas.width = 1;
    sampleCanvas.height = 1;
    const sampleCtx = (() => {
        try {
            return sampleCanvas.getContext("2d", { willReadFrequently: true, alpha: false });
        } catch {
            return null;
        }
    })();

    const proc: Record<string, any> = {
        ready: false,
        _rendering: false,
        _destroyed: false,
        _rvfc: null,
        _rafIdLoop: null,
        _rafIdSchedule: null,
        _seekRaf: null,
        _lastHeavyRenderAt: 0,
        _throttleTimer: null,
        _connectRAF: null,
        _connectTries: 0,
        _buffer: null,
        _lut: null,
        _lutKey: "",
        _lastFrameTime: -1,
        _lastHeavySig: "",
        _runtimePaused: false,
    };

    const unsubs: any[] = [];

    const computeScale = (w: any, h: any) => computeProcessorScale(maxProcPixelsVideo, w, h);

    const ensureSizeFromVideo = () => {
        try {
            const w0 = Number(videoEl?.videoWidth) || 0;
            const h0 = Number(videoEl?.videoHeight) || 0;
            if (!(w0 > 0 && h0 > 0)) return false;
            proc.naturalW = w0;
            proc.naturalH = h0;
            proc.scale = computeScale(w0, h0);

            const w = Math.max(1, Math.round(w0 * proc.scale));
            const h = Math.max(1, Math.round(h0 * proc.scale));
            if (srcCanvas.width !== w) srcCanvas.width = w;
            if (srcCanvas.height !== h) srcCanvas.height = h;

            // When WebGL is active it owns canvas dimensions (ensureSize in
            // videoProcessorWebGL.js).  Setting canvas.width here would clear
            // the WebGL backbuffer and fight the GL viewport.
            if (!glProc) {
                if (canvas.width !== w) canvas.width = w;
                if (canvas.height !== h) canvas.height = h;
            }

            canvas._mjrNaturalW = w0;
            canvas._mjrNaturalH = h0;
            canvas._mjrPixelScale = proc.scale;

            proc.ready = true;
            return true;
        } catch {
            return false;
        }
    };

    const drawSource = () => {
        if (!srcCtx) return false;
        if (!proc.ready) return false;
        try {
            srcCtx.drawImage(videoEl, 0, 0, srcCanvas.width, srcCanvas.height);
            return true;
        } catch {
            return false;
        }
    };

    const renderProcessedFrame = () => {
        if (!proc.ready) return;

        const params = proc.lastParams || getGradeParams?.() || {};

        if (glProc) {
            glProc.update(params);
            return;
        }

        if (!ctx || !srcCtx) return;

        // Optimization: if no grading controls are active, bypass the expensive readback
        // and draw the video element directly to the destination canvas.
        if (isDefaultGrade?.(params)) {
            try {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height);
            } catch (e: any) {
                console.debug?.(e);
            }
            return;
        }

        // Grading active: requires Source -> Buffer -> CPU Transform -> Dest
        if (!drawSource()) return;

        let src;
        try {
            src = srcCtx.getImageData(0, 0, srcCanvas.width, srcCanvas.height);
        } catch {
            try {
                // Fallback if readback fails (e.g. tainted canvas)
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                ctx.drawImage(videoEl, 0, 0, canvas.width, canvas.height);
            } catch (e: any) {
                console.debug?.(e);
            }
            return;
        }

        const w = srcCanvas.width;
        const h = srcCanvas.height;

        // Reuse buffer if possible to reduce GC churn
        let dst = proc._buffer;
        if (!dst || dst.width !== w || dst.height !== h) {
            try {
                dst = ctx.createImageData(w, h);
                proc._buffer = dst;
            } catch {
                try {
                    dst = new ImageData(w, h);
                    proc._buffer = dst;
                } catch {
                    return;
                }
            }
        }

        // Safety check if buffer creation failed
        if (!dst) return;

        const exposureEV = Number(params.exposureEV) || 0;
        const gamma = Math.max(0.1, Math.min(3, Number(params.gamma) || 1));
        const invGamma = 1 / gamma;
        const channel = String(params.channel || "rgb");
        const analysisMode = String(params.analysisMode || "none");
        const zebraThreshold = clamp01(params.zebraThreshold ?? 0.95);
        const exposureScale = Math.pow(2, exposureEV);

        const s = src.data;
        const d = dst.data;
        const canUseLut = analysisMode !== "zebra" && channel === "rgb";
        let lut: any = null;
        if (canUseLut) {
            const lutKey = `${exposureScale.toFixed(6)}|${invGamma.toFixed(6)}`;
            if (!proc._lut || proc._lutKey !== lutKey) {
                const nextLut = new Uint8ClampedArray(256);
                for (let j = 0; j < 256; j += 1) {
                    const v = j / 255;
                    nextLut[j] = Math.round(Math.pow(clamp01(v * exposureScale), invGamma) * 255);
                }
                proc._lut = nextLut;
                proc._lutKey = lutKey;
            }
            lut = proc._lut;
        }

        if (lut) {
            for (let i = 0; i < d.length; i += 4) {
                d[i] = lut[s[i] ?? 0];
                d[i + 1] = lut[s[i + 1] ?? 0];
                d[i + 2] = lut[s[i + 2] ?? 0];
                d[i + 3] = 255;
            }
        } else {
            for (let i = 0; i < d.length; i += 4) {
                const r0 = (s[i] ?? 0) / 255;
                const g0 = (s[i + 1] ?? 0) / 255;
                const b0 = (s[i + 2] ?? 0) / 255;
                const a0 = (s[i + 3] ?? 255) / 255;

                let rr = r0 * exposureScale;
                let gg = g0 * exposureScale;
                let bb = b0 * exposureScale;

                const lum = 0.2126 * rr + 0.7152 * gg + 0.0722 * bb;

                if (analysisMode === "zebra") {
                    // Input is 8-bit video; avoid tonemapping (it alters baseline contrast/colors).
                    const over = clamp01(lum) >= zebraThreshold;
                    if (over) {
                        const stripe = (((Math.floor(i / 4) % w) + Math.floor(i / 4 / w)) & 7) < 3;
                        rr = stripe ? 1 : 0;
                        gg = stripe ? 1 : 0;
                        bb = stripe ? 1 : 0;
                    } else {
                        rr = Math.pow(clamp01(rr), invGamma);
                        gg = Math.pow(clamp01(gg), invGamma);
                        bb = Math.pow(clamp01(bb), invGamma);
                    }
                } else {
                    rr = Math.pow(clamp01(rr), invGamma);
                    gg = Math.pow(clamp01(gg), invGamma);
                    bb = Math.pow(clamp01(bb), invGamma);
                }

                if (channel === "r") {
                    gg = rr;
                    bb = rr;
                } else if (channel === "g") {
                    rr = gg;
                    bb = gg;
                } else if (channel === "b") {
                    rr = bb;
                    gg = bb;
                } else if (channel === "a") {
                    rr = a0;
                    gg = a0;
                    bb = a0;
                } else if (channel === "l") {
                    const l = clamp01(lum);
                    const lg = Math.pow(l, invGamma);
                    rr = lg;
                    gg = lg;
                    bb = lg;
                }

                d[i] = Math.round(clamp01(rr) * 255);
                d[i + 1] = Math.round(clamp01(gg) * 255);
                d[i + 2] = Math.round(clamp01(bb) * 255);
                d[i + 3] = 255;
            }
        }

        try {
            ctx.putImageData(dst, 0, 0);
        } catch (e: any) {
            console.debug?.(e);
        }
    };

    const renderOnce = () => {
        if (proc._destroyed) return;
        if (!canvas?.isConnected) return;
        if (!proc.ready) ensureSizeFromVideo();
        if (!proc.ready) return;
        // Skip duplicate heavy renders when neither time nor grading params changed.
        try {
            const params = proc.lastParams || getGradeParams?.() || {};
            const heavy = !isDefaultGrade?.(params);
            if (heavy) {
                const t = Number(videoEl?.currentTime) || 0;
                const sig = `${Number(params.exposureEV) || 0}|${Number(params.gamma) || 1}|${String(params.channel || "rgb")}|${String(params.analysisMode || "none")}|${Number(params.zebraThreshold ?? 0.95)}`;
                if (
                    Math.abs(t - (Number(proc._lastFrameTime) || 0)) < 1e-6 &&
                    sig === String(proc._lastHeavySig || "")
                ) {
                    return;
                }
                proc._lastFrameTime = t;
                proc._lastHeavySig = sig;
            }
        } catch (e: any) {
            console.debug?.(e);
        }
        renderProcessedFrame();
    };

    const scheduleWhenConnected = () => {
        if (proc._destroyed) return;
        try {
            if (canvas?.isConnected) {
                proc._connectRAF = null;
                proc._connectTries = 0;
                scheduleRender();
                return;
            }
        } catch (e: any) {
            console.debug?.(e);
        }
        if (proc._connectRAF != null) return;
        proc._connectTries = (Number(proc._connectTries) || 0) + 1;
        if (proc._connectTries > 20) {
            proc._connectRAF = null;
            proc._connectTries = 0;
            return;
        }
        try {
            proc._connectRAF = requestAnimationFrame(() => {
                proc._connectRAF = null;
                scheduleWhenConnected();
            });
        } catch {
            proc._connectRAF = null;
        }
    };

    const _getThrottleMs = () => {
        try {
            const fps = Number(throttleFps);
            if (!Number.isFinite(fps) || fps <= 0) return 0;
            return Math.max(0, Math.floor(1000 / Math.max(1, fps)));
        } catch {
            return 0;
        }
    };

    const scheduleRender = () => {
        if (proc._destroyed) return;
        if (proc._runtimePaused) return;
        if (proc._rendering) return;
        if (!canvas?.isConnected) {
            scheduleWhenConnected();
            return;
        }

        const params = proc.lastParams || getGradeParams?.() || {};
        const heavy = !isDefaultGrade?.(params);
        const throttleMs = heavy && !videoEl?.paused ? _getThrottleMs() : 0;
        if (throttleMs > 0) {
            const now = Date.now();
            const nextOkAt = (Number(proc._lastHeavyRenderAt) || 0) + throttleMs;
            if (now < nextOkAt) {
                try {
                    if (proc._throttleTimer) clearTimeout(proc._throttleTimer);
                } catch (e: any) {
                    console.debug?.(e);
                }
                try {
                    proc._throttleTimer = setTimeout(
                        () => {
                            try {
                                proc._throttleTimer = null;
                            } catch (e: any) {
                                console.debug?.(e);
                            }
                            scheduleRender();
                        },
                        Math.min(250, Math.max(0, nextOkAt - now)),
                    );
                } catch (e: any) {
                    console.debug?.(e);
                }
                return;
            }
        }

        proc._rendering = true;
        try {
            proc._rafIdSchedule = requestAnimationFrame(() => {
                proc._rafIdSchedule = null;
                proc._rendering = false;
                renderOnce();
                try {
                    if (heavy) proc._lastHeavyRenderAt = Date.now();
                } catch (e: any) {
                    console.debug?.(e);
                }
            });
        } catch {
            proc._rendering = false;
        }
    };

    const startFrameLoop = () => {
        if (proc._destroyed) return;
        if (proc._runtimePaused) return;
        // Cancel any previously-registered frame loops before starting new ones.
        // Without this, every pause->play cycle (including seeks that temporarily
        // pause the video) accumulates an extra rVFC/RAF callback.  After N cycles
        // N callbacks fire every frame, each enqueuing a redundant scheduleRender().
        try {
            if (proc._rvfc != null && typeof videoEl?.cancelVideoFrameCallback === "function") {
                videoEl.cancelVideoFrameCallback(proc._rvfc);
                proc._rvfc = null;
            }
        } catch (e: any) {
            console.debug?.(e);
        }
        try {
            if (proc._rafIdLoop != null) {
                cancelAnimationFrame(proc._rafIdLoop);
                proc._rafIdLoop = null;
            }
        } catch (e: any) {
            console.debug?.(e);
        }
        try {
            if (typeof videoEl?.requestVideoFrameCallback === "function") {
                const cb = () => {
                    if (proc._destroyed) return;
                    if (!canvas?.isConnected) return;
                    scheduleRender();
                    if (!videoEl.paused) {
                        try {
                            proc._rvfc = videoEl.requestVideoFrameCallback(cb);
                        } catch (e: any) {
                            console.debug?.(e);
                        }
                    }
                };
                try {
                    proc._rvfc = videoEl.requestVideoFrameCallback(cb);
                } catch (e: any) {
                    console.debug?.(e);
                }
                return;
            }
        } catch (e: any) {
            console.debug?.(e);
        }
        const loop = () => {
            if (proc._destroyed) return;
            if (!canvas?.isConnected) return;
            scheduleRender();
            if (!videoEl.paused) {
                try {
                    proc._rafIdLoop = requestAnimationFrame(loop);
                } catch (e: any) {
                    console.debug?.(e);
                }
            }
        };
        try {
            proc._rafIdLoop = requestAnimationFrame(loop);
        } catch (e: any) {
            console.debug?.(e);
        }
    };

    const setParams = (params: any) => {
        proc.lastParams = params || proc.lastParams || getGradeParams?.();
        scheduleRender();
    };

    const pauseProcessing = () => {
        proc._runtimePaused = true;
        try {
            if (proc._throttleTimer) clearTimeout(proc._throttleTimer);
        } catch (e: any) {
            console.debug?.(e);
        }
        proc._throttleTimer = null;
        try {
            if (proc._rvfc != null && typeof videoEl?.cancelVideoFrameCallback === "function") {
                videoEl.cancelVideoFrameCallback(proc._rvfc);
            }
        } catch (e: any) {
            console.debug?.(e);
        }
        proc._rvfc = null;
        try {
            if (proc._rafIdLoop != null) cancelAnimationFrame(proc._rafIdLoop);
        } catch (e: any) {
            console.debug?.(e);
        }
        proc._rafIdLoop = null;
        try {
            if (proc._rafIdSchedule != null) cancelAnimationFrame(proc._rafIdSchedule);
        } catch (e: any) {
            console.debug?.(e);
        }
        proc._rafIdSchedule = null;
        try {
            if (proc._seekRaf != null) cancelAnimationFrame(proc._seekRaf);
        } catch (e: any) {
            console.debug?.(e);
        }
        proc._seekRaf = null;
        proc._rendering = false;
    };

    const resumeProcessing = () => {
        proc._runtimePaused = false;
        if (videoEl?.paused) {
            scheduleRender();
            return;
        }
        startFrameLoop();
        scheduleRender();
    };

    const sampleAtOriginal = (x: any, y: any) => {
        try {
            if (!proc.ready) ensureSizeFromVideo();
            if (!proc.ready) return null;
            drawSource();

            const scale = proc.scale || 1;
            const sx = Math.max(
                0,
                Math.min(srcCanvas.width - 1, Math.floor((Number(x) || 0) * scale)),
            );
            const sy = Math.max(
                0,
                Math.min(srcCanvas.height - 1, Math.floor((Number(y) || 0) * scale)),
            );
            if (!sampleCtx) return null;
            sampleCtx.clearRect(0, 0, 1, 1);
            sampleCtx.drawImage(srcCanvas, sx, sy, 1, 1, 0, 0, 1, 1);
            const data = sampleCtx.getImageData(0, 0, 1, 1)?.data;
            if (!data || data.length < 4) return null;
            const r = data[0] ?? 0;
            const g = data[1] ?? 0;
            const b = data[2] ?? 0;
            const a = data[3] ?? 255;

            const raw = [r / 255, g / 255, b / 255, a / 255];
            const ev = Number(proc.lastParams?.exposureEV) || 0;
            const exposureScale = Math.pow(2, ev);
            const lin = [
                raw[0] * exposureScale,
                raw[1] * exposureScale,
                raw[2] * exposureScale,
                raw[3],
            ];

            return { r, g, b, a, raw, lin, scale: proc.scale };
        } catch {
            return null;
        }
    };

    const onMeta = () => {
        ensureSizeFromVideo();
        scheduleRender();
        try {
            onReady?.({ naturalW: proc.naturalW, naturalH: proc.naturalH, pixelScale: proc.scale });
        } catch (e: any) {
            console.debug?.(e);
        }
    };

    const onRuntimeStatus = (event: any) => {
        if (!shouldPauseDuringExecution) return;
        const activePromptId = String(event?.detail?.active_prompt_id || "").trim();
        if (activePromptId) {
            pauseProcessing();
            return;
        }
        resumeProcessing();
    };

    try {
        const onPlay = () => {
            if (proc._runtimePaused) return;
            startFrameLoop();
        };
        // Live scrub: while the element is actively seeking (e.g. the user is
        // dragging the timeline while paused), the browser coalesces rapid
        // currentTime changes and only fires a single `seeked` once it settles.
        // Without this, the canvas stays frozen on the last fully-seeked frame
        // and the previewed image does not follow the playhead during the drag.
        // Drive repaints with a short rAF loop so each newly-decoded frame is
        // drawn to the canvas as the seek progresses.
        const stopSeekLoop = () => {
            try {
                if (proc._seekRaf != null) cancelAnimationFrame(proc._seekRaf);
            } catch (e: any) {
                console.debug?.(e);
            }
            proc._seekRaf = null;
        };
        const onSeeking = () => {
            if (proc._destroyed || proc._runtimePaused) return;
            // While playing, the normal frame loop (rVFC/rAF) already repaints.
            try {
                if (!videoEl?.paused) {
                    scheduleRender();
                    return;
                }
            } catch (e: any) {
                console.debug?.(e);
            }
            if (proc._seekRaf != null) return;
            const loop = () => {
                proc._seekRaf = null;
                if (proc._destroyed) return;
                if (!canvas?.isConnected) return;
                scheduleRender();
                let stillSeeking = false;
                try {
                    stillSeeking = !!videoEl?.seeking;
                } catch (e: any) {
                    console.debug?.(e);
                }
                if (stillSeeking) {
                    try {
                        proc._seekRaf = requestAnimationFrame(loop);
                    } catch (e: any) {
                        console.debug?.(e);
                    }
                }
            };
            try {
                proc._seekRaf = requestAnimationFrame(loop);
            } catch (e: any) {
                console.debug?.(e);
            }
        };
        const onSeeked = () => {
            stopSeekLoop();
            scheduleRender();
        };
        const onError = () => {
            proc.ready = false;
            try {
                const errCode = videoEl?.error?.code;
                const errMsg = videoEl?.error?.message || "";
                // MediaError codes: 1=aborted, 2=network, 3=decode, 4=src_not_supported
                let msg, hint;
                if (errCode === 2) {
                    msg = "Failed to load video (network / path error)";
                    hint = "Check file permissions / path, or try re-indexing.";
                } else if (errCode === 3) {
                    msg = "Failed to load video (decode error - unsupported codec?)";
                    hint =
                        "Browser may not support this codec (e.g. H.265/HEVC). Try converting to H.264/MP4.";
                } else if (errCode === 4) {
                    msg = "Failed to load video (unsupported format or codec)";
                    hint =
                        "Browser cannot decode this file (e.g. H.265/HEVC). Try converting to H.264/MP4.";
                } else {
                    msg = "Failed to load video";
                    hint = errMsg || "Check file permissions / path, or try re-indexing.";
                }
                console.warn("[MJR] Video load error", {
                    code: errCode,
                    message: errMsg,
                    src: videoEl?.src,
                });
                drawMediaError(canvas, msg, hint);
            } catch (e: any) {
                console.debug?.(e);
            }
        };

        unsubs.push(
            safeAddListener?.(videoEl, "loadedmetadata", onMeta, { once: true }) || (() => {}),
        );
        unsubs.push(
            safeAddListener?.(videoEl, "seeking", onSeeking, { passive: true }) || (() => {}),
        );
        unsubs.push(
            safeAddListener?.(videoEl, "seeked", onSeeked, { passive: true }) || (() => {}),
        );
        unsubs.push(
            safeAddListener?.(videoEl, "pause", scheduleRender, { passive: true }) || (() => {}),
        );
        unsubs.push(safeAddListener?.(videoEl, "play", onPlay, { passive: true }) || (() => {}));
        unsubs.push(
            safeAddListener?.(
                videoEl,
                "timeupdate",
                () => {
                    try {
                        if (
                            !videoEl?.paused &&
                            typeof videoEl?.requestVideoFrameCallback === "function"
                        ) {
                            return;
                        }
                    } catch (e: any) {
                        console.debug?.(e);
                    }
                    scheduleRender();
                },
                { passive: true },
            ) || (() => {}),
        );
        unsubs.push(safeAddListener?.(videoEl, "error", onError, { passive: true }) || (() => {}));
        if (shouldPauseDuringExecution) {
            window.addEventListener(EVENTS.RUNTIME_STATUS, onRuntimeStatus);
            if (String((window as any)?.__MJR_EXECUTION_RUNTIME__?.active_prompt_id || "").trim()) {
                pauseProcessing();
            }
        }
    } catch (e: any) {
        console.debug?.(e);
    }

    return {
        setParams,
        sampleAtOriginal,
        getInfo: () => ({ ...proc, renderer: glProc ? "webgl" : "2d" }),
        pause: pauseProcessing,
        resume: resumeProcessing,
        destroy: () => {
            if (glProc) glProc.destroy();
            proc._destroyed = true;
            try {
                window.removeEventListener(EVENTS.RUNTIME_STATUS, onRuntimeStatus);
            } catch (e: any) {
                console.debug?.(e);
            }
            try {
                if (proc._throttleTimer) clearTimeout(proc._throttleTimer);
            } catch (e: any) {
                console.debug?.(e);
            }
            proc._throttleTimer = null;
            try {
                if (proc._connectRAF != null) cancelAnimationFrame(proc._connectRAF);
            } catch (e: any) {
                console.debug?.(e);
            }
            proc._connectRAF = null;
            proc._connectTries = 0;
            try {
                if (proc._rvfc != null && typeof videoEl?.cancelVideoFrameCallback === "function") {
                    videoEl.cancelVideoFrameCallback(proc._rvfc);
                }
            } catch (e: any) {
                console.debug?.(e);
            }
            try {
                if (proc._rafIdLoop != null) cancelAnimationFrame(proc._rafIdLoop);
            } catch (e: any) {
                console.debug?.(e);
            }
            try {
                if (proc._rafIdSchedule != null) cancelAnimationFrame(proc._rafIdSchedule);
            } catch (e: any) {
                console.debug?.(e);
            }
            try {
                if (proc._seekRaf != null) cancelAnimationFrame(proc._seekRaf);
            } catch (e: any) {
                console.debug?.(e);
            }
            try {
                for (const u of unsubs) safeCall?.(u);
            } catch (e: any) {
                console.debug?.(e);
            }
            try {
                srcCanvas.width = 0;
                srcCanvas.height = 0;
            } catch (e: any) {
                console.debug?.(e);
            }
            try {
                canvas.width = 0;
                canvas.height = 0;
            } catch (e: any) {
                console.debug?.(e);
            }
            proc._buffer = null;
        },
    };
}
