import { Maybe } from "@eeue56/ts-core";

export type Tag =
    | "a"
    | "abbr"
    | "address"
    | "area"
    | "article"
    | "aside"
    | "audio"
    | "b"
    | "base"
    | "bdi"
    | "bdo"
    | "blockquote"
    | "body"
    | "br"
    | "button"
    | "canvas"
    | "caption"
    | "cite"
    | "code"
    | "col"
    | "colgroup"
    | "data"
    | "datalist"
    | "dd"
    | "del"
    | "details"
    | "dfn"
    | "dialog"
    | "div"
    | "dl"
    | "dt"
    | "em"
    | "embed"
    | "fieldset"
    | "figure"
    | "footer"
    | "form"
    | "h1"
    | "h2"
    | "h3"
    | "h4"
    | "h5"
    | "h6"
    | "head"
    | "header"
    | "hgroup"
    | "hr"
    | "html"
    | "i"
    | "iframe"
    | "img"
    | "input"
    | "ins"
    | "kbd"
    | "keygen"
    | "label"
    | "legend"
    | "li"
    | "link"
    | "main"
    | "map"
    | "mark"
    | "menu"
    | "menuitem"
    | "meta"
    | "meter"
    | "nav"
    | "noscript"
    | "object"
    | "ol"
    | "optgroup"
    | "option"
    | "output"
    | "p"
    | "param"
    | "pre"
    | "progress"
    | "q"
    | "rb"
    | "rp"
    | "rt"
    | "rtc"
    | "ruby"
    | "s"
    | "samp"
    | "script"
    | "section"
    | "select"
    | "small"
    | "source"
    | "span"
    | "strong"
    | "style"
    | "sub"
    | "summary"
    | "sup"
    | "table"
    | "tbody"
    | "td"
    | "template"
    | "textarea"
    | "tfoot"
    | "th"
    | "thead"
    | "time"
    | "title"
    | "tr"
    | "track"
    | "u"
    | "ul"
    | "var"
    | "video"
    | "wbr";

type None = {
    kind: "none";
};

type StringAttribute = {
    kind: "string";
    key: string;
    value: string;
};

type StyleAttribute = {
    kind: "style";
    key: string;
    value: string;
};

type NumberAttribute = {
    kind: "number";
    key: string;
    value: string;
};

type BooleanAttribute = {
    kind: "boolean";
    key: string;
    value: boolean;
};

/**
Used to represent the different types of attributes possible.
*/
export type Attribute =
    | None
    | StringAttribute
    | NumberAttribute
    | StyleAttribute
    | BooleanAttribute;

/**
Creates a class attribute - classes are combined by the html creator, so you can use it like:
```
html.div([ ], [ class_("one"), class_("two") ], [ ])
```
*/
export function class_(str: string): Attribute {
    return {
        kind: "string",
        key: "class",
        value: str,
    };
}

/**
Creates a style attribute - styles are combined by the html creator, so you can use it like:
```
html.div([ ], [ style_("color", "red"), style_("background-color", "blue") ], [ ])
```
*/
export function style_(key: string, value: string): Attribute {
    return {
        kind: "style",
        key: key,
        value: value,
    };
}

/**
An empty attribute - filtered by the html creator on creation. This is useful if you have a tenary
operator, e.g:
```
html.div([ ], [ somethingTruthy ? none() : class_("something") ], [ ])
```
*/
export function none(): Attribute {
    return {
        kind: "none",
    };
}

/**
Create an attribute with a given key and value. This is set via `setAttribute` at runtime.
*/
export function attribute(key: string, value: string): Attribute {
    if (key === "style")
        return style_(value.split(":")[0], value.split(":")[1]);

    return {
        kind: "string",
        key: key,
        value: value,
    };
}

/**
Create an attribute with a given key and value. This is set via `setAttribute` at runtime.
*/
export function booleanAttribute(key: string, value: boolean): Attribute {
    return {
        kind: "boolean",
        key: key,
        value: value,
    };
}

/**
Every event has a `name`, like `click`, and a tagger which produces a message of the right type
*/
export type Event<Msg> = {
    name: string;
    tagger(data: any): Msg;
};

