/**
 * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact schukai GmbH.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

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_BIND_TYPE,
    ATTRIBUTE_UPDATER_INSERT,
    ATTRIBUTE_UPDATER_INSERT_REFERENCE,
    ATTRIBUTE_UPDATER_REMOVE,
    ATTRIBUTE_UPDATER_REPLACE,
    ATTRIBUTE_UPDATER_SELECT_THIS,
} from "./constants.mjs";

import {Base} from "../types/base.mjs";
import {isArray, isInteger, isString, 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 {addAttributeToken, addToObjectLink} from "./attributes.mjs";
import {
    CustomElement,
    updaterTransformerMethodsSymbol,
} from "./customelement.mjs";
import {findTargetElementFromEvent} from "./events.mjs";
import {findDocumentTemplate} from "./template.mjs";
import {getWindow} from "./util.mjs";
import {DeadMansSwitch} from "../util/deadmansswitch.mjs";
import {addErrorAttribute, removeErrorAttribute} from "./error.mjs";


export {Updater, addObjectWithUpdaterToElement};


/**
 * @private
 * @type {symbol}
 */
const timerElementEventHandlerSymbol = Symbol("timerElementEventHandler");

/**
 * 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 [monsterjs.org](https://monsterjs.org/)
 *
 * 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.
 *
 * @example /examples/libraries/dom/updater/simple/ Simple example
 *
 * @license AGPLv3
 * @since 1.8.0
 * @copyright schukai GmbH
 * @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 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);

                const promises = [];

                for (const [, change] of Object.entries(diffResult)) {
                    promises.push(
                        new Promise((resolve, reject) => {
                            getWindow().requestAnimationFrame(() => {
                                try {
                                    removeElement.call(this, change);
                                    insertElement.call(this, change);
                                    updateContent.call(this, change);
                                    updateAttributes.call(this, change);

                                    resolve();
                                } catch (error) {
                                    reject(error);
                                }
                            });
                        }),
                    );
                }

                return Promise.all(promises);
            }),
        );
    }

    /**
     * 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.
     *
     * ```js
     * 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.
     *
     * ```js
     * 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
     * @return {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() {
    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() {
    if (this[symbol]) {
        return this[symbol];
    }

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

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

        if (this[timerElementEventHandlerSymbol] instanceof DeadMansSwitch) {
            try {
                this[timerElementEventHandlerSymbol].touch();
                return;
            } catch (e) {
                delete this[timerElementEventHandlerSymbol];
            }
        }

        this[timerElementEventHandlerSymbol] = new DeadMansSwitch(50, () => {
            try {
                retrieveAndSetValue.call(this, element);
            } catch (e) {
                addErrorAttribute(element, e);
            }
        });
    };

    return this[symbol];
}

/**
 * @throws {Error} the bind argument must start as a value with a path
 * @param {HTMLElement} element
 * @return void
 * @private
 */
function retrieveAndSetValue(element) {
    const pathfinder = new Pathfinder(this[internalSymbol].subject.getSubject());

    let path = element.getAttribute(ATTRIBUTE_UPDATER_BIND);
    if (path === null)
        throw new Error("the bind argument must start as a value with a path");

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

    path = path.substring(5); // remove path: from the string

    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 custom elements
    } 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 type = element.getAttribute(ATTRIBUTE_UPDATER_BIND_TYPE);
    switch (type) {

        case "integer?":
        case "int?":
        case "number?":
            value = Number(value);
            if (isNaN(value)||0===value) {
                value = undefined;
            }
            break;

        case "number":
        case "int":
        case "float":
        case "integer":
            value = Number(value);
            if (isNaN(value)) {
                value = 0;
            }
            break;
        case "boolean":
        case "bool":
        case "checkbox":
            value = value === "true" || value === "1" || value === "on" || value === true;
            break;

        case "string[]":
            if (isString(value)) {

                if (value.trim() === "") {
                    value = [];
                } else {
                    value = value.split(",").map((v) => `${v}`);
                }
            } else if (isInteger(value)) {
                value = [`${value}`];
            } else if (value === undefined || value === null) {
                value = [];
            } else if (isArray(value)) {
                value = value.map((v) => `${v}`);
            } else {
                throw new Error("unsupported value");
            }

            break;

        case "int[]":
        case "integer[]":

            if (isString(value)) {

                if (value.trim() === "") {
                    value = [];
                } else {
                    value = value.split(",").map((v) => {
                        try {
                            return parseInt(v, 10);
                        } catch (e) {
                        }
                        return -1;
                    }).filter((v) => v !== -1);
                }

            } else if (isInteger(value)) {
                value = [value];
            } else if (value === undefined || value === null) {
                value = [];
            } else if (isArray(value)) {
                value = value.split(",").map((v) => {
                    try {
                        return parseInt(v, 10);
                    } catch (e) {
                    }
                    return -1;
                }).filter((v) => v !== -1);
            } else {
                throw new Error("unsupported value");
            }

            break;
        case "[]":
        case "array":
        case "list":

            if (isString(value)) {
                if (value.trim() === "") {
                    value = [];
                } else {
                    value = value.split(",");
                }
            } else if (isInteger(value)) {
                value = [value];
            } else if (value === undefined || value === null) {
                value = [];
            } else if (isArray(value)) {
                // nothing to do
            } else {
                throw new Error("unsupported value for array");
            }
            break;
        case "object":
        case "json":

            if (isString(value)) {
                value = JSON.parse(value);
            } else {
                throw new Error("unsupported value for object");
            }

            break;
        default:
            break;
    }

    const copy = clone(this[internalSymbol].subject.getRealSubject());

    const pf = new Pathfinder(copy);
    pf.setVia(path, value);

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

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

