<?php
declare(strict_types=1);

if (!defined('ABSPATH')) { exit; }

/**
 * Animator (Logo Animator) element renderer (shared).
 *
 * Phase 1, step 28. The Animator was 3,000+ lines of inline JS+CSS embedded
 * in PHP — every page that included even one animator block shipped 134KB
 * of <script> body inline. The block also had drifted between frontend and
 * editor: editor was running an older engine that lacked the 3D presets,
 * gyroscope handler, depth-of-field blur, and per-device data-attribute
 * resolver. This refactor:
 *
 *   1. Moves the entire engine to assets/js/impulse-animator.js (canonical
 *      frontend version) and assets/css/impulse-animator.css. Browsers cache
 *      them; the per-page bytes drop from 134KB inline to a single
 *      <link>+<script src> pair (singleton-gated as before).
 *
 *   2. Editor preview now loads the same engine as the published page,
 *      so 3D presets, gyro, and depth-of-field finally work in the canvas.
 *
 *   3. Unifies the PHP shell: settings → markup → data-attributes. The
 *      engine reads everything from data-* at runtime, including per-device
 *      overrides via the _laDV helper inside the static JS.
 *
 *   4. Per-device data emission: frontend canonical. _laDA/_laDMs closures
 *      that previously lived inline are private helpers here, used to emit
 *      data-{key}-tablet / data-{key}-mobile when the setting was saved
 *      with a tablet/mobile override.
 *
 * Pre-extraction differences resolved:
 *
 *   - Editor used `impulse_device_val()` to pre-resolve the current device's
 *     value at PHP time. Dropped — the static JS does this dynamically with
 *     `_laDV(el, key, default)` so resize triggers correct re-evaluation.
 *
 *   - Editor emitted `data-flash` (a removed legacy toggle). Frontend never
 *     did. Dropped from the unified renderer.
 *
 *   - Frontend respected `$_hdrForceImmediate` (a page-level flag that
 *     disables scroll-trigger for above-fold animators). Editor ignored
 *     it. Unified: both contexts check the global; editor never sets it
 *     so behavior is unchanged in the canvas.
 *
 *   - Frontend used `impulse_auto_mobile_defaults()` to materialise mobile
 *     overrides from missing settings. Preserved — only called when the
 *     function exists (frontend bootstrap) so editor isn't affected.
 *
 * @sideeffect Sets $GLOBALS['impulse_needs_three'] when preset starts with
 *             '3d_', flagging the Three.js loader to enqueue.
 * @sideeffect Sets $GLOBALS['impulse_needs_heading_tilt'] when la_tilt > 0,
 *             flagging the universal heading-tilt script.
 * @sideeffect Singleton emit of the engine's <link>+<script> via
 *             $GLOBALS['impulse_animator_js_emitted'].
 */
function impulse_element_render_title_animated($el, array $settings, string $context): string {
    if (is_object($el)) $el = (array) $el;
    $isEditor = ($context === 'editor');

    // Element id — frontend uses raw $el['id'], editor used the legacy $id
    // extracted at the top of its switch. The legacy $id and $el['id'] are
    // the same value; just read from $el directly.
    $rawId  = (string) ($el['id'] ?? uniqid());
    $laElId = 'la-' . preg_replace('/[^a-z0-9_-]/i', '', $rawId);

    // Materialise tablet/mobile defaults from missing keys. The function
    // is defined in includes/impulse-animation-helpers.php (shared between
    // frontend bootstrap and editor preview as of 2026-05-26), so this
    // now runs in BOTH contexts — without it the editor mobile tab
    // rendered raw desktop sizes until the user touched a setting.
    if (function_exists('impulse_auto_mobile_defaults')) {
        impulse_auto_mobile_defaults($settings, 'title', 'tablet');
        impulse_auto_mobile_defaults($settings, 'title', 'mobile');
    }

    // ── Settings ────────────────────────────────────────────────────
    $laContentType = (string) ($settings['la_content_type'] ?? 'text');
    // Frontend canonical: empty fallback (placeholder 'Your text' leaked to
    // published pages). Editor showed 'Logo or Heading' — but the live engine
    // re-reads the asset from DOM so the placeholder serves no purpose.
    $laText        = (string) ($settings['la_text'] ?? '');
    // Per-device sizing (editor). The preview iframe does NOT print the
    // responsive @media CSS — those rules live on the frontend's
    // instinctor-page-inline stylesheet — so on the Mobile/Tablet tab the
    // asset kept its desktop inline size and the canvas rasterised oversized
    // (iframe is ~430px but the asset's computed font stayed 84px). Resolve
    // the size keys against the preview device here, the same way the align
    // block below resolves alignment, so the editor bakes the correct
    // per-device size inline. On the frontend $_devForSize is 'desktop', so
    // this returns the desktop value unchanged and the @media override still
    // drives tablet/mobile there. Sergei 2026-05-28.
    $_devForSize = $isEditor ? ($GLOBALS['impulse_preview_device'] ?? 'desktop') : 'desktop';
    $_laSizeVal  = function (string $key, int $default) use ($settings, $_devForSize) {
        return function_exists('impulse_device_val')
            ? intval(impulse_device_val($settings, $key, $_devForSize, $default))
            : intval($settings[$key] ?? $default);
    };
    $laFontSize    = $_laSizeVal('la_font_size', 64);
    $laColor       = (string) ($settings['la_color']    ?? '#1d2327');
    $laWeight      = (string) ($settings['la_font_weight'] ?? '700');
    $laImageSrc    = (string) ($settings['la_image_src'] ?? '');
    $laImageWidth  = $_laSizeVal('la_image_width', 280);
    $laBg          = (string) ($settings['la_bg']        ?? 'transparent');
    $laPreset      = (string) ($settings['la_preset']    ?? 'shatter_in');
    $laSpring      = intval($settings['la_spring']       ?? 5);
    // 2026-05-23: animator container's data-tilt drives the mouse
    // parallax in the animator's tick() loop (impulse-animator.js,
    // line ~3000). When invoked from the Title (heading) block, the
    // panel's Sensitivity slider writes to `mouse_sens` (0..50 range,
    // clamped to 30 below by impulse_la_postUpdate). Read mouse_sens
    // first so heading-driven animator titles get mouse interaction;
    // fall back to la_tilt for standalone Animator blocks / legacy.
    $laTilt        = max(0, min(50, intval($settings['mouse_sens'] ?? $settings['la_tilt'] ?? 0)));
    $laDepth       = intval($settings['la_depth']        ?? 50);
    // la_ring_arc retired 2026-05-23. Slider removed from panel; engine
    // (_ringWarp / applyRingBend) deleted from impulse-animator.js.
    $laGapBelow    = $_laSizeVal('la_gap_below', 0);
    $laEasing      = (string) ($settings['la_easing']    ?? 'ease-in-out');
    $laFontFamily  = (string) ($settings['font_family']  ?? '');
    // Align is per-device. Editor resolves it server-side from the
    // preview-device global; frontend emits desktop inline + @media
    // overrides at the end of the markup (search for `$alignTab`
    // below). Reading only `settings['align']` here was the bug behind
    // "Sell anything, natively" left-align ignored on mobile when the
    // heading uses an animator preset (la_preset != ''): the panel
    // saved align_mobile fine, but $laAlign stuck to the desktop
    // value, so the wrapper's flex justify-content stayed at center.
    // Locked 2026-05-27.
    $laAlignDeskRaw = (string) ($settings['align']        ?? 'center');
    $laAlignTabRaw  = (string) ($settings['align_tablet'] ?? '');
    $laAlignMobRaw  = (string) ($settings['align_mobile'] ?? '');
    if ($isEditor) {
        $_devForAlign = $GLOBALS['impulse_preview_device'] ?? 'desktop';
        if      ($_devForAlign === 'mobile' && $laAlignMobRaw !== '') $laAlign = $laAlignMobRaw;
        elseif  ($_devForAlign === 'tablet' && $laAlignTabRaw !== '') $laAlign = $laAlignTabRaw;
        else                                                          $laAlign = $laAlignDeskRaw;
    } else {
        $laAlign = $laAlignDeskRaw;
    }

    // Duration/delay: stored as float seconds in the DB; emit as integer ms.
    $laDurRaw   = floatval($settings['la_duration'] ?? 1.2);
    $laDuration = $laDurRaw > 10 ? intval($laDurRaw) : intval(round($laDurRaw * 1000));
    $laDelayRaw = floatval($settings['la_delay']    ?? 0);
    $laDelay    = $laDelayRaw > 10 ? intval($laDelayRaw) : intval(round($laDelayRaw * 1000));

    // 3D presets need Three.js — flag for the loader.
    if (strpos($laPreset, '3d_') === 0) $GLOBALS['impulse_needs_three'] = true;
    // Mouse-tilt routed through the universal heading-tilt script.
    if ($laTilt > 0)                    $GLOBALS['impulse_needs_heading_tilt'] = true;

    // Above-fold force-immediate (from the section header context). Editor
    // never sets this — preview always uses scroll-trigger.
    $forceImmediate = !empty($GLOBALS['_hdrForceImmediate'] ?? null);
    $laScrollTrig   = $forceImmediate ? 0 : 1;

    // ── Inner asset markup ──────────────────────────────────────────
    $laInner = '';
    if ($laContentType === 'image' && $laImageSrc) {
        $laSvgColors = (string) ($settings['la_svg_colors'] ?? '');
        $isSvg       = function_exists('str_ends_with')
            ? str_ends_with(strtolower($laImageSrc), '.svg')
            : (substr(strtolower($laImageSrc), -4) === '.svg');
        $laSvgAttr   = ($laSvgColors && $isSvg)
            ? ' data-svg-colors="' . esc_attr($laSvgColors) . '"'
            : '';
        $laInner = '<img src="' . esc_attr($laImageSrc) . '" class="impulse-la-asset"' . $laSvgAttr
                 . ' style="width:' . $laImageWidth . 'px;max-width:100%;display:block;opacity:0" alt="">';
    } else {
        // Main colour swatch (la_color) IS the gradient start. The legacy
        // `la_gradient_from` mirror drifts out of sync — ignored.
        $laGradFrom  = $laColor;
        $laGradTo    = (string) ($settings['la_gradient_to']    ?? '');
        $laGradAngle = intval($settings['la_gradient_angle']    ?? 135);
        $laLetterSpacing = isset($settings['la_letter_spacing'])
            ? intval($settings['la_letter_spacing']) . 'px'
            : '-0.01em';

        // Wrap inside narrow containers — flex item default `min-width:auto`
        // (intrinsic) would otherwise let the asset bust max-width.
        $laFontStyle = 'font-size:' . $laFontSize . 'px;'
                     . 'font-weight:' . esc_attr($laWeight) . ';'
                     . 'line-height:1.15;text-align:center;'
                     . 'letter-spacing:' . $laLetterSpacing . ';'
                     . 'opacity:0;max-width:100%;min-width:0;'
                     . 'word-break:break-word;overflow-wrap:break-word';

        if ($laGradTo !== '' && $laGradFrom !== '') {
            // CSS 0deg=up vs canvas/panel 0deg=right — add 90°.
            $laCssAngle = ($laGradAngle + 90) % 360;
            $laFontStyle .= ';background-image:linear-gradient(' . $laCssAngle . 'deg,'
                          . esc_attr($laGradFrom) . ',' . esc_attr($laGradTo) . ');'
                          . '-webkit-background-clip:text;background-clip:text;'
                          . 'color:transparent;-webkit-text-fill-color:transparent';
        } else {
            $laFontStyle .= ';color:' . esc_attr($laColor);
        }
        if ($laFontFamily !== '') {
            // font_family stores a system-font stack with literal " — DO NOT
            // esc_attr() here; $laFontStyle is esc_attr()'d at emission
            // (see laInner below). Double-escape turned " → &amp;quot; and
            // the browser ignored the font-family rule. 2026-05-29.
            $laFontStyle .= ';font-family:' . $laFontFamily;
        }
        $safeText = function_exists('impulse_text_safe_breaks')
            ? impulse_text_safe_breaks($laText)
            : nl2br(esc_html($laText));
        $laInner = '<div class="impulse-la-asset" style="' . esc_attr($laFontStyle) . '">' . $safeText . '</div>';
    }

    // ── Per-device data-attribute emitters ──────────────────────────
    // _laDA: integer override (font sizes, dims).
    // _laDMs: seconds-stored timing → ms.
    $da = static function (string $key, string $attr) use ($settings): string {
        $o = '';
        foreach (['tablet', 'mobile'] as $d) {
            $dk = $key . '_' . $d;
            if (isset($settings[$dk]) && $settings[$dk] !== '') {
                $o .= ' data-' . $attr . '-' . $d . '="' . intval($settings[$dk]) . '"';
            }
        }
        return $o;
    };
    $dms = static function (string $key, string $attr) use ($settings): string {
        $o = '';
        foreach (['tablet', 'mobile'] as $d) {
            $dk = $key . '_' . $d;
            if (isset($settings[$dk]) && $settings[$dk] !== '') {
                $r = floatval($settings[$dk]);
                $o .= ' data-' . $attr . '-' . $d . '="' . ($r > 10 ? intval($r) : intval(round($r * 1000))) . '"';
            }
        }
        return $o;
    };

    // ── Wrapper style ───────────────────────────────────────────────
    $laJustify = $laAlign === 'left' ? 'flex-start' : ($laAlign === 'right' ? 'flex-end' : 'center');
    // Halo — soft glow around rendered text/image. The animator renders to
    // <canvas> so a wrapper text-shadow does nothing; filter:drop-shadow on
    // the wrapper traces every child's painted pixels including the canvas.
    $halo    = max(0, min(100, intval($settings['text_halo'] ?? 0)));
    $haloCss = ($halo > 0 && function_exists('impulse_drop_shadow_halo_css'))
        ? impulse_drop_shadow_halo_css($halo, $laColor)
        : '';
    // Editor preview also needs a minimum height so the empty animator is
    // visible in the canvas before the engine paints its first frame.
    $minH = $isEditor ? ';min-height:80px' : '';
    $wrapStyle = 'display:flex;align-items:center;justify-content:' . $laJustify
               . ';background:' . esc_attr($laBg)
               . ($laGapBelow > 0 ? ';margin-bottom:' . $laGapBelow . 'px' : '')
               . ';overflow:visible;position:relative;width:100%' . $minH
               . ';' . $haloCss;

    // ── Build the markup ────────────────────────────────────────────
    $html  = '<div id="' . esc_attr($laElId) . '" class="impulse-logo-animator"';
    $html .= ' data-preset="' . esc_attr($laPreset) . '"';
    $html .= ' data-spring="' . $laSpring . '"' . $da('la_spring', 'spring');
    $html .= ' data-duration="' . $laDuration . '"' . $dms('la_duration', 'duration');
    $html .= ' data-delay="' . $laDelay . '"' . $dms('la_delay', 'delay');
    $html .= ' data-easing="' . esc_attr($laEasing) . '"';
    $html .= ' data-scroll-trigger="' . $laScrollTrig . '"';
    $html .= ' data-tilt="' . $laTilt . '"' . $da('la_tilt', 'tilt');
    $html .= ' data-depth="' . $laDepth . '"' . $da('la_depth', 'depth');
    $html .= ' data-color="' . esc_attr($laColor) . '"';
    $html .= ' data-gradient-from="' . esc_attr($laColor) . '"';
    $html .= ' data-gradient-to="' . esc_attr((string) ($settings['la_gradient_to'] ?? '')) . '"';
    $html .= ' data-gradient-angle="' . intval($settings['la_gradient_angle'] ?? 135) . '"'
           . $da('la_gradient_angle', 'gradient-angle');
    $html .= ' style="' . esc_attr($wrapStyle) . '">';
    $html .= $laInner;
    $html .= '</div>';

    // ── Responsive CSS overrides ────────────────────────────────────
    if (function_exists('impulse_responsive_css') && function_exists('wp_add_inline_style')) {
        $laResp = impulse_responsive_css($settings, '#' . esc_attr($laElId) . ' .impulse-la-asset', [
            'la_font_size'   => ['font-size', 'px'],
            'la_image_width' => ['width', 'px'],
        ]);
        $laRespGap = impulse_responsive_css($settings, '#' . esc_attr($laElId), [
            'la_gap_below' => ['margin-bottom', 'px'],
        ]);
        if ($laResp || $laRespGap) {
            wp_add_inline_style('instinctor-page-inline', $laResp . $laRespGap);
        }
    }

    // ── Singleton: enqueue the engine asset once per page ───────────
    if (!isset($GLOBALS['impulse_animator_js_emitted'])) {
        $GLOBALS['impulse_animator_js_emitted'] = true;
        $base = defined('INSTINCTOR_PLUGIN_URL') ? INSTINCTOR_PLUGIN_URL : '/instinctor-plugin/';
        $ver  = defined('INSTINCTOR_VERSION')    ? INSTINCTOR_VERSION    : '0';
        // Engine CSS + JS via registered handles (flushed in the page footer).
        // The plugin-URL global must exist before the engine boots, so attach
        // it as a 'before' inline script on the engine handle.
        $bootJson = function_exists('wp_json_encode') ? wp_json_encode($base) : wp_json_encode($base);
        \Instinctor\Core\Assets::enqueue('instinctor-animator');
        \Instinctor\Core\Assets::enqueue('instinctor-animator-js');
        if (function_exists('wp_add_inline_script')) {
            wp_add_inline_script('instinctor-animator-js', 'window._instinctorPluginUrl=' . $bootJson . ';', 'before');
        }
    }

    // Frontend per-device align overrides. The wrapper's justify-content is
    // inline (so inline beats any plain CSS rule); we emit @media rules
    // with !important so tablet/mobile align overrides actually take.
    // Editor path resolves align per device server-side above, so this
    // only runs in frontend context. Locked 2026-05-27.
    if (!$isEditor && ($laAlignTabRaw !== '' || $laAlignMobRaw !== '')) {
        $_jcFor = function ($a) {
            return $a === 'left' ? 'flex-start' : ($a === 'right' ? 'flex-end' : 'center');
        };
        if ($laAlignTabRaw !== '' && $laAlignTabRaw !== $laAlignDeskRaw) {
            wp_add_inline_style('instinctor-page-inline', '@media(max-width:1024px){#' . esc_attr($laElId)
                  . '{justify-content:' . $_jcFor($laAlignTabRaw) . ' !important}}');
        }
        if ($laAlignMobRaw !== '' && $laAlignMobRaw !== $laAlignDeskRaw) {
            wp_add_inline_style('instinctor-page-inline', '@media(max-width:767px){#' . esc_attr($laElId)
                  . '{justify-content:' . $_jcFor($laAlignMobRaw) . ' !important}}');
        }
    }

    return $html;
}