/**
Creates an event handler for passing to a html node
*/
export function on<Msg>(
    name: string,
    tagger: (data: any) => Msg,
    stopPropagation: boolean = true,
    preventDefault: boolean = true
): Event<Msg> {
    return {
        name: name,
        tagger: (event: any) => {
            if (stopPropagation) {
                event.stopPropagation();
            }
            if (preventDefault) {
                event.preventDefault();
            }
            return tagger(event);
        },
    };
}

/**
Special-cased input handler
*/
export function onInput<Msg>(tagger: (data: string) => Msg): Event<Msg> {
    return {
        name: "input",
        tagger: (event: any) => {
            event.stopPropagation();
            event.preventDefault();
            return tagger(event.target.value);
        },
    };
}

type TextNode = {
    kind: "text";
    text: string;
};

type RegularNode<Msg> = {
    kind: "regular";
    tag: Tag;
    events: Event<Msg>[];
    attributes: Attribute[];
    children: HtmlNode<Msg>[];
    _eventListeners: any[];
};

type VoidNode<Msg> = {
    kind: "void";
    tag: Tag;
    events: Event<Msg>[];
    attributes: Attribute[];
    _eventListeners: any[];
};

/**
A HtmlNode is either a text, like:
```
html.text("hello world")
```
Or html, like:
```
html.div([ ], [ ], [ ])
```
*/
export type HtmlNode<Msg> = TextNode | RegularNode<Msg> | VoidNode<Msg>;

/**
Creates a text node
*/
export function text(str: string): TextNode {
    return {
        kind: "text",
        text: str,
    };
}

/**
Creates a html node with a given tag name, any events, any attributes and any children.
*/
export function node<Msg>(
    tag: Tag,
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): RegularNode<Msg> {
    return {
        kind: "regular",
        tag: tag,
        events: events,
        attributes: combineAttributes(attributes),
        children: children,
        _eventListeners: [ ],
    };
}

/**
Creates a void html node with a given tag name, any events, any attributes.
*/
export function voidNode<Msg>(
    tag: Tag,
    events: Event<Msg>[],
    attributes: Attribute[]
): VoidNode<Msg> {
    return {
        kind: "void",
        tag: tag,
        events: events,
        attributes: combineAttributes(attributes),
        _eventListeners: [ ],
    };
}

function combineAttributes(attributes: Attribute[]): Attribute[] {
    const knownStringAttributes: { [id: string]: StringAttribute[] } = {};
    const knownStyleAttributes: StyleAttribute[] = [ ];
    const otherAttributes: Attribute[] = [ ];

    // group attribute values
    attributes.forEach((attribute: Attribute) => {
        switch (attribute.kind) {
            case "string":
                if (!knownStringAttributes[attribute.key]) {
                    knownStringAttributes[attribute.key] = [ ];
                }

                knownStringAttributes[attribute.key].push(attribute);
                break;
            case "style":
                knownStyleAttributes.push(attribute);
                break;
            default:
                otherAttributes.push(attribute);
        }
    });

    const combinedAttributes: Attribute[] = otherAttributes.filter(
        (attribute) => attribute.kind !== "none"
    );

    // actually combine attributes together
    Object.keys(knownStringAttributes).map((key: string) => {
        combinedAttributes.push(
            knownStringAttributes[key].reduce(
                (acc: StringAttribute, currentValue: StringAttribute) => {
                    if (key === "class") {
                        acc.value += " " + currentValue.value;
                    }
                    return acc;
                }
            )
        );
    });

    if (knownStyleAttributes.length > 0) {
        // actually combine attributes together
        combinedAttributes.push(
            knownStyleAttributes.reduce(
                (acc: StringAttribute, currentValue: StyleAttribute) => {
                    if (typeof acc.value === "undefined") acc.value = "";
                    acc.value +=
                        currentValue.key + ":" + currentValue.value + ";";
                    return acc;
                },
                attribute("style", "") as StringAttribute
            )
        );
    }

    return combinedAttributes;
}

