function parsePropertiesXml(str) {
    const dom = new DOMParser().parseFromString(str, 'application/xml');
    const entries = dom.getElementsByTagName('entry');
    const result = new Map();
    for (const entry of entries) {
        const key = entry.getAttribute('key');
        const value = entry.textContent;
        result.set(key, value);
    }
    return result;
}

const HOST_DATA_SYMBOL = Symbol();
function setHostData(document, value) {
    // @ts-expect-error
    document[HOST_DATA_SYMBOL] = value;
}
function getHostData(document) {
    // @ts-expect-error
    return document[HOST_DATA_SYMBOL] || null;
}
function readHostData(document) {
    const tag = document.getElementById(HOST_DATA_SCRIPT_TAG);
    if (!(tag instanceof HTMLScriptElement)) {
        throw new Error('Expected script tag with id ' + HOST_DATA_SCRIPT_TAG + ' but it does not exist.' +
            ' Unable to load window initialization data. This error is caused by a bug in jade-servlet.');
    }
    const props = parsePropertiesXml(atob(tag.textContent));
    return {
        windowId: props.get('windowId'),
        asyncNavigatePath: props.get('asyncNavigatePath'),
        asyncActionPath: props.get('asyncActionPath'),
        eventStreamPath: props.get('eventStreamPath'),
        path: props.get('path'),
        route: props.get('route')
    };
}
const HOST_DATA_SCRIPT_TAG = "net.jadeframework.servlet.Renderer.HOST_DATA_SCRIPT_TAG";
function loadHostData(document) {
    const hostData = readHostData(document);
    setHostData(document, hostData);
}

async function* asAsyncIterable(stream) {
    const reader = stream.getReader();
    try {
        while (true) {
            const { done, value } = await reader.read();
            if (done)
                return;
            yield value;
        }
    }
    finally {
        reader.releaseLock();
    }
}

async function* lines(stream) {
    const decoder = new TextDecoder('utf-8');
    let strings = [];
    for await (const chunk of asAsyncIterable(stream)) {
        let string = decoder.decode(chunk, { stream: true });
        while (string.length > 0) {
            const index = string.indexOf('\n');
            const stop = (index == -1 ? string.length : index + 1);
            const prefix = string.substring(0, stop);
            strings.push(prefix);
            if (prefix.endsWith('\n')) {
                const joined = strings.join('');
                const trimmed = joined.substring(0, joined.length - 1);
                strings = [];
                yield trimmed;
            }
            string = string.substring(stop);
        }
    }
}

/**
 * A userland implementation of {@link EventSource}, backed by {@link fetch}.
 *
 * This class conforms to the server-sent events specification outlined
 * here {@link https://html.spec.whatwg.org/multipage/server-sent-events.html}.*
 * Behavior that would otherwise be determined by the user agent is configured
 * explicitly.
 *
 * *This class implements a subset of the specification. Support for more of the spec is introduced
 * as needed. There is no point in implementing the parts that are never used.
 */
