/**
 * Lightweight DOM Patcher for LiveComponent Fragments
 *
 * Efficiently patches specific DOM fragments without full re-render.
 * Optimized for LiveComponent use case with minimal overhead.
 *
 * Features:
 * - Smart element matching by tag and data-lc-fragment attribute
 * - Attribute diffing and patching
 * - Text content updates
 * - Child node reconciliation
 * - Preserves focus and scroll position where possible
 *
 * Philosophy:
 * - Keep it simple and focused
 * - No external dependencies
 * - Framework-compliant modern JavaScript
 */
export class DomPatcher {
    /**
     * Patch a specific fragment within a container
     *
     * @param {HTMLElement} container - Container element to search in
     * @param {string} fragmentName - Fragment name (data-lc-fragment value)
     * @param {string} newHtml - New HTML for the fragment
     * @returns {boolean} - True if fragment was patched, false if not found
     */
    patchFragment(container, fragmentName, newHtml) {
        // Find existing fragment element
        const existingElement = container.querySelector(`[data-lc-fragment="${fragmentName}"]`);

        if (!existingElement) {
            console.warn(`[DomPatcher] Fragment not found: ${fragmentName}`);
            return false;
        }

        // Parse new HTML into element
        const temp = document.createElement('div');
        temp.innerHTML = newHtml;
        const newElement = temp.firstElementChild;

        if (!newElement) {
            console.warn(`[DomPatcher] Invalid HTML for fragment: ${fragmentName}`);
            return false;
        }

        // Verify fragment name matches
        if (newElement.getAttribute('data-lc-fragment') !== fragmentName) {
            console.warn(`[DomPatcher] Fragment name mismatch: expected ${fragmentName}, got ${newElement.getAttribute('data-lc-fragment')}`);
            return false;
        }

        // Patch the element in place
        this.patchElement(existingElement, newElement);

        return true;
    }

    /**
     * Patch multiple fragments at once
     *
     * @param {HTMLElement} container - Container element
     * @param {Object.<string, string>} fragments - Map of fragment names to HTML
     * @returns {Object.<string, boolean>} - Map of fragment names to success status
     */
    patchFragments(container, fragments) {
        const results = {};

        for (const [fragmentName, html] of Object.entries(fragments)) {
            results[fragmentName] = this.patchFragment(container, fragmentName, html);
        }

        return results;
    }

    /**
     * Patch an element with new content
     *
     * Core patching logic that efficiently updates only what changed.
     *
     * @param {HTMLElement} oldElement - Existing element to patch
     * @param {HTMLElement} newElement - New element with updated content
     */
    patchElement(oldElement, newElement) {
        // 1. Patch attributes
        this.patchAttributes(oldElement, newElement);

        // 2. Patch child nodes
        this.patchChildren(oldElement, newElement);
    }

    /**
     * Patch element attributes
     *
     * Only updates attributes that actually changed.
     *
     * @param {HTMLElement} oldElement - Existing element
     * @param {HTMLElement} newElement - New element
     */
    patchAttributes(oldElement, newElement) {
        // Get all attributes from both elements
        const oldAttrs = new Map();
        const newAttrs = new Map();

        for (const attr of oldElement.attributes) {
            oldAttrs.set(attr.name, attr.value);
        }

        for (const attr of newElement.attributes) {
            newAttrs.set(attr.name, attr.value);
        }

        // Remove attributes that no longer exist
        for (const [name, value] of oldAttrs) {
            if (!newAttrs.has(name)) {
                oldElement.removeAttribute(name);
            }
        }

        // Add or update attributes
        for (const [name, value] of newAttrs) {
            if (oldAttrs.get(name) !== value) {
                oldElement.setAttribute(name, value);
            }
        }
    }

