import { IgniteObject, IgniteProperty, IgniteRenderingContext } from './ignite-html.js';
import { IgniteElement } from './ignite-element.js';

/**
 * The outline of a ignite template. Templates are a blueprint that specify's
 * how to construct an element and then can be used to construct the element. Everything
 * starts with a template.
 * 
 * @example
 * //You can easily create a template to construct any html element. See the following:
 * class div extends IgniteTemplate {
 *  constructor(...items) {
 *      super("div", items);
 *  }
 * }
 * 
 * @example
 * IgniteTemplate's construct method can be extended by adding a callback function to _constructors under a template:
 * template._constructors.push(() => console.log('constructed'));
 * 
 * 
 * @example
 * IgniteTemplate's deconstruct method can be extended by adding a callback function to _destructors under a template:
 * template._destructors.push(() => console.log('destructed'));
 */
class IgniteTemplate {

    /**
     * Creates a new IgniteTemplate with the tag name of the element that will be constructed and an
     * array of child elements.
     * @param {String} tagName The tag name of the element this template will construct.
     * @param {String|Number|IgniteProperty|IgniteTemplate} children An array of child elements to be added to this template.
     */
    constructor(tagName = null, children = null) {
        this.children = [];
        this.tagName = tagName;
        this.tagNamespace = null;
        this.element = null;
        this._attributes = {};
        this._classes = [];
        this._properties = {};
        this._variables = {};
        this._reflecting = {};
        this._refs = [];
        this._callbacks = [];
        this._events = {};
        this._styles = {};
        this._constructors = [];
        this._destructors = [];
        this._elementValue = null;
        this._elementInnerHTML = null;
        this._elementInnerText = null;
        this._resizeObserverCallback = [];
        this._resizeObserver = null;
        this._intersectObserverCallback = [];
        this._intersectObserver = null;

        if (children) {
            for (var i = 0; i < children.length; i++) {
                if (children[i] === undefined || children[i] === null) {
                    continue; //Skip undefined or null children.
                } else if (children[i] instanceof IgniteProperty) {
                    this.children.push(new html(children[i]));
                } else if (children[i] instanceof String || typeof children[i] === 'string') {
                    this.children.push(new html(children[i]));
                } else if (children[i] instanceof Number || typeof children[i] === 'number') {
                    this.children.push(new html(children[i]));
                } else if (children[i] instanceof IgniteTemplate || children[i].prototype instanceof IgniteTemplate) {
                    this.children.push(children[i]);
                } else {
                    throw `Attempted to add a child for template: ${this.tagName} which is not supported. Child: ${children[i]}`;
                }
            }
        }
    }

    /**
     * Adds a CSS class to be added once this template is constructed.
     * @param {String|IgniteProperty} name Name of the CSS class to add. Multiple CSS classes are supported if they are separated by a space.
     * @param {Function} converter Optional function that can convert the class name into a different one.
     * @example
     *  .class("row justify-content-center")
     * @returns This ignite template so function calls can be chained.
     */
    class(name, converter = null) {
        IgniteRenderingContext.push();

