import { IgniteProperty, IgniteCallback, IgniteRenderingContext } from './ignite-html.js';
import { IgniteTemplate } from './ignite-template.js';

/**
 * The outline of a Ignite Element that extends the html element
 * and can be used to create custom components. Ignite Element's use an Ignite Template
 * for the render function.
 * 
 * @example
 * 
 * class MainApp extends IgniteElement {
 *   constructor() {
 *       super();
 *   }
 *
 *   get properties() {
 *       return {
 *       };
 *   }
 *
 *   render() {
 *       return this.template
 *           .child(
 *               new h1(`<i class="fad fa-fire-alt" style="--fa-primary-color: #FFC107; --fa-secondary-color: #FF5722; --fa-secondary-opacity: 1.0;"></i> Ignite HTML`),
 *               new h4(`Adding more fire to the web.`)
 *           );
 *    }
 * }
 * 
 * customElements.define("main-app", MainApp);
 * 
 * @example
 * //If you want to easily use an Ignite Element with templates see the following which can be added
 * //to any component file
 * class MainAppTemplate extends IgniteTemplate {
 *   constructor(...children) {
 *       super("main-app", children);
 *   }
 * }
 * 
 * export {
 *  MainAppTemplate as MainApp
 * }
 * 
 * @example
 * //If a template was created for a Ignite Element (like the previous example) it can be used just like any other template:
 * new MainApp()
 */
class IgniteElement extends HTMLElement {
    constructor() {
        super();

        this.elementConnected = false; //Used to know if the connectedCallback was already called.
        this.template = null;
        this.elements = [];
        this.readyCallback = new IgniteCallback(() => this.ready());
        this.onDisconnectCallbacks = [];

        //Create the variables for this element.
        this.createVariables();

        //Create the properties for this element.
        this.createProperties();

        //Init the element before connected callback is fired
        //Create a new rendering context so the init method can access properties correctly.
        IgniteRenderingContext.push();
        this.init();
        IgniteRenderingContext.pop();
    }

    /**
     * Returns an object with all the variables for this ignite element. Variables
     * unlike properties have no callbacks or events, they are just data.
     * 
     * @returns An object with properties that will be assigned to this ignite element as variables.
     * 
     * @example
     * get variables() {
     *  return {
     *      dialog: null,
     *      clickCallback: null
     *  };
     * }
     */
    get variables() {
        return {};
    }

    /**
     * Returns an object with all the properties for this ignite element. If null or empty
     * then no properties will be created. To change a property and read it's current value see below.
     * 
     * @returns An object with properties that will be assigned to this ignite element.
     * 
     * @example
     * get properties() {
     *  return {
     *      show: false,
     *      title: "This is a title",
     *      items: [1, 2, 3]
     *  };
     * }
     * 
     * @example
     * //To change a property access it via this.PROP_NAME
     * this.show = false;
     * 
     * //To get a properties value access if via this.PROP_NAME
     * console.log(this.title);
     */
    get properties() {
        return {};
    }

    /**
     * Returns any CSS styling code for this ignite element. If this returns a non null
     * value the CSS will auto be injected into the current HTML page once and reused for the life
     * of the ignite element.
     * 
     * @returns A string containing CSS code to be used with this ignite element.
     *  
     * @example
     * get styles() {
     *  return `
     *      h1 {
     *          color: black;
     *      }
     *  `;
     * }
     */
    get styles() {
        return null;
    }

    /**
     * Creates the variables for this ignite element and initializes them.
     * @ignore
     */
    createVariables() {
        var vars = this.variables;
        if (vars != null) {
            Object.keys(vars).forEach(name => this[name] = vars[name]);
        }
    }

    /**
     * Resets the variables for this element back to their original default
     * values.
     */
    resetVariables() {
        var vars = this.variables;
        if (vars != null) {
            Object.keys(vars).forEach(name => this[name] = vars[name]);
        }
    }

    /**
     * Gets all the property values from this element and returns it.
     */
    getProperties() {
        var ret = {};
        var props = this.properties;
        IgniteRenderingContext.push();
        Object.keys(props).forEach(name => ret[name] = this[name]);
        IgniteRenderingContext.pop();
        return ret;
    }

    /**
     * Sets all the property values on this element.
     */
    setProperties(props) {
        if (props) {
            Object.keys(props).forEach(name => this[name] = props[name]);
        }
    }

    /**
     * Creates the getters/setters for properties in this
     * ignite element and initializes everything.
     * @ignore
     */
    createProperties() {
        var props = this.properties;
        if (props != null) {
            Object.keys(props).forEach(name => {
                var prop = (props[name] instanceof IgniteProperty ? props[name] : new IgniteProperty(props[name]));
                Object.defineProperty(this, name, {
                    get: () => { return (IgniteRenderingContext.rendering ? prop : prop.value); },
                    set: (value) => { prop.value = value; }
                });
            });
        }
    }