function renderAttribute(attribute: Attribute): string {
    switch (attribute.kind) {
        case "string":
            if (attribute.value.indexOf('"') > 0) {
                return `${attribute.key}='${attribute.value}'`;
            }
            return `${attribute.key}="${attribute.value}"`;
        case "number":
            return `${attribute.key}=${attribute.value}`;
        case "style":
            return "";
        case "boolean":
            return attribute.value ? `${attribute.key}="${attribute.key}"` : "";
        case "none":
            return "";
    }
}

/**
Renders a HtmlNode tree as a string.
*/
export function render<Msg>(node: HtmlNode<Msg>, depth = 0): string {
    const whitespace = " ".repeat(depth * 4);
    switch (node.kind) {
        case "text":
            return whitespace + node.text;

        case "void":
        case "regular":
            const renderedAttributes = node.attributes
                .map(renderAttribute)
                .join(" ");
            const attributes =
                (renderedAttributes.length > 0 ? " " : "") + renderedAttributes;

            switch (node.kind) {
                case "void":
                    return whitespace + `<${node.tag}${attributes}>`;

                case "regular": {
                    if (node.children.length > 0) {
                        return (
                            whitespace +
                            `<${node.tag}${attributes}>
${node.children.map((child) => render(child, depth + 1)).join("\n")}
${whitespace}</${node.tag}>`
                        );
                    }

                    return (
                        whitespace + `<${node.tag}${attributes}></${node.tag}>`
                    );
                }
            }
    }
}

/**
Render a node without whitespace
*/
export function flatRender<Msg>(node: HtmlNode<Msg>): string {
    switch (node.kind) {
        case "text":
            return node.text;

        case "void":
        case "regular":
            const attributes =
                (node.attributes.length > 0 ? " " : "") +
                node.attributes.map(renderAttribute).join(" ");

            switch (node.kind) {
                case "void":
                    return `<${node.tag}${attributes}>`;

                case "regular": {
                    if (node.children.length > 0) {
                        return `<${node.tag}${attributes}>${node.children
                            .map((child) => flatRender(child))
                            .join("")}</${node.tag}>`;
                    }

                    return `<${node.tag}${attributes}></${node.tag}>`;
                }
            }
    }
}

/**
Hydrates a root from a given program. Program must have root set as the string "hydration"
*/
export function hydrate<Model, Msg>(
    program: RunningProgram<Model, Msg>,
    root: Element
) {
    program.program.root = root as HTMLElement;
    const node = program.program.view(program.program.initialModel);

    if (node.kind === "text") return;

    if (root.children.length === 0) {
        console.error(
            "This root has no children. Did you correctly server-side render content?"
        );
        console.error(
            `Your html should look like <div id="root">{your content}</div>`
        );
        console.error(
            "The root node should have exactly one child, which is your generated html."
        );
    }
    hydrateNode(node, program.send, root.children[0]);
}

/**
Attaches event listeners to nodes
*/
export function hydrateNode<Msg>(
    node: HtmlNode<Msg>,
    listener: (msg: Msg) => void,
    root: Element
): void {
    switch (node.kind) {
        case "text": {
            return;
        }
        case "void":
        case "regular": {
            node.events.forEach((event: Event<Msg>) => {
                const listenerFunction = (data: globalThis.Event) => {
                    listener(event.tagger(data));
                };

                root.addEventListener(event.name, listenerFunction, {
                    once: true,
                });

                node._eventListeners.push({
                    event: event,
                    listener: listenerFunction,
                });
            });
        }
    }

    if (node.kind === "regular") {
        let i = 0;
        for (const child of node.children) {
            if (child.kind === "text") continue;
            const newRoot = root.children[i];
            hydrateNode(child, listener, newRoot);
            i++;
        }
    }
}

/**
Builds a HTMLElement tree from a HtmlNode tree, with event triggers being sent to the runner via the listener
This function should not be needed by most usage.
*/
export function buildTree<Msg>(
    listener: (msg: Msg) => void,
    node: HtmlNode<Msg>
): HTMLElement | Text {
    switch (node.kind) {
        case "text":
            return document.createTextNode(node.text);
        case "void":
        case "regular": {
            const element = document.createElement(node.tag);

            node.attributes.forEach((attribute: Attribute) => {
                setAttributeOnElement(element, attribute);
            });

            node.events.forEach((event: Event<Msg>) => {
                const listenerFunction = (data: globalThis.Event) => {
                    listener(event.tagger(data));
                };

                element.addEventListener(event.name, listenerFunction, {
                    once: true,
                });

                node._eventListeners.push({
                    event: event,
                    listener: listenerFunction,
                });
            });

            if (node.kind === "regular") {
                const children = node.children.map((child) =>
                    buildTree(listener, child)
                );
                children.forEach((child) => {
                    element.appendChild(child);
                });
            }

            return element;
        }
    }
}