        if (name instanceof IgniteProperty) {
            this._callbacks.push(name.attachOnChange((oldValue, newValue) => this.onClassChanged((converter != null ? converter(oldValue) : oldValue), (converter != null ? converter(newValue) : newValue))));
            this._callbacks.push(name.attachOnPush((list, items) => this.onClassChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
            this._callbacks.push(name.attachOnUnshift((list, items) => this.onClassChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
            this._callbacks.push(name.attachOnPop((list) => this.onClassChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
            this._callbacks.push(name.attachOnShift((list) => this.onClassChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));
            this._callbacks.push(name.attachOnSplice((list, start, deleteCount, items) => this.onClassChanged((converter != null ? converter(list) : list), (converter != null ? converter(list) : null))));

            var value = (converter != null ? converter(name.value) : name.value);
            (value != null ? value.toString().split(" ") : []).forEach(cl => {
                if (cl.length > 0) {
                    this._classes.push(cl);
                }
            });
        } else if (Array.isArray(name) && name.length > 0 && name[0] instanceof IgniteProperty) {
            //There must be a converter for this to work correctly
            if (!converter) {
                throw "Cannot pass an array of properties without using a converter!";
            }

            //Attack a callback for all the properties
            name.forEach(prop => {
                if (prop instanceof IgniteProperty) {
                    this._callbacks.push(prop.attachOnChange((oldValue, newValue) => this.onClassChanged(converter(...name.getOldPropertyValues(prop, oldValue)), converter(...name.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPush((list, items) => this.onClassChanged(converter(...name.getOldPropertyValues(prop, list)), converter(...name.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnUnshift((list, items) => this.onClassChanged(converter(...name.getOldPropertyValues(prop, list)), converter(...name.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPop((list) => this.onClassChanged(converter(...name.getOldPropertyValues(prop, list)), converter(...name.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnShift((list) => this.onClassChanged(converter(...name.getOldPropertyValues(prop, list)), converter(...name.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnSplice((list, start, deleteCount, items) => this.onClassChanged(converter(...name.getOldPropertyValues(prop, list)), converter(...name.getPropertyValues()))));
                }
            });

            var value = converter(...name.getPropertyValues());
            (value != null ? value.toString().split(" ") : []).forEach(cl => {
                if (cl.length > 0) {
                    this._classes.push(cl);
                }
            });
        } else {
            var value = (converter != null ? converter(name) : name);
            (value != null ? value.toString().split(" ") : []).forEach(cl => {
                if (cl.length > 0) {
                    this._classes.push(cl);
                }
            });
        }

        IgniteRenderingContext.pop();
        return this;
    }

    /**
     * Adds a html element attribute to this template to be added once this template is constructed.
     * @param {String} name The name of the attribute to add
     * @param {String|IgniteProperty} value The value of the attribute to set, can be anything. If Property is passed it will auto update.
     * @param {Function} converter Optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    attribute(name, value, converter = null) {
        IgniteRenderingContext.push();

        if (value instanceof IgniteProperty) {
            this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onAttributeChanged(name, (converter ? converter(newValue) : newValue))));
            this._callbacks.push(value.attachOnPush((list, items) => this.onAttributeChanged(name, converter ? converter(list) : null)));
            this._callbacks.push(value.attachOnUnshift((list, items) => this.onAttributeChanged(name, converter ? converter(list) : null)));
            this._callbacks.push(value.attachOnPop((list) => this.onAttributeChanged(name, converter ? converter(list) : null)));
            this._callbacks.push(value.attachOnShift((list) => this.onAttributeChanged(name, converter ? converter(list) : null)));
            this._callbacks.push(value.attachOnSplice((list, start, deleteCount, items) => this.onAttributeChanged(name, converter ? converter(list) : null)));
            this._attributes[name] = converter ? converter(value.value) : value.value;
        } else if (Array.isArray(value) && value.length > 0 && value[0] instanceof IgniteProperty) {
            //There must be a converter for this to work correctly
            if (!converter) {
                throw "Cannot pass an array of properties without using a converter!";
            }

            //Attack a callback for all the properties
            value.forEach(prop => {
                if (prop instanceof IgniteProperty) {
                    this._callbacks.push(prop.attachOnChange((oldValue, newValue) => this.onAttributeChanged(name, converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPush((list, items) => this.onAttributeChanged(name, converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnUnshift((list, items) => this.onAttributeChanged(name, converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPop((list) => this.onAttributeChanged(name, converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnShift((list) => this.onAttributeChanged(name, converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnSplice((list, start, deleteCount, items) => this.onAttributeChanged(name, converter(...value.getPropertyValues()))));
                }
            });

            this._attributes[name] = converter(...value.getPropertyValues());
        } else {
            this._attributes[name] = converter ? converter(value) : value;
        }

        IgniteRenderingContext.pop();
        return this;
    }

    /**
     * Sets the value of the element this template is constructing with the option to reflect changes
     * to the value.
     * @param {String|IgniteProperty} value The value to set on the element.
     * @param {Boolean|Function} reflect Whether or not to reflect changes to the value of the element back to the property if one was used.
     * @param {Function} converter Optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    value(value, reflect = false, converter = null) {
        IgniteRenderingContext.push();

        if (value instanceof IgniteProperty) {
            this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onValueChanged((converter != null ? converter(newValue) : newValue))));
            this._callbacks.push(value.attachOnPush((list, items) => this.onValueChanged((converter != null ? converter(list) : null))));
            this._callbacks.push(value.attachOnUnshift((list, items) => this.onValueChanged((converter != null ? converter(list) : null))));
            this._callbacks.push(value.attachOnPop((list) => this.onValueChanged((converter != null ? converter(list) : null))));
            this._callbacks.push(value.attachOnShift((list) => this.onValueChanged((converter != null ? converter(list) : null))));
            this._callbacks.push(value.attachOnSplice((list, start, deleteCount, items) => this.onValueChanged((converter != null ? converter(list) : null))));

            if (reflect != null && ((typeof (reflect) == "boolean" && reflect == true) || reflect instanceof Function)) {
                var valueChanged = () => {
                    var newValue = null;

                    var type = this.element.hasAttribute("type") ? this.element.getAttribute("type").toLowerCase().trim() : null;

                    if (type == "checkbox") {
                        newValue = this.element.checked;
                    } else if (type == "radio") {
                        newValue = this.element.checked;
                    } else if (type == "number") {
                        newValue = Number(this.element.value);
                    } else if (this.element.hasAttribute("contenteditable") && this.element.getAttribute("contenteditable").toLowerCase().trim() == "true") {
                        newValue = this.element.textContent;
                    } else {
                        newValue = this.element.value;
                    }

                    if (reflect instanceof Function) {
                        reflect(newValue);
                    } else {
                        value.setValue(newValue, true);
                    }

                    this._elementValue = newValue;
                };

                this.on("change", valueChanged);
                this.on("keyup", valueChanged);
            }

            this._elementValue = (converter != null ? converter(value.value) : value.value);
        } else {
            this._elementValue = (converter != null ? converter(value) : value);
        }

        IgniteRenderingContext.pop();
        return this;
    }

    /**
     * Sets a property on the element this template will construct. (Shorthand for property())
     * @param {String} name Name of the property to set.
     * @param {Any|IgniteProperty} value Value of the property to use. If a Property is passed the value will auto update.
     * @param {Boolean} reflect If true whenever this property is changed it's value will be passed back to the Property that was passed as value if one was passed.
     * @param {Function} converter Optional function that can be used to convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    prop(name, value, reflect = false, converter = null) {
        return this.property(name, value, reflect, converter);
    }

    /**
     * Sets a property on the element this template will construct.
     * @param {String} name Name of the property to set.
     * @param {Any|IgniteProperty} value Value of the property to use. If a Property is passed the value will auto update.
     * @param {Boolean} reflect If true whenever this property is changed it's value will be passed back to the Property that was passed as value if one was passed.
     * @param {Function} converter Optional function that can be used to convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    property(name, value, reflect = false, converter = null) {
        IgniteRenderingContext.push();

        if (this._properties[name]) {
            throw `Attempted to set a property twice on a IgniteTemplate: ${this.tagName}. This is not allowed and should be avoided.`;
        }

        if (reflect && converter != null) {
            throw `Cannot add a property to an IgniteTemplate: ${this.tagName} with reflect and a converter used at the same time.`;
        }

        if (value instanceof IgniteProperty) {
            this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onPropertyChanged(name, converter ? converter(newValue) : newValue)));
            this._callbacks.push(value.attachOnPush((list, items) => this.onPropertyChanged(name, converter ? converter(list) : list)));
            this._callbacks.push(value.attachOnUnshift((list, items) => this.onPropertyChanged(name, converter ? converter(list) : list)));
            this._callbacks.push(value.attachOnPop((list) => this.onPropertyChanged(name, converter ? converter(list) : list)));
            this._callbacks.push(value.attachOnShift((list) => this.onPropertyChanged(name, converter ? converter(list) : list)));
            this._callbacks.push(value.attachOnSplice((list, start, deleteCount, items) => this.onPropertyChanged(name, converter ? converter(list) : list)));

            this._properties[name] = {
                value: (converter != null ? converter(value.value) : value.value),
                reflect: (reflect == true ? value : null)
            };
        } else {
            this._properties[name] = {
                value: (converter != null ? converter(value) : value),
                reflect: null
            };
        }

        IgniteRenderingContext.pop();
        return this;
    }

    /**
     * Sets a variable on the element this template will construct.
     * @param {String} name Name of the variable to set
     * @param {Any|IgniteProperty} value Value of the variable to set
     * @param {Function} converter Optional function that can be used to convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    variable(name, value, converter = null) {
        IgniteRenderingContext.push();

        if (this._variables[name]) {
            throw `Attempted to set a variable twice on a IgniteTemplate: ${this.tagName}. This is not allowed and should be avoided.`;
        }

        if (value instanceof IgniteProperty) {
            this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onVariableChanged(name, converter ? converter(newValue) : newValue)));
            this._callbacks.push(value.attachOnPush((list, items) => this.onVariableChanged(name, converter ? converter(list) : list)));
            this._callbacks.push(value.attachOnUnshift((list, items) => this.onVariableChanged(name, converter ? converter(list) : list)));
            this._callbacks.push(value.attachOnPop((list) => this.onVariableChanged(name, converter ? converter(list) : list)));
            this._callbacks.push(value.attachOnShift((list) => this.onVariableChanged(name, converter ? converter(list) : list)));
            this._callbacks.push(value.attachOnSplice((list, start, deleteCount, items) => this.onVariableChanged(name, converter ? converter(list) : list)));

            this._variables[name] = {
                value: (converter != null ? converter(value.value) : value.value)
            };
        } else {
            this._variables[name] = {
                value: (converter != null ? converter(value) : value)
            };
        }

        IgniteRenderingContext.pop();
        return this;
    }

    /**
     * Makes a property on this template reflect its value back to the given target.
     * (You can reflect a property more than once if it's needed.)
     * @param {String} name Name of the property to reflect.
     * @param {IgniteProperty|Function} target The target for the value to be reflected to.
     * @returns This ignite template so function calls can be chained.
     */
    reflect(name, target) {
        IgniteRenderingContext.push();

        if (this._reflecting[name] == undefined || this._reflecting[name] == null) {
            this._reflecting[name] = [];
        }

        if (target instanceof IgniteProperty) {
            this._reflecting[name].push(target);
        } else if (target instanceof Function) {
            this._reflecting[name].push(target);
        }

        IgniteRenderingContext.pop();
        return this;
    }

    /**
     * Adds a set of properties from an object to be added to this template once it's constructed.
     * @param {Object|IgniteObject} props The object value that property names/values will be pulled from.
     * @returns This ignite template so function calls can be chained.
     */
    properties(props) {
        //Make sure we have a valid props.
        if (props == null || props == undefined) {
            return;
        }
        else if (!(typeof props === 'object')) {
            throw `Cannot set properties with a non object set of properties: ${props}`;
        }

        if (props instanceof IgniteObject) {
            props.update();

            Object.getOwnPropertyNames(props).forEach(name => this.property(name, props[name], true));
        }
        else {
            Object.keys(props).forEach(name => this.property(name, props[name], false, null));
        }

        return this;
    }

    /**
     * Sets the inner html of the element to be constructed by this template.
     * @param {String|IgniteProperty} value InnerHTML to set for element. If a property is passed the html will auto update.
     * @param {Function} converter Optional function that can be used to convert the value if needed.
     * @returns This ignite template so funciton calls can be chained.
     */
    innerHTML(value, converter = null) {
        IgniteRenderingContext.push();

        if (value instanceof IgniteProperty) {
            this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onInnerHTMLChanged(converter != null ? converter(newValue) : newValue)));
            this._callbacks.push(value.attachOnPush((list, items) => this.onInnerHTMLChanged(converter != null ? converter(list) : null)));
            this._callbacks.push(value.attachOnUnshift((list, items) => this.onInnerHTMLChanged(converter != null ? converter(list) : null)));
            this._callbacks.push(value.attachOnPop((list) => this.onInnerHTMLChanged(converter != null ? converter(list) : null)));
            this._callbacks.push(value.attachOnShift((list) => this.onInnerHTMLChanged(converter != null ? converter(list) : null)));
            this._callbacks.push(value.attachOnSplice((list, start, deleteCount, items) => this.onInnerHTMLChanged(converter != null ? converter(list) : null)));

            this._elementInnerHTML = (converter != null ? converter(value.value) : value.value);
        } else if (Array.isArray(value) && value.length > 0 && value[0] instanceof IgniteProperty) {
            //There must be a converter for this to work correctly
            if (!converter) {
                throw "Cannot pass an array of properties without using a converter!";
            }

            //Attack a callback for all the properties
            value.forEach(prop => {
                if (prop instanceof IgniteProperty) {
                    this._callbacks.push(prop.attachOnChange((oldValue, newValue) => this.onInnerHTMLChanged(converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPush((list, items) => this.onInnerHTMLChanged(converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnUnshift((list, items) => this.onInnerHTMLChanged(converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPop((list) => this.onInnerHTMLChanged(converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnShift((list) => this.onInnerHTMLChanged(converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnSplice((list, start, deleteCount, items) => this.onInnerHTMLChanged(converter(...value.getPropertyValues()))));
                }
            });

            this._elementInnerHTML = converter(...value.getPropertyValues());
        } else {
            this._elementInnerHTML = (converter != null ? converter(value) : value);
        }

        IgniteRenderingContext.pop();
        return this;
    }

    /**
     * Sets the inner text of the element to be constructed by this template.
     * @param {String|IgniteProperty|IgniteProperty[]} value text to be set for this element. If a property is passed the text will auto update.
     * @param {Function} converter Optional function that can be used to convert the value if needed.
     * @returns This ignite template.
     */
    innerText(value, converter = null) {
        IgniteRenderingContext.push();

        if (value instanceof IgniteProperty) {
            this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onInnerTextChanged(converter != null ? converter(newValue) : newValue)));
            this._callbacks.push(value.attachOnPush((list, items) => this.onInnerTextChanged(converter != null ? converter(list) : null)));
            this._callbacks.push(value.attachOnUnshift((list, items) => this.onInnerTextChanged(converter != null ? converter(list) : null)));
            this._callbacks.push(value.attachOnPop((list) => this.onInnerTextChanged(converter != null ? converter(list) : null)));
            this._callbacks.push(value.attachOnShift((list) => this.onInnerTextChanged(converter != null ? converter(list) : null)));
            this._callbacks.push(value.attachOnSplice((list, start, deleteCount, items) => this.onInnerTextChanged(converter != null ? converter(list) : null)));

            this._elementInnerText = (converter != null ? converter(value.value) : value.value);
        } else if (Array.isArray(value) && value.length > 0 && value[0] instanceof IgniteProperty) {
            //There must be a converter for this to work correctly
            if (!converter) {
                throw "Cannot pass an array of properties without using a converter!";
            }

            //Attack a callback for all the properties
            value.forEach(prop => {
                if (prop instanceof IgniteProperty) {
                    this._callbacks.push(prop.attachOnChange((oldValue, newValue) => this.onInnerTextChanged(converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPush((list, items) => this.onInnerTextChanged(converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnUnshift((list, items) => this.onInnerTextChanged(converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPop((list) => this.onInnerTextChanged(converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnShift((list) => this.onInnerTextChanged(converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnSplice((list, start, deleteCount, items) => this.onInnerTextChanged(converter(...value.getPropertyValues()))));
                }
            });

            this._elementInnerText = converter(...value.getPropertyValues());
        } else {
            this._elementInnerText = (converter != null ? converter(value) : value);
        }

        IgniteRenderingContext.pop();
        return this;
    }

    /**
     * Adds a single or series of children to be added once this template
     * is constructed. Numbers, Strings, and Properties passed will be added as HTML child elements.
     * @param  {...Number|String|IgniteProperty|IgniteTemplate} items A series of children to be added to this template.
     * @returns This ignite template so function calls can be chained.
     */
    child(...items) {
        if (items) {
            for (var i = 0; i < items.length; i++) {
                if (items[i] === undefined || items[i] === null) {
                    continue; //Skip undefined or null items.
                } else if (items[i] instanceof IgniteProperty) {
                    this.children.push(new html(items[i]));
                } else if (items[i] instanceof String || typeof items[i] === 'string') {
                    this.children.push(new html(items[i]));
                } else if (items[i] instanceof Number || typeof items[i] === 'number') {
                    this.children.push(new html(items[i]));
                } else if (items[i] instanceof IgniteTemplate || items[i].prototype instanceof IgniteTemplate) {
                    this.children.push(items[i]);
                } else {
                    throw `Attempted to add a child for template: ${this.tagName} which is not supported. Child: ${items[i]}`;
                }
            }
        }

        return this;
    }

    /**
     * Adds a reference callback function to be invoked once this template is constructed. The function will be invoked
     * with the constructed HTMLElement. If an IgniteProperty is passed it's value will be set to the constructed HTMLElement once
     * the template is constructed.
     * @param {Function|IgniteProperty} refCallback The callback to be invoked with the HTMLElement of the element this template constructed.
     * @returns This ignite template so function calls can be chained.
     */
    ref(refCallback) {
        if (refCallback instanceof IgniteProperty) {
            this._callbacks.push(refCallback.attachOnChange((oldValue, newValue) => this.onRefChanged(refCallback, newValue)));
            this._refs.push(element => refCallback.value = element);
        } else {
            this._refs.push(refCallback);
        }

        return this;
    }

    /**
     * Adds an event by its name and the function to invoke once the event fires. Properties may be used
     * for the function, but their value must be a valid function in order to get a proper event callback.
     * @param {String} eventName The name of the event to add.
     * @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
     * @returns This ignite template so function calls can be chained.
     */
    on(eventName, eventCallback) {
        IgniteRenderingContext.push();

        if (!this._events[eventName]) {
            this._events[eventName] = [];
        }

        if (eventCallback instanceof IgniteProperty) {
            this._callbacks.push(eventCallback.attachOnChange((oldValue, newValue) => this.onEventChanged(oldValue, newValue, eventName)));
            this._events[eventName].push(eventCallback.value);
        } else {
            this._events[eventName].push(eventCallback);
        }

        IgniteRenderingContext.pop();
        return this;
    }

    /**
     * Adds a click event handler to this template.
     * @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
     * @returns This ignite template so function calls can be chained.
     */
    onClick(eventCallback) {
        return this.on("click", eventCallback);
    }

    /**
     * Adds a touch event handler to this template.
     * @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
     * @returns This ignite template so function calls can be chained.
     */
    onTouch(eventCallback) {
        return this.on("touch", eventCallback);
    }

    /**
     * Adds a onblur event handler to this template.
     * @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
     * @returns This ignite template so function calls can be chained.
     */
    onBlur(eventCallback) {
        return this.on("blur", eventCallback);
    }

    /**
     * Adds a onfocus event handler to this template.
     * @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
     * @returns This ignite template so function calls can be chained.
     */
    onFocus(eventCallback) {
        return this.on("focus", eventCallback);
    }

    /**
     * Adds a onchange event handler to this template.
     * @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
     * @returns This ignite template so function calls can be chained.
     */
    onChange(eventCallback) {
        return this.on("change", eventCallback);
    }

    /**
     * 
     * @param {Function|IgniteProperty} eventCallback The callback function to be invoked once the event fires.
     * @returns This ignite template.
     */
    onPaste(eventCallback) {
        return this.on("paste", eventCallback);
    }

    /**
     * Adds a on enter key press event handler to this template.
     * @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
     * @returns This ignite template so function calls can be chained.
     */
    onEnter(eventCallback) {
        var eventName = "keydown";

        if (!this._events[eventName]) {
            this._events[eventName] = [];
        }

        if (eventCallback instanceof IgniteProperty) {
            this._callbacks.push(eventCallback.attachOnChange((oldValue, newValue) => {
                //Create a new wrapped function to check for the enter key being pressed.
                var wrapped = (e) => {
                    if (e.key === 'Enter') {
                        newValue(e);
                    }
                };

                eventCallback._value = wrapped; //Store the wrapped function into the property so we can remove it later
                this.onEventChanged(oldValue, wrapped, eventName); //Invoke event changed with the old value and wrapped one.
            }));

            //Create the initial wrapper
            var target = eventCallback._value;
            var wrapped = (e) => {
                if (e.key === 'Enter') {
                    target(e);
                }
            };

            //Store the wrapped so that it's the old value next time around.
            eventCallback._value = wrapped;

            this._events[eventName].push(wrapped);
        } else {
            this.on(eventName, (e) => {
                if (e.key === 'Enter') {
                    eventCallback(e);
                }
            });
        }

        return this;
    }

    /**
    * Adds a on backspace key press event handler to this template.
    * @param {Function|IgniteProperty} eventCallback The callback function to be invoked by the event once it fires.
    * @returns This ignite template so function calls can be chained.
    */
    onBackspace(eventCallback) {
        var eventName = "keydown";

        if (!this._events[eventName]) {
            this._events[eventName] = [];
        }

        if (eventCallback instanceof IgniteProperty) {
            this._callbacks.push(eventCallback.attachOnChange((oldValue, newValue) => {
                //Create a new wrapped function to check for the enter key being pressed.
                var wrapped = (e) => {
                    if (e.key === 'Backspace') {
                        newValue(e);
                    }
                };

                eventCallback._value = wrapped; //Store the wrapped function into the property so we can remove it later
                this.onEventChanged(oldValue, wrapped, eventName); //Invoke event changed with the old value and wrapped one.
            }));

            //Create the initial wrapper
            var target = eventCallback._value;
            var wrapped = (e) => {
                if (e.key === 'Backspace') {
                    target(e);
                }
            };

            //Store the wrapped so that it's the old value next time around.
            eventCallback._value = wrapped;

            this._events[eventName].push(wrapped);
        } else {
            this.on(eventName, (e) => {
                if (e.key === 'Backspace') {
                    eventCallback(e);
                }
            });
        }

        return this;
    }

    /**
     * Adds a special on resize event handler to this template that will
     * fire anytime the element is resized by using a resize observer. You can call this more than once to attach more than one callback.
     * @param {Function|IgniteProperty} callback The callback function to be invoked by the event once it fires.
     * @returns This ignite template so function calls can be chained.
     */
    onResize(callback) {
        IgniteRenderingContext.push();

        if (callback instanceof IgniteProperty) {
            this._resizeObserverCallback.push(callback.value);
        } else if (callback instanceof Function) {
            this._resizeObserverCallback.push(callback);
        }

        IgniteRenderingContext.pop();
        return this;
    }

    /**
     * Adds a special on intersect event handler to this template that will fire
     * once the element is in view. You can call this more than once to attach more than one callback.
     * @param {Function|IgniteProperty} callback The callback function to be invoked by the event once it fires.
     * @returns This ignite template so function calls can be chained.
     */
    onIntersect(callback) {
        IgniteRenderingContext.push();

        if (callback instanceof IgniteProperty) {
            this._intersectObserverCallback.push(callback.value);
        } else if (callback instanceof Function) {
            this._intersectObserverCallback.push(callback);
        }

        IgniteRenderingContext.pop();
        return this;
    }

    /**
     * Adds a CSS property to this template with a value and priority.
     * @param {String} name The name of the CSS property to set.
     * @param {String|IgniteProperty} value The value to set for the property. If an IgniteProperty is used it will auto update this style.
     * @param {String} priority If set to "important" then the style will be marked with !important. Acceptable values: important, !important, true, false, null
     * @param {Function} converter Optional function to convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    style(name, value, priority = null, converter = null) {
        IgniteRenderingContext.push();

        //If the name has a : remove it.
        if (name && typeof name === "string" && name.includes(":")) {
            name = name.replace(":", "");
        }

        //If the priority starts with a ! remove it.
        if (priority && typeof priority === "string" && priority.trim().startsWith("!")) {
            priority = priority.split("!")[1].trim();
        } else if (priority && typeof priority === "boolean") {
            priority = "important"; //If priority is true, set it to important
        } else if (!priority && typeof priority === "boolean") {
            priority = null; //If priority is false, set it to null.
        }

        //If the value has a ; remove it.
        if (value && typeof value === "string" && value.includes(";")) {
            value = value.replace(";", "");
        }

        if (value instanceof IgniteProperty) {
            this._callbacks.push(value.attachOnChange((oldValue, newValue) => this.onStyleChanged(name, converter ? converter(newValue) : newValue)));
            this._callbacks.push(value.attachOnPush((list, items) => this.onStyleChanged(name, converter ? converter(list) : null)));
            this._callbacks.push(value.attachOnUnshift((list, items) => this.onStyleChanged(name, converter ? converter(list) : null)));
            this._callbacks.push(value.attachOnPop((list) => this.onStyleChanged(name, converter ? converter(list) : null)));
            this._callbacks.push(value.attachOnShift((list) => this.onStyleChanged(name, converter ? converter(list) : null)));
            this._callbacks.push(value.attachOnSplice((list, start, deleteCount, items) => this.onStyleChanged(name, converter ? converter(list) : null)));

            this._styles[name] = { name: name, value: converter ? converter(value.value) : value.value, priority: priority };
        } else if (Array.isArray(value) && value.length > 0 && value[0] instanceof IgniteProperty) {
            //There must be a converter for this to work correctly
            if (!converter) {
                throw "Cannot pass an array of properties without using a converter!";
            }

            //Attack a callback for all the properties
            value.forEach(prop => {
                if (prop instanceof IgniteProperty) {
                    this._callbacks.push(prop.attachOnChange((oldValue, newValue) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPush((list, items) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnUnshift((list, items) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPop((list) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnShift((list) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnSplice((list, start, deleteCount, items) => this.onStyleChanged(name, converter(...value.getPropertyValues()))));
                }
            });

            this._styles[name] = { name: name, value: converter(...value.getPropertyValues()), priority: priority };
        } else {
            this._styles[name] = { name: name, value: (converter != null ? converter(value) : value), priority: priority };
        }

        IgniteRenderingContext.pop();

        return this;
    }

    /**
     * Hides the element this template is constructing if the value is true.
     * @param {Boolean|IgniteProperty} value If true hides the element this template is constructing. If an IgniteProperty is passed it's value will auto update this.
     * @param {Function} converter An optional function to convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    hide(value, converter = null) {
        return this.style("display", value, true, (...params) => {
            return ((converter != null && converter(...params)) || (converter == null && params[0])) ? "none" : null;
        });
    }

    /**
     * Hides the element this template is constructing if the value is true.
     * @param {Boolean|IgniteProperty} value If true hides the element this template is constructing. If an IgniteProperty is passed it's value will auto update this.
     * @param {Function} converter An optional function to convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    invisible(value, converter = null) {
        return this.style("visibility", value, true, (...params) => {
            return ((converter != null && converter(...params)) || (converter == null && params[0])) ? "hidden" : null;
        });
    }

    /**
     * Shows the element this template is constructing if the value is true.
     * @param {Boolean|IgniteProperty} value 
     * @param {Function} converter 
     * @returns This ignite template so function calls can be chained.
     */
    show(value, converter = null) {
        return this.style("display", value, true, (...params) => {
            return ((converter != null && converter(...params)) || (converter == null && params[0])) ? null : "none";
        });
    }

    /**
     * Shows the element this template is constructing if the value is true.
     * @param {Boolean|IgniteProperty} value 
     * @param {Function} converter 
     * @returns This ignite template so function calls can be chained.
     */
    visibile(value, converter = null) {
        return this.style("visibility", value, true, (...params) => {
            return ((converter != null && converter(...params)) || (converter == null && params[0])) ? null : "visible";
        });
    }

    /**
     * Sets the id attribute of the element to be constructed by this template.
     * @param {String|IgniteProperty} value The value to set for the id attribute of the element this template will construct.
     * @param {Function} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    id(value, converter = null) {
        return this.attribute("id", value, converter);
    }

    /**
     * Sets the title attribute of the element to be constructed by this template.
     * @param {String|IgniteProperty} value The value to set for the title attribute of the element this template will construct.
     * @param {Function} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    title(value, converter = null) {
        return this.attribute("title", value, converter);
    }

    /**
     * Sets the for attribute of the element to be constructed by this template.
     * @param {String|IgniteProperty} value The value to set for the for attribute of the element this template will construct.
     * @param {Function} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    for(value, converter = null) {
        return this.attribute("for", value, converter);
    }

    /**
     * Adds a checked attribute to this template.
     * @param {Boolean|IgniteProperty} value The value to set for the checked attribute. 
     * @param {*} converter Optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    checked(value, converter = null) {
        return this.attribute("checked", value, converter);
    }

    /**
     * Adds a disabled attribute and class to this template.
     * @param {Boolean|IgniteProperty} value A value to determine whether or not the element should be marked as disable dor not. 
     * @param {*} converter Optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    disabled(value, converter = null) {
        if (value instanceof IgniteProperty) {
            this.attribute("disabled", value, convert => {
                convert = (converter != null ? converter(convert) : convert);
                if (convert) {
                    return "disabled";
                } else {
                    return null;
                }
            });

            this.class(value, convert => {
                convert = (converter != null ? converter(convert) : convert);
                if (convert) {
                    return "disabled";
                } else {
                    return null;
                }
            });
        } else if (value) {
            this.attribute("disabled", "disabled");
            this.class("disabled");
        }

        return this;
    }

    /**
     * Sets the type attribute of the element to be constructed by this template.
     * @param {String|IgniteProperty} value The value to set for the type attribute of the element this template will construct.
     * @param {Function} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    type(value, converter = null) {
        return this.attribute("type", value, converter);
    }

    /**
     * Sets the min attribute of the element to be constructed by this template.
     * @param {String|IgniteProperty} value The value to set for the type attribute of the element this template will construct.
     * @param {Function} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    min(value, converter = null) {
        return this.attribute("min", value, converter);
    }

    /**
     * Sets the max attribute of the element to be constructed by this template.
     * @param {String|IgniteProperty} value The value to set for the type attribute of the element this template will construct.
     * @param {Function} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    max(value, converter = null) {
        return this.attribute("max", value, converter);
    }

    /**
     * Sets the step attribute of the element to be constructed by this template.
     * @param {String|IgniteProperty} value The value to set for the type attribute of the element this template will construct.
     * @param {Function} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    step(value, converter = null) {
        return this.attribute("step", value, converter);
    }

    /**
     * Sets a data attribute on the element to be constructed by this template.
     * @param {String} name The name of the data attribute to set on the element this template will construct.
     * @param {String|IgniteProperty} value The value to set for the data attribute of the element this template will construct. 
     * @param {*} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    data(name, value, converter = null) {
        return this.attribute(`data-${name}`, value, converter);
    }

    /**
     * Sets the src attribute of the element to be constructed by this template.
     * @param {String|IgniteProperty} value The value to set for the src attribute of the element to be constructed by this template.
     * @param {Function} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    src(value, converter = null) {
        return this.attribute("src", value, converter);
    }

    /**
     * Sets the href attribute of the element to be constructed by this template.
     * @param {String|IgniteProperty} value The value to set for the href attribute of the element to be constructed by this template. 
     * @param {Function} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    href(value, converter = null) {
        return this.attribute("href", value, converter);
    }

    /**
     * Sets the target attribute of the element to be constructed by this template.
     * @param {String|IgniteProperty} value The value to set for the target attribute of the element to be constructed by this template.
     * @param {Function} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    target(value, converter = null) {
        return this.attribute("target", value, converter);
    }

    /**
     * Sets the name attribute of the element to be constructed by this template.
     * @param {String|IgniteProperty} value The value to set for the name attribute of the element to be constructed by this template.
     * @param {Function} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    name(value, converter = null) {
        return this.attribute("name", value, converter);
    }

    /**
     * Sets the placeholder attribute of the element to be constructed by this template.
     * @param {String|IgniteProperty} value The value to set for the placeholder attribute of the element.
     * @param {Function} converter An optional function that can convert the value if needed.
     * @returns This ignite template so function calls can be chained.
     */
    placeholder(value, converter = null) {
        //If this is a input element, modify the attribute, otherwise add the placeholder class.
        if (this.tagName == "input") {
            return this.attribute("placeholder", value, converter);
        } else {
            if (value instanceof IgniteProperty) {
                return this.class(value, convert => {
                    convert = (converter != null ? converter(convert) : convert);
                    if (convert) {
                        return "placeholder";
                    } else {
                        return null;
                    }
                });
            } else {
                return this.class("placeholder");
            }
        }
    }

    /**
     * Constructs this template and adds it to the DOM if this template
     * has not already been constructed.
     * @param {HTMLElement} parent Parent element that will contain the constructed element.
     * @param {HTMLElement} sibling Optional sibling element that can be used to add the element adjacantly.
     */
    construct(parent, sibling) {
        //Don't construct if we have no parent, no sibling and no element.
        if (!parent && !sibling && !this.element) {
            return;
        }

        //Construct this element if we haven't already
        if (!this.element) {
            //If we have a tag namespace use that to construct this element. This is mainly
            //for svg elements since they have to be created this way.
            if (this.tagNamespace) {
                this.element = window.document.createElementNS(this.tagNamespace, this.tagName);
            } else {
                this.element = window.document.createElement(this.tagName);
            }

            //Pass back our template to the element we are creating if it's not a ignite element, since
            //it will have it's own template automatically.
            if (!(this.element instanceof IgniteElement)) {
                this.element.template = this;
            }

            //Invoke any refs we have and pass back the element reference.
            this._refs.forEach((ref) => ref(this.element));
        }

        //Set the classes on this element
        for (var i = 0; i < this._classes.length; i++) {
            if (this._classes[i] !== null && this._classes[i] !== undefined && this._classes[i] !== "") {
                this.element.classList.add(this._classes[i]);
            }
        }

        //Set the attributes on this element
        var keys = Object.keys(this._attributes);
        for (var i = 0; i < keys.length; i++) {
            if (this._attributes[keys[i]] !== null && this._attributes[keys[i]] !== undefined) {
                this.element.setAttribute(keys[i], this._attributes[keys[i]]);
            }
        }

        //Set the events on this element
        var keys = Object.keys(this._events);
        for (var i = 0; i < keys.length; i++) {
            this._events[keys[i]].forEach((event) => {
                if (event !== null && event !== undefined) {
                    this.element.addEventListener(keys[i], event);
                }
            });
        }

        //Set the styles on this element.
        var keys = Object.keys(this._styles);
        for (var i = 0; i < keys.length; i++) {
            var style = this._styles[keys[i]];
            this.element.style.setProperty(style.name, style.value, style.priority);
        }

        //Set the properties on this element
        var keys = Object.keys(this._properties);
        for (var i = 0; i < keys.length; i++) {
            this.element[keys[i]] = this._properties[keys[i]].value;

            //Reflect the property if it can be on the element.
            if (this._properties[keys[i]].reflect != null && this.element[keys[i]] instanceof IgniteProperty) {
                this.element[keys[i]].reflected.push(this._properties[keys[i]].reflect);
            }
        }

        //Set the variables on this element.
        var keys = Object.keys(this._variables);
        for (var i = 0; i < keys.length; i++) {
            this.element[keys[i]] = this._variables[keys[i]].value;
        }

        //Setup any reflecting properties on this element
        var keys = Object.keys(this._reflecting);
        for (var i = 0; i < keys.length; i++) {
            this._reflecting[keys[i]].forEach(value => this.element[keys[i]].reflected.push(value));
        }

        //Set the elements inner html if it was set
        if (this._elementInnerHTML != null) {
            this.element.innerHTML = this._elementInnerHTML;
        }

        //Set the elements inner text if it was set
        if (this._elementInnerText != null) {
            this.element.innerText = this._elementInnerText;
        }

        //Construct the children under this element
        for (var i = 0; i < this.children.length; i++) {
            this.children[i].construct(this.element);
        }

        //Set the elements value if there is one.
        if (this._elementValue != null) {
            if (this.element.hasAttribute("type") && (this.element.getAttribute("type").toLowerCase().trim() == "checkbox" || this.element.getAttribute("type").toLowerCase().trim() == "radio")) {
                this.element.checked = this._elementValue;
            } else if (this.element.hasAttribute("contenteditable") && this.element.getAttribute("contenteditable").toLowerCase().trim() == "true") {
                this.element.textContent = this._elementValue.toString();
            } else {
                this.element.value = this._elementValue;
            }
        }

        //Setup a resize observer if needed
        if (this._resizeObserverCallback && this._resizeObserverCallback.length > 0) {
            this._resizeObserver = new ResizeObserver(e => {
                this._resizeObserverCallback.forEach(callback => callback(e));
            });
            this._resizeObserver.observe(this.element);
        }

        //Setup a intersect observer if needed
        if (this._intersectObserverCallback && this._intersectObserverCallback.length > 0) {
            this._intersectObserver = new IntersectionObserver(results => {
                if (results[0].isIntersecting) {
                    this._intersectObserverCallback.forEach(callback => callback(results[0]));
                }
            }, { root: null, trackVisibility: true, delay: 1000 });
            this._intersectObserver.observe(this.element);
        }

        //Invoke any custom constructors.
        this._constructors.forEach(c => c(parent, sibling));

        //If our element has not been added to the dom yet, then add it.
        if (this.element.isConnected == false && this.element.parentElement == null) {
            if (sibling) {
                parent.insertBefore(this.element, sibling);
            } else {
                parent.appendChild(this.element);
            }
        }
    }

    /**
     * Deconstructs this template and cleans up all resources to make sure
     * there are no memory leaks.
     */
    deconstruct() {
        //Remove and disconnect all events
        if (this.element && this._events) {
            var keys = Object.keys(this._events);
            for (var i = 0; i < keys.length; i++) {
                this.element.removeEventListener(keys[i], this._events[keys[i]]);
            }
            this._events = null;
        }

        //Remove our element if we have one.
        if (this.element) {
            this.element.remove();
            this.element = null;
        }

        //Deconstruct all children elements.
        if (this.children) {
            for (var i = 0; i < this.children.length; i++) {
                if (this.children[i] instanceof IgniteTemplate || this.children[i].prototype instanceof IgniteTemplate) {
                    this.children[i].deconstruct();
                }
            }
            this.children = null;
        }

        //Disconnect all callbacks
        if (this._callbacks) {
            this._callbacks.forEach((item) => item.disconnect());
            this._callbacks = null;
        }

        //Stop observing resize events if we need to.
        if (this._resizeObserver) {
            this._resizeObserver.disconnect();
            this._resizeObserver = null;
            this._resizeObserverCallback = null;
        }

        //Stop observing intersects if we need to.
        if (this._intersectObserver) {
            this._intersectObserver.disconnect();
            this._intersectObserver = null;
            this._intersectObserverCallback = null;
        }

        //Remove any refs
        if (this._refs) {
            //Pass null to our refs so that the reference is updated.
            this._refs.forEach(ref => ref(null));
            this._refs = null;
        }

        //Invoke any custom destructors
        if (this._destructors) {
            this._destructors.forEach(d => d());
            this._destructors = null;
        }
    }

    /**
     * Called when a class on this template was changed and needs to be updated
     * on the template's element.
     * @param {any} oldValue 
     * @param {any} newValue 
     * @param {Function} converter Optional converter for the value if needed.
     * @ignore
     */
    onClassChanged(oldValue, newValue) {
        var oldClasses = (oldValue != null && oldValue != "" ? oldValue.toString().split(" ") : []);
        var newClasses = (newValue != null && newValue != "" ? newValue.toString().split(" ") : []);

        if (this.element) {
            oldClasses.forEach((cl) => this.element.classList.remove(cl));
            newClasses.forEach((cl) => this.element.classList.add(cl));
        }

        //Remove the old values from the template, but only remove one copy.
        oldClasses.forEach((cl) => this._classes.splice(this._classes.indexOf(cl), 1));

        //Add the new classes to the template.
        newClasses.forEach((cl) => this._classes.push(cl));

        //For any classes that are missing on the element, add them. If we have duplicates this
        //can happen.
        this._classes.forEach((cl) => {
            if (!this.element.classList.contains(cl)) {
                this.element.classList.add(cl);
            }
        });
    }

    /**
     * Called when a attribute on this template was changed and needs to be updated
     * on the template's element.
     * @param {string} name 
     * @param {any} newValue 
     * @ignore
     */
    onAttributeChanged(name, newValue) {
        if (this.element) {
            if (newValue == null || newValue == undefined) {
                this.element.removeAttribute(name);
            } else {
                this.element.setAttribute(name, newValue);
            }
        }

        this._attributes[name] = newValue;
    }

    /**
     * Called when a value for this template was changed and needs to be updated on the template's element.
     * @param {any} newValue 
     * @ignore
     */
    onValueChanged(newValue) {
        //Only update the elements value if it actually changed.
        //This is to prevent endless looping potentially.
        if (this.element) {
            if (this.element.hasAttribute("type") && (this.element.getAttribute("type").toLowerCase().trim() == "checkbox" || this.element.getAttribute("type").toLowerCase().trim() == "radio")) {
                if (this.element.checked != newValue) {
                    this.element.checked = newValue;
                }
            } else if (this.element.hasAttribute("contenteditable") && this.element.getAttribute("contenteditable").toLowerCase().trim() == "true") {
                if (this.element.textContent != newValue.toString()) {
                    this.element.textContent = newValue.toString();
                }
            } else {
                if (this.element.value != newValue) {
                    this.element.value = newValue;
                }
            }
        }

        this._elementValue = newValue;
    }

    /**
     * Called when a property on this template was changed and needs to be updated
     * on the template's element.
     * @param {string} propertyName 
     * @param {any} newValue
     * @ignore
     */
    onPropertyChanged(propertyName, newValue) {
        if (this.element) {
            //Use the set value function and don't reflect the change because it came from above which
            //would be the reflected property, this reduces some functions being called twice that don't need to be.
            IgniteRenderingContext.enter();
            if (this.element[propertyName] instanceof IgniteProperty) {
                this.element[propertyName].setValue(newValue, false);
            } else {
                this.element[propertyName] = newValue;
            }
            IgniteRenderingContext.leave();
        }

        this._properties[propertyName].value = newValue;
    }

    /**
     * Called when a variable on this template was changed and needs to be updated.
     * @param {string} variableName 
     * @param {any} newValue 
     * @ignore
     */
    onVariableChanged(variableName, newValue) {
        if (this.element) {
            this.element[variableName] = newValue;
        }

        this._variables[variableName].value = newValue;
    }

    /**
     * Called when the inner html for this template was changed and needs to be updated
     * on the template's element.
     * @param {any} newValue 
     * @ignore
     */
    onInnerHTMLChanged(newValue) {
        if (this.element) {
            this.element.innerHTML = newValue;
        }

        this._elementInnerHTML = newValue;
    }

    /**
     * Called when the inner text for this template was changed and needs to be updated
     * on the template's element.
     * @param {any} newValue 
     * @ignore
     */
    onInnerTextChanged(newValue) {
        if (this.element) {
            this.element.innerText = newValue;
        }

        this._elementInnerText = newValue;
    }

    /**
     * Called when a ref was changed and we need to update the refs
     * value to match this elements reference.
     * @param {any} ref 
     * @param {any} newValue 
     * @ignore
     */
    onRefChanged(ref, newValue) {
        //Only set the reference value to ourself if it's not our element.
        //Otherwise we will get a never ending loop.
        if (this.element != newValue) {
            ref.value = this.element;
        }
    }

    /**
     * Called when a event was changed and we need to update it.
     * @param {any} oldValue 
     * @param {any} newValue 
     * @param {any} eventName 
     * @ignore
     */
    onEventChanged(oldValue, newValue, eventName) {
        if (this.element) {
            if (oldValue !== null && oldValue !== undefined) {
                this.element.removeEventListener(eventName, oldValue);
            }

            if (newValue !== null && newValue !== undefined) {
                this.element.addEventListener(eventName, newValue);
            }

            //Remove the old value from the events
            this._events[eventName] = this._events[eventName].filter(ev => ev != oldValue);

            //Add the new value if it's needed
            if (newValue !== null && newValue !== undefined) {
                this._events[eventName].push(newValue);
            }
        }
    }

    /**
     * Called when a css value was changed and we need to update the styling.
     * @param {String} name
     * @param {any} newValue 
     * @ignore 
     */
    onStyleChanged(name, newValue) {
        //Remove the ; from the value if there is one.
        if (newValue && typeof newValue === "string" && newValue.includes(";")) {
            newValue = newValue.replace(";", "");
        }

        if (this.element) {
            this.element.style.setProperty(name, newValue, this._styles[name].priority);
        }

        this._styles[name].value = newValue;
    }
}

/**
 * An ignite template that can be used to construct a div element.
 */
class div extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("div", children);
    }
}

/**
 * An ignite template that can be used to construct a hyperlink element.
 */
class a extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("a", children);
    }
}

/**
 * An ignite template that can be used to construct a input element.
 */
class input extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("input", children);
    }
}

/**
 * An ignite template that can be used to construct a textarea element.
 */
class textarea extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("textarea", children);
    }
}

/**
 * An ignite template that can be used to construct a button element.
 */
class button extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("button", children);

        this.type("button");
    }
}

/**
 * An ignite template that can be used to construct a h1 element.
 */
class h1 extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("h1", children);
    }
}

/**
 * An ignite template that can be used to construct a h2 element.
 */
class h2 extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("h2", children);
    }
}

/**
 * An ignite template that can be used to construct a h3 element.
 */
class h3 extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("h3", children);
    }
}

/**
 * An ignite template that can be used to construct a h4 element.
 */
class h4 extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("h4", children);
    }
}

/**
 * An ignite template that can be used to construct a h5 element.
 */
class h5 extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("h5", children);
    }
}

/**
 * An ignite template that can be used to construct a h6 element.
 */
class h6 extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("h6", children);
    }
}

/**
 * An ignite template that can be used to construct a hr element.
 */
class hr extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("hr", children);
    }
}

/**
 * An ignite template that can be used to construct a p element.
 */
class p extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("p", children);
    }
}

/**
 * An ignite template that can be used to construct a ul element.
 */
class ul extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("ul", children);
    }
}

/**
 * An ignite template that can be used to construct a li element.
 */
class li extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("li", children);
    }
}

/**
 * An ignite template that can be used to construct a span element.
 */
class span extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("span", children);
    }
}

/**
 * An ignite template that can be used to construct a small element.
 */
class small extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("small", children);
    }
}

/**
 * An ignite template that can be used to construct a strong element.
 */
class strong extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("strong", children);
    }
}

/**
 * An ignite template that can be used to construct an i element.
 */
class i extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("i", children);
    }
}

