/**
 * Sifency Addons — Shader Overlay Widget
 * Handle : sifency-shader-overlay
 * Version: 1.0.0
 *
 * Reads all configuration from a single `data-shader-config` JSON attribute
 * on the `.sifency-shader-wrap` element.
 *
 * Works in Elementor editor live-preview via the `elementor/frontend/init`
 * hook pattern — the handler function re-runs on every widget re-render.
 */

(function () {
    'use strict';

    /* ── helpers ─────────────────────────────────── */

    /**
     * Convert a CSS hex colour string to a GLSL vec3 literal.
     *
     * @param  {string} hex  e.g. "#F4793A" or "#f47"
     * @return {string}      e.g. "vec3(0.9569,0.4745,0.2275)"
     */
    function hexToVec3(hex) {
        hex = hex.replace('#', '');
        if (hex.length === 3) {
            hex = hex.split('').map(function (c) { return c + c; }).join('');
        }
        var r = parseInt(hex.slice(0, 2), 16) / 255;
        var g = parseInt(hex.slice(2, 4), 16) / 255;
        var b = parseInt(hex.slice(4, 6), 16) / 255;
        return 'vec3(' + r.toFixed(4) + ',' + g.toFixed(4) + ',' + b.toFixed(4) + ')';
    }

    /* ── GLSL builder ────────────────────────────── */

    /**
     * Build the fragment shader source with all config values inlined.
     * Inlining avoids uniform-upload overhead and allows GLSL to
     * compile constants as literals (branch-free loops, etc.).
     *
     * @param  {Object} cfg
     * @return {string}
     */
    function buildFragShader(cfg) {
        return [
            'precision mediump float;',
            'uniform float u_time;',
            'uniform vec2  u_res;',

            'vec3 colorBase  = ' + cfg.colorBase + ';',
            'vec3 colorLight = ' + cfg.colorLight + ';',
            'const int OCTAVES = ' + cfg.octaves + ';',

            'float hash( vec2 p ) {',
            '  return fract( sin( dot( p, vec2( 127.1, 311.7 ) ) ) * 43758.5453 );',
            '}',

            'float noise( vec2 p ) {',
            '  vec2 i = floor( p );',
            '  vec2 f = fract( p );',
            '  f = f * f * ( 3.0 - 2.0 * f );',
            '  float a = hash( i );',
            '  float b = hash( i + vec2( 1.0, 0.0 ) );',
            '  float c = hash( i + vec2( 0.0, 1.0 ) );',
            '  float d = hash( i + vec2( 1.0, 1.0 ) );',
            '  return mix( mix( a, b, f.x ), mix( c, d, f.x ), f.y );',
            '}',

            'float fbm( vec2 p ) {',
            '  float v = 0.0;',
            '  float a = 0.5;',
            '  for ( int i = 0; i < OCTAVES; i++ ) {',
            '    v += a * noise( p );',
            '    p *= 2.0;',
            '    a *= 0.5;',
            '  }',
            '  return v;',
            '}',

            'vec2 flowField( vec2 p, float t ) {',
            '  float angle = fbm( p * 1.2 + vec2( t * 0.15, 0.0 ) ) * 6.2831;',
            '  return vec2( cos( angle ), sin( angle ) );',
            '}',

            'void main() {',
            '  vec2 uv     = gl_FragCoord.xy / u_res;',
            '  float aspect = u_res.x / u_res.y;',
            '  vec2 p      = uv * vec2( aspect, 1.0 );',
            '  float t     = u_time * ' + cfg.speed.toFixed(4) + ';',

            '  vec2 flow   = flowField( p * 2.0, t );',
            '  vec2 warped = p + flow * ' + cfg.warp.toFixed(4) + ';',

            '  float n1 = fbm( warped * 2.0 + t * 0.08 );',
            '  float n2 = fbm( warped * 4.0 - t * 0.05 + 1.7 );',
            '  float n3 = fbm( p * 6.0 + flow + t * 0.12 );',

            '  float shape  = smoothstep( 0.35, 0.65, n1 * 0.6 + n2 * 0.4 );',
            '  float detail = n3 * 0.25;',
            '  vec3 col = mix( colorBase, colorLight, shape + detail );',

            '  float rim = fbm( p * 8.0 + t * 0.1 ) * 0.18;',
            '  col = mix( col, colorLight * 1.08, rim * ( 1.0 - shape ) );',

            '  float vg  = 1.0 - smoothstep( 0.3, 0.9, length( uv - 0.5 ) * 1.4 );',
            '  col *= mix( ' + cfg.vignette.toFixed(4) + ', 1.0, vg );',

            '  float grain = ( hash( uv + fract( t * 0.03 ) ) - 0.5 ) * ' + cfg.grain.toFixed(4) + ';',
            '  col += grain;',

            '  float scan = sin( uv.y * u_res.y * 0.5 ) * ' + cfg.scan.toFixed(4) + ';',
            '  col += scan;',

            '  gl_FragColor = vec4( col, 1.0 );',
            '}',
        ].join('\n');
    }

    /* ── core initialiser ────────────────────────── */

    /**
     * Initialise one widget wrapper element.
     *
     * @param {HTMLElement} wrap  .sifency-shader-wrap
     */
    function initShader(wrap) {
        // Parse config from data attribute.
        var rawConfig;
        try {
            rawConfig = JSON.parse(wrap.getAttribute('data-shader-config') || '{}');
        } catch (e) {
            rawConfig = {};
        }

        var cfg = {
            colorBase: hexToVec3(rawConfig.colorBase),
            colorLight: hexToVec3(rawConfig.colorLight),
            speed: parseFloat(rawConfig.speed) || 1.0,
            warp: parseFloat(rawConfig.warp) || 0.12,
            octaves: parseInt(rawConfig.octaves) || 5,
            grain: parseFloat(rawConfig.grain) || 0.035,
            vignette: parseFloat(rawConfig.vignette) || 0.75,
            scan: parseFloat(rawConfig.scan) || 0.015,
            pauseOffscreen: rawConfig.pauseOffscreen !== false,
        };

        // Find or create canvas.
        var canvas = wrap.querySelector('.sifency-shader-canvas');
        if (!canvas) {
            return;
        }

        // WebGL context.
        var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
        if (!gl) {
            wrap.classList.add('sifency-no-webgl');
            return;
        }

        // Size canvas to physical pixels.
        var dpr = window.devicePixelRatio || 1;
        var W = wrap.offsetWidth || 680;
        var H = canvas.offsetHeight || 500;
        canvas.width = Math.round(W * dpr);
        canvas.height = Math.round(H * dpr);

        // Compile shaders.
        var vertSrc = [
            'attribute vec2 a_pos;',
            'void main() { gl_Position = vec4( a_pos, 0.0, 1.0 ); }',
        ].join('\n');

        function compileShader(type, src) {
            var shader = gl.createShader(type);
            gl.shaderSource(shader, src);
            gl.compileShader(shader);
            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                console.error('[sifency-shader] compile error:', gl.getShaderInfoLog(shader));
                gl.deleteShader(shader);
                return null;
            }
            return shader;
        }

        var vert = compileShader(gl.VERTEX_SHADER, vertSrc);
        var frag = compileShader(gl.FRAGMENT_SHADER, buildFragShader(cfg));
        if (!vert || !frag) {
            return;
        }

        var prog = gl.createProgram();
        gl.attachShader(prog, vert);
        gl.attachShader(prog, frag);
        gl.linkProgram(prog);
        if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
            console.error('[sifency-shader] link error:', gl.getProgramInfoLog(prog));
            return;
        }
        gl.useProgram(prog);

        // Fullscreen quad.
        var buf = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buf);
        gl.bufferData(gl.ARRAY_BUFFER,
            new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
            gl.STATIC_DRAW
        );
        var aPos = gl.getAttribLocation(prog, 'a_pos');
        gl.enableVertexAttribArray(aPos);
        gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

        // Uniforms.
        var uTime = gl.getUniformLocation(prog, 'u_time');
        var uRes = gl.getUniformLocation(prog, 'u_res');
        gl.uniform2f(uRes, canvas.width, canvas.height);

        // Render loop.
        var startTime = performance.now();
        var raf = null;

        function frame() {
            var elapsed = (performance.now() - startTime) / 1000;
            gl.uniform1f(uTime, elapsed);
            gl.viewport(0, 0, canvas.width, canvas.height);
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
            raf = requestAnimationFrame(frame);
        }

        function startLoop() {
            if (!raf) { frame(); }
        }

        function stopLoop() {
            if (raf) { cancelAnimationFrame(raf); raf = null; }
        }

        // Intersection observer — pause when off-screen.
        if (cfg.pauseOffscreen && 'IntersectionObserver' in window) {
            var observer = new IntersectionObserver(function (entries) {
                entries.forEach(function (entry) {
                    if (entry.isIntersecting) { startLoop(); } else { stopLoop(); }
                });
            }, { threshold: 0.01 });
            observer.observe(wrap);
        } else {
            startLoop();
        }

        // Resize handler.
        var resizeTimer;
        window.addEventListener('resize', function () {
            clearTimeout(resizeTimer);
            resizeTimer = setTimeout(function () {
                var nW = wrap.offsetWidth || 680;
                var nH = canvas.offsetHeight || 500;
                canvas.width = Math.round(nW * dpr);
                canvas.height = Math.round(nH * dpr);
                gl.viewport(0, 0, canvas.width, canvas.height);
                gl.uniform2f(uRes, canvas.width, canvas.height);
            }, 150);
        });
    }

    /* ── boot helpers ────────────────────────────── */

    /**
     * Find every un-initialised shader wrapper inside `scope` and init it.
     *
     * @param {Document|HTMLElement} scope
     */
    function initAllInScope(scope) {
        var wraps = (scope || document).querySelectorAll('.sifency-shader-wrap:not([data-shader-ready])');
        wraps.forEach(function (wrap) {
            wrap.setAttribute('data-shader-ready', '1');
            initShader(wrap);
        });
    }

    /* ── Elementor frontend integration ──────────── */

    /**
     * Register an Elementor widget handler so the shader re-initialises
     * whenever the user updates a control in the editor panel.
     */
    function registerElementorHandler() {
        if (typeof elementorFrontend === 'undefined') {
            return;
        }

        elementorFrontend.hooks.addAction(
            'frontend/element_ready/sifency-shader-overlay.default',
            function ($scope) {
                // Remove ready flag so initAllInScope treats this as fresh.
                var wrap = $scope[0] ? $scope[0].querySelector('.sifency-shader-wrap') : null;
                if (wrap) {
                    wrap.removeAttribute('data-shader-ready');
                }
                initAllInScope($scope[0]);
            }
        );
    }

    /* ── DOM ready ───────────────────────────────── */

    function onReady() {
        initAllInScope(document);
        registerElementorHandler();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', onReady);
    } else {
        onReady();
    }

})();