/**
Triggers the event by name, passing it the payload provided.
This function is useful for testing but not much else
 */
export function triggerEvent<Msg>(
    eventName: string,
    payload: any,
    node: HtmlNode<Msg>
): Maybe.Maybe<Msg> {
    payload = {
        stopPropagation: () => undefined,
        preventDefault: () => undefined,
        ...payload,
    };
    switch (node.kind) {
        case "text":
            return Maybe.Nothing();
        case "void":
        case "regular":
            const events = node.events.filter(
                (event) => event.name === eventName
            );
            if (events.length > 0) {
                return Maybe.Just(events[0].tagger(payload));
            } else {
                return Maybe.Nothing();
            }
    }
}

/**
Converts a `HtmlNode` of type `A` to a `HtmlNode` of type `B`, including children.
*/
export function map<A, B>(tagger: (a: A) => B, tree: HtmlNode<A>): HtmlNode<B> {
    switch (tree.kind) {
        case "text":
            return tree as HtmlNode<B>;
        case "void":
            return voidNode(
                tree.tag,
                tree.events.map((event: Event<A>) => {
                    return on(event.name, (data: any) =>
                        tagger(event.tagger(data))
                    );
                }),
                tree.attributes
            );
        case "regular":
            return node(
                tree.tag,
                tree.events.map((event: Event<A>) => {
                    return on(event.name, (data: any) =>
                        tagger(event.tagger(data))
                    );
                }),
                tree.attributes,
                tree.children.map((child: HtmlNode<A>) => {
                    return map(tagger, child);
                })
            );
    }
}

function isProperty(tag: string, key: string): boolean {
    switch (tag) {
        case "INPUT":
            return (
                key === "checked" ||
                key === "indeterminate" ||
                key === "value" ||
                key === "readonly" ||
                key === "disabled"
            );
        case "OPTION":
            return key === "selected" || key === "disabled";
        case "TEXTAREA":
            return key === "value" || key === "readonly" || key === "disabled";
        case "SELECT":
            return key === "value" || key === "disabled";
        case "BUTTON":
        case "OPTGROUP":
            return key === "disabled";
    }
    return false;
}

function setAttributeOnElement(
    element: HTMLElement,
    attribute: Attribute
): boolean {
    switch (attribute.kind) {
        case "string":
        case "number":
            if (isProperty(element.tagName, attribute.key)) {
                (element as any)[attribute.key] = attribute.value;
                return true;
            } else {
                element.setAttribute(attribute.key, attribute.value);
                return true;
            }
        case "style":
            element.removeAttribute("style");
            const styles = attribute.value.split(";");

            for (var i = 0; i < styles.length; i++) {
                const styleName: string = styles[i].split(":")[0];
                const styleValue = styles[i].split(":")[1];
                element.style[styleName as any] = styleValue;
            }
            return true;
        case "boolean": {
            if (attribute.value) {
                if (isProperty(element.tagName, attribute.key)) {
                    (element as any)[attribute.key] = attribute.value;
                    return true;
                }
                element.setAttribute(attribute.key, attribute.key);
            } else {
                if (element.getAttribute(attribute.key) === attribute.key) {
                    element.removeAttribute(attribute.key);
                }
            }
            return true;
        }
        case "none":
            return true;
    }
}