    /**
     * Patch child nodes
     *
     * Reconciles child nodes between old and new elements.
     * Uses simple key-based matching for efficiency.
     *
     * @param {HTMLElement} oldElement - Existing element
     * @param {HTMLElement} newElement - New element
     */
    patchChildren(oldElement, newElement) {
        const oldChildren = Array.from(oldElement.childNodes);
        const newChildren = Array.from(newElement.childNodes);

        const maxLength = Math.max(oldChildren.length, newChildren.length);

        for (let i = 0; i < maxLength; i++) {
            const oldChild = oldChildren[i];
            const newChild = newChildren[i];

            if (!oldChild && newChild) {
                // New child added - append it
                oldElement.appendChild(newChild.cloneNode(true));
            } else if (oldChild && !newChild) {
                // Child removed - remove it
                oldElement.removeChild(oldChild);
            } else if (oldChild && newChild) {
                // Both exist - patch or replace
                if (this.shouldPatch(oldChild, newChild)) {
                    if (oldChild.nodeType === Node.ELEMENT_NODE) {
                        this.patchElement(oldChild, newChild);
                    } else if (oldChild.nodeType === Node.TEXT_NODE) {
                        if (oldChild.nodeValue !== newChild.nodeValue) {
                            oldChild.nodeValue = newChild.nodeValue;
                        }
                    }
                } else {
                    // Different node types or tags - replace
                    oldElement.replaceChild(newChild.cloneNode(true), oldChild);
                }
            }
        }
    }

    /**
     * Determine if two nodes should be patched or replaced
     *
     * Nodes should be patched if they are compatible (same type and tag).
     *
     * @param {Node} oldNode - Existing node
     * @param {Node} newNode - New node
     * @returns {boolean} - True if nodes should be patched
     */
    shouldPatch(oldNode, newNode) {
        // Different node types - replace
        if (oldNode.nodeType !== newNode.nodeType) {
            return false;
        }

        // Text nodes can always be patched
        if (oldNode.nodeType === Node.TEXT_NODE) {
            return true;
        }

        // Element nodes - check if same tag
        if (oldNode.nodeType === Node.ELEMENT_NODE) {
            if (oldNode.tagName !== newNode.tagName) {
                return false;
            }

            // Check for special keys that indicate identity
            const oldKey = oldNode.getAttribute('data-lc-key') || oldNode.getAttribute('id');
            const newKey = newNode.getAttribute('data-lc-key') || newNode.getAttribute('id');

            // If both have keys, they must match
            if (oldKey && newKey) {
                return oldKey === newKey;
            }

            // Otherwise, assume they match (same tag is enough)
            return true;
        }

        // Other node types - replace
        return false;
    }

    /**
     * Preserve focus state before patching
     *
     * Returns a function to restore focus after patching.
     *
     * @param {HTMLElement} container - Container being patched
     * @returns {Function} - Restore function
     */
    preserveFocus(container) {
        const activeElement = document.activeElement;

        // Check if focused element is within container
        if (!activeElement || !container.contains(activeElement)) {
            return () => {}; // No-op restore
        }

        // Get selector for focused element
        const selector = this.getElementSelector(activeElement);
        const selectionStart = activeElement.selectionStart;
        const selectionEnd = activeElement.selectionEnd;

        // Return restore function
        return () => {
            try {
                if (selector) {
                    const element = container.querySelector(selector);
                    if (element && element.focus) {
                        element.focus();

                        // Restore selection for input/textarea
                        if (element.setSelectionRange &&
                            typeof selectionStart === 'number' &&
                            typeof selectionEnd === 'number') {
                            element.setSelectionRange(selectionStart, selectionEnd);
                        }
                    }
                }
            } catch (e) {
                // Focus restoration failed - not critical
                console.debug('[DomPatcher] Could not restore focus:', e);
            }
        };
    }

    /**
     * Get a selector for an element
     *
     * Tries to create a unique selector using ID, name, or data attributes.
     *
     * @param {HTMLElement} element - Element to get selector for
     * @returns {string|null} - CSS selector or null
     */
    getElementSelector(element) {
        if (element.id) {
            return `#${element.id}`;
        }

        if (element.name) {
            return `[name="${element.name}"]`;
        }

        const lcKey = element.getAttribute('data-lc-key');
        if (lcKey) {
            return `[data-lc-key="${lcKey}"]`;
        }

        // Fallback - not guaranteed to be unique
        return element.tagName.toLowerCase();
    }
}

// Create singleton instance
export const domPatcher = new DomPatcher();

export default domPatcher;
