/**
 * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
 * Node module: @schukai/monster
 * This file is licensed under the AGPLv3 License.
 * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
 */

import {internalSymbol} from "../constants.mjs";
import {diff} from "../data/diff.mjs";
import {Pathfinder} from "../data/pathfinder.mjs";
import {Pipe} from "../data/pipe.mjs";
import {
    ATTRIBUTE_ERRORMESSAGE,
    ATTRIBUTE_UPDATER_ATTRIBUTES,
    ATTRIBUTE_UPDATER_BIND,
    ATTRIBUTE_UPDATER_INSERT,
    ATTRIBUTE_UPDATER_INSERT_REFERENCE,
    ATTRIBUTE_UPDATER_REMOVE,
    ATTRIBUTE_UPDATER_REPLACE,
    ATTRIBUTE_UPDATER_SELECT_THIS,
    ATTRIBUTE_UPDATER_INSERT_TEMPLATE_ID
} from "../dom/constants.mjs";

import {Base} from "../types/base.mjs";
import {isArray, isInstance, isIterable} from "../types/is.mjs";
import {Observer} from "../types/observer.mjs";
import {ProxyObserver} from "../types/proxyobserver.mjs";
import {validateArray, validateInstance} from "../types/validate.mjs";
import {clone} from "../util/clone.mjs";
import {trimSpaces} from "../util/trimspaces.mjs";
import {addToObjectLink} from "./attributes.mjs";
import {findTargetElementFromEvent} from "./events.mjs";
import {findDocumentTemplate} from "./template.mjs";

export {Updater, addObjectWithUpdaterToElement};

/**
 * The updater class connects an object with the dom. In this way, structures and contents in the DOM can be programmatically adapted via attributes.
 *
 * For example, to include a string from an object, the attribute `data-monster-replace` can be used.
 * a further explanation can be found under {@tutorial dom-based-templating-implementation}.
 *
 * Changes to attributes are made only when the direct values are changed. If you want to assign changes to other values
 * as well, you have to insert the attribute `data-monster-select-this`. This should be done with care, as it can reduce performance.
 *
 * @externalExample ../../example/dom/updater.mjs
 * @license AGPLv3
 * @since 1.8.0
 * @copyright schukai GmbH
 * @memberOf Monster.DOM
 * @throws {Error} the value is not iterable
 * @throws {Error} pipes are not allowed when cloning a node.
 * @throws {Error} no template was found with the specified key.
 * @throws {Error} the maximum depth for the recursion is reached.
 * @throws {TypeError} value is not a object
 * @throws {TypeError} value is not an instance of HTMLElement
 * @summary The updater class connects an object with the dom
 */
class Updater extends Base {
    /**
     * @since 1.8.0
     * @param {HTMLElement} element
     * @param {object|ProxyObserver|undefined} subject
     * @throws {TypeError} value is not a object
     * @throws {TypeError} value is not an instance of HTMLElement
     * @see {@link Monster.DOM.findDocumentTemplate}
     */
    constructor(element, subject) {
        super();

        /**
         * @type {HTMLElement}
         */
        if (subject === undefined) subject = {};
        if (!isInstance(subject, ProxyObserver)) {
            subject = new ProxyObserver(subject);
        }

        this[internalSymbol] = {
            element: validateInstance(element, HTMLElement),
            last: {},
            callbacks: new Map(),
            eventTypes: ["keyup", "click", "change", "drop", "touchend", "input"],
            subject: subject,
        };

        this[internalSymbol].callbacks.set("checkstate", getCheckStateCallback.call(this));

        this[internalSymbol].subject.attachObserver(
            new Observer(() => {
                const s = this[internalSymbol].subject.getRealSubject();

                const diffResult = diff(this[internalSymbol].last, s);
                this[internalSymbol].last = clone(s);

                for (const [, change] of Object.entries(diffResult)) {
                    removeElement.call(this, change);
                    insertElement.call(this, change);
                    updateContent.call(this, change);
                    updateAttributes.call(this, change);
                }
            }),
        );
    }