function patchFacts<Msg>(
    previousTree: HtmlNode<Msg>,
    nextTree: HtmlNode<Msg>,
    elements: HTMLElement
) {
    switch (nextTree.kind) {
        case "void":
        case "regular": {
            // remove previous attributes that no longer exist on the next dom version

            if (previousTree.kind === nextTree.kind) {
                const nextAttributes = [ ];
                for (const attr of nextTree.attributes) {
                    if (attr.kind != "none") {
                        nextAttributes.push(attr.key);
                    }
                }

                for (const attribute of previousTree.attributes) {
                    if (
                        attribute.kind !== "none" &&
                        nextAttributes.indexOf(
                            (
                                attribute as
                                    | StringAttribute
                                    | NumberAttribute
                                    | BooleanAttribute
                                    | StyleAttribute
                            ).key
                        ) === -1
                    ) {
                        elements.removeAttribute(attribute.key);
                    }
                }
            }

            nextTree.attributes.forEach((attribute: Attribute) => {
                setAttributeOnElement(elements, attribute);
            });
            return;
        }
        case "text":
            return;
    }
}

function patchEvents<Msg>(
    listener: (msg: Msg) => void,
    previousTree: HtmlNode<Msg>,
    nextTree: HtmlNode<Msg>,
    elements: HTMLElement
) {
    switch (nextTree.kind) {
        case "void":
        case "regular":
            (
                previousTree as RegularNode<Msg> | VoidNode<Msg>
            )._eventListeners.forEach((eventListeners) => {
                elements.removeEventListener(
                    eventListeners.event.name,
                    eventListeners.listener
                );
            });

            (nextTree as RegularNode<Msg> | VoidNode<Msg>).events.forEach(
                (event: Event<Msg>) => {
                    const listenerFunction = (data: globalThis.Event) => {
                        listener(event.tagger(data));
                    };

                    elements.addEventListener(event.name, listenerFunction, {
                        once: true,
                    });

                    nextTree._eventListeners.push({
                        event: event,
                        listener: listenerFunction,
                    });
                }
            );
            return;
        case "text":
            return;
    }
}

function patch<Msg>(
    listener: (msg: Msg) => void,
    currentTree: HtmlNode<Msg>,
    nextTree: HtmlNode<Msg>,
    elements: HTMLElement | Text
): HtmlNode<Msg> {
    if (currentTree.kind != nextTree.kind) {
        elements.replaceWith(buildTree(listener, nextTree));
        return nextTree;
    }

    switch (currentTree.kind) {
        case "text":
            nextTree = nextTree as TextNode;
            elements = elements as Text;

            if (currentTree.text == nextTree.text) {
                return currentTree;
            } else {
                elements.replaceWith(document.createTextNode(nextTree.text));
                return nextTree;
            }

        case "void": {
            currentTree = currentTree as VoidNode<Msg>;
            nextTree = nextTree as VoidNode<Msg>;

            if (currentTree.tag != nextTree.tag) {
                elements.replaceWith(buildTree(listener, nextTree));
                return nextTree;
            } else {
                patchFacts(currentTree, nextTree, elements as HTMLElement);

                patchEvents(
                    listener,
                    currentTree,
                    nextTree,
                    elements as HTMLElement
                );
            }
            return nextTree;
        }

        case "regular":
            currentTree = currentTree as RegularNode<Msg>;
            nextTree = nextTree as RegularNode<Msg>;

            const currentTreeId = (
                currentTree.attributes.filter(
                    (x) => x.kind === "string" && x.key === "id"
                )[0] as StringAttribute
            )?.value;
            const nextTreeId = (
                nextTree.attributes.filter(
                    (x) => x.kind === "string" && x.key === "id"
                )[0] as StringAttribute
            )?.value;

            if (
                currentTree.tag !== nextTree.tag ||
                currentTreeId !== nextTreeId
            ) {
                elements.replaceWith(buildTree(listener, nextTree));
                return nextTree;
            } else {
                patchFacts(currentTree, nextTree, elements as HTMLElement);

                patchEvents(
                    listener,
                    currentTree,
                    nextTree,
                    elements as HTMLElement
                );
                const htmlElements = elements as HTMLElement;

                for (var i = 0; i < nextTree.children.length; i++) {
                    const currentChild = currentTree.children[i];
                    const nextChild = nextTree.children[i];
                    const node = htmlElements.childNodes[i];

                    if (typeof node === "undefined") {
                        htmlElements.appendChild(
                            buildTree(listener, nextChild)
                        );
                        continue;
                    }

                    switch (node.nodeType) {
                        case Node.ELEMENT_NODE:
                            const element = node as HTMLElement;
                            patch(listener, currentChild, nextChild, element);
                            break;

                        case Node.TEXT_NODE:
                            const text = (node as unknown) as Text;
                            patch(listener, currentChild, nextChild, text);
                            break;
                    }
                }

                for (
                    var i = htmlElements.childNodes.length - 1;
                    i > nextTree.children.length - 1;
                    i--
                ) {
                    const node = htmlElements.childNodes[i];
                    htmlElements.removeChild(node);
                }
            }
            return nextTree;
    }
}

