<?php
/**
 * PageComplexity — heuristic GPU/CPU cost score for an Impulse page.
 *
 * The editor renders this as a VU-style segmented bar in the topbar so
 * designers can see when a page is approaching the "will stutter on
 * mid-range phones" zone before they ship. Advisory only — never blocks.
 *
 * Score model: each costly element contributes a fixed weight. The
 * weights are calibrated so that a page lights up:
 *   - 0–3  segments → comfortable (no badge shown — badge hidden < 3)
 *   - 4–6  segments → heavy, watch on low-end devices
 *   - 7–10 segments → likely to stutter on phones / older laptops
 *
 * Updating this table is the only ongoing maintenance: when a new
 * effect ships, add a row to WEIGHTS or extend one of the per-element
 * inspectors below. There is no auto-detection — better to under-score
 * a new effect (false-green) than over-score (false-red, kills trust).
 *
 * Output shape (also serialised to JSON for the frontend panel):
 *   {
 *     score: int,                 // sum of all contributors
 *     level: 'green'|'yellow'|'red',
 *     contributors: [
 *       { id, type, label, weight, reason }, …
 *     ]
 *   }
 */

namespace Instinctor\Core;


if (!defined('ABSPATH')) exit;
class PageComplexity
{
    /** Page-level (page_settings) heavy effects. */
    private const PAGE_ENV_HEAVY  = ['depth_image', 'cosmos', 'particles', 'plexus', 'crystal', 'wormhole', 'fireflies', 'columns'];
    private const PAGE_ENV_MEDIUM = ['lava', 'wave', 'aurora', 'aura', 'rays', 'topography', 'bubbles', 'blob', 'terrain', 'caustics', 'glass', 'bubble', 'flow', 'smoke', 'fractal', 'plasma', 'ripple', 'swirl', 'kaleidoscope', 'membrane', 'neon', 'noise', 'grid', 'dots', 'ribbons'];

    /** Per-element weights — block type → weight when "active." */
    private const WEIGHTS = [
        'page_env_heavy'   => 3,   // depth_image, cosmos, particles…
        'page_env_medium'  => 2,   // lava, wave, aurora…
        'heading_3d'       => 2,   // 3D-text engine: stroke or depth_effects
        'animator_preset'  => 2,   // animator with active preset
        'video_bg'         => 1,   // section bg_video
        'gradient_ring'    => 0.5, // mouse / spin gradient ring (button or grid cell)
        'mouse_grad'       => 0.5, // section / text bg-grad with mouse_sens
    ];

    /**
     * Compute the complexity for a page.
     *
     * Scene-mode aware: in scene mode (screen_mode === '1'), only one
     * top-level section is visible at any moment — the pause governor
     * in impulse-3d-text.js + the editor preview iframe ensure that
     * inactive screens' heavy elements aren't rendering. So the
     * simultaneous load = page-level + heaviest single section, NOT
     * the sum of every section. A page with three screens that each
     * have a stroke heading is no heavier in any single moment than a
     * page with one screen + one stroke heading.
     *
     * In non-scene-mode (regular scrolling page), every element renders
     * at once — sum everything as before.
     *
     * Accepts elements as a flat list (canonical PageService shape with
     * parent_id) or nested tree (with optional `children`).
     *
     * @param array $elements    Element list (flat or nested).
     * @param array $pageSettings Page-level settings.
     * @return array { score: int, level: string, contributors: array, mode: string }
     */
    public static function compute(array $elements, array $pageSettings = []): array
    {
        $contributors = [];
        self::scorePageLevel($pageSettings, $contributors);

        $isSceneMode = true; // every page is a screen sequence

        if ($isSceneMode && !empty($elements) && self::looksFlat($elements)) {
            // Scene mode + flat list: group by parent_id, take page-level
            // + heaviest section + any non-section root elements (nav,
            // popup, etc. that stay visible across screen changes).
            $byParent = [];
            foreach ($elements as $el) {
                $pid = (int)($el['parent_id'] ?? 0);
                if (!isset($byParent[$pid])) $byParent[$pid] = [];
                $byParent[$pid][] = $el;
            }
            $roots = $byParent[0] ?? [];
            $heaviestSectionContribs = [];
            $heaviestSectionWeight = -1;
            $alwaysVisible = []; // non-section roots (popup, modal, header, footer)
            $alwaysVisibleTypes = ['modal', 'header', 'footer'];
            foreach ($roots as $root) {
                $rootType = (string)($root['type'] ?? $root['element_type'] ?? '');
                $subtree  = array_merge([$root], self::collectDescendants((int)($root['id'] ?? 0), $byParent));
                $contribs = [];
                self::walk($subtree, $contribs);
                if ($rootType === 'section') {
                    $w = 0;
                    foreach ($contribs as $c) $w += (float)$c['weight'];
                    if ($w > $heaviestSectionWeight) {
                        $heaviestSectionWeight = $w;
                        $heaviestSectionContribs = $contribs;
                    }
                } elseif (in_array($rootType, $alwaysVisibleTypes, true)) {
                    $alwaysVisible = array_merge($alwaysVisible, $contribs);
                }
            }
            $contributors = array_merge($contributors, $heaviestSectionContribs, $alwaysVisible);
        } else {
            // Page mode (or nested tree): everything renders, sum all.
            self::walk($elements, $contributors);
        }

        // Sum + bucket. Round (not ceil) — half-points shouldn't tip up.
        $score = 0;
        foreach ($contributors as $c) $score += (float) $c['weight'];
        $score = (int) round($score);

        if ($score >= 9)      $level = 'red';
        elseif ($score >= 6)  $level = 'yellow';
        else                  $level = 'green';

        return [
            'score'        => $score,
            'level'        => $level,
            'contributors' => $contributors,
            'mode'         => $isSceneMode ? 'scene' : 'page',
        ];
    }