/**
 * @license AGPLv3
 * @since 1.27.0
 * @return void
 * @private
 */
function retrieveFromBindings() {
    if (this[internalSymbol].element.matches(`[${ATTRIBUTE_UPDATER_BIND}]`)) {
        retrieveAndSetValue.call(this, this[internalSymbol].element);
    }

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

/**
 * @private
 * @license AGPLv3
 * @since 1.8.0
 * @param {object} change
 * @return {void}
 */
function removeElement(change) {
    for (const [, element] of this[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 subject = this[internalSymbol].subject.getRealSubject();

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

    const container = this[internalSymbol].element;

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

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

        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,
                );
                if (attributes === null) continue;

                const def = trimSpaces(attributes);
                const i = def.indexOf(" ");
                const key = trimSpaces(def.substr(0, i));
                const refPrefix = `${key}-`;
                const 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.");
                }

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

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

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

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

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

                const available = new Set();

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

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

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

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

                const 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) {
                            addErrorAttribute(containerElement, e);
                        }
                    }
                }
            }

            p.pop();
        }

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

/**
 *
 * @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) {
    const template = findDocumentTemplate(key, container);

    const 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)) {
        return
    }

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

    if (node.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) {
        const 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)) {
        if (child instanceof HTMLElement) {
            applyRecursive(child, key, path);
        }
    }

}

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

    const 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);

    const 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);
            const cmd = trimSpaces(attributes);

            const 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) {
                    addErrorAttribute(element, e);
                }
            } else {
                element.innerHTML = value;
            }
        }
    }
}

/**
 * @private
 * @since 1.8.0
 * @param {object} change
 * @return {void}
 */
function updateAttributes(change) {
    const subject = this[internalSymbol].subject.getRealSubject();
    const 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) {
    if (!isArray(parts)) return;
    parts = clone(parts);

    const 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}], [${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);

            // this case occurs when the ATTRIBUTE_UPDATER_SELECT_THIS attribute is set
            if (!element.hasAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES)) {
                continue;
            }

            const attributes = element.getAttribute(ATTRIBUTE_UPDATER_ATTRIBUTES);

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

                const pipe = new Pipe(cmd);

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

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

                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) {
    if (element instanceof HTMLSelectElement) {
        switch (element.type) {
            case "select-multiple":
                for (const [index, opt] of Object.entries(element.options)) {
                    opt.selected = value.indexOf(opt.value) !== -1;
                }

                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") {
                    element.checked = value !== undefined;
                }
                break;

            case "checkbox":
                if (name === "checked") {
                    element.checked = value !== undefined;
                }
                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
 * @param {object} config
 *
 * Config: enableEventProcessing {boolean} - default: false - enables the event processing
 *
 * @return {Promise[]}
 * @license AGPLv3
 * @since 1.23.0
 * @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, config = {}) {
    if (!(this 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})`,
        );
    }

    const result = [];

    const updaterCallbacks = [];
    const cb = this?.[updaterTransformerMethodsSymbol];
    if (this instanceof HTMLElement && typeof cb === "function") {
        const callbacks = cb.call(this);
        if (typeof callbacks === "object") {
            for (const [name, callback] of Object.entries(callbacks)) {
                if (typeof callback === "function") {
                    updaterCallbacks.push([name, callback]);
                } else {
                    addErrorAttribute(this, `onUpdaterPipeCallbacks: ${name} is not a function`);
                }
            }
        } else {
            addErrorAttribute(
                this,
                `onUpdaterPipeCallbacks do not return an object with functions`
            );
        }
    }

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

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

        if (updaterCallbacks.length > 0) {
            for (const [name, callback] of updaterCallbacks) {
                u.setCallback(name, callback);
            }
        }

        result.push(
            u.run().then(() => {
                if (config.eventProcessing === true) {
                    u.enableEventProcessing();
                }

                return u;
            }),
        );
    });

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

    return result;
}