class FetchEventSource extends EventTarget {
    static CLOSED = 2;
    static CONNECTING = 0;
    static OPEN = 1;
    readyState = FetchEventSource.CONNECTING;
    url;
    withCredentials;
    abort = null;
    lastEventId = undefined;
    constructor(url, init) {
        super();
        this.url = url;
        this.withCredentials = Boolean(init?.withCredentials);
        window.queueMicrotask(() => this.loop());
    }
    addEventListener(type, listener, options) {
        super.addEventListener(type, listener, options);
    }
    close() {
        this.readyState = FetchEventSource.CLOSED;
        this.abort?.abort();
    }
    async loop() {
        while (this.readyState !== FetchEventSource.CLOSED) {
            const retrying = (this.readyState === FetchEventSource.OPEN);
            this.readyState = FetchEventSource.CONNECTING;
            if (retrying) {
                await new Promise(resolve => setTimeout(resolve, 1000));
            }
            try {
                await this.fetch();
            }
            catch (caught) {
                console.error(caught);
            }
        }
    }
    async fetch() {
        this.abort = new AbortController();
        const headers = {};
        headers['Accept'] = 'text/event-stream';
        if (this.lastEventId)
            headers['Last-Event-Id'] = this.lastEventId;
        const response = await fetch(this.url, {
            credentials: this.withCredentials ? 'include' : 'omit',
            headers,
            signal: this.abort.signal
        });
        if (response.status === 204) {
            this.readyState = FetchEventSource.CLOSED;
            return;
        }
        this.readyState = FetchEventSource.OPEN;
        await this.consumeLines(lines(response.body));
    }
    async consumeLines(lines) {
        // The default event type is "message".
        let type = '';
        let data = [];
        for await (const line of lines) {
            // If the line is empty (a blank line), dispatch the event.
            if (line.length === 0) {
                // If the data buffer is an empty string, set the data buffer and the event type
                // buffer to the empty string and return.
                if (data.length > 0) {
                    let joined = data.join('\n');
                    // If the event type buffer has a value other than the empty string,
                    // change the type of the newly created event to equal the value of the event
                    // type buffer.
                    const type0 = type === '' ? 'message' : type;
                    const options = {};
                    options.data = joined;
                    if (this.lastEventId)
                        options.lastEventId = this.lastEventId;
                    const event = new MessageEvent(type0, options);
                    window.queueMicrotask(() => {
                        if (this.readyState !== FetchEventSource.CLOSED) {
                            this.dispatchEvent(event);
                        }
                    });
                }
                data = [];
                type = '';
                // "The last event ID string of the event source remains set to this value until the next time it is
                // set by the server."
                // noinspection SillyAssignmentJS
                type = type;
                continue;
            }
            let colon = line.includes(':')
                ? line.indexOf(':')
                : line.length;
            let field = line.substring(0, colon);
            let value = line.substring(colon + 1).trimStart();
            switch (field) {
                case 'event':
                    type = value;
                    break;
                case 'data':
                    data.push(value);
                    break;
                case 'id':
                    this.lastEventId = value;
                    break;
            }
        }
    }
}

// -- JADE_NODE_LUT ---------------------------------------------------------------------------------------------------
const JADE_NODE_LUT = Symbol();
function getJadeNodeLut(document) {
    // @ts-expect-error
    return document[JADE_NODE_LUT] || null;
}
function setJadeNodeLut(document, value) {
    // @ts-expect-error
    document[JADE_NODE_LUT] = value;
}
// -- JADE_EVENT_SOURCE -----------------------------------------------------------------------------------------------
const JADE_EVENT_SOURCE = Symbol();
function getJadeEventSource(document) {
    // @ts-expect-error
    const value = document[JADE_EVENT_SOURCE];
    if (value instanceof FetchEventSource) {
        return value;
    }
    else {
        return null;
    }
}
function setJadeEventSource(document, jadeEventSource) {
    // @ts-expect-error
    document[JADE_EVENT_SOURCE] = jadeEventSource;
}

async function handleFormInteraction(event) {
    const form = event.currentTarget;
    const eventSource = getJadeEventSource(form.ownerDocument);
    const asyncActionPath = getHostData(form.ownerDocument).asyncActionPath;
    if (eventSource.readyState === FetchEventSource.OPEN) {
        event.preventDefault();
        const formData = new FormData(form, event.submitter);
        // @ts-expect-error
        const body = new URLSearchParams(formData);
        await fetch(asyncActionPath, { method: 'POST', body });
    }
}

async function handleLinkInteraction(event) {
    const anchor = event.currentTarget;
    const eventSource = getJadeEventSource(anchor.ownerDocument);
    const hostData = getHostData(anchor.ownerDocument);
    const id = anchor.getAttribute('data-jade-id');
    if (eventSource.readyState === FetchEventSource.OPEN) {
        event.preventDefault();
        const formData = new FormData();
        formData.set('net.jadeframework.servlet.JadeAsyncNavigateServlet.WINDOW_ID_PARAM', hostData.windowId);
        formData.set('net.jadeframework.servlet.JadeAsyncNavigateServlet.TARGET_ELEMENT_NODE_ID_PARAM', id);
        // @ts-expect-error
        const body = new URLSearchParams(formData);
        await fetch(hostData.asyncNavigatePath, { method: 'POST', body });
    }
}

