Source: lib/core/renderer/listManager.js

import { DomPatcher } from './domPatch.js';

/**
 * Handles efficient rendering of lists by managing DOM fragments and performing keyed diffing.
 */
export class ListManager {
    /**
     * @param {DynamicEvaluator} evaluator - The expression evaluator.
     * @param {TemplateRenderer} renderer - The template renderer.
     */
    constructor(evaluator, renderer) {
        this.evaluator = evaluator;
        this.renderer = renderer;
        this.patcher = new DomPatcher();
    }

    /**
     * Processes all template-based lists within a root element.
     * @param {Element} root - The root element to search in.
     * @param {Object} scope - The evaluation scope.
     * @param {Object} state - The component state.
     */
    process(root, scope, state) {
        const templates = root.querySelectorAll('template[data-ax-for]');
        templates.forEach(template => {
            let parent = template.parentNode;
            let insideSlot = false;
            while (parent) {
                if (parent.nodeName === 'SLOT' && parent.hasAttribute && parent.hasAttribute('data-avenx-transcluded')) {
                    insideSlot = true;
                    break;
                }
                parent = parent.parentNode;
            }
            if (!insideSlot) {
                this.#updateList(template, scope, state);
            }
        });
    }

    /**
     * Updates a specific list based on its template and current state.
     * @param {HTMLTemplateElement} template - The list template.
     * @param {Object} scope - The evaluation scope.
     * @param {Object} state - The component state.
     * @private
     */
    #updateList(template, scope, state) {
        const listExpr = template.getAttribute('data-ax-for');
        const itemVar = template.getAttribute('data-ax-as');
        const keyExpr = template.getAttribute('data-ax-key');
        
        let list;
        try {
            list = this.evaluator.evaluateExpression(listExpr, scope, state);
        } catch (e) {
            console.warn(`[ListManager] Failed to evaluate list expression: ${listExpr}`, e);
            return;
        }

        if (!Array.isArray(list)) return;

        const currentItems = this.#getCurrentItems(template);
        const nextItems = list.map((item, index) => {
            const itemScope = { ...scope, [itemVar]: item, index };
            let key = index;
            if (keyExpr) {
                try {
                    key = this.evaluator.evaluateExpression(keyExpr, itemScope, state);
                } catch (e) {
                    console.warn(`[ListManager] Failed to evaluate key expression: ${keyExpr}`, e);
                }
            }
            return { item, key: String(key), itemScope };
        });

        // 1. Remove items that are no longer in the list
        const nextKeys = new Set(nextItems.map(i => i.key));
        for (const [key, element] of currentItems.entries()) {
            if (!nextKeys.has(key)) {
                element.remove();
            }
        }

        // 2. Add or move items
        let lastElement = template;
        const itemTemplate = template.innerHTML.replace(/{%/g, '{{').replace(/%}/g, '}}');

        nextItems.forEach(({ key, itemScope }) => {
            let element = currentItems.get(key);
            const html = this.renderer.render(
                itemTemplate,
                expr => this.evaluator.evaluateExpression(expr, itemScope, state)
            ).trim();

            if (element) {
                const temp = document.createElement('div');
                temp.innerHTML = html;
                const newElement = temp.firstElementChild;
                if (newElement) {
                    newElement.setAttribute('data-ax-list-item', '');
                    newElement.setAttribute('data-ax-key-val', key);
                    if (element.outerHTML !== newElement.outerHTML) {
                        this.patcher.patchElement(element, newElement);
                    }
                }
            } else {
                // Create new element
                const temp = document.createElement('div');
                temp.innerHTML = html;
                element = temp.firstElementChild;
                if (element) {
                    element.setAttribute('data-ax-list-item', '');
                    element.setAttribute('data-ax-key-val', key);
                }
            }
            
            if (element) {
                // Ensure correct order
                if (element.previousElementSibling !== lastElement) {
                    lastElement.after(element);
                }
                lastElement = element;
            }
        });
    }

    /**
     * Retrieves currently rendered items for a template by scanning subsequent siblings.
     * @param {HTMLTemplateElement} template - The template.
     * @returns {Map<string, Element>}
     * @private
     */
    #getCurrentItems(template) {
        const items = new Map();
        let current = template.nextElementSibling;
        while (current && current.hasAttribute('data-ax-list-item')) {
            const key = current.getAttribute('data-ax-key-val');
            items.set(key, current);
            current = current.nextElementSibling;
        }
        return items;
    }
}