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

class IgniteTemplate {
    constructor(items) {
        this.siblings = [];
        this.children = [];
        this.attributes = {};
        this.classes = [];
        this.tagName = null;
        this.element = null;
        this.properties = {};
        this.refs = [];
        this.callbacks = [];
        this.events = {};

        if (items) {
            for (var i = 0; i < items.length; i++) {
                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 {
                    console.warn("Attempted to add a child for template: ", this, " which is not supported. Unsupported child element: ", items[i]);
                }
            }
        }
    }

    /**
     * Adds a single or series of classes to this template
     * to be added once this template is constructed.
     * @param  {...any} items 
     */
    class(...items) {
        for (var i = 0; i < items.length; i++) {
            if (items[i] instanceof IgniteProperty) {
                this.callbacks.push(items[i].attach((oldValue, newValue) => this.onClassChanged(oldValue, newValue)));
                this.classes.push(items[i].value);
            } else {
                this.classes.push(items[i]);
            }
        }

        return this;
    }

    /**
     * Adds a attribute to this template to be added once this template is constructed.
     * @param {string} name 
     * @param {any} value 
     */
    attribute(name, value) {
        if (value instanceof IgniteProperty) {
            this.callbacks.push(value.attach((oldValue, newValue) => this.onAttributeChanged(oldValue, newValue, name)));
            this.attributes[name] = value.value;
        } else {
            this.attributes[name] = value;
        }

        return this;
    }

    /**
     * Adds a property to this template to be added once this template is constructed.
     * @param {string} name 
     * @param {any} value 
     */
    property(name, value) {
        if (value instanceof IgniteProperty) {
            this.callbacks.push(value.attach((oldValue, newValue) => this.onPropertyChanged(oldValue, newValue, name)));
            this.properties[name] = value.value;
        } else {
            this.properties[name] = value;
        }

        return this;
    }

    /**
     * Adds a single or series of children to be added once this template
     * is constructed.
     * @param  {...any} items 
     */
    child(...items) {
        for (var i = 0; i < items.length; i++) {
            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 {
                console.warn("Attempted to add a child for template: ", this, " which is not supported. Unsupported child element: ", items[i]);
            }
        }

        return this;
    }

    /**
     * Adds a ref callback function to be invoked once this template is constructed.
     * @param {function} item 
     */
    ref(item) {
        if (item instanceof IgniteProperty) {
            this.callbacks.push(item.attach((oldValue, newValue) => this.onRefChanged(oldValue, newValue, item)));
            this.refs.push((element) => item.value = element);
        } else {
            this.refs.push(item);
        }

        return this;
    }

    /**
     * Adds a event by its name and the func to invoke once the event fires.
     * @param {String} eventName 
     * @param {Any} func 
     */
    on(eventName, func) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }

        if (func instanceof IgniteProperty) {
            this.callbacks.push(func.attach((oldValue, newValue) => this.onEventChanged(oldValue, newValue, eventName)));
            this.events[eventName].push(func.value);
        } else {
            this.events[eventName].push(func);
        }

        return this;
    }

    /**
     * Constructs this template and adds it to the DOM if this template
     * has not already been constructed.
     * @param {HTMLElement} parent 
     * @param {HTMLElement} sibling 
     */
    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) {
            this.element = window.document.createElement(this.tagName);

            //Pass back our template to the element we are creating.
            this.element.template = this;

            //If the element has a onDisconnected function, attach to it
            //(This way if a custom element is removed we can deconstruct and cleanup)
            if (this.element.onDisconnected !== undefined) {
                this.element.onDisconnected = () => this.deconstruct();
            }

            //Invoke any refs we have
            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.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 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]];
        }

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

        //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() {
        console.log(`Deconstructing ${this.tagName}`);

        //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;
        }

        //Remove any refs
        if (this.refs) {
            this.refs = 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 
     */
    onClassChanged(oldValue, newValue) {
        console.log(`Class changed, oldValue: ${oldValue} newValue: ${newValue}`);
        if (this.element) {
            if (oldValue !== null && oldValue !== undefined && oldValue !== "" && oldValue !== " ") {
                this.element.classList.remove(oldValue);
            }

            if (newValue !== null && newValue !== undefined && newValue !== "" && newValue !== " ") {
                this.element.classList.add(newValue);
            }
        }

        //Remove the old value
        this.classes = this.classes.filter(cl => cl != oldValue && cl != newValue);

        //Add the new value if its valid.
        if (newValue !== null && newValue !== undefined) {
            this.classes.push(newValue);
        }
    }

    /**
     * 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 
     */
    onAttributeChanged(oldValue, newValue, attributeName) {
        console.log(`Attribute changed, oldValue: ${oldValue} newValue: ${newValue} attribute: ${attributeName}`);
        if (this.element) {
            if (newValue == null || newValue == undefined) {
                this.element.removeAttribute(attributeName);
            } else {
                this.element.setAttribute(attributeName, newValue);
            }
        }

        this.attributes[attributeName] = newValue;
    }

    /**
     * Called when a property on this template was changed and needs to be updated
     * on the template's element.
     * @param {any} oldValue 
     * @param {any} newValue 
     * @param {string} propertyName 
     */
    onPropertyChanged(oldValue, newValue, propertyName) {
        console.log(`Property changed, oldValue: ${oldValue} newValue: ${newValue} property: ${propertyName}`);
        if (this.element) {
            this.element[propertyName] = newValue;
        }

        this.properties[propertyName] = newValue;
    }

    /**
     * Called when a ref was changed and we need to update the refs
     * value to match this elements reference.
     * @param {any} oldValue 
     * @param {any} newValue 
     * @param {any} ref 
     */
    onRefChanged(oldValue, newValue, ref) {
        console.log(`Ref changed, oldValue: ${oldValue} newValue: ${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 
     */
    onEventChanged(oldValue, newValue, eventName) {
        console.log(`Event changed: ${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);
            }
        }
    }
}

class div extends IgniteTemplate {
    constructor(...items) {
        super(items);

        this.tagName = "div";
    }
}

class a extends IgniteTemplate {
    constructor(...items) {
        super(items);

        this.tagName = "a";
    }
}

class html extends IgniteTemplate {
    constructor(code) {
        super([]);

        if (code instanceof IgniteProperty) {
            this.callbacks.push(code.attach((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) {
            var template = window.document.createElement("template");
            template.innerHTML = this.code;
            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 them html
        this.construct(null, null);
    }
}

class list extends IgniteTemplate {
    constructor(list, forEach) {
        super([]);
        this.tagName = "shadow list";

        if (list instanceof IgniteProperty) {
            this.callbacks.push(list.attach((oldValue, newValue) => {
                this.list = newValue;
                this.construct(null); //If the list changed, reconstruct this template.
            }));

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

        this.forEach = forEach;
        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(""); //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 = [];
        }

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

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

class slot extends IgniteTemplate {
    constructor(element) {
        super(null);

        this.parent = element;
    }

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

export {
    IgniteTemplate,
    div,
    html,
    list,
    a,
    slot
};