function visit(document) {
    const lut = new Map();
    const iterator = document.createNodeIterator(document, NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_ELEMENT);
    let node;
    while ((node = iterator.nextNode())) {
        if (node instanceof Comment) {
            if (node.data.startsWith('jade-text-end')) {
                const id = Number.parseInt(node.data.substring(22));
                lut.set(id, node);
            }
        }
        if (node instanceof Element) {
            if (node.hasAttribute('data-jade-id')) {
                const idAttrValue = node.getAttribute('data-jade-id');
                const id = Number.parseInt(idAttrValue);
                lut.set(id, node);
            }
            if (node.hasAttribute('data-jade-bare-text-id')) {
                const textIdAttrValue = node.getAttribute('data-jade-bare-text-id');
                const textId = Number.parseInt(textIdAttrValue);
                node.removeAttribute('data-jade-bare-text-id');
                lut.set(textId, node.firstChild);
            }
        }
        if (node instanceof HTMLAnchorElement) {
            if (node.hasAttribute('data-jade-unenhanced-link')) {
                node.removeAttribute('data-jade-unenhanced-link');
                node.addEventListener('click', handleLinkInteraction);
            }
        }
        if (node instanceof HTMLFormElement) {
            if (node.hasAttribute('data-jade-javaaction')) {
                node.addEventListener('submit', handleFormInteraction);
            }
        }
    }
    setJadeNodeLut(document, lut);
}

