/**
 * Audace — Pure-JS sticky-slide effect for `.is-style-slides-container`.
 *
 * Improvements over v1:
 * - topOffset() is re-read on every update() instead of cached at build
 *   time, so address-bar show/hide on iOS never misaligns the slides.
 * - cLeft / cWidth are re-read on every update() for the same reason
 *   (scrollbar appearing / disappearing shifts layout).
 * - A post-layout rAF double-buffer in build() ensures getBoundingClientRect
 *   is read after the browser has committed the new container height, killing
 *   the one-frame gap between slides.
 * - tallestOverflowingCol uses only scrollHeight (rounded) to avoid the
 *   offsetHeight / scrollHeight disagreement that caused spurious 1-px pins.
 * - All slides below the currently-exiting last slide receive the same
 *   containerShift, so no lower-z slide peeks out during fast scrolls.
 * - will-change is cleared in clearSlide() / teardown().
 * - Mobile: bottom-sticky cover height is measured from the DOM instead of
 *   being hardcoded as 40vh.
 * - A ResizeObserver watches each container so content changes (accordions,
 *   lazy images) trigger a rebuild without a full-page resize.
 */
(function () {
    'use strict';

    var MIN_WIDTH          = 782;
    var CONTAINER_SELECTOR = '.is-style-slides-container';
    var SLIDE_SELECTOR     = ':scope > .is-style-slide';

    // containers: Array<{
    //   el, slides: Array<{ el, tallCol, entry, pin, exit, start }>,
    //   total, resizeObs
    // }>
    var containers = [];
    var rafId      = 0;
    var scheduled  = false;

    // ─── Helpers ──────────────────────────────────────────────────────────────

    function topOffset() {
        var adminBar = document.getElementById('wpadminbar');
        var header   = document.querySelector(
            '.wp-block-template-part.site-header,' +
            'header.wp-block-template-part,' +
            '#masthead'
        );
        var adminH  = adminBar ? adminBar.getBoundingClientRect().height : 0;
        var headerH = header   ? header.getBoundingClientRect().height   : 0;
        return adminH + headerH;
    }

    // Returns the measured left edge and width of an element in page coords.
    // Re-computed on every update() so layout shifts (scrollbar, reflow) never
    // leave slides misaligned.
    function layoutRect(el) {
        var r = el.getBoundingClientRect();
        return { left: r.left + window.scrollX, width: r.width };
    }

    function ensureSlideInner(slide) {
        var first = slide.firstElementChild;
        if (
            slide.children.length === 1 &&
            first &&
            first.classList.contains('audace-slide-inner')
        ) {
            first.style.display = 'flow-root';
            return first;
        }
        var inner = slide.ownerDocument.createElement('div');
        inner.className = 'audace-slide-inner';
        inner.style.display = 'flow-root';
        while (slide.firstChild) inner.appendChild(slide.firstChild);
        slide.appendChild(inner);
        return inner;
    }

    // Measure the bottom-sticky cover's actual height instead of hardcoding 40 vh.
    function bottomStickyHeight(slide) {
        var cover = slide.querySelector(
            ':scope > .wp-block-column:nth-child(2) .wp-block-cover.is-style-bottom-sticky'
        );
        return cover ? cover.getBoundingClientRect().height : 0;
    }

    function tallestOverflowingCol(slide, maxH) {
        var cols = slide.querySelectorAll(':scope > .wp-block-column');
        if (cols.length) {
            var pick   = null;
            var picked = 0;
            for (var i = 0; i < cols.length; i++) {
                // Round scrollHeight to avoid sub-pixel disagreements.
                var over = Math.round(cols[i].scrollHeight) - maxH;
                if (over > picked) { picked = over; pick = cols[i]; }
            }
            return pick;
        }
        if (Math.round(slide.scrollHeight) > maxH + 1) {
            return ensureSlideInner(slide);
        }
        return null;
    }

    function clearSlide(slide) {
        [
            'position','top','left','right','width','height',
            'zIndex','transform','visibility','overflow',
            'justifyContent','alignItems','willChange'
        ].forEach(function (p) { slide.style[p] = ''; });

        slide.querySelectorAll(
            ':scope > .wp-block-column, :scope > .audace-slide-inner'
        ).forEach(function (c) {
            c.style.transform = '';
            c.style.translate  = '';
            c.style.willChange = '';
        });
    }

    // ─── Core build / teardown ────────────────────────────────────────────────

    function teardown() {
        containers.forEach(function (c) {
            c.el.style.height   = '';
            c.el.style.position = '';
            if (c.resizeObs) c.resizeObs.disconnect();
            c.slides.forEach(function (s) { clearSlide(s.el); });
        });
        containers = [];
    }

    function build() {
        teardown();

        var topH   = topOffset();
        var slideH = window.innerHeight - topH;
        var mobile = window.innerWidth < MIN_WIDTH;

        document.querySelectorAll(CONTAINER_SELECTOR).forEach(function (container) {
            var slideEls = container.querySelectorAll(SLIDE_SELECTOR);
            if (!slideEls.length) return;

            var slides = [];
            var cursor = 0;

            slideEls.forEach(function (el, idx) {
                var bsH    = mobile ? bottomStickyHeight(el) : 0;
                var colMaxH = slideH - bsH;
                var tall   = tallestOverflowingCol(el, colMaxH);

                var availH = colMaxH;
                if (tall && tall.classList.contains('audace-slide-inner')) {
                    var cs = getComputedStyle(el);
                    availH = colMaxH -
                        (parseFloat(cs.paddingTop)    || 0) -
                        (parseFloat(cs.paddingBottom) || 0);
                    el.style.justifyContent = 'flex-start';
                    el.style.alignItems     = 'stretch';
                }

                var tallH = tall
                    ? Math.round(Math.max(tall.offsetHeight || 0, tall.scrollHeight || 0))
                    : 0;
                var pin   = tall ? Math.max(0, tallH - availH) : 0;
                var entry = idx === 0 ? 0 : slideH;
                var exit  = idx === slideEls.length - 1 ? slideH : 0;

                slides.push({ el: el, tallCol: tall, entry: entry, pin: pin, exit: exit, start: cursor });
                cursor += entry + pin + exit;
            });

            // Set the container height so the page has real scroll distance.
            container.style.position = 'relative';
            container.style.height   = cursor + 'px';

            // Pin slides. left / width are re-read each update(); we only set
            // the properties that don't change per-frame here.
            slideEls.forEach(function (el, idx) {
                el.style.position   = 'fixed';
                el.style.top        = topH + 'px';
                el.style.height     = slideH + 'px';
                el.style.zIndex     = String(idx + 1);
                el.style.overflow   = 'hidden';
                el.style.willChange = 'transform';
            });

            // Watch for content changes (lazy images, accordions, …).
            var obs = null;
            if (typeof ResizeObserver !== 'undefined') {
                obs = new ResizeObserver(scheduleBuild);
                slideEls.forEach(function (el) { obs.observe(el); });
            }

            containers.push({
                el:        container,
                slides:    slides,
                total:     cursor,
                topH:      topH,
                maxH:      slideH,
                resizeObs: obs
            });
        });

        // Read layout positions AFTER the browser has committed the new
        // container height. A double rAF ensures we're past both the style
        // and layout passes so getBoundingClientRect is accurate.
        requestAnimationFrame(function () {
            requestAnimationFrame(function () {
                update();
            });
        });
    }

    // Debounced rebuild triggered by ResizeObserver or width-change resize.
    var buildTimer = 0;
    function scheduleBuild() {
        clearTimeout(buildTimer);
        buildTimer = setTimeout(build, 150);
    }

    // ─── Per-frame update ─────────────────────────────────────────────────────

    function update() {
        var scrollY = window.scrollY;
        // Re-read topOffset every frame — iOS address bar changes innerHeight
        // without a width-change resize, so topH can drift between builds.
        var topH = topOffset();

        containers.forEach(function (c) {
            // Re-read the container's horizontal position every frame.
            var lr   = layoutRect(c.el);
            var cTop = c.el.getBoundingClientRect().top + window.scrollY;
            var p    = scrollY - (cTop - topH);

            var insideContainer = p >= 0 && p <= c.total;

            var last      = c.slides[c.slides.length - 1];
            var exitStart = last.start + last.entry + last.pin;
            var containerShift = p > exitStart
                ? -Math.min(p - exitStart, last.exit)
                : 0;

            c.slides.forEach(function (s, idx) {
                var local = p - s.start;

                // Re-apply horizontal position every frame.
                s.el.style.left  = lr.left  + 'px';
                s.el.style.width = lr.width + 'px';
                // Re-apply top in case topH drifted (iOS address bar).
                s.el.style.top   = topH + 'px';

                var translateY = 0;
                var colOffset  = 0;
                var visible    = insideContainer;

                if (!insideContainer) {
                    // Outside the container window — hide completely.
                    visible = false;
                } else if (local < 0) {
                    // Below the viewport, waiting for its entry phase.
                    translateY = c.maxH;
                    visible    = false;
                } else if (local < s.entry) {
                    // Entry phase: slide rises from below.
                    translateY = c.maxH - local;
                } else if (local < s.entry + s.pin) {
                    // Pin phase: slide is stationary; tall column translates up.
                    colOffset = local - s.entry;
                } else {
                    // Past its own phases; the container-wide exit carries it off.
                    colOffset = s.pin;
                }

                // Apply the container-wide exit shift to ALL slides so no
                // lower-z slide peeks out during fast scrolls.
                translateY += containerShift;

                s.el.style.transform  = 'translate3d(0,' + translateY + 'px,0)';
                s.el.style.visibility = visible ? '' : 'hidden';

                if (s.tallCol) {
                    var isInner = s.tallCol.classList.contains('audace-slide-inner');
                    if (isInner) {
                        s.tallCol.style.translate = colOffset
                            ? '0 ' + (-colOffset) + 'px'
                            : '';
                    } else {
                        s.tallCol.style.transform = colOffset
                            ? 'translate3d(0,' + (-colOffset) + 'px,0)'
                            : '';
                    }
                }
            });
        });
    }

    // Throttle update() to one call per animation frame.
    function scheduleUpdate() {
        if (scheduled) return;
        scheduled = true;
        rafId = requestAnimationFrame(function () {
            scheduled = false;
            if (containers.length) update();
        });
    }

    // ─── Resize handling ──────────────────────────────────────────────────────

    var lastBuildW = window.innerWidth;
    function onResize() {
        // iOS Safari fires resize when only the address bar changes — that
        // changes innerHeight but NOT innerWidth. We only rebuild on true
        // width changes (orientation / window resize) to avoid mid-scroll
        // jumps. update() re-reads topH every frame, so address-bar drift
        // is absorbed without a rebuild.
        if (window.innerWidth === lastBuildW) return;
        lastBuildW = window.innerWidth;
        scheduleBuild();
    }

    // ─── Public API ───────────────────────────────────────────────────────────

    window.audaceScrollYFor = function (el) {
        if (!el) return null;
        var topH = topOffset();
        for (var i = 0; i < containers.length; i++) {
            var c = containers[i];
            for (var j = 0; j < c.slides.length; j++) {
                var slide = c.slides[j];
                if (slide.el === el || slide.el.contains(el)) {
                    var cRect = c.el.getBoundingClientRect();
                    var cTop  = cRect.top + window.scrollY;
                    return (cTop - topH) + slide.start + slide.entry;
                }
            }
        }
        var r = el.getBoundingClientRect();
        return window.scrollY + r.top - topH;
    };

    // Allow external code (e.g. slide-scroll-lock.js) to ask for the current
    // pin state of a slide, so it can align its trigger thresholds accurately.
    window.audaceSlideState = function () {
        return containers;
    };

    // ─── Boot ─────────────────────────────────────────────────────────────────

    function init() {
        build();
        window.addEventListener('scroll', scheduleUpdate, { passive: true });
        window.addEventListener('resize', onResize);
        // Rebuild after all resources (fonts, images) have loaded — they can
        // change element heights and invalidate the scroll budget.
        window.addEventListener('load', build);
    }

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