/**
 * Svelte action: applies ambient decorative animations to an element. These
 * sit on top of the morph/transition system — they're continuous loops driven
 * by the same virtual-clock restart registry (__svgAnimRestart) so they reset
 * cleanly on slide enter and align in Flow-mode GIF exports.
 *
 * Effects:
 *   • glow         — pulsing colored box-shadow halo
 *   • shimmer      — diagonal highlight stripe sweeping across the element
 *   • gradientShift — animates background-position on a multi-color gradient
 *   • rgbSplit     — chromatic-aberration-style R/B channel offset (drop-shadow)
 */
// Hexagon and star are rendered by Shape.svelte as REGULAR polygons inscribed
// in min(w, h) and centered. Triangle and ellipse stretch to fill the box.
// Compute clip-path / mask coords from the actual element size so the overlay
// matches the visible silhouette rather than stretching with the bounding box.
function regularPolygonPoints(sides, w, h) {
    const cx = w / 2, cy = h / 2;
    const r = Math.min(w, h) / 2;
    return Array.from({ length: sides }, (_, i) => {
        const a = (i * Math.PI * 2) / sides - Math.PI / 2;
        return { x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) };
    });
}
function regularStarPoints(w, h) {
    const cx = w / 2, cy = h / 2;
    const outerR = Math.min(w, h) / 2;
    const innerR = outerR * 0.4;
    return Array.from({ length: 10 }, (_, i) => {
        const a = (i * Math.PI / 5) - Math.PI / 2;
        const r = i % 2 === 0 ? outerR : innerR;
        return { x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) };
    });
}
function ptsToClipPath(pts) {
    return `polygon(${pts.map(p => `${p.x}px ${p.y}px`).join(', ')})`;
}
function ptsToSvgPolygon(pts) {
    return pts.map(p => `${p.x},${p.y}`).join(' ');
}
function shapeClipPath(shape, w, h) {
    const cx = w / 2, cy = h / 2;
    switch (shape.type) {
        case 'circle': {
            const r = Math.min(w, h) / 2;
            return `circle(${r}px at ${cx}px ${cy}px)`;
        }
        case 'ellipse':
            return `ellipse(${w / 2}px ${h / 2}px at ${cx}px ${cy}px)`;
        case 'triangle':
            return `polygon(${cx}px 0px, 0px ${h}px, ${w}px ${h}px)`;
        case 'hexagon':
            return ptsToClipPath(regularPolygonPoints(6, w, h));
        case 'star':
            return ptsToClipPath(regularStarPoints(w, h));
        case 'rectangle':
            return shape.borderRadius && shape.borderRadius > 0
                ? `inset(0 round ${shape.borderRadius}px)`
                : null;
    }
}
// SVG mask for the gradient border ring. ViewBox matches the element's actual
// pixel size so polygon coords map 1:1, and `vector-effect=non-scaling-stroke`
// keeps the stroke a constant pixel width. The caller pairs this with the
// silhouette clip-path: stroke is centered on the path, so half falls outside
// and gets trimmed — leaving an inner ring of `borderWidth` px. We therefore
// set stroke-width to `borderWidth * 2`.
function silhouetteMaskUrl(shapeType, borderWidth, w, h) {
    const sw = borderWidth * 2;
    const common = `vector-effect="non-scaling-stroke" fill="none" stroke="#fff" stroke-width="${sw}"`;
    const cx = w / 2, cy = h / 2;
    let inner = '';
    switch (shapeType) {
        case 'circle': {
            const r = Math.min(w, h) / 2;
            inner = `<circle cx="${cx}" cy="${cy}" r="${r}" ${common}/>`;
            break;
        }
        case 'ellipse':
            inner = `<ellipse cx="${cx}" cy="${cy}" rx="${w / 2}" ry="${h / 2}" ${common}/>`;
            break;
        case 'triangle':
            inner = `<polygon points="${cx},0 0,${h} ${w},${h}" ${common}/>`;
            break;
        case 'hexagon':
            inner = `<polygon points="${ptsToSvgPolygon(regularPolygonPoints(6, w, h))}" ${common}/>`;
            break;
        case 'star':
            inner = `<polygon points="${ptsToSvgPolygon(regularStarPoints(w, h))}" ${common}/>`;
            break;
        default:
            inner = `<rect x="0" y="0" width="${w}" height="${h}" ${common}/>`;
    }
    const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none">${inner}</svg>`;
    return `url("data:image/svg+xml;utf8,${encodeURIComponent(svg)}")`;
}
function effectiveCycle(requested, slideDuration) {
    if (slideDuration && slideDuration > 0) {
        return slideDuration / Math.max(1, Math.round(slideDuration / requested));
    }
    return requested;
}
export function decorations(node, params) {
    let raf = 0;
    let shimmerEl = null;
    // Dedicated child overlay for the animated gradient background. We CANNOT
    // put the gradient on the wrapper itself if a shape clip is needed —
    // `clip-path` on the wrapper would also clip the wrapper's `filter:
    // drop-shadow` (the glow halo), so the glow can never extend past the
    // silhouette. By putting the gradient on a clipped child, the wrapper
    // stays unclipped and the glow halo renders correctly outside the shape.
    let gradientEl = null;
    let resizeObs = null;
    let originalBoxShadow = '';
    let originalFilter = '';
    let savedOriginal = false;
    function saveOriginalStyles() {
        if (savedOriginal)
            return;
        originalBoxShadow = node.style.boxShadow;
        originalFilter = node.style.filter;
        savedOriginal = true;
    }
    function restoreOriginalStyles() {
        node.style.boxShadow = originalBoxShadow;
        node.style.filter = originalFilter;
        if (shimmerEl) {
            shimmerEl.remove();
            shimmerEl = null;
        }
        if (gradientEl) {
            gradientEl.remove();
            gradientEl = null;
        }
    }
    function reset() {
        if (raf)
            cancelAnimationFrame(raf);
        raf = 0;
        if (resizeObs) {
            resizeObs.disconnect();
            resizeObs = null;
        }
        restoreOriginalStyles();
    }
    function run() {
        reset();
        const cfg = params.config;
        if (!cfg)
            return;
        const anyEnabled = cfg.glow?.enabled || cfg.shimmer?.enabled || cfg.gradientShift?.enabled || cfg.rgbSplit?.enabled;
        if (!anyEnabled)
            return;
        saveOriginalStyles();
        // Shape clip-path / silhouette mask are SIZE-AWARE: hexagon and star are
        // rendered as REGULAR polygons inscribed in min(w, h), so % coords would
        // stretch on non-square boxes and miss the actual shape. We compute in
        // pixels from the element's current size and refresh on resize.
        const useSilhouetteMask = !!(params.shape && params.shape.type !== 'rectangle');
        const borderWidth = params.config?.gradientShift?.borderWidth ?? 3;
        function applyShapeOverlays() {
            if (!params.shape)
                return;
            const w = node.offsetWidth;
            const h = node.offsetHeight;
            if (w <= 0 || h <= 0)
                return;
            const clip = shapeClipPath(params.shape, w, h);
            if (shimmerEl && clip)
                shimmerEl.style.clipPath = clip;
            if (gradientEl && useSilhouetteMask) {
                if (clip)
                    gradientEl.style.clipPath = clip;
                const url = silhouetteMaskUrl(params.shape.type, borderWidth, w, h);
                gradientEl.style.webkitMaskImage = url;
                gradientEl.style.maskImage = url;
            }
        }
        // Initial computation. Falls back to null when the element has no shape
        // info (text/code/icon/image/rect) — those use static rules.
        const shapeClip = params.shape
            ? shapeClipPath(params.shape, node.offsetWidth || 1, node.offsetHeight || 1)
            : null;
        // Shimmer: a child overlay with the diagonal stripe gradient. Clipped
        // to the shape silhouette individually so circle/hexagon/etc. shapes
        // don't get rectangular shimmer.
        if (cfg.shimmer?.enabled) {
            if (getComputedStyle(node).position === 'static') {
                node.style.position = 'relative';
            }
            const angle = cfg.shimmer.angle ?? 110;
            const widthPct = cfg.shimmer.widthPct ?? 25;
            const color = cfg.shimmer.color ?? 'rgba(255, 255, 255, 0.4)';
            shimmerEl = document.createElement('div');
            shimmerEl.className = 'animot-shimmer';
            Object.assign(shimmerEl.style, {
                position: 'absolute',
                inset: '0',
                pointerEvents: 'none',
                overflow: 'hidden',
                borderRadius: 'inherit',
                background: `linear-gradient(${angle}deg, transparent 0%, transparent ${50 - widthPct / 2}%, ${color} 50%, transparent ${50 + widthPct / 2}%, transparent 100%)`,
                backgroundSize: '300% 300%',
                backgroundRepeat: 'no-repeat',
                backgroundPosition: '-100% -100%',
                willChange: 'background-position',
                ...(shapeClip ? { clipPath: shapeClip } : {})
            });
            node.appendChild(shimmerEl);
        }
        // Animated gradient: border-only ring along the element's silhouette.
        // The user's fillColor stays visible inside; only the perimeter receives
        // the animated gradient. Two strategies:
        //  • Polygon / curved shapes (circle, ellipse, triangle, hexagon, star):
        //    SVG mask of a stroked silhouette (vector-effect=non-scaling-stroke)
        //    combined with the shape clip-path that trims the outer half of the
        //    stroke — the inner half (= borderWidth px) forms the visible ring.
        //  • Rectangles / non-shape elements (text, code, image, icon):
        //    classic CSS gradient-border trick — transparent border + two-layer
        //    mask with `mask-composite: exclude` so only the border ring shows.
        //    Respects border-radius via `borderRadius: inherit`.
        if (cfg.gradientShift?.enabled) {
            if (getComputedStyle(node).position === 'static') {
                node.style.position = 'relative';
            }
            const colors = cfg.gradientShift.colors ?? ['#7c3aed', '#06b6d4', '#ec4899', '#7c3aed'];
            const angle = cfg.gradientShift.angle ?? 135;
            const direction = cfg.gradientShift.direction ?? 'forward';
            gradientEl = document.createElement('div');
            gradientEl.className = 'animot-gradient-shift';
            Object.assign(gradientEl.style, {
                position: 'absolute',
                inset: '0',
                pointerEvents: 'none',
                zIndex: '0'
            });
            if (useSilhouetteMask) {
                gradientEl.style.webkitMaskRepeat = 'no-repeat';
                gradientEl.style.maskRepeat = 'no-repeat';
                gradientEl.style.webkitMaskSize = '100% 100%';
                gradientEl.style.maskSize = '100% 100%';
                // applyShapeOverlays() below sets the actual maskImage + clipPath
                // using current pixel dimensions; ResizeObserver keeps it in sync.
            }
            else {
                gradientEl.style.boxSizing = 'border-box';
                gradientEl.style.border = `${borderWidth}px solid transparent`;
                gradientEl.style.borderRadius = 'inherit';
                const m = 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)';
                gradientEl.style.webkitMask = m;
                gradientEl.style.mask = m;
                gradientEl.style.webkitMaskComposite = 'xor';
                gradientEl.style.maskComposite = 'exclude';
            }
            if (direction === 'chase') {
                const stops = [...colors, colors[0]].join(', ');
                gradientEl.style.backgroundImage = `conic-gradient(from 0deg at 50% 50%, ${stops})`;
                gradientEl.style.backgroundSize = '100% 100%';
                gradientEl.style.backgroundPosition = '0 0';
            }
            else {
                gradientEl.style.backgroundImage = `linear-gradient(${angle}deg, ${colors.join(', ')})`;
                gradientEl.style.backgroundSize = '400% 400%';
            }
            // Insert as the FIRST child so it sits behind the actual element
            // content (text, code block, etc.) instead of covering it.
            node.insertBefore(gradientEl, node.firstChild);
        }
        // Apply size-aware clip-path / silhouette mask now, then keep them in
        // sync with element resizes (drag-resize, parent transform changes,
        // initial layout passes that report 0×0 then resolve later).
        if (params.shape) {
            applyShapeOverlays();
            resizeObs = new ResizeObserver(() => applyShapeOverlays());
            resizeObs.observe(node);
        }
        const start = performance.now();
        function tick(now) {
            // Re-alias cfg as non-null inside the tick closure — TS loses the
            // `if (!cfg) return` narrowing across the function boundary,
            // otherwise every cfg.* access flags as "possibly undefined".
            const c = cfg;
            const elapsed = now - start;
            // GLOW — pulsing halo. Uses `filter: drop-shadow` (not box-shadow)
            // so the halo follows the element's actual ALPHA MASK — SVG shapes,
            // icons, and PNGs with transparency get a halo around the visible
            // silhouette, not around their rectangular bounding box. Composed
            // with rgbSplit below into a single filter string.
            let glowFilter = '';
            if (c.glow?.enabled) {
                const cycle = effectiveCycle(c.glow.speedMs ?? 2400, params.slideDuration);
                const phase = (elapsed % cycle) / cycle;
                const wave = (Math.sin(phase * Math.PI * 2) + 1) / 2;
                const intensity = c.glow.intensity ?? 0.6;
                const blur = 6 + wave * 24 * intensity;
                // Two stacked drop-shadows give a denser halo than a single one
                // (drop-shadow has no `spread` parameter, so we layer them).
                glowFilter = `drop-shadow(0 0 ${blur}px ${c.glow.color}) drop-shadow(0 0 ${blur * 0.4}px ${c.glow.color})`;
            }
            // SHIMMER — sweep the stretched gradient ALONG its own gradient
            // direction (derived from `angle`) so the stripe glides perpendicular
            // to itself for any angle.
            //
            // Optional `randomness` (0–1) jitters each cycle's duration and
            // inserts a random pause between sweeps so the effect feels alive
            // instead of metronomic. Per-cycle state (start time, duration,
            // pause length) is stashed on the wrapper so we don't burn a closure
            // rebuild every frame.
            if (c.shimmer?.enabled && shimmerEl) {
                const baseSpeed = c.shimmer.speedMs ?? 3000;
                const baseCycle = effectiveCycle(baseSpeed, params.slideDuration);
                const randomness = Math.max(0, Math.min(1, c.shimmer.randomness ?? 0));
                const w = shimmerEl;
                if (typeof w.__shimCycleStart !== 'number') {
                    w.__shimCycleStart = elapsed;
                    w.__shimCycleDur = baseCycle;
                    w.__shimCyclePause = 0;
                    w.__shimSeed = 1;
                }
                let local = elapsed - w.__shimCycleStart;
                const total = w.__shimCycleDur + w.__shimCyclePause;
                if (local >= total) {
                    // Roll a new cycle. Hash-based pseudo-random keeps the sequence
                    // deterministic for a given seed — same shimmer plays back the
                    // same way on /present reload, useful when reviewing.
                    w.__shimSeed = (w.__shimSeed * 1103515245 + 12345) & 0x7fffffff;
                    const r1 = (w.__shimSeed % 1000) / 1000;
                    const r2 = ((w.__shimSeed >> 8) % 1000) / 1000;
                    // Duration multiplier: at randomness=1 it's 0.5×–2× base; at
                    // randomness=0 it's exactly base.
                    const jitter = (r1 * 2 - 1) * randomness; // -randomness..+randomness
                    const durMul = jitter >= 0 ? 1 + jitter : 1 / (1 - jitter * 0.5);
                    w.__shimCycleDur = Math.max(300, baseCycle * durMul);
                    // Pause between sweeps: 0 to randomness × baseCycle.
                    w.__shimCyclePause = baseCycle * randomness * r2;
                    w.__shimCycleStart = elapsed;
                    local = 0;
                }
                const angle = c.shimmer.angle ?? 110;
                const rad = angle * Math.PI / 180;
                const dirX = Math.sin(rad);
                const dirY = -Math.cos(rad);
                if (local > w.__shimCycleDur) {
                    // Pause window — park the stripe far offscreen so nothing renders.
                    shimmerEl.style.backgroundPosition = `-300% -300%`;
                }
                else {
                    const phase = local / w.__shimCycleDur;
                    const offset = (phase - 0.5) * 300;
                    const posX = 50 + offset * dirX;
                    const posY = 50 + offset * dirY;
                    shimmerEl.style.backgroundPosition = `${posX}% ${posY}%`;
                }
            }
            // GRADIENT SHIFT — direction modes:
            //   • forward — phase ramps 0 → 1, position increases monotonically
            //   • reverse — phase ramps 1 → 0
            //   • snake   — phase oscillates via sin so the gradient slithers
            //               back and forth along its own angle
            //   • chase   — colors rotate around the element via a conic gradient
            //               (with 4 colors this is the "each color shifts to the
            //               next side" pinwheel)
            // Sweep direction (for non-chase modes) is derived from `angle` so
            // the gradient flows perpendicular to its own bands.
            if (c.gradientShift?.enabled) {
                const cycle = effectiveCycle(c.gradientShift.speedMs ?? 6000, params.slideDuration);
                const direction = c.gradientShift.direction ?? 'forward';
                // Write to the gradient child element instead of the wrapper —
                // the wrapper carries the glow filter, which would be clipped
                // if the wrapper itself had clip-path.
                const target = gradientEl ?? node;
                if (direction === 'chase') {
                    const colors = c.gradientShift.colors ?? ['#7c3aed', '#06b6d4', '#ec4899', '#7c3aed'];
                    const stops = [...colors, colors[0]].join(', ');
                    const rotateDeg = ((elapsed % cycle) / cycle) * 360;
                    target.style.backgroundImage = `conic-gradient(from ${rotateDeg}deg at 50% 50%, ${stops})`;
                }
                else {
                    const angle = c.gradientShift.angle ?? 135;
                    const rad = angle * Math.PI / 180;
                    const dirX = Math.sin(rad);
                    const dirY = -Math.cos(rad);
                    let phase;
                    if (direction === 'snake') {
                        phase = (Math.sin((elapsed / cycle) * Math.PI * 2) + 1) / 2;
                    }
                    else if (direction === 'reverse') {
                        phase = 1 - ((elapsed % cycle) / cycle);
                    }
                    else {
                        phase = (elapsed % cycle) / cycle;
                    }
                    const range = 200;
                    const offset = (phase - 0.5) * range;
                    const posX = 50 + offset * dirX;
                    const posY = 50 + offset * dirY;
                    target.style.backgroundPosition = `${posX}% ${posY}%`;
                }
            }
            // RGB SPLIT — chromatic aberration via stacked drop-shadows.
            let rgbSplitFilter = '';
            if (c.rgbSplit?.enabled) {
                const offset = c.rgbSplit.offset ?? 3;
                let dx = offset;
                if ((c.rgbSplit.speedMs ?? 0) > 0) {
                    const cycle = effectiveCycle(c.rgbSplit.speedMs, params.slideDuration);
                    const phase = (elapsed % cycle) / cycle;
                    dx = offset * Math.sin(phase * Math.PI * 2);
                }
                rgbSplitFilter = `drop-shadow(${dx}px 0 0 rgba(255, 0, 64, 0.7)) drop-shadow(${-dx}px 0 0 rgba(0, 200, 255, 0.7))`;
            }
            // Compose the user's original filter + glow + rgbSplit into a single
            // `filter` value. Writing each effect to node.style.filter
            // independently would clobber the others, since CSS filter is one
            // string property — not a multi-value list.
            const composed = [originalFilter, glowFilter, rgbSplitFilter].filter(Boolean).join(' ');
            node.style.filter = composed;
            raf = requestAnimationFrame(tick);
        }
        raf = requestAnimationFrame(tick);
    }
    queueMicrotask(run);
    if (typeof window !== 'undefined') {
        const reg = (window.__svgAnimRestart ||= []);
        reg.push(run);
        node.__decorationsRestart = run;
    }
    let lastKey = params.key;
    return {
        update(p) {
            const keyChanged = p.key !== lastKey;
            params = p;
            if (keyChanged) {
                lastKey = p.key;
                queueMicrotask(run);
            }
        },
        destroy() {
            reset();
            if (typeof window !== 'undefined') {
                const reg = window.__svgAnimRestart;
                const fn = node.__decorationsRestart;
                if (reg && fn) {
                    const idx = reg.indexOf(fn);
                    if (idx >= 0)
                        reg.splice(idx, 1);
                }
            }
        }
    };
}