function addHandler(source, handler) {
    source.addEventListener('moveText', event => handler.onMoveText(MoveTextData.decode(event.data)));
    source.addEventListener('moveElement', event => handler.onMoveElement(MoveElementData.decode(event.data)));
    source.addEventListener('resetForm', event => handler.onResetForm(ResetFormData.decode(event.data)));
    source.addEventListener('attachAction', event => handler.onAttachAction(AttachActionData.decode(event.data)));
    source.addEventListener('detachAction', event => handler.onDetachAction(AttachActionData.decode(event.data)));
    source.addEventListener('attachRoute', event => handler.onAttachRoute(AttachRouteData.decode(event.data)));
    source.addEventListener('detachRoute', event => handler.onDetachRoute(DetachRouteData.decode(event.data)));
    source.addEventListener('mountElement', event => handler.onMountElement(MountElementData.decode(event.data)));
    source.addEventListener('unmountElement', event => handler.onUnmountElement(UnmountElementData.decode(event.data)));
    source.addEventListener('mountText', event => handler.onMountText(MountTextData.decode(event.data)));
    source.addEventListener('unmountText', event => handler.onUnmountText(UnmountTextData.decode(event.data)));
    source.addEventListener('setAttribute', event => handler.onSetAttribute(SetAttributeData.decode(event.data)));
    source.addEventListener('removeAttribute', event => handler.onRemoveAttribute(RemoveAttributeData.decode(event.data)));
    source.addEventListener('pushPage', event => handler.onPushPage(PushPageData.decode(event.data)));
    source.addEventListener('replaceRoute', event => handler.onReplaceRoute(ReplaceRouteData.decode(event.data)));
    source.addEventListener('replaceAll', event => handler.onReplaceAll(ReplaceAllData.decode(event.data)));
    source.addEventListener('scrollTo', event => handler.onScrollTo(ScrollToData.decode(event.data)));
    source.addEventListener('commit', event => handler.onCommit(CommitData.decode(event.data)));
    source.addEventListener('discard', event => handler.onDiscard(DiscardData.decode(event.data)));
}
class MoveTextData {
    id;
    parentId;
    prevSiblingId;
    constructor(id, parentId, prevSiblingId) {
        this.id = id;
        this.parentId = parentId;
        this.prevSiblingId = prevSiblingId;
    }
    static decode(data) {
        const args = data.split("\n");
        const id = Number.parseInt(args[0]);
        const parentId = parseNullableNodeId(args[1]);
        const prevSiblingId = parseNullableNodeId(args[2]);
        return new MoveTextData(id, parentId, prevSiblingId);
    }
}
class MoveElementData {
    id;
    parentId;
    prevSiblingId;
    constructor(id, parentId, prevSiblingId) {
        this.id = id;
        this.parentId = parentId;
        this.prevSiblingId = prevSiblingId;
    }
    static decode(data) {
        const args = data.split("\n");
        const id = Number.parseInt(args[0]);
        const parentId = parseNullableNodeId(args[1]);
        const prevSiblingId = parseNullableNodeId(args[2]);
        return new MoveElementData(id, parentId, prevSiblingId);
    }
}
class ResetFormData {
    id;
    constructor(id) {
        this.id = id;
    }
    static decode(data) {
        const args = data.split("\n");
        const id = Number.parseInt(args[0]);
        return new ResetFormData(id);
    }
}
class AttachActionData {
    id;
    constructor(id) {
        this.id = id;
    }
    static decode(data) {
        const args = data.split("\n");
        const id = Number.parseInt(args[0]);
        return new AttachActionData(id);
    }
}
class AttachRouteData {
    id;
    route;
    constructor(id, route) {
        this.id = id;
        this.route = route;
    }
    static decode(data) {
        const args = data.split("\n");
        const id = Number.parseInt(args[0]);
        const route = atob(args[1]);
        return new AttachRouteData(id, route);
    }
}
class DetachRouteData {
    id;
    constructor(id) {
        this.id = id;
    }
    static decode(data) {
        const args = data.split("\n");
        const id = Number.parseInt(args[0]);
        return new DetachRouteData(id);
    }
}
class MountElementData {
    tag;
    id;
    parentId;
    prevSiblingId;
    constructor(tag, id, parentId, prevSiblingId) {
        this.tag = tag;
        this.id = id;
        this.parentId = parentId;
        this.prevSiblingId = prevSiblingId;
    }
    static decode(data) {
        const args = data.split("\n");
        const tag = atob(args[0]);
        const id = Number.parseInt(args[1]);
        const parentId = parseNullableNodeId(args[2]);
        const prevSiblingId = parseNullableNodeId(args[3]);
        return new MountElementData(tag, id, parentId, prevSiblingId);
    }
}
class UnmountElementData {
    id;
    constructor(id) {
        this.id = id;
    }
    static decode(data) {
        const args = data.split("\n");
        const id = Number.parseInt(args[0]);
        return new UnmountElementData(id);
    }
}
class MountTextData {
    id;
    parentId;
    prevSiblingId;
    bare;
    text;
    constructor(id, parentId, prevSiblingId, bare, text) {
        this.id = id;
        this.parentId = parentId;
        this.prevSiblingId = prevSiblingId;
        this.bare = bare;
        this.text = text;
    }
    static decode(data) {
        const args = data.split("\n");
        const id = Number.parseInt(args[0]);
        const parentId = parseNullableNodeId(args[1]);
        const prevSiblingId = parseNullableNodeId(args[2]);
        const bare = JSON.parse(args[3]);
        const text = atob(args[4]);
        return new MountTextData(id, parentId, prevSiblingId, bare, text);
    }
}
class UnmountTextData {
    id;
    constructor(id) {
        this.id = id;
    }
    static decode(data) {
        const args = data.split("\n");
        const id = Number.parseInt(args[0]);
        return new UnmountTextData(id);
    }
}
class SetAttributeData {
    id;
    attr;
    value;
    constructor(id, attr, value) {
        this.id = id;
        this.attr = attr;
        this.value = value;
    }
    static decode(data) {
        const args = data.split("\n");
        const id = Number.parseInt(args[0]);
        const attr = atob(args[1]);
        const hasValue = JSON.parse(args[2]);
        const value = hasValue ? atob(args[3]) : "";
        return new SetAttributeData(id, attr, value);
    }
}
class RemoveAttributeData {
    id;
    attr;
    constructor(id, attr) {
        this.id = id;
        this.attr = attr;
    }
    static decode(data) {
        const args = data.split("\n");
        const id = Number.parseInt(args[0]);
        const attr = atob(args[1]);
        return new RemoveAttributeData(id, attr);
    }
}
class PushPageData {
    route;
    constructor(route) {
        this.route = route;
    }
    static decode(data) {
        const args = data.split("\n");
        const route = atob(args[0]);
        return new PushPageData(route);
    }
}
class ReplaceRouteData {
    route;
    constructor(route) {
        this.route = route;
    }
    static decode(data) {
        const args = data.split("\n");
        const route = atob(args[0]);
        return new ReplaceRouteData(route);
    }
}
class ReplaceAllData {
    html;
    constructor(html) {
        this.html = html;
    }
    static decode(data) {
        const args = data.split("\n");
        const html = atob(args[0]);
        return new ReplaceAllData(html);
    }
}
class ScrollToData {
    x;
    y;
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    static decode(data) {
        const args = data.split("\n");
        const x = Number.parseFloat(args[0]);
        const y = Number.parseFloat(args[1]);
        return new ScrollToData(x, y);
    }
}
class CommitData {
    static decode(_) {
        return new CommitData();
    }
}
class DiscardData {
    static decode(_) {
        return new DiscardData();
    }
}
// -- Primitives ------------------------------------------------------------------------------------------------------
function parseNullableNodeId(str) {
    const value = Number.parseInt(str);
    if (value === -1) {
        return null;
    }
    else {
        return value;
    }
}