/**
Every Coed program follows the model-view-update (MVU) pattern made popular on the frontend by Elm.
An initial model is given, which is passed to the view function which then populates the `root` element.
Any events triggered within the view will use the `update` function to create a new model.
Async updates can be handled via the optional `send` callback within the update function.
*/
export type Program<Model, Msg> = {
    initialModel: Model;
    view(model: Model): HtmlNode<Msg>;
    update(msg: Msg, model: Model, send?: (msg: Msg) => void): Model;
    root: HTMLElement | "hydration";
};

/**
Every running program can be interacted with via `send`.
For example you may want to start a program but send some data to it after loading a network request.
*/
export type RunningProgram<Model, Msg> = {
    program: Program<Model, Msg>;
    send: (msg: Msg) => void;
};

/**
Takes in a program, sets it up and runs it as a main loop
*/
export function program<Model, Msg>(
    program: Program<Model, Msg>
): RunningProgram<Model, Msg> {
    let model = program.initialModel;
    let previousView = program.view(program.initialModel);
    let currentTree: HTMLElement | Text | null = null;

    const listener = (msg: Msg) => {
        if (currentTree === null) {
            currentTree = buildTree(listener, previousView);
            if (program.root !== "hydration") {
                while (program.root.firstChild) {
                    program.root.removeChild(program.root.firstChild);
                }
                program.root.appendChild(currentTree);
            }
        }
        model = program.update(msg, model, listener);

        const nextView = program.view(model);
        patch(listener, previousView, nextView, currentTree);
        previousView = nextView;
    };

    if (program.root !== "hydration") {
        currentTree = buildTree(listener, previousView);
        program.root.appendChild(currentTree);
    }

    return {
        program: program,
        send: listener,
    };
}

// tags

export function a<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("a", events, attributes, children);
}

export function abbr<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("abbr", events, attributes, children);
}

export function address<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("address", events, attributes, children);
}

export function area<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("area", events, attributes);
}

export function article<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("article", events, attributes, children);
}

export function aside<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("aside", events, attributes, children);
}

export function audio<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("audio", events, attributes, children);
}

export function b<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("b", events, attributes, children);
}

export function base<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("base", events, attributes);
}

export function bdi<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("bdi", events, attributes, children);
}

export function bdo<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("bdo", events, attributes, children);
}

export function blockquote<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("blockquote", events, attributes, children);
}

export function body<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("body", events, attributes, children);
}

export function br<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("br", events, attributes);
}

export function button<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("button", events, attributes, children);
}

export function canvas<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("canvas", events, attributes, children);
}

export function caption<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("caption", events, attributes, children);
}

export function cite<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("cite", events, attributes, children);
}

export function code<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("code", events, attributes, children);
}

export function col<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("col", events, attributes);
}

export function colgroup<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("colgroup", events, attributes, children);
}

export function data<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("data", events, attributes, children);
}

export function datalist<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("datalist", events, attributes, children);
}

export function dd<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("dd", events, attributes, children);
}

export function del<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("del", events, attributes, children);
}

export function details<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("details", events, attributes, children);
}

export function dfn<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("dfn", events, attributes, children);
}

export function dialog<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("dialog", events, attributes, children);
}

export function div<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("div", events, attributes, children);
}

export function dl<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("dl", events, attributes, children);
}

export function dt<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("dt", events, attributes, children);
}

export function em<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("em", events, attributes, children);
}

export function embed<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("embed", events, attributes);
}

export function fieldset<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("fieldset", events, attributes, children);
}

export function figure<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("figure", events, attributes, children);
}

export function footer<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("footer", events, attributes, children);
}