    /**
     * Defaults: 'keyup', 'click', 'change', 'drop', 'touchend'
     *
     * @see {@link https://developer.mozilla.org/de/docs/Web/Events}
     * @since 1.9.0
     * @param {Array} types
     * @return {Updater}
     */
    setEventTypes(types) {
        this[internalSymbol].eventTypes = validateArray(types);
        return this;
    }

    /**
     * With this method, the eventlisteners are hooked in and the magic begins.
     *
     * ```
     * updater.run().then(() => {
     *   updater.enableEventProcessing();
     * });
     * ```
     *
     * @since 1.9.0
     * @return {Updater}
     * @throws {Error} the bind argument must start as a value with a path
     */
    enableEventProcessing() {
        this.disableEventProcessing();

        for (const type of this[internalSymbol].eventTypes) {
            // @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
            this[internalSymbol].element.addEventListener(type, getControlEventHandler.call(this), {
                capture: true,
                passive: true,
            });
        }

        return this;
    }

    /**
     * This method turns off the magic or who loves it more profane it removes the eventListener.
     *
     * @since 1.9.0
     * @return {Updater}
     */
    disableEventProcessing() {
        for (const type of this[internalSymbol].eventTypes) {
            this[internalSymbol].element.removeEventListener(type, getControlEventHandler.call(this));
        }

        return this;
    }

    /**
     * The run method must be called for the update to start working.
     * The method ensures that changes are detected.
     *
     * ```
     * updater.run().then(() => {
     *   updater.enableEventProcessing();
     * });
     * ```
     *
     * @summary Let the magic begin
     * @return {Promise}
     */
    run() {
        // the key __init__has no further meaning and is only
        // used to create the diff for empty objects.
        this[internalSymbol].last = {__init__: true};
        return this[internalSymbol].subject.notifyObservers();
    }

    /**
     * Gets the values of bound elements and changes them in subject
     *
     * @since 1.27.0
     * @return {Monster.DOM.Updater}
     */
    retrieve() {
        retrieveFromBindings.call(this);
        return this;
    }

    /**
     * If you have passed a ProxyObserver in the constructor, you will get the object that the ProxyObserver manages here.
     * However, if you passed a simple object, here you will get a proxy for that object.
     *
     * For changes the ProxyObserver must be used.
     *
     * @since 1.8.0
     * @return {Proxy}
     */
    getSubject() {
        return this[internalSymbol].subject.getSubject();
    }

    /**
     * This method can be used to register commands that can be called via call: instruction.
     * This can be used to provide a pipe with its own functionality.
     *
     * @param {string} name
     * @param {function} callback
     * @returns {Transformer}
     * @throws {TypeError} value is not a string
     * @throws {TypeError} value is not a function
     */
    setCallback(name, callback) {
        this[internalSymbol].callbacks.set(name, callback);
        return this;
    }
}

/**
 * @private
 * @license AGPLv3
 * @since 1.9.0
 * @return {function
 * @this Updater
 */
function getCheckStateCallback() {
    const self = this;

    return function (current) {
        // this is a reference to the current object (therefore no array function here)
        if (this instanceof HTMLInputElement) {
            if (["radio", "checkbox"].indexOf(this.type) !== -1) {
                return `${this.value}` === `${current}` ? "true" : undefined;
            }
        } else if (this instanceof HTMLOptionElement) {
            if (isArray(current) && current.indexOf(this.value) !== -1) {
                return "true";
            }

            return undefined;
        }
    };
}

/**
 * @private
 */
const symbol = Symbol("@schukai/monster/updater@@EventHandler");

/**
 * @private
 * @return {function}
 * @this Updater
 * @throws {Error} the bind argument must start as a value with a path
 */
function getControlEventHandler() {
    const self = this;

    if (self[symbol]) {
        return self[symbol];
    }

    /**
     * @throws {Error} the bind argument must start as a value with a path.
     * @throws {Error} unsupported object
     * @param {Event} event
     */
    self[symbol] = (event) => {
        const element = findTargetElementFromEvent(event, ATTRIBUTE_UPDATER_BIND);

        if (element === undefined) {
            return;
        }

        retrieveAndSetValue.call(self, element);
    };

    return self[symbol];
}

/**
 * @throws {Error} the bind argument must start as a value with a path
 * @param {HTMLElement} element
 * @return void
 * @memberOf Monster.DOM
 * @private
 */