function mapNonNull(nullable, map) {
    if (nullable === null)
        return null;
    return map(nullable);
}

const JadeFormParams = {
    TARGET_ELEMENT_NODE_ID_PARAM: "net.jadeframework.servlet.JadeFormParams.TARGET_ELEMENT_NODE_ID_PARAM",
    WINDOW_ID_PARAM: "net.jadeframework.servlet.JadeFormParams.WINDOW_ID_PARAM"
};

// -- Handler ---------------------------------------------------------------------------------------------------------
class JadeSSEHandlerImpl {
    window;
    scheduled = [];
    constructor(window) {
        this.window = window;
    }
    onMoveText(data) {
        this.scheduled.push(() => {
            const tail = this.lookupNodeById(data.id);
            const text = tail.previousSibling;
            const head = text?.previousSibling;
            const parent = mapNonNull(data.parentId, id => this.lookupNodeById(id));
            const prevSibling = mapNonNull(data.prevSiblingId, id => this.lookupNodeById(id));
            this.insertAfter(parent, prevSibling, head);
            head.after(text);
            text.after(tail);
        });
    }
    onMoveElement(data) {
        this.scheduled.push(() => {
            const parent = mapNonNull(data.parentId, id => this.lookupNodeById(id));
            const prevSibling = mapNonNull(data.prevSiblingId, id => this.lookupNodeById(id));
            const node = this.lookupNodeById(data.id);
            this.insertAfter(parent, prevSibling, node);
        });
    }
    onResetForm(data) {
        this.scheduled.push(() => {
            const node = this.lookupNodeById(data.id);
            if (!(node instanceof HTMLFormElement))
                throw new Error();
            node.reset();
        });
    }
    onAttachAction(data) {
        this.scheduled.push(() => {
            const node = this.lookupNodeById(data.id);
            if (!(node instanceof HTMLFormElement))
                throw new Error();
            node.setAttribute('method', 'POST');
            node.setAttribute('data-jade-javaaction', '');
            node.addEventListener('submit', handleFormInteraction);
            const targetElementNodeIdHiddenInput = document.createElement('input');
            targetElementNodeIdHiddenInput.setAttribute('type', 'hidden');
            targetElementNodeIdHiddenInput.setAttribute('name', JadeFormParams.TARGET_ELEMENT_NODE_ID_PARAM);
            targetElementNodeIdHiddenInput.setAttribute('value', String(data.id));
            node.appendChild(targetElementNodeIdHiddenInput);
            const windowIdHiddenInput = document.createElement('input');
            windowIdHiddenInput.setAttribute('type', 'hidden');
            windowIdHiddenInput.setAttribute('name', JadeFormParams.WINDOW_ID_PARAM);
            windowIdHiddenInput.setAttribute('value', getHostData(this.window.document).windowId);
            node.appendChild(windowIdHiddenInput);
        });
    }
    onDetachAction(data) {
        this.scheduled.push(() => {
            const node = this.lookupNodeById(data.id);
            if (!(node instanceof HTMLFormElement))
                throw new Error();
            node.removeAttribute('method');
            node.removeAttribute('data-jade-javaaction');
            this.findInputByName(node, JadeFormParams.TARGET_ELEMENT_NODE_ID_PARAM).remove();
            this.findInputByName(node, JadeFormParams.WINDOW_ID_PARAM).remove();
        });
    }
    onAttachRoute(data) {
        this.scheduled.push(() => {
            const node = this.lookupNodeById(data.id);
            if (!(node instanceof HTMLAnchorElement))
                throw new Error();
            const href = getHostData(this.window.document).path + '/' + data.route;
            node.setAttribute('href', href);
            node.addEventListener('click', handleLinkInteraction);
        });
    }
    onDetachRoute(data) {
        this.scheduled.push(() => {
            const node = this.lookupNodeById(data.id);
            if (!(node instanceof HTMLAnchorElement))
                throw new Error();
            node.removeAttribute('href');
            node.removeEventListener('click', handleLinkInteraction);
        });
    }
    onMountElement(data) {
        this.scheduled.push(() => {
            const parent = mapNonNull(data.parentId, id => this.lookupNodeById(id));
            const prevSibling = mapNonNull(data.prevSiblingId, id => this.lookupNodeById(id));
            const node = this.window.document.createElement(data.tag);
            node.setAttribute('data-jade-id', String(data.id));
            this.insertAfter(parent, prevSibling, node);
            this.putNodeLut(data.id, node);
        });
    }
    onUnmountElement(data) {
        this.scheduled.push(() => {
            const node = this.lookupNodeById(data.id);
            if (!(node instanceof HTMLElement))
                throw new Error();
            node.remove();
            this.deleteNodeLut(data.id);
        });
    }
    onMountText(data) {
        this.scheduled.push(() => {
            const parent = mapNonNull(data.parentId, id => this.lookupNodeById(id));
            const prevSibling = mapNonNull(data.prevSiblingId, id => this.lookupNodeById(id));
            const text = this.window.document.createTextNode(data.text);
            if (data.bare) {
                this.insertAfter(parent, prevSibling, text);
                this.putNodeLut(data.id, text);
            }
            else {
                const head = this.window.document.createComment('jade-text-start jade-id=' + data.id);
                this.insertAfter(parent, prevSibling, head);
                head.after(text);
                const tail = this.window.document.createComment('jade-text-end jade-id=' + data.id);
                text.after(tail);
                this.putNodeLut(data.id, tail);
            }
        });
    }
    onUnmountText(data) {
        this.scheduled.push(() => {
            const node = this.lookupNodeById(data.id);
            // Handle 'bare' text (text nodes not wrapped in a head and tail comment).
            if (node instanceof Text) {
                node.remove();
            }
            // Handle standard text (wrapped in head and tail comment).
            if (node instanceof Comment) {
                // noinspection UnnecessaryLocalVariableJS
                const tail = node;
                const text = tail.previousSibling;
                if (!(text instanceof Text))
                    throw new Error();
                const head = text.previousSibling;
                if (!(head instanceof Comment))
                    throw new Error();
                tail.remove();
                text.remove();
                head.remove();
            }
            this.deleteNodeLut(data.id);
        });
    }
    onSetAttribute(data) {
        this.scheduled.push(() => {
            const node = this.lookupNodeById(data.id);
            if (!(node instanceof HTMLElement))
                throw new Error();
            node.setAttribute(data.attr, data.value);
        });
    }
    onRemoveAttribute(data) {
        this.scheduled.push(() => {
            const node = this.lookupNodeById(data.id);
            if (!(node instanceof HTMLElement))
                throw new Error();
            node.removeAttribute(data.attr);
        });
    }
    onPushPage(data) {
        this.scheduled.push(() => {
            const path = getHostData(this.window.document).path + '/' + data.route;
            this.window.history.pushState({ route: data.route }, '', path);
        });
    }
    onReplaceRoute(data) {
        this.scheduled.push(() => {
            const path = getHostData(this.window.document).path + '/' + data.route;
            this.window.history.replaceState(data.route, '', path);
        });
    }
    onReplaceAll(data) {
        this.scheduled.push(() => {
            this.window.document.documentElement.innerHTML = data.html;
            visit(this.window.document);
        });
    }
    onScrollTo(data) {
        this.scheduled.push(() => {
            this.window.scrollTo(data.x, data.y);
        });
    }
    onCommit(_) {
        const tasks = [...this.scheduled];
        this.scheduled = [];
        for (const task of tasks) {
            task();
        }
    }
    onDiscard(_) {
        this.scheduled = [];
    }
    // Jade node LUT utils
    lookupNodeById(id) {
        return getJadeNodeLut(this.window.document).get(id);
    }
    putNodeLut(id, node) {
        getJadeNodeLut(this.window.document).set(id, node);
    }
    deleteNodeLut(id) {
        getJadeNodeLut(this.window.document).delete(id);
    }
    // DOM utils
    insertAfter(parent, prevSibling, node) {
        if (prevSibling) {
            prevSibling.after(node);
        }
        else {
            if (parent == null) {
                this.window.document.prepend(node);
            }
            else {
                parent.prepend(node);
            }
        }
    }
    findInputByName(form, name) {
        for (const el of form.children) {
            if (el.tagName !== 'INPUT')
                continue;
            if (el.getAttribute('name') !== name)
                continue;
            return el;
        }
        return null;
    }
}
// -- Mutations -------------------------------------------------------------------------------------------------------

