function log(...args) { console.log(...args) }
if(!$DEBUG)log=()=>{}

// log("[+] hy-client injected.")

window.hy = {}

function scanAndInitialize(rootElement) {
    const elementsToInit = rootElement.querySelectorAll('[hy-init]');
    elementsToInit.forEach(el => {
        const expression = el.getAttribute('hy-init');
        try {
            new Function(expression).call(el);
        } catch (e) {
            console.error(`Error executing hy-init expression on element:`, el, e);
            console.error(`Expression was: ${expression}`);
        }
    });
}

function parseAction(actionStr) {
    const match = actionStr.match(/(\w+)\((.*)\)/);
    if (match) {
        const name = match[1];
        const argsStr = match[2];

        try {
            const args = JSON.parse(`[${argsStr}]`);
            return { name, args };
        } catch (e) {
            console.error("Could not parse action args:", argsStr, e);
            return null;
        }
    }
    return { name: actionStr, args: [] };
}

const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const ws = new WebSocket(`${protocol}//${window.location.host}/_hy/ws`);
ws.binaryType = "arraybuffer";

/* Portal */
class Portal extends EventTarget {
    constructor() {
        super();
        this._emitter = new Map();
    }

    on(name, cb) {
        if (!this._emitter.has(name)) this._emitter.set(name, new Set());
        this._emitter.get(name).add(cb);
    }

    off(name, cb) {
        if (!this._emitter.has(name)) return;
        const s = this._emitter.get(name);
        s.delete(cb);
        if (!s.size) this._emitter.delete(name);
    }

    once(name, cb) {
        const wrapper = (...args) => { this.off(name, wrapper); cb(...args); };
        this.on(name,  wrapper);
    }

    _emit(name, args=[], kwargs={}) {
        if (this._emitter.has(name)) {
            for (const cb of Array.from(this._emitter.get(name))) {
                try { cb(...args, kwargs); } catch(e) { console.error(e); }
            }
        }
    }
}

let callId = 0;
const pendingCalls = new Map();

const portalBase = new Portal();

const portal = new Proxy(portalBase, { 
    get(target, prop) {
        if (prop in target) return target[prop];

        return async (...args) => {
            return new Promise((resolve, reject) => {
                const id = ++callId;
                pendingCalls.set(id, { resolve, reject });
                ws.send(MessagePack.encode({ type: 'rpc', id, name: prop, args }))
            })
        }
    }
 });

window.portal = portal; // for backwards compatibility
hy.portal = portal;

hy.refreshIcons = window.lucide.createIcons;

const components = document.querySelectorAll('[hy-vm]');
ws.onopen = () => {
    log("[+] Connected.");
    const componentsToInit = Array.from(components).map(el => ({
        hy_id: el.id,
        vm_name: el.getAttribute('hy-vm'),
    }));
    ws.send(MessagePack.encode({ type: 'init', components: componentsToInit }));
    const event = new CustomEvent('hy:connected');
    document.dispatchEvent(event);
}

ws.onmessage = (event) => {
    const data = MessagePack.decode(event.data);
    log(`[ws] Received:`, data)
    if (data.type === 'update' && data.id) {
        const targetEl = document.getElementById(data.id);
        if (targetEl) {
            morphdom(targetEl, data.html);
            window.lucide?.createIcons();
        }
    } else if (data.id && ('result' in data || 'error' in data)) {
        if (pendingCalls.has(data.id)) {
            const { resolve, reject } = pendingCalls.get(data.id);
            if (data.error) {
                reject(new Error(data.error));
            } else {
                resolve(data.result);
            }
            pendingCalls.delete(data.id);
        }
    } else if (data.type === 'event') {
        const args = data.payload?.args ?? [];
        const kwargs = data.payload?.kwargs ?? {};

        portalBase.dispatchEvent(new CustomEvent(data.name, { detail: { args, kwargs } }));
        portal._emit(data.name, args, kwargs);
    }
}

document.addEventListener('DOMContentLoaded', () => {
    scanAndInitialize(document.body);
})

const observer = new MutationObserver((mutationsList) => {
    for (const mutation of mutationsList) {
        if (mutation.type === 'childList') {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === 1) { 
                    scanAndInitialize(node);
                }
            });
        }
    }
});

components.forEach(component => {
    observer.observe(component, { childList: true, subtree: true });
})

document.addEventListener('click', (e) => {
    const target = e.target.closest('[hy-click]');
    const componentRoot = target?.closest('[hy-vm]');
    if (target && componentRoot) {
        const actionStr = target.getAttribute('hy-click');
        const action = parseAction(actionStr);
        if (action) {
            const payload = {
                type: 'action',
                hy_id: componentRoot.id,
                ...action
            };
            ws.send(MessagePack.encode(payload));
            log(`[ws] Sent:`, payload)
        }
    }
})

document.addEventListener('submit', (e) => {
    const form = e.target.closest('form[hy-submit]');
    const componentRoot = form?.closest('[hy-vm]');
    if (form && componentRoot) {
        e.preventDefault();
        const actionStr = form.getAttribute('hy-submit');
        const formData = new FormData(form);
        const formPayload = Object.fromEntries(formData.entries());
        const payload = {
            type: 'action',
            hy_id: componentRoot.id,
            name: actionStr,
            args: [formPayload]
        };
        ws.send(MessagePack.encode(payload));
        log(`[ws] Sent:`, payload)
    }
})

hy.action = (viewModelName, action) => {
    if (!viewModelName || !action || !action.name) {
        console.error("[hy.action] Error: You must provide a viewModelName and an action object with a 'name' property.");
        return;
    }
    const componentRoot = document.querySelector(`[hy-vm="${viewModelName}"]`);
    if (!componentRoot) {
        console.error(`[hy.action] Error: Could not find a component for ViewModel '${viewModelName}'.`);
        return;
    }
    const payload = {
        type: 'action',
        hy_id: componentRoot.id,
        ...action
    };
    ws.send(MessagePack.encode(payload));
    log(`[ws] Sent:`, payload)
}

document.addEventListener('DOMContentLoaded', () => {
    window.lucide?.createIcons();
})