// it is important that no more than content than
// neccesary is fetched from the server
const preLoadCache = [];
export function SideLoad(path) {
    return new Promise((resolve) => {
        if (preLoadCache[path]) {
            resolve(preLoadCache[path]);
        } else {
            const fetchPromise = fetch(path).then(response => response.text());
            preLoadCache[path] = fetchPromise;
            resolve(fetchPromise);
        }
    });
}

export function RegisterComponent(componentClass) {
    const name = componentClass.__IDENTIFY();
    console.log('registering component: ' + name);
    customElements.define(`${name}-component`, componentClass);
}

export class Component extends HTMLElement {
    constructor(child) {
        super();
        this.root = this.attachShadow({ mode: 'open' });
        this.state = {};
        this.child = child;
    }

    // Override these
    OnMount() { }
    Update(attributeChanged, newState) { }
    Render() { Component.__WARN('Render'); }
    OnRender() { }
    OnUnMount() { }
    static __IDENTIFY() { Component.__WARN('identify'); }

    async connectedCallback() {
        if (!this.root.isConnected) {
            return;
        }
        // set up to watch all attributes for changes
        this.watchAttributeChange(this.attributeChangedCallback.bind(this));

        // if there are any attributes related to the element
        // be sure to include them in the state to be sure that
        // they can be resolved
        let stateUpdateQueue = { ...this.state };
        for (const attribute of this.attributes) {
            stateUpdateQueue = { ...stateUpdateQueue, [attribute.name]: attribute.value };
        }
        this.state = stateUpdateQueue;

        await this.OnMount(Object.bind(this));

        this.Update(Object.bind(this));

        this.setState(this.state, false);
        this.__INVOKE_RENDER(Object.bind(this));
    }

    disconnectedCallback() {
        this.root.innerHTML = '';
        this.OnUnMount();
    }

    watchAttributeChange(callback) {
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.type === 'attributes') {
                    const newVal = mutation.target.getAttribute(mutation.attributeName);
                    callback(mutation.attributeName, newVal);
                }
            });
        });
        observer.observe(this, { attributes: true });
    }

    attributeChangedCallback(name, newValue) {
        console.log(`attribute changed: ${name} ${newValue}`);
        this.Update(Object.bind(this, name, {
            ...this.state,
            [name]: newValue,
        }));
        this.setState({ ...this.state, [name]: newValue }, false);
        this.__INVOKE_RENDER(Object.bind(this));
    }

    get getState() {
        return this.state;
    }

    setState(newState, doRender = true) {
        this.state = newState;
        if (!doRender) return;
        this.__INVOKE_RENDER(Object.bind(this));
    }

    async __INVOKE_RENDER() {
        let res = this.Render(Object.bind(this));

        if (res instanceof Promise) {
            res = await res;
        }

        if (res.template === undefined || res.style === undefined) {
            Component.__ERR('no template or style');
            return;
        }

        // way to formally update state WITHOUT triggering a re-render
        if (res.state) {
            this.state = res.state;
        }

        // if res.template is a promise, we need to wait to resolve it
        if (res.template instanceof Promise) {
            res.template = await res.template;
        }
        if (res.style instanceof Promise) {
            res.style = await res.style;
        }

        // go through and resolve all of the "state" dependancies
        let resolved = res.template;
        const parserRegex = /{(.*?)}/gm;
        for (let m; (m = parserRegex.exec(res.template)) !== null;) {
            if (m.index === parserRegex.lastIndex) {
                parserRegex.lastIndex++;
            }

            // resolve the state dependancy and replace it in the template
            if (m[1].startsWith('this.state')) {
                const stateKey = m[1].substring(11);
                const stateValue = this.state[stateKey];
                // console.log('attempting to replace', m[0], 'with', stateValue);
                if (stateValue === undefined) {
                    continue;
                }

                // console.log('replacing', m[0], 'with', stateValue);
                resolved = resolved.replace(m[0], stateValue);
            }
        }

        this.root.innerHTML = resolved;

        const style = document.createElement('style');
        style.textContent = res.style;
        this.root.appendChild(style);

        this.OnRender();
    }

    static __WARN(caller) {
        console.error(`WARNING: ${caller} is not implemented`);
    }

    static __ERR(msg) {
        console.error(`ERROR: idiot ${msg}`);
    }
}