    /**
     * Resets the properties for this element back to their original default
     * values.
     */
    resetProperties() {
        var props = this.properties;
        if (props != null) {
            Object.keys(props).forEach(name => this[name] = props[name]);
        }
    }

    /**
     * Setups this ignite element and constructs it's template when
     * this function is called by the DOM upon this element being created somewhere.
     * @ignore
     */
    connectedCallback() {
        //Only run this if we haven't been connected before.
        //If this element is moved the instance will be the same but it will call this again
        //and we want to trap this and not run the code below twice.
        if (this.elementConnected) {
            return;
        } else {
            this.elementConnected = true;
        }

        //See if a styling sheet has been created for this element if it's needed.
        if (this.styles !== null && this.styles !== "") {
            if (document.getElementsByClassName(`_${this.tagName}_styling_`).length == 0) {
                var styleEl = document.createElement("style");
                styleEl.classList.add(`_${this.tagName}_styling_`);
                styleEl.innerHTML = this.styles;
                document.body.prepend(styleEl);
            }
        }

        //If we don't already have a template, make sure we create one,
        //this can happen if this element was constructed in the DOM instead of within a template.
        if (!this.template) {
            this.template = new IgniteTemplate();
            this.template.element = this;
            this.template.tagName = this.tagName;
        }

        //Add any childNodes we have to the elements list within this
        this.childNodes.forEach((item) => this.elements.push(item));

        //Enter a rendering context so properties don't expose their values until we are done.
        IgniteRenderingContext.enter();

        //Make sure the render template is our template, if not, add it as a child.
        var renderTemplate = this.render();
        if (renderTemplate !== this.template && renderTemplate) {
            this.template.child(renderTemplate);
        } else if (!renderTemplate) {
            throw `RenderTemplate was null for template: ${this.tagName}. Is render() returning null or not returning a template?`;
        }

        //Construct our template.
        try {
            this.template.construct(this.parentElement);
        } catch (error) {
            console.error(error);
        }

        //Leave the rendering context.
        IgniteRenderingContext.leave();

        //Let the rendering context know this element is ready.
        IgniteRenderingContext.ready(this.readyCallback);
    }

    /**
     * Cleanups this element and deconstructs everything when this element is removed from
     * the DOM.
     * @ignore
     */
    disconnectedCallback() {
        //Run a test here, if the element was moved but not removed it will have a disconnect
        //get called but then be reconnected, so use a timer to see if this is what happened.
        setTimeout(() => {
            //If the element is connected then don't do this, more than likely
            //the element was moved or something.
            if (this.isConnected) {
                return;
            }

            //If we still have a reference to our template, deconstruct it.
            if (this.template) {
                this.template.deconstruct();
            }

            //Detach the after render callback
            this.readyCallback.disconnect();

            //Call any on disconnected callbacks
            if (this.onDisconnectCallbacks) {
                this.onDisconnectCallbacks.forEach(callback => callback.invoke(this));
            }

            //Cleanup this element if we need to.
            this.cleanup();
        }, 1);
    }

    /**
     * Attaches a function to the on disconnect event for this element and returns a callback.
     * @param {Function} onDisconnect Disconnect function to be called when on disconnect is raised.
     * @returns IgniteCallback created for this callback.
     */
    attachOnDisconnect(onDisconnect) {
        var callback = new IgniteCallback(onDisconnect, detach => this.detachOnDisconnect(detach));
        this.onDisconnectCallbacks.push(callback);
        return callback;
    }

    /**
     * Removes an ignite callback from the on disconnect event.
     * @param {IgniteCallback} callback The ignite callback to disconnect.
     * @ignore
     */
    detachOnDisconnect(callback) {
        this.onDisconnectCallbacks = this.onDisconnectCallbacks.filter(item => item != callback);
    }

    /**
     * Returns the template to be rendered for this element.
     * 
     * @returns An ignite template to be used to construct this ignite element.
     * 
     * @see this.template is automatically created for each IgniteElement and must be used in the render() function.
     * 
     * @example
     * render() {
     *  return this.template
     *       .child(
     *           new h1(`<i class="fad fa-fire-alt" style="--fa-primary-color: #FFC107; --fa-secondary-color: #FF5722; --fa-secondary-opacity: 1.0;"></i> Ignite HTML`),
     *           new h4(`Adding more fire to the web.`)
     *       );
     * }
     */
    render() {
        return null;
    }

    /**
     * Called wehn this ignite element is being initialized. When this is called
     * the element has not been created. This is good for login checking code or special setup code.
     */
    init() {

    }

    /**
     * Called when this ignite element is ready. This will invoke once all
     * ignite elements on the current page have been rendered and setup. You can safely know
     * all elements have be initialized and are ready once this is called.
     */
    ready() {

    }

    /**
     * Called when this ignite element should cleanup
     * any resources like events, timers, ect. This is called when the element
     * is being destroyed.
     */
    cleanup() {

    }

    /**
     * Generates a uuid and returns it.
     */
    uuid() {
        return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
            (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
        );
    }
}

export {
    IgniteElement
};