/**
 * renderer.js — DOM rendering and patching for el-ui.
 *
 * v0.1 strategy: full string re-render with event rebinding.
 * The architecture is designed for future targeted DOM patching:
 * `patch()` will accept the activated node set and only update
 * DOM subtrees whose data-el-tag corresponds to an activated node.
 *
 * Event binding uses data-el-* attributes set by the compiler,
 * which avoids innerHTML XSS risks for handler references while
 * keeping the runtime small (no virtual DOM, no event delegation table).
 */

export class Renderer {
    /**
     * @param {HTMLElement} root  the mount point
     * @param {import('./index.js').Component} component  component instance
     */
    constructor(root, component) {
        this.root = root;
        this.component = component;
        this._currentHtml = '';
        this._boundHandlers = new WeakMap();
    }

    /**
     * Initial mount — render and bind events.
     */
    mount() {
        this._currentHtml = this.component.render();
        this.root.innerHTML = this._currentHtml;
        this._bindEvents();
        if (typeof this.component.onMount === 'function') {
            this.component.onMount();
        }
    }

    /**
     * Patch the DOM after a state change.
     * Called by the component when spreading activation completes.
     *
     * @param {Set<string>} [_activatedNodes]  future: targeted patching by node set
     */
    patch(_activatedNodes) {
        const newHtml = this.component.render();
        if (newHtml !== this._currentHtml) {
            // Preserve scroll position and focused element identity
            const focused = document.activeElement;
            const focusedId = focused && focused !== document.body ? focused.id : null;

            this.root.innerHTML = newHtml;
            this._bindEvents();
            this._currentHtml = newHtml;

            // Restore focus if element still exists
            if (focusedId) {
                const el = this.root.querySelector(`#${CSS.escape(focusedId)}`);
                if (el) el.focus();
            }
        }
    }

    /**
     * Bind all data-el-* event handlers declared by the compiler.
     *
     * The compiler emits attributes like:
     *   data-el-click="__self.handleClick()"
     *   data-el-input="(e) => __self.setState('value', e.target.value)"
     *
     * We eval these in the context of the component instance.
     * (For production use, a full CSP-compatible binding would generate
     * handler functions at compile time.)
     *
     * @private
     */
    _bindEvents() {
        const comp = this.component;
        const eventNames = ['click', 'input', 'change', 'mouseenter', 'mouseleave',
                            'keydown', 'keyup', 'keypress', 'focus', 'blur',
                            'submit', 'mousedown', 'mouseup', 'dblclick', 'contextmenu'];

        for (const eventName of eventNames) {
            const attr = `data-el-${eventName}`;
            this.root.querySelectorAll(`[${attr}]`).forEach(el => {
                const handlerExpr = el.getAttribute(attr);
                if (!handlerExpr) return;

                // Build a handler function in the component's scope
                const handler = this._compileHandler(handlerExpr, comp);
                if (handler) {
                    // Remove previous binding if any
                    const prev = this._boundHandlers.get(el);
                    if (prev) el.removeEventListener(eventName, prev[eventName]);

                    el.addEventListener(eventName, handler);
                    if (!this._boundHandlers.has(el)) this._boundHandlers.set(el, {});
                    this._boundHandlers.get(el)[eventName] = handler;
                }
            });
        }
    }

    /**
     * Compile a handler expression string into a callable function.
     * The expression is evaluated with `__self` bound to the component.
     *
     * @private
     * @param {string} expr
     * @param {object} comp
     * @returns {function|null}
     */
    _compileHandler(expr, comp) {
        try {
            // Decode HTML entities (attrs may have &quot; etc.)
            const decoded = expr
                .replace(/&quot;/g, '"')
                .replace(/&#39;/g, "'")
                .replace(/&amp;/g, '&')
                .replace(/&lt;/g, '<')
                .replace(/&gt;/g, '>');

            // If it looks like an arrow function or function reference, wrap it
            const isArrow = decoded.includes('=>');
            const isCall = decoded.includes('(');

            if (isArrow) {
                // Arrow function: `(e) => __self.setState(...)` or `() => ...`
                // eslint-disable-next-line no-new-func
                const fn = new Function('__self', `return ${decoded}`)(comp);
                return typeof fn === 'function' ? fn : null;
            } else if (isCall) {
                // Method call: `__self.handleClick()`
                // eslint-disable-next-line no-new-func
                return new Function('__self', 'event', `${decoded}`).bind(null, comp);
            } else {
                // Bare identifier: method name on the component
                return comp[decoded] ? comp[decoded].bind(comp) : null;
            }
        } catch (e) {
            console.warn(`el-ui: failed to compile handler: ${expr}`, e);
            return null;
        }
    }
}
