window.grafxPreviewer = (() => {
    let _canvas = null;
    let _ctx = null;
    let _pixelData = null;   // raw frame data from .NET
    let _cache = null;       // lazily filled: cached ImageData per frame
    let _frameIndex = 0;
    let _maxWidth = 0;
    let _maxFrames = 0;
    let _interval = 25;
    let _playing = false;
    let _dotNetRef = null;
    let _rafId = null;
    let _lastFrameTime = 0;
    let _lastNotifiedFrame = -1;
    let _lastNotifyTime = 0;
    const NOTIFY_THROTTLE_MS = 200;

    const CANVAS_WIDTH = 800;
    const CANVAS_HEIGHT = 125;
    const BLADE_WIDTH_PX = 570;
    const BLADE_START_X = 160;

    function init(dotNetRef, canvasId) {
        _dotNetRef = dotNetRef;
        _canvas = document.getElementById(canvasId);
        if (!_canvas) return;
        _ctx = _canvas.getContext('2d');
        _canvas.width = CANVAS_WIDTH;
        _canvas.height = CANVAS_HEIGHT;
        clearCanvas();
    }

    function loadBitmap(pixelFrames, width, interval) {
        stop();
        _pixelData = pixelFrames;
        _maxFrames = pixelFrames.length;
        _maxWidth = width;
        _interval = interval > 0 ? interval : 25;
        _frameIndex = _maxFrames - 1;
        _cache = new Array(_maxFrames); // empty — filled lazily

        _canvas.width = CANVAS_WIDTH;
        _canvas.height = CANVAS_HEIGHT;

        renderFrame();
    }

    /** Parse hex color string once and return [r, g, b] or null. */
    function parseHex(color) {
        if (!color || color.length < 4 || color === '#000' || color === '#000000') return null;
        const r = parseInt(color.substring(1, 3), 16);
        const g = parseInt(color.substring(3, 5), 16);
        const b = parseInt(color.substring(5, 7), 16);
        if ((r + g + b) < 15) return null; // skip very dark
        return [r, g, b];
    }

    /**
     * Renders the current frame.
     * Uses a lazy ImageData cache: first render builds and caches,
     * subsequent renders are a single putImageData().
     * No ctx.filter blur — uses radial gradients for glow (GPU-friendly).
     */
    function renderFrame() {
        if (!_ctx || !_pixelData || _maxFrames === 0) return;

        // Check cache
        if (_cache[_frameIndex]) {
            _ctx.putImageData(_cache[_frameIndex], 0, 0);
            return;
        }

        const frame = _pixelData[_frameIndex];
        if (!frame) return;

        const ctx = _ctx;
        ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
        ctx.fillStyle = '#111';
        ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

        const centerY = CANVAS_HEIGHT / 2;
        const ledCount = _maxWidth;
        const scaleFactor = BLADE_WIDTH_PX / ledCount;

        // Use 'lighter' composite for additive glow
        ctx.save();
        ctx.globalCompositeOperation = 'lighter';

        for (let i = 0; i < ledCount && i < frame.length; i++) {
            const rgb = parseHex(frame[i]);
            if (!rgb) continue;

            const [r, g, b] = rgb;
            const xCenter = Math.floor(i * scaleFactor) + BLADE_START_X + Math.ceil(scaleFactor) / 2;

            // Outer glow — radial gradient (no ctx.filter needed)
            const outerRadius = 14;
            const grad = ctx.createRadialGradient(xCenter, centerY, 0, xCenter, centerY, outerRadius);
            grad.addColorStop(0, `rgba(${r},${g},${b},0.6)`);
            grad.addColorStop(0.4, `rgba(${r},${g},${b},0.25)`);
            grad.addColorStop(1, `rgba(${r},${g},${b},0)`);
            ctx.fillStyle = grad;
            ctx.fillRect(xCenter - outerRadius, centerY - outerRadius, outerRadius * 2, outerRadius * 2);

            // Core — bright tight dot
            const coreRadius = 4;
            const coreGrad = ctx.createRadialGradient(xCenter, centerY, 0, xCenter, centerY, coreRadius);
            coreGrad.addColorStop(0, `rgba(255,255,255,0.9)`);
            coreGrad.addColorStop(0.3, `rgba(${r},${g},${b},0.8)`);
            coreGrad.addColorStop(1, `rgba(${r},${g},${b},0)`);
            ctx.fillStyle = coreGrad;
            ctx.fillRect(xCenter - coreRadius, centerY - coreRadius, coreRadius * 2, coreRadius * 2);
        }

        ctx.restore();

        // Cache this frame as ImageData for instant replay
        _cache[_frameIndex] = ctx.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    }

    function showFrame() {
        renderFrame();
    }

    function play() {
        if (_playing) return;
        _playing = true;
        _lastFrameTime = performance.now();
        _rafId = requestAnimationFrame(animationLoop);
    }

    function animationLoop(timestamp) {
        if (!_playing) return;

        const elapsed = timestamp - _lastFrameTime;
        if (elapsed >= _interval) {
            _lastFrameTime = timestamp - (elapsed % _interval);
            _frameIndex--;
            if (_frameIndex < 0) _frameIndex = _maxFrames - 1;
            showFrame();
            notifyFrameThrottled(timestamp);
        }

        _rafId = requestAnimationFrame(animationLoop);
    }

    function notifyFrameThrottled(timestamp) {
        if (!_dotNetRef) return;
        if (_frameIndex === _lastNotifiedFrame) return;
        if (timestamp - _lastNotifyTime < NOTIFY_THROTTLE_MS) return;
        _lastNotifyTime = timestamp;
        _lastNotifiedFrame = _frameIndex;
        _dotNetRef.invokeMethodAsync('OnFrameChanged', _frameIndex);
    }

    function stop() {
        _playing = false;
        if (_rafId !== null) {
            cancelAnimationFrame(_rafId);
            _rafId = null;
        }
        if (_dotNetRef && _lastNotifiedFrame !== _frameIndex) {
            _lastNotifiedFrame = _frameIndex;
            _dotNetRef.invokeMethodAsync('OnFrameChanged', _frameIndex);
        }
    }

    function seekFrame(index) {
        if (index >= 0 && index < _maxFrames) {
            _frameIndex = index;
            showFrame();
            if (_dotNetRef) {
                _lastNotifiedFrame = _frameIndex;
                _dotNetRef.invokeMethodAsync('OnFrameChanged', _frameIndex);
            }
        }
    }

    function setInterval2(ms) {
        _interval = ms > 0 ? ms : 25;
    }

    function clearCanvas() {
        if (_ctx && _canvas) {
            _canvas.width = CANVAS_WIDTH;
            _canvas.height = CANVAS_HEIGHT;
            _ctx.fillStyle = '#111';
            _ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
        }
    }

    function dispose() {
        stop();
        _pixelData = null;
        _cache = null;
        _canvas = null;
        _ctx = null;
        _dotNetRef = null;
    }

    return { init, loadBitmap, play, stop, seekFrame, setInterval: setInterval2, clearCanvas, dispose };
})();