function retrieveAndSetValue(element) {
    const self = this;

    const pathfinder = new Pathfinder(self[internalSymbol].subject.getSubject());

    let path = element.getAttribute(ATTRIBUTE_UPDATER_BIND);

    if (path.indexOf("path:") !== 0) {
        throw new Error("the bind argument must start as a value with a path");
    }

    path = path.substring(5);

    let value;

    if (element instanceof HTMLInputElement) {
        switch (element.type) {
            case "checkbox":
                value = element.checked ? element.value : undefined;
                break;
            default:
                value = element.value;
                break;
        }
    } else if (element instanceof HTMLTextAreaElement) {
        value = element.value;
    } else if (element instanceof HTMLSelectElement) {
        switch (element.type) {
            case "select-one":
                value = element.value;
                break;
            case "select-multiple":
                value = element.value;

                let options = element?.selectedOptions;
                if (options === undefined) options = element.querySelectorAll(":scope option:checked");
                value = Array.from(options).map(({value}) => value);

                break;
        }

        // values from customelements
    } else if (
        (element?.constructor?.prototype &&
            !!Object.getOwnPropertyDescriptor(element.constructor.prototype, "value")?.["get"]) ||
        element.hasOwnProperty("value")
    ) {
        value = element?.["value"];
    } else {
        throw new Error("unsupported object");
    }

    const copy = clone(self[internalSymbol].subject.getRealSubject());
    const pf = new Pathfinder(copy);
    pf.setVia(path, value);

    const diffResult = diff(copy, self[internalSymbol].subject.getRealSubject());

    if (diffResult.length > 0) {
        pathfinder.setVia(path, value);
    }
}

/**
 * @license AGPLv3
 * @since 1.27.0
 * @return void
 * @private
 */