    /** Page-level contributors (env effects, depth-image parallax). */
    private static function scorePageLevel(array $pageSettings, array &$contributors): void
    {
        $cloud = (string)($pageSettings['screen_cloud_effect'] ?? 'none');
        if ($cloud !== '' && $cloud !== 'none') {
            if (in_array($cloud, self::PAGE_ENV_HEAVY, true)) {
                $contributors[] = [
                    'id'     => 0,
                    'type'   => 'page',
                    'label'  => 'Environment effect: ' . ucwords(str_replace('_', ' ', $cloud)),
                    'weight' => self::WEIGHTS['page_env_heavy'],
                    'reason' => 'Heavy WebGL scene running continuously.',
                ];
            } elseif (in_array($cloud, self::PAGE_ENV_MEDIUM, true)) {
                $contributors[] = [
                    'id'     => 0,
                    'type'   => 'page',
                    'label'  => 'Environment effect: ' . ucwords(str_replace('_', ' ', $cloud)),
                    'weight' => self::WEIGHTS['page_env_medium'],
                    'reason' => 'WebGL scene running continuously.',
                ];
            }
        }
        if (!empty($pageSettings['screen_depth_enabled'])
            && !empty($pageSettings['screen_depth_image'])
            && $cloud !== 'depth_image') {
            $contributors[] = [
                'id'     => 0,
                'type'   => 'page',
                'label'  => 'Depth image parallax',
                'weight' => self::WEIGHTS['page_env_heavy'],
                'reason' => 'WebGL parallax scene plus mouse tracking.',
            ];
        }
    }

    /** Walk the flat list rooted at $rootId, return all descendants. */
    private static function collectDescendants(int $rootId, array &$byParent): array
    {
        $out = [];
        $queue = [$rootId];
        while ($queue) {
            $id = (int) array_shift($queue);
            $kids = $byParent[$id] ?? [];
            foreach ($kids as $kid) {
                $out[] = $kid;
                $queue[] = (int)($kid['id'] ?? 0);
            }
        }
        return $out;
    }

    /** Detect a flat (parent_id) list vs a nested (children) tree. */
    private static function looksFlat(array $elements): bool
    {
        // First non-empty element decides — flat lists carry parent_id
        // on every row, nested trees carry `children` arrays. Either is
        // valid; the scoring path picks the right walker.
        foreach ($elements as $el) {
            if (isset($el['children']) && is_array($el['children']) && !empty($el['children'])) {
                return false; // nested
            }
            if (array_key_exists('parent_id', $el)) {
                return true;  // flat
            }
        }
        return true; // empty list, doesn't matter
    }