/**
 * An ignite template that can be used to construct a table element.
 */
class table extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("table", children);
    }
}

/**
 * An ignite template that can be used to construct a td element.
 */
class td extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("td", children);
    }
}

/**
 * An ignite template that can be used to construct a th element.
 */
class th extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("th", children);
    }
}

/**
 * An ignite template that can be used to construct a tr element.
 */
class tr extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("tr", children);
    }
}

/**
 * An ignite template that can be used to construct a thead element.
 */
class thead extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("thead", children);
    }
}

/**
 * An ignite template that can be used to construct a tbody element.
 */
class tbody extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("tbody", children);
    }
}

/**
 * An ignite template that can be used to construct a br element.
 */
class br extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("br", children);
    }
}

/**
 * An ignite template that can be used to construct a img element.
 */
class img extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("img", children);
    }
}

/**
 * An ignite template that can be used to construct a label element.
 */
class label extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("label", children);
    }
}

/**
 * An ignite template that can be used to construct a select element.
 */
class select extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("select", children);
    }
}

/**
 * An ignite template that can be used to construct a option element.
 */
class option extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("option", children);
    }
}

/**
 * An ignite template that can be used to construct a script element.
 */
class script extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("script", children);
    }
}

/**
 * An ignite template that can be used to construct a form element.
 */