function retrieveFromBindings() {
    const self = this;

    if (self[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`)) {
        retrieveAndSetValue.call(self, self[internalSymbol].element);
    }

    for (const [, element] of self[internalSymbol].element.querySelectorAll(`[${ATTRIBUTE_UPDATER_BIND}]`).entries()) {
        retrieveAndSetValue.call(self, element);
    }
}

/**
 * @private
 * @license AGPLv3
 * @since 1.8.0
 * @param {object} change
 * @return {void}
 */
function removeElement(change) {
    const self = this;

    for (const [, element] of self[internalSymbol].element
        .querySelectorAll(`:scope [${ATTRIBUTE_UPDATER_REMOVE}]`)
        .entries()) {
        element.parentNode.removeChild(element);
    }
}

/**
 * @private
 * @license AGPLv3
 * @since 1.8.0
 * @param {object} change
 * @return {void}
 * @throws {Error} the value is not iterable
 * @throws {Error} pipes are not allowed when cloning a node.
 * @throws {Error} no template was found with the specified key.
 * @throws {Error} the maximum depth for the recursion is reached.
 * @this Updater
 */
function insertElement(change) {
    const self = this;
    const subject = self[internalSymbol].subject.getRealSubject();

    let mem = new WeakSet();
    let wd = 0;

    const container = self[internalSymbol].element;

    while (true) {
        let found = false;
        wd++;

        let p = clone(change?.["path"]);
        if (!isArray(p)) return self;

        while (p.length > 0) {
            const current = p.join(".");

            let iterator = new Set();
            const query = `[${ATTRIBUTE_UPDATER_INSERT}*="path:${current}"]`;

            const e = container.querySelectorAll(query);

            if (e.length > 0) {
                iterator = new Set([...e]);
            }

            if (container.matches(query)) {
                iterator.add(container);
            }

            for (const [, containerElement] of iterator.entries()) {
                if (mem.has(containerElement)) continue;
                mem.add(containerElement);

                found = true;

                const attributes = containerElement.getAttribute(ATTRIBUTE_UPDATER_INSERT);
                let def = trimSpaces(attributes);
                let i = def.indexOf(" ");
                let key = trimSpaces(def.substr(0, i));
                let refPrefix = `${key}-`;
                let cmd = trimSpaces(def.substr(i));

                // this case is actually excluded by the query but is nevertheless checked again here
                if (cmd.indexOf("|") > 0) {
                    throw new Error("pipes are not allowed when cloning a node.");
                }

                let pipe = new Pipe(cmd);
                self[internalSymbol].callbacks.forEach((f, n) => {
                    pipe.setCallback(n, f);
                });

                let value;
                try {
                    containerElement.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
                    value = pipe.run(subject);
                } catch (e) {
                    containerElement.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message);
                }

                let dataPath = cmd.split(":").pop();

                let insertPoint;
                if (containerElement.hasChildNodes()) {
                    insertPoint = containerElement.lastChild;
                }

                if (!isIterable(value)) {
                    throw new Error("the value is not iterable");
                }

                let available = new Set();

                for (const [i, obj] of Object.entries(value)) {
                    let ref = refPrefix + i;
                    let currentPath = `${dataPath}.${i}`;

                    available.add(ref);
                    let refElement = containerElement.querySelector(`[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}="${ref}"]`);

                    if (refElement instanceof HTMLElement) {
                        insertPoint = refElement;
                        continue;
                    }

                    appendNewDocumentFragment(containerElement, key, ref, currentPath);
                }

                let nodes = containerElement.querySelectorAll(
                    `[${ATTRIBUTE_UPDATER_INSERT_REFERENCE}*="${refPrefix}"]`,
                );
                for (const [, node] of Object.entries(nodes)) {
                    if (!available.has(node.getAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE))) {
                        try {
                            containerElement.removeChild(node);
                        } catch (e) {
                            containerElement.setAttribute(
                                ATTRIBUTE_ERRORMESSAGE,
                                `${containerElement.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${e.message}`.trim(),
                            );
                        }
                    }
                }
            }

            p.pop();
        }

        if (found === false) break;
        if (wd++ > 200) {
            throw new Error("the maximum depth for the recursion is reached.");
        }
    }
}

/**
 * @private
 * @param container
 * @param key
 * @param ref
 * @param path
 * @returns {any}
 */
function internalTemplateLookUp(container, key, ref, path) {

    let templateID = key;
    let template;

    if (container.hasAttribute(ATTRIBUTE_UPDATER_INSERT_TEMPLATE_ID)) {
        templateID = container.getAttribute(ATTRIBUTE_UPDATER_INSERT_TEMPLATE_ID);
        template = findDocumentTemplate(templateID, container);
        if (template instanceof HTMLTemplateElement) {
            return template;
        }
    }

    if (container.closest(`[${ATTRIBUTE_UPDATER_INSERT_TEMPLATE_ID}]`)) {
        templateID = container.closest(`[${ATTRIBUTE_UPDATER_INSERT_TEMPLATE_ID}]`).getAttribute(ATTRIBUTE_UPDATER_INSERT_TEMPLATE_ID);
        template = findDocumentTemplate(templateID, container);
        if (template instanceof HTMLTemplateElement) {
            return template;
        }
    }

    return findDocumentTemplate(templateID, container);
}

/**
 *
 * @private
 * @license AGPLv3
 * @since 1.8.0
 * @param {HTMLElement} container
 * @param {string} key
 * @param {string} ref
 * @param {string} path
 * @throws {Error} no template was found with the specified key.
 */
function appendNewDocumentFragment(container, key, ref, path) {

    let template = internalTemplateLookUp(container, key, ref, path);

    let nodes = template.createDocumentFragment();
    for (const [, node] of Object.entries(nodes.childNodes)) {
        if (node instanceof HTMLElement) {
            applyRecursive(node, key, path);
            node.setAttribute(ATTRIBUTE_UPDATER_INSERT_REFERENCE, ref);
        }

        container.appendChild(node);
    }
}

/**
 * @private
 * @license AGPLv3
 * @since 1.10.0
 * @param {HTMLElement} node
 * @param {string} key
 * @param {string} path
 * @return {void}
 */
function applyRecursive(node, key, path) {
    if (node instanceof HTMLElement) {
        if (node.hasAttribute(ATTRIBUTE_UPDATER_REPLACE)) {
            let value = node.getAttribute(ATTRIBUTE_UPDATER_REPLACE);
            node.setAttribute(ATTRIBUTE_UPDATER_REPLACE, value.replaceAll(`path:${key}`, `path:${path}`));
        }

        if (node.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) {
            let value = node.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);
            node.setAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES, value.replaceAll(`path:${key}`, `path:${path}`));
        }

        for (const [, child] of Object.entries(node.childNodes)) {
            applyRecursive(child, key, path);
        }
    }
}

/**
 * @private
 * @license AGPLv3
 * @since 1.8.0
 * @param {object} change
 * @return {void}
 * @this Updater
 */
function updateContent(change) {
    const self = this;
    const subject = self[internalSymbol].subject.getRealSubject();

    let p = clone(change?.["path"]);
    runUpdateContent.call(this, this[internalSymbol].element, p, subject);

    const slots = this[internalSymbol].element.querySelectorAll("slot");
    if (slots.length > 0) {
        for (const [, slot] of Object.entries(slots)) {
            for (const [, element] of Object.entries(slot.assignedNodes())) {
                runUpdateContent.call(this, element, p, subject);
            }
        }
    }
}

/**
 * @private
 * @license AGPLv3
 * @since 1.8.0
 * @param {HTMLElement} container
 * @param {array} parts
 * @param {object} subject
 * @return {void}
 */
function runUpdateContent(container, parts, subject) {
    if (!isArray(parts)) return;
    if (!(container instanceof HTMLElement)) return;
    parts = clone(parts);

    let mem = new WeakSet();

    while (parts.length > 0) {
        const current = parts.join(".");
        parts.pop();

        // Unfortunately, static data is always changed as well, since it is not possible to react to changes here.
        const query = `[${ATTRIBUTE_UPDATER_REPLACE}^="path:${current}"], [${ATTRIBUTE_UPDATER_REPLACE}^="static:"], [${ATTRIBUTE_UPDATER_REPLACE}^="i18n:"]`;
        const e = container.querySelectorAll(`${query}`);

        const iterator = new Set([...e]);

        if (container.matches(query)) {
            iterator.add(container);
        }

        /**
         * @type {HTMLElement}
         */
        for (const [element] of iterator.entries()) {
            if (mem.has(element)) return;
            mem.add(element);

            const attributes = element.getAttribute(ATTRIBUTE_UPDATER_REPLACE);
            let cmd = trimSpaces(attributes);

            let pipe = new Pipe(cmd);
            this[internalSymbol].callbacks.forEach((f, n) => {
                pipe.setCallback(n, f);
            });

            let value;
            try {
                element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
                value = pipe.run(subject);
            } catch (e) {
                element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message);
            }

            if (value instanceof HTMLElement) {
                while (element.firstChild) {
                    element.removeChild(element.firstChild);
                }

                try {
                    element.appendChild(value);
                } catch (e) {
                    element.setAttribute(
                        ATTRIBUTE_ERRORMESSAGE,
                        `${element.getAttribute(ATTRIBUTE_ERRORMESSAGE)}, ${e.message}`.trim(),
                    );
                }
            } else {
                element.innerHTML = value;
            }
        }
    }
}

/**
 * @private
 * @license AGPLv3
 * @since 1.8.0
 * @param {string} path
 * @param {object} change
 * @return {void}
 */
function updateAttributes(change) {
    const subject = this[internalSymbol].subject.getRealSubject();
    let p = clone(change?.["path"]);
    runUpdateAttributes.call(this, this[internalSymbol].element, p, subject);
}

/**
 * @private
 * @param {HTMLElement} container
 * @param {array} parts
 * @param {object} subject
 * @return {void}
 * @this Updater
 */
function runUpdateAttributes(container, parts, subject) {
    const self = this;

    if (!isArray(parts)) return;
    parts = clone(parts);

    let mem = new WeakSet();

    while (parts.length > 0) {
        const current = parts.join(".");
        parts.pop();

        let iterator = new Set();

        const query = `[${ATTRIBUTE_UPDATER_SELECT_THIS}], [${ATTRIBUTE_UPDATER_ATTRIBUTES}*="path:${current}"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="static:"], [${ATTRIBUTE_UPDATER_ATTRIBUTES}^="i18n:"]`;

        const e = container.querySelectorAll(query);

        if (e.length > 0) {
            iterator = new Set([...e]);
        }

        if (container.matches(query)) {
            iterator.add(container);
        }

        for (const [element] of iterator.entries()) {
            if (mem.has(element)) return;
            mem.add(element);

            const attributes = element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);

            for (let [, def] of Object.entries(attributes.split(","))) {
                def = trimSpaces(def);
                let i = def.indexOf(" ");
                let name = trimSpaces(def.substr(0, i));
                let cmd = trimSpaces(def.substr(i));

                let pipe = new Pipe(cmd);

                self[internalSymbol].callbacks.forEach((f, n) => {
                    pipe.setCallback(n, f, element);
                });

                let value;
                try {
                    element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
                    value = pipe.run(subject);
                } catch (e) {
                    element.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.message);
                }

                if (value === undefined) {
                    element.removeAttribute(name);
                } else if (element.getAttribute(name) !== value) {
                    element.setAttribute(name, value);
                }

                handleInputControlAttributeUpdate.call(this, element, name, value);
            }
        }
    }
}

/**
 * @private
 * @param {HTMLElement|*} element
 * @param {string} name
 * @param {string|number|undefined} value
 * @return {void}
 * @this Updater
 */

function handleInputControlAttributeUpdate(element, name, value) {
    const self = this;

    if (element instanceof HTMLSelectElement) {
        switch (element.type) {
            case "select-multiple":
                for (const [index, opt] of Object.entries(element.options)) {
                    if (value.indexOf(opt.value) !== -1) {
                        opt.selected = true;
                    } else {
                        opt.selected = false;
                    }
                }

                break;
            case "select-one":
                // Only one value may be selected

                for (const [index, opt] of Object.entries(element.options)) {
                    if (opt.value === value) {
                        element.selectedIndex = index;
                        break;
                    }
                }

                break;
        }
    } else if (element instanceof HTMLInputElement) {
        switch (element.type) {
            case "radio":
                if (name === "checked") {
                    if (value !== undefined) {
                        element.checked = true;
                    } else {
                        element.checked = false;
                    }
                }

                break;

            case "checkbox":
                if (name === "checked") {
                    if (value !== undefined) {
                        element.checked = true;
                    } else {
                        element.checked = false;
                    }
                }

                break;
            case "text":
            default:
                if (name === "value") {
                    element.value = value === undefined ? "" : value;
                }

                break;
        }
    } else if (element instanceof HTMLTextAreaElement) {
        if (name === "value") {
            element.value = value === undefined ? "" : value;
        }
    }
}

/**
 * @param {NodeList|HTMLElement|Set<HTMLElement>} elements
 * @param {Symbol} symbol
 * @param {object} object
 * @return {Promise[]}
 * @license AGPLv3
 * @since 1.23.0
 * @memberOf Monster.DOM
 * @throws {TypeError} elements is not an instance of NodeList, HTMLElement or Set
 * @throws {TypeError} the context of the function is not an instance of HTMLElement
 * @throws {TypeError} symbol must be an instance of Symbol
 */
function addObjectWithUpdaterToElement(elements, symbol, object) {
    const self = this;
    if (!(self instanceof HTMLElement)) {
        throw new TypeError("the context of this function must be an instance of HTMLElement");
    }

    if (!(typeof symbol === "symbol")) {
        throw new TypeError("symbol must be an instance of Symbol");
    }

    const updaters = new Set();

    if (elements instanceof NodeList) {
        elements = new Set([...elements]);
    } else if (elements instanceof HTMLElement) {
        elements = new Set([elements]);
    } else if (elements instanceof Set) {
    } else {
        throw new TypeError(`elements is not a valid type. (actual: ${typeof elements})`);
    }

    let result = [];

    elements.forEach((element) => {
        if (!(element instanceof HTMLElement)) return;
        if (element instanceof HTMLTemplateElement) return;

        const u = new Updater(element, object);
        updaters.add(u);

        result.push(
            u.run().then(() => {
                return u.enableEventProcessing();
            }),
        );
    });

    if (updaters.size > 0) {
        addToObjectLink(self, symbol, updaters);
    }

    return result;
}