function connect(window) {
    const path = getHostData(window.document).eventStreamPath;
    const source = new FetchEventSource(path, { withCredentials: true });
    setJadeEventSource(window.document, source);
    const handler = new JadeSSEHandlerImpl(window);
    addHandler(source, handler);
}

async function handlePopState(event) {
    const window = event.currentTarget;
    const document = window.document;
    const hostData = getHostData(document);
    const formData = new FormData();
    formData.set('net.jadeframework.servlet.JadeAsyncNavigateServlet.WINDOW_ID_PARAM', hostData.windowId);
    formData.set('net.jadeframework.servlet.JadeAsyncNavigateServlet.ROUTE_PARAM', event.state.route);
    formData.set('net.jadeframework.servlet.JadeAsyncNavigateServlet.OMIT_PAGE_PUSH_EVENT_PARAM', 'true');
    formData.set('net.jadeframework.servlet.JadeAsyncNavigateServlet.SCROLL_X_PARAM', String(event.state.scrollPosition?.x || 0));
    formData.set('net.jadeframework.servlet.JadeAsyncNavigateServlet.SCROLL_Y_PARAM', String(event.state.scrollPosition?.y || 0));
    // @ts-expect-error
    const body = new URLSearchParams(formData);
    const response = await fetch(hostData.asyncNavigatePath, { method: 'POST', body });
    // If we can't navigate asynchronously, fallback to native navigation.
    if (!response.ok)
        window.location.reload();
}

window.history.scrollRestoration = 'manual';
setInterval(() => {
    let changed = false;
    changed ||= (window.history.state?.scrollPosition?.x !== window.scrollX);
    changed ||= (window.history.state?.scrollPosition?.y !== window.scrollY);
    if (changed) {
        window.history.replaceState({
            ...window.history.state,
            scrollPosition: {
                x: window.scrollX,
                y: window.scrollY
            }
        }, '');
    }
}, 100, 100);
loadHostData(document);
visit(document);
connect(window);
window.addEventListener('popstate', handlePopState);