class form extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("form", children);
    }
}

/**
 * An ignite template that can be used to construct a header element.
 */
class header extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("header", children);
    }
}

/**
 * An ignite template that can be used to construct a footer element.
 */
class footer extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("footer", children);
    }
}

/**
 * An ignite template that can be used to construct a progress element.
 */
class progress extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("progress", children);
    }
}

/**
 * An ignite template that can be used to construct a svg element.
 */
class svg extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("svg", children);
        this.tagNamespace = "http://www.w3.org/2000/svg";
    }
}

/**
 * An ignite template that can be used to construct a g element.
 */
class g extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("g", children);
        this.tagNamespace = "http://www.w3.org/2000/svg";
    }
}

/**
 * An ignite template that can be used to construct a path element.
 */
class path extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("path", children);
        this.tagNamespace = "http://www.w3.org/2000/svg";
    }
}

/**
 * An ignite template that can be used to construct a circle element.
 */
class circle extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("circle", children);
        this.tagNamespace = "http://www.w3.org/2000/svg";
    }
}

/**
 * An ignite template that can be used to construct a line element.
 */
class line extends IgniteTemplate {
    /** 
     * @param  {...String|Number|IgniteProperty|IgniteTemplate} children A series of children to be added to this template.
     */
    constructor(...children) {
        super("line", children);
        this.tagNamespace = "http://www.w3.org/2000/svg";
    }
}