    /**
     * Walker — scores each element by type/settings. Iterates the flat
     * list AND recurses into any nested `children` so both element
     * shapes work: flat (PageService::getElements) and tree (templates).
     * Mutates $contributors in place to keep the traversal cheap.
     */
    private static function walk(array $items, array &$contributors): void
    {
        foreach ($items as $it) {
            $type = (string)($it['type'] ?? $it['element_type'] ?? '');
            $id   = (int)($it['id'] ?? 0);
            $s    = (array)($it['settings'] ?? []);

            // Heading with 3D engine active (stroke OR depth)
            if ($type === 'title') {
                $hasStroke = !empty($s['stroke_enabled']);
                $hasDepth  = !empty($s['depth_effects'])
                          || (int)($s['line_1_depth'] ?? 0) !== 0
                          || (int)($s['line_1_yrot']  ?? 0) !== 0;
                if ($hasStroke || $hasDepth) {
                    $reasons = [];
                    if ($hasStroke) $reasons[] = 'stroke';
                    if ($hasDepth)  $reasons[] = '3D depth';
                    $contributors[] = [
                        'id'     => $id,
                        'type'   => 'title',
                        'label'  => 'Heading: ' . self::truncate((string)($s['line_1'] ?? 'Heading'), 38),
                        'weight' => self::WEIGHTS['heading_3d'],
                        'reason' => 'WebGL ' . implode(' + ', $reasons) . ' (per-letter scene).',
                    ];
                }
            }

            // Animator with active preset
            if ($type === 'title' && !empty($s['la_preset'])) {
                $contributors[] = [
                    'id'     => $id,
                    'type'   => 'title',
                    'label'  => 'Animator: ' . self::truncate((string)($s['la_text'] ?? 'Logo'), 38),
                    'weight' => self::WEIGHTS['animator_preset'],
                    'reason' => 'Canvas animation: ' . (string)$s['la_preset'] . '.',
                ];
            }

            // Section with background video
            if ($type === 'section' && !empty($s['bg_video'])) {
                $contributors[] = [
                    'id'     => $id,
                    'type'   => 'section',
                    'label'  => 'Section background video',
                    'weight' => self::WEIGHTS['video_bg'],
                    'reason' => 'Auto-playing video element.',
                ];
            }

            // Section mouse-driven gradient: removed 2026-05-17 (v2.1.270)
            // when section gradients were replaced by mesh BG. Mesh is pure
            // compositor (transform translate3d on coprime periods) and
            // doesn't add to complexity weight — leave nothing here.

            // Text block with text-fill gradient + mouse/spin
            if ($type === 'text' && !empty($s['text_gradient_to'])
                && ((int)($s['text_mouse_sens'] ?? 0) > 0 || (int)($s['text_spin_speed'] ?? 0) > 0)) {
                $contributors[] = [
                    'id'     => $id,
                    'type'   => 'text',
                    'label'  => 'Text: animated gradient',
                    'weight' => self::WEIGHTS['mouse_grad'],
                    'reason' => 'Per-frame angle update on text-fill.',
                ];
            }

            // Button with gradient ring (mouse / spin)
            if ($type === 'button' && !empty($s['border_gradient_to'])
                && ((int)($s['border_mouse_sens'] ?? 0) > 0 || (int)($s['border_spin_speed'] ?? 0) > 0)) {
                $contributors[] = [
                    'id'     => $id,
                    'type'   => 'button',
                    'label'  => 'Button: rotating gradient ring',
                    'weight' => self::WEIGHTS['gradient_ring'],
                    'reason' => 'Per-frame angle update on border.',
                ];
            }

            // Grid cells — scan for animated rings + mouse-driven bg
            if ($type === 'grid') {
                $columns = (int)($s['columns'] ?? 2);
                $rows    = (int)($s['rows'] ?? 1);
                $cellMax = max(0, $columns * $rows);
                $animatedCells = 0;
                for ($i = 0; $i < $cellMax; $i++) {
                    if (!empty($s['cell_border_gradient_to_' . $i])
                        && (int)($s['cell_border_mouse_sens_' . $i] ?? 0) > 0) {
                        $animatedCells++;
                    }
                    if (!empty($s['cell_bg_gradient_to_' . $i])
                        && (int)($s['cell_bg_mouse_sens_' . $i] ?? 0) > 0) {
                        $animatedCells++;
                    }
                }
                if ($animatedCells > 0) {
                    $contributors[] = [
                        'id'     => $id,
                        'type'   => 'grid',
                        'label'  => 'Grid: ' . $animatedCells . ' animated ' . ($animatedCells === 1 ? 'cell' : 'cells'),
                        'weight' => self::WEIGHTS['gradient_ring'] * $animatedCells,
                        'reason' => 'Per-cell mouse-driven gradient.',
                    ];
                }
            }

            // Recurse
            if (!empty($it['children']) && is_array($it['children'])) {
                self::walk($it['children'], $contributors);
            }
        }
    }

    private static function truncate(string $s, int $n): string
    {
        $s = trim(wp_strip_all_tags($s));
        if (mb_strlen($s) <= $n) return $s;
        return mb_substr($s, 0, $n - 1) . '…';
    }
}