export function form<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("form", events, attributes, children);
}

export function h1<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("h1", events, attributes, children);
}

export function h2<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("h2", events, attributes, children);
}

export function h3<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("h3", events, attributes, children);
}

export function h4<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("h4", events, attributes, children);
}

export function h5<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("h5", events, attributes, children);
}

export function h6<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("h6", events, attributes, children);
}

export function head<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("head", events, attributes, children);
}

export function header<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("header", events, attributes, children);
}

export function hgroup<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("hgroup", events, attributes, children);
}

export function hr<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("hr", events, attributes);
}

export function html<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("html", events, attributes, children);
}

export function i<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("i", events, attributes, children);
}

export function iframe<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("iframe", events, attributes, children);
}

export function img<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("img", events, attributes);
}

export function input<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("input", events, attributes);
}

export function ins<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("ins", events, attributes, children);
}

export function kbd<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("kbd", events, attributes, children);
}

export function keygen<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("keygen", events, attributes, children);
}

export function label<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("label", events, attributes, children);
}

export function legend<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("legend", events, attributes, children);
}

export function li<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("li", events, attributes, children);
}

export function link<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("link", events, attributes);
}

export function main<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("main", events, attributes, children);
}

export function map_<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("map", events, attributes, children);
}

export function mark<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("mark", events, attributes, children);
}

export function menu<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("menu", events, attributes, children);
}

export function menuitem<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("menuitem", events, attributes, children);
}

export function meta<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("meta", events, attributes);
}

export function meter<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("meter", events, attributes, children);
}

export function nav<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("nav", events, attributes, children);
}

export function noscript<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("noscript", events, attributes, children);
}

export function object<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("object", events, attributes, children);
}

export function ol<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("ol", events, attributes, children);
}

export function optgroup<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("optgroup", events, attributes, children);
}

export function option<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("option", events, attributes, children);
}

export function output<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("output", events, attributes, children);
}

export function p<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("p", events, attributes, children);
}

export function param<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("param", events, attributes);
}

export function pre<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("pre", events, attributes, children);
}

export function progress<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("progress", events, attributes, children);
}

export function q<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("q", events, attributes, children);
}

export function rb<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("rb", events, attributes, children);
}

export function rp<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("rp", events, attributes, children);
}

export function rt<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("rt", events, attributes, children);
}

export function rtc<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("rtc", events, attributes, children);
}

export function ruby<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("ruby", events, attributes, children);
}

export function s<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("s", events, attributes, children);
}

export function samp<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("samp", events, attributes, children);
}

export function script<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("script", events, attributes, children);
}

export function section<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("section", events, attributes, children);
}

export function select<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("select", events, attributes, children);
}

export function small<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("small", events, attributes, children);
}

export function source<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("source", events, attributes);
}

export function span<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("span", events, attributes, children);
}

export function strong<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("strong", events, attributes, children);
}

export function style<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("style", events, attributes, children);
}

export function sub<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("sub", events, attributes, children);
}

export function summary<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("summary", events, attributes, children);
}

export function sup<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("sup", events, attributes, children);
}

export function table<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("table", events, attributes, children);
}

export function tbody<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("tbody", events, attributes, children);
}

export function td<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("td", events, attributes, children);
}

export function template<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("template", events, attributes, children);
}

export function textarea<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("textarea", events, attributes, children);
}

export function tfoot<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("tfoot", events, attributes, children);
}

export function th<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("th", events, attributes, children);
}

export function thead<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("thead", events, attributes, children);
}

export function time<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("time", events, attributes, children);
}

export function title<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("title", events, attributes, children);
}

export function tr<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("tr", events, attributes, children);
}

export function track<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("track", events, attributes);
}

export function u<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("u", events, attributes, children);
}

export function ul<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("ul", events, attributes, children);
}

export function var_<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("var", events, attributes, children);
}

export function video<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[],
    children: HtmlNode<Msg>[]
): HtmlNode<Msg> {
    return node("video", events, attributes, children);
}

export function wbr<Msg>(
    events: Event<Msg>[],
    attributes: Attribute[]
): HtmlNode<Msg> {
    return voidNode("wbr", events, attributes);
}