/**
 * Text is a special template that will construct a text element and automatically update the dom
 * if it's content changes.
 * @example
 * new text(`<script>Will show up as text</script>`)
 */
class text extends IgniteTemplate {
    /**
     * Constructs a text template with the text to render.
     * @param {String|IgniteProperty} text The text to render within this text template.
     * @param {Function} converter An optional function that can be used to convert the text. 
     */
    constructor(text, converter) {
        super();

        if (text instanceof IgniteProperty) {
            this._callbacks.push(text.attachOnChange((oldValue, newValue) => this.onTextChanged(converter != null ? converter(newValue) : newValue)));
            this._callbacks.push(text.attachOnPush((list, items) => this.onTextChanged(converter != null ? converter(list) : null)));
            this._callbacks.push(text.attachOnUnshift((list, items) => this.onTextChanged(converter != null ? converter(list) : null)));
            this._callbacks.push(text.attachOnPop((list) => this.onTextChanged(converter != null ? converter(list) : null)));
            this._callbacks.push(text.attachOnShift((list) => this.onTextChanged(converter != null ? converter(list) : null)));
            this._callbacks.push(text.attachOnSplice((list, start, deleteCount, items) => this.onTextChanged(converter != null ? converter(list) : null)));

            this._text = (converter != null ? converter(text.value) : text.value);
        } else if (Array.isArray(text) && text.length > 0 && text[0] instanceof IgniteProperty) {
            //There must be a converter for this to work correctly
            if (!converter) {
                throw "Cannot pass an array of properties without using a converter!";
            }

            //Attack a callback for all the properties
            text.forEach(prop => {
                if (prop instanceof IgniteProperty) {
                    this._callbacks.push(prop.attachOnChange((oldValue, newValue) => this.onTextChanged(converter(...text.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPush((list, items) => this.onTextChanged(converter(...text.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnUnshift((list, items) => this.onTextChanged(converter(...text.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPop((list) => this.onTextChanged(converter(...text.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnShift((list) => this.onTextChanged(converter(...text.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnSplice((list, start, deleteCount, items) => this.onTextChanged(converter(...text.getPropertyValues()))));
                }
            });

            this._text = converter(...text.getPropertyValues());
        } else {
            this._text = (converter != null ? converter(text) : text);
        }
    }

    construct(parent, sibling) {
        //Don't construct if we have no parent, no sibling and no element.
        if (!parent && !sibling && !this.element) {
            return;
        }

        if (!this.element) {
            this.element = window.document.createTextNode("");

            if (sibling) {
                sibling.parentElement.insertBefore(this.element, sibling);
            } else {
                parent.appendChild(this.element);
            }
        }

        this.element.data = this._text;
    }

    onTextChanged(newValue) {
        if (this.element) {
            this.element.data = newValue;
        }

        this._text = newValue;
    }
}

/**
 * Html is a special template that can construct raw html or properties into the dom and automatically
 * update the dom if the property changes.
 * 
 * @example
 * new html(`<h1>Hello world!</h1>`)
 */
class html extends IgniteTemplate {
    /**
     * @param {String|IgniteProperty} code HTML code to be constructed within this template. If an IgniteProperty is passed it's value will be used.
     */
    constructor(code) {
        super();

        if (code instanceof IgniteProperty) {
            this._callbacks.push(code.attachOnChange((oldValue, newValue) => this.onPropertyChanged(oldValue, newValue)));
            this.code = code.value;
        } else {
            this.code = code;
        }

        this.tagName = "shadow html";
        this.elements = [];
    }

    construct(parent, sibling) {
        //Don't construct if we have no parent, no sibling and no element.
        if (!parent && !sibling && !this.element) {
            return;
        }

        if (!this.element) {
            this.element = window.document.createTextNode("");

            if (sibling) {
                sibling.parentElement.insertBefore(this.element, sibling);
            } else {
                parent.appendChild(this.element);
            }
        }

        //Create a template to hold the elements that will be created from the
        //properties value and then add them to the DOM and store their pointer.
        if (this.elements.length == 0 && this.code !== null && this.code !== undefined) {
            //If the code is an ignite template then reder that template.
            if (this.code instanceof IgniteTemplate) {
                this.code.construct(parent, this.element);
                this.elements.push(this.code.element);
            } else {
                var template = window.document.createElement("template");
                template.innerHTML = this.code.toString();
                while (template.content.childNodes.length > 0) {
                    var item = template.content.childNodes[0];
                    this.element.parentElement.insertBefore(item, this.element);
                    this.elements.push(item);
                }
                template.remove();
            }
        }
    }

    onPropertyChanged(oldValue, newValue) {
        //Update our code to the new value from the property.
        this.code = newValue;

        //Remove any elements that already exist.
        this.elements.forEach((item) => item.remove());
        this.elements = [];

        //Reconstruct the html
        this.construct(null, null);
    }
}

/**
 * A special ignite template that constructs a list of items using a template
 * that is dynamically created for each item.
 * 
 * @example
 * new list(["1", "2", "3"], (item) => {
 *  return new h1(item);
 * })
 */
class list extends IgniteTemplate {
    /**
     * @param {Array|IgniteProperty} list The list of items to construct within this template.
     * @param {Function(item, index, count)} forEach A function that constructs a template foreach item.
     * @param {Boolean} reflect If true any items removed from the DOM will be removed from the list if they exist. By default this is false.
     */
    constructor(list, forEach, reflect = false) {
        super();

        if (list instanceof IgniteProperty) {
            this._callbacks.push(list.attachOnChange((oldValue, newValue) => this.onListChanged(newValue)));
            this._callbacks.push(list.attachOnPush((list, items) => this.onListPush(items)));
            this._callbacks.push(list.attachOnUnshift((list, items) => this.onListUnshift(items)));
            this._callbacks.push(list.attachOnPop(list => this.onListPop()));
            this._callbacks.push(list.attachOnShift(list => this.onListShift()));
            this._callbacks.push(list.attachOnSplice((list, start, deleteCount, items) => this.onListSplice(start, deleteCount, items)));

            this.list = list.value;
        } else {
            this.list = list;
        }

        this.reflecting = reflect;
        this.reflectCallbacks = [];
        this.forEach = forEach;
        this.elements = [];
        this.tagName = "shadow list";
    }

    construct(parent, sibling) {
        //Don't construct if we have no parent, no sibling and no element.
        if (!parent && !sibling && !this.element) {
            return;
        }

        if (!this.element) {
            this.element = window.document.createTextNode(""); //Use a textnode as our placeholder

            if (sibling) {
                sibling.parentElement.insertBefore(this.element, sibling);
            } else {
                parent.appendChild(this.element);
            }
        } else {
            parent = this.element.parentElement;
        }

        //If we have any reflect callbacks, remove them.
        //(If we dont do this, we will accidentally remove items from the list potentially)
        if (this.reflectCallbacks.length > 0) {
            for (var i = 0; i < this.reflectCallbacks.length; i++) {
                this.reflectCallbacks[i].disconnect();
            }
            this.reflectCallbacks = [];
        }

        //If we already have elements we created, destroy them.
        if (this.elements.length > 0) {
            for (var i = 0; i < this.elements.length; i++) {
                this.elements[i].remove();
            }
            this.elements = [];
        }

        //If we already have children elements, deconstruct them.
        //(Because we are about to recreate them)
        if (this.children.length > 0) {
            for (var i = 0; i < this.children.length; i++) {
                this.children[i].deconstruct();
            }
            this.children = [];
        }

        //Construct all the items in our list and use the container
        if (this.list) {
            for (var i = 0; i < this.list.length; i++) {
                var template = this.forEach(this.list[i], i, this.list.length);
                if (template) {
                    template.construct(parent, this.element);

                    //If we are reflecting, attach to the elements disconnect event.
                    if (this.reflecting) {
                        this.reflectCallbacks.push(template.element.attachOnDisconnect(disconnect => this.onItemRemove(this.list[i])));
                    }

                    this.children.push(template);
                    this.elements.push(template.element);
                } else {
                    this.children.push(null);
                    this.elements.push(null);
                }
            }
        }
    }

    onListChanged(newValue) {
        this.list = newValue;

        IgniteRenderingContext.enter();

        try {
            //Reset the list so it's scroll resets too. ScrollTop is unreliable.
            this.element.parentElement.parentElement.replaceChild(this.element.parentElement, this.element.parentElement);

            this.construct(null); //The list changed, reconstruct this template.
        } catch (error) {
            console.error("An error occurred during onListChanged:", error);
        }

        IgniteRenderingContext.leave();
    }

    onListPush(items) {
        IgniteRenderingContext.enter();

        try {
            items.forEach(item => {
                var template = this.forEach(item, this.children.length);

                if (this.elements.length > 0) {
                    template.construct(this.element.parentElement, this.elements[this.elements.length - 1].nextSibling);
                } else {
                    template.construct(this.element.parentElement, this.element);
                }

                //If we are reflecting, attach to the elements disconnect event.
                if (this.reflecting) {
                    this.reflectCallbacks.push(template.element.attachOnDisconnect(disconnect => this.onItemRemove(item)));
                }

                this.children.push(template);
                this.elements.push(template.element);
            });
        } catch (error) {
            console.error("An error occurred during onListPush:", error);
        }

        IgniteRenderingContext.leave();
    }

    onListUnshift(items) {
        IgniteRenderingContext.enter();

        try {
            items.reverse();
            items.forEach(item => {
                var template = this.forEach(item, 0);

                if (this.elements.length > 0) {
                    template.construct(this.element.parentElement, this.elements[0]);
                } else {
                    template.construct(this.element.parentElement, this.element);
                }

                if (this.reflecting) {
                    this.reflectCallbacks.unshift(template.element.attachOnDisconnect(disconnect => this.onItemRemove(item)));
                }

                this.children.unshift(template);
                this.elements.unshift(template.element);
            });
        } catch (error) {
            console.error("An error occurred during onListUnshift:", error);
        }

        IgniteRenderingContext.leave();
    }

    onListPop() {
        if (this.children.length > 0) {
            this.children[this.children.length - 1].deconstruct();
            this.children.pop();
            this.elements.pop();

            if (this.reflecting) {
                this.reflectCallbacks[this.reflectCallbacks.length - 1].disconnect();
                this.reflectCallbacks.pop();
            }
        }
    }

    onListShift() {
        if (this.children.length > 0) {
            this.children[0].deconstruct();
            this.children.shift();
            this.elements.shift();

            if (this.reflecting) {
                this.reflectCallbacks[0].disconnect();
                this.reflectCallbacks.shift();
            }
        }
    }

    onListSplice(start, deleteCount, items) {
        IgniteRenderingContext.enter();

        //Remove any items that will no longer exist.
        if (deleteCount > 0 && this.children.length > 0) {
            for (var i = start; i < Math.min(this.children.length, start + deleteCount); i++) {
                this.children[i].deconstruct();

                if (this.reflecting) {
                    this.reflectCallbacks[i].disconnect();
                }
            }

            this.children.splice(start, deleteCount);
            this.elements.splice(start, deleteCount);

            if (this.reflecting) {
                this.reflectCallbacks.splice(start, deleteCount);
            }
        }

        //If the start is greater than the length of the items adjust it.
        if (start > this.children.length) {
            start = Math.max(this.children.length - 1, 0);
        }

        //Append any new items if there are any.
        if (items) {
            items.forEach(item => {
                var template = this.forEach(item, start);

                if (this.elements.length > 0) {
                    template.construct(this.element.parentElement, this.elements[start]);
                } else {
                    template.construct(this.element.parentElement, this.element);
                }

                if (this.reflecting) {
                    this.reflectCallbacks.splice(start, 0, template.element.attachOnDisconnect(disconnect => this.onItemRemove(item)));
                }

                this.children.splice(start, 0, template);
                this.elements.splice(start, 0, template.element);

                start += 1;
            });
        }

        IgniteRenderingContext.leave();
    }

    onItemRemove(item) {
        var index = this.list.indexOf(item);

        if (index >= 0) {
            this.list.splice(index, 1);
        }
    }

    onStyleChanged(name, newValue) {
        this.elements.forEach((element) => {
            element.style.setProperty(name, newValue, this._styles[name].priority);
        });

        this._styles[name].value = newValue;
    }
}

/**
 * A slot template that mimicks the functionality of a slot element in Web Components. This can
 * be used to place children of a IgniteElement anywhere in the DOM. Slots don't actually construct an element,
 * they simply just place children in place where the slot was used. If classes, styles, or attributes are applied
 * to the slot they will be applied to the children of the slot.
 * 
 * @example
 * //You must pass the ignite element who owns the slot of the first param.
 * new slot(this)
 * 
 * @example
 * //Slots can apply classes, attributes, and styles to children within the slot
 * new slot(this).class("active") //< Would apply .active to all children
 * 
 * @example
 * //You can also use properties to have dynamic classes, styles, or attributes on slot children
 * new slot(this).class(this.someClass)
 */
class slot extends IgniteTemplate {
    /**
     * Creates a new slot with the element who's children will be injected.
     * @param {IgniteElement} element The parent IgniteElement that this slot is for.
     */
    constructor(element) {
        super();

        this.parent = element;
    }

    /**
     * Constructs this slot from this template.
     * @param {HTMLElement} parent 
     * @param {HTMLElement} sibling 
     * @ignore
     */
    construct(parent, sibling) {
        //Don't construct if we have no parent, no sibling and no element.
        if (!parent && !sibling && !this.element) {
            return;
        }

        if (!this.element) {
            this.element = window.document.createTextNode("");

            if (sibling) {
                sibling.parentElement.insertBefore(this.element, sibling);
            } else {
                parent.appendChild(this.element);
            }

            //Add any slot elements after this element.
            this.parent.elements.forEach((item) => {
                this.element.parentElement.insertBefore(item, this.element);

                //Set the classes on the item
                if (item.classList) {
                    for (var i = 0; i < this._classes.length; i++) {
                        if (this._classes[i] !== null && this._classes[i] !== undefined && this._classes[i] !== "") {
                            item.classList.add(this._classes[i]);
                        }
                    }
                }

                //Set the attributes on the item
                var keys = Object.keys(this._attributes);
                for (var i = 0; i < keys.length; i++) {
                    if (this._attributes[keys[i]] !== null && this._attributes[keys[i]] !== undefined) {
                        item.setAttribute(keys[i], this._attributes[keys[i]]);
                    }
                }

                //Set the styles on the item
                if (item.style) {
                    var keys = Object.keys(this._styles);
                    for (var i = 0; i < keys.length; i++) {
                        var style = this._styles[keys[i]];
                        item.style.setProperty(style.name, style.value, style.priority);
                    }
                }
            });
        }
    }

    /**
     * Called when a class on this slot changes and needs to be updated on the
     * elements within this slot.
     * @param {Any} oldValue 
     * @param {Any} newValue 
     * @param {Function} converter
     * @ignore 
     */
    onClassChanged(oldValue, newValue) {
        var oldClasses = (oldValue != null && oldValue != "" ? oldValue.toString().split(" ") : []);
        var newClasses = (newValue != null && newValue != "" ? newValue.toString().split(" ") : []);

        //Remove the old values from the template, but only remove one copy.
        oldClasses.forEach((cl) => this._classes.splice(this._classes.indexOf(cl), 1));

        //Add the new classes to the template.
        newClasses.forEach((cl) => this._classes.push(cl));

        //Add the classes to the elements
        this.parent.elements.forEach((element) => {
            //Only do this if the element has a class list.
            if (!element.classList) { return; }

            //Remove the old ones first.
            oldClasses.forEach((cl) => element.classList.remove(cl));

            //Add the new ones.
            newClasses.forEach((cl) => element.classList.add(cl));

            //Add any missing ones
            this._classes.forEach((cl) => {
                if (!element.classList.contains(cl)) {
                    element.classList.add(cl);
                }
            });
        });
    }

    /**
     * Called when a attribute on this template was changed and needs to be updated
     * on the template's element.
     * @param {any} oldValue 
     * @param {any} newValue 
     * @param {string} attributeName 
     * @param {Function} converter Optional converter function for the value if needed.
     * @ignore
     */
    onAttributeChanged(name, newValue) {
        this.parent.elements.forEach((element) => {
            if (newValue == null || newValue == undefined) {
                element.removeAttribute(name);
            } else {
                element.setAttribute(name, newValue);
            }
        });

        this._attributes[name] = newValue;
    }

    /**
     * Called when a css value was changed and we need to update the styling.
     * @param {String} name
     * @param {any} newValue 
     * @ignore 
     */
    onStyleChanged(name, newValue) {
        this.parent.elements.forEach((element) => {
            element.style.setProperty(name, newValue, this._styles[name].priority);
        });

        this._styles[name].value = newValue;
    }
}

/**
 * A pagination is a template that segments a list of items into pages based
 * on the items, page size, current page.
 */
class pagination extends IgniteTemplate {
    constructor(list, pageSize, currentPage, forEach) {
        super();

        if (list instanceof IgniteProperty) {
            this._callbacks.push(list.attachOnChange((oldValue, newValue) => this.onListChanged(oldValue, newValue)));
            this._callbacks.push(list.attachOnPush((list, items) => this.onListPush(list, items)));
            this._callbacks.push(list.attachOnUnshift((list, items) => this.onListUnshift(list, items)));
            this._callbacks.push(list.attachOnPop(list => this.onListPop(list)));
            this._callbacks.push(list.attachOnShift(list => this.onListShift(list)));
            this._callbacks.push(list.attachOnSplice((list, start, deleteCount, items) => this.onListSplice(list, start, deleteCount, items)));

            this.list = list.value;
        } else {
            this.list = list;
        }

        if (pageSize instanceof IgniteProperty) {
            this._callbacks.push(pageSize.attachOnChange((oldValue, newValue) => this.onPageSizeChanged(newValue)));
            this.pageSize = pageSize.value;
        } else {
            this.pageSize = pageSize;
        }

        if (currentPage instanceof IgniteProperty) {
            this._callbacks.push(currentPage.attachOnChange((oldValue, newValue) => this.onCurrentPageChanged(newValue)));
            this.currentPage = currentPage.value;
        } else {
            this.currentPage = currentPage;
        }

        this.forEach = forEach;
        this.elements = [];
        this.tagName = "shadow pagination";
        this.pages = [];
    }

    recalculate() {
        //Hide the elements in the current page.
        this.pages[this.currentPage].forEach(item => item.style.setProperty("display", "none", "important"));

        //Recreate the pages.
        var pages = parseInt((this.list.length / this.pageSize)) + (this.list.length % this.pageSize > 0 ? 1 : 0);
        this.pages = [];
        for (var i = 0; i < pages; i++) {
            this.pages.push([]);
        }

        //Add the elements to the correct pages.
        for (var i = 0; i < this.elements.length; i++) {
            var page = parseInt(i / this.pageSize);
            this.pages[page].push(this.elements[i]);
        }

        //Adjust the current page if it's now incorrect.
        if (this.currentPage >= pages) {
            this.currentPage = pages - 1;
        }

        //Show the elements in the current page
        this.pages[this.currentPage].forEach(item => item.style.removeProperty("display"));
    }

    construct(parent, sibling) {
        //Don't construct if we have no parent, no sibling and no element.
        if (!parent && !sibling && !this.element) {
            return;
        }

        if (!this.element) {
            this.element = window.document.createTextNode(""); //Use a textnode as our placeholder

            if (sibling) {
                sibling.parentElement.insertBefore(this.element, sibling);
            } else {
                parent.appendChild(this.element);
            }
        } else {
            parent = this.element.parentElement;
        }

        //If we already have elements we created, destroy them.
        if (this.elements.length > 0) {
            for (var i = 0; i < this.elements.length; i++) {
                this.elements[i].remove();
            }
            this.elements = [];
        }

        //If we already have children elements, deconstruct them.
        //(Because we are about to recreate them)
        if (this.children.length > 0) {
            for (var i = 0; i < this.children.length; i++) {
                this.children[i].deconstruct();
            }
            this.children = [];
        }

        //Init our pages to put elements into.
        var pages = parseInt((this.list.length / this.pageSize)) + (this.list.length % this.pageSize > 0 ? 1 : 0);
        this.pages = [];
        for (var i = 0; i < pages; i++) {
            this.pages.push([]);
        }

        //Construct all the items in our list and use the container
        if (this.list) {
            for (var i = 0; i < this.list.length; i++) {
                var template = this.forEach(this.list[i]);
                template.construct(parent, this.element);

                var page = parseInt(i / this.pageSize);
                if (page != this.currentPage) {
                    template.element.style.setProperty("display", "none", "important");
                } else {
                    template.element.style.removeProperty("display");
                }

                this.pages[page].push(template.element);
                this.children.push(template);
                this.elements.push(template.element);
            }
        }
    }

    onPageSizeChanged(newValue) {
        //Set the new page size
        this.pageSize = newValue;

        //Recalculate the pagination.
        this.recalculate();
    }

    onCurrentPageChanged(newValue) {
        //If the new page is invalid don't do this.
        if (newValue >= this.pages.length) {
            return;
        } else if (newValue < 0) {
            return;
        }

        //Hide all the elements in the current page
        this.pages[this.currentPage].forEach(item => item.style.setProperty("display", "none", "important"));

        //Set the new current page.
        this.currentPage = newValue;

        //Show the elements in the next page
        this.pages[this.currentPage].forEach(item => item.style.removeProperty("display"));
    }

    onListChanged(oldValue, newValue) {
        this.list = newValue;

        IgniteRenderingContext.enter();

        try {
            this.construct(null); //The list changed, reconstruct this template.
        } catch (error) {
            console.error("An error occurred during onListChanged:", error);
        }

        IgniteRenderingContext.leave();
    }

    onListPush(list, items) {
        IgniteRenderingContext.enter();

        try {
            items.forEach(item => {
                var template = this.forEach(item);

                if (this.elements.length > 0) {
                    template.construct(this.element.parentElement, this.elements[this.elements.length - 1].nextSibling);
                } else {
                    template.construct(this.element.parentElement, this.element);
                }

                this.children.push(template);
                this.elements.push(template.element);
            });

            //Recalculate the pagination.
            this.recalculate();
        } catch (error) {
            console.error("An error occurred during onListPush:", error);
        }

        IgniteRenderingContext.leave();
    }

    onListUnshift(list, items) {
        IgniteRenderingContext.enter();

        try {
            items.reverse();
            items.forEach(item => {
                var template = this.forEach(item);

                if (this.elements.length > 0) {
                    template.construct(this.element.parentElement, this.elements[0]);
                } else {
                    template.construct(this.element.parentElement, this.element);
                }

                this.children.unshift(template);
                this.elements.unshift(template.element);
            });

            //Recalculate the pagination.
            this.recalculate();
        } catch (error) {
            console.error("An error occurred during onListUnshift:", error);
        }

        IgniteRenderingContext.leave();
    }

    onListPop(list) {
        if (this.children.length > 0) {
            this.children[this.children.length - 1].deconstruct();
            this.children.pop();
            this.elements.pop();
        }

        //Recalculate the pagination.
        this.recalculate();
    }

    onListShift(list) {
        if (this.children.length > 0) {
            this.children[0].deconstruct();
            this.children.shift();
            this.elements.shift();
        }

        //Recalculate the pagination.
        this.recalculate();
    }

    onListSplice(list, start, deleteCount, items) {
        IgniteRenderingContext.enter();

        //Remove any items that are needed.
        if (deleteCount > 0 && this.children.length > 0) {
            for (var i = start; i < Math.min(this.children.length, start + deleteCount); i++) {
                this.children[i].deconstruct();
            }

            this.children.splice(start, deleteCount);
            this.elements.splice(start, deleteCount);
        }

        //If the start is greater than the length of the items adjust it.
        if (start > this.children.length) {
            start = Math.max(this.children.length - 1, 0);
        }

        //Append any new items if there are any.
        if (items) {
            items.forEach(item => {
                var template = this.forEach(item);

                if (this.elements.length > 0) {
                    template.construct(this.element.parentElement, this.elements[start]);
                } else {
                    template.construct(this.element.parentElement, this.element);
                }

                this.children.splice(start, 0, template);
                this.elements.splice(start, 0, template.element);

                start += 1;
            });
        }

        //Recalculate the pagination.
        this.recalculate();

        IgniteRenderingContext.leave();
    }
}

/**
 * An ignite template that can construct a population of items
 * based on a count.
 */
class population extends IgniteTemplate {
    /**
     * Creates a new population with the number of items in it, a converter if needed, and a foreach function.
     * @param {Number|IgniteProperty} count The number of items in this population.
     * @param {Function} forEach A function to generate items in the population.
     * @param {Function?} converter A converter to be used to convert the count if needed.
     */
    constructor(count, forEach, converter = null) {
        super();

        if (count instanceof IgniteProperty) {
            this._callbacks.push(count.attachOnChange((oldValue, newValue) => this.onCountChange(converter != null ? converter(newValue) : newValue)));
            this.count = count.value;
        } else if (Array.isArray(count) && count.length > 0 && count[0] instanceof IgniteProperty) {
            //There must be a converter for this to work correctly
            if (!converter) {
                throw "Cannot pass an array of properties without using a converter!";
            }

            //Attack a callback for all the properties
            count.forEach(prop => {
                if (prop instanceof IgniteProperty) {
                    this._callbacks.push(prop.attachOnChange((oldValue, newValue) => this.onCountChange(converter(...count.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPush((list, items) => this.onCountChange(converter(...count.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnUnshift((list, items) => this.onCountChange(converter(...count.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnPop((list) => this.onCountChange(converter(...count.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnShift((list) => this.onCountChange(converter(...count.getPropertyValues()))));
                    this._callbacks.push(prop.attachOnSplice((list, start, deleteCount, items) => this.onCountChange(converter(...count.getPropertyValues()))));
                }
            });

            this.count = converter(...count.getPropertyValues());
        } else {
            this.count = (converter != null ? converter(count) : count);
        }

        this.forEach = forEach;
        this.elements = [];
        this.tagName = "shadow population";
    }

    construct(parent, sibling) {
        //Don't construct if we have no parent, no sibling and no element.
        if (!parent && !sibling && !this.element) {
            return;
        }

        if (!this.element) {
            this.element = window.document.createTextNode(""); //Use a textnode as our placeholder

            if (sibling) {
                sibling.parentElement.insertBefore(this.element, sibling);
            } else {
                parent.appendChild(this.element);
            }
        } else {
            parent = this.element.parentElement;
        }

        //If we already have elements we created, destroy them.
        if (this.elements.length > 0) {
            this.elements.forEach(item => item.remove());
            this.elements = [];
        }

        //Construct all the elements for this population.
        for (var i = 0; i < this.count; i++) {
            var template = this.forEach(i);

            if (this.elements.length > 0) {
                template.construct(this.element.parentElement, this.elements[this.elements.length - 1].nextSibling);
            } else {
                template.construct(this.element.parentElement, this.element);
            }

            this.elements.push(template.element);
        }
    }

    onCountChange(newValue) {
        IgniteRenderingContext.enter();

        if (newValue != this.elements.length) {
            //Remove all our existing elements.
            this.elements.forEach(item => item.remove());
            this.elements = [];

            //Create new elements.
            for (var i = 0; i < newValue; i++) {
                var template = this.forEach(i);

                if (this.elements.length > 0) {
                    template.construct(this.element.parentElement, this.elements[this.elements.length - 1].nextSibling);
                } else {
                    template.construct(this.element.parentElement, this.element);
                }

                this.elements.push(template.element);
            }

            //Set the new count
            this.count = newValue;
        }

        IgniteRenderingContext.leave();
    }
}

export {
    IgniteTemplate,
    div,
    text,
    html,
    list,
    a,
    input,
    textarea,
    button,
    h1,
    h2,
    h3,
    h4,
    h5,
    h6,
    hr,
    p,
    span,
    small,
    strong,
    i,
    ul,
    li,
    br,
    img,
    label,
    select,
    option,
    script,
    slot,
    pagination,
    population,
    table,
    tr,
    th,
    td,
    tbody,
    thead,
    progress,
    svg,
    g,
    path,
    circle,
    line,
    form,
    header,
    footer
};