// @ts-check

import { Observable, Computed } from "./observable.js"

/**
 * Class supports data-binding operations
 */
export class DataBinding {

    /**
     * Simple evaluation
     * @param {string} js The JavaScript to evaluate 
     */
    execute(js) {
        return eval(js);
    }

    /**
     * Evaluates JavaScript with a constrained context (scope)
     * @param {string} src The JavaScript to evaluate 
     * @param {object} context The context (data) to evaluate with
     * @returns {object} The result of the evaluation 
     */
    executeInContext(src, context, attachBindingHelpers = false) {
        if (attachBindingHelpers) {
            context.observable = this.observable;
            context.computed = this.computed;
            context.bindValue = this.bindValue;
        }
        return this.execute.call(context, src);
    }

    /**
     * A simple observable implementation
     * @param {object} value Any value to observe
     * @returns {Observable} The observable instance to use
     */
    observable(value) {
        return new Observable(value);
    }

    /**
     * Creates an observed computed property
     * @param {function} calculation Calculated value 
     * @param {Observable[]} deps The list of dependent observables
     * @returns {Computed} The observable computed value 
     */
    computed(calculation, deps) {
        return new Computed(calculation, deps);
    }

    /**
     * Binds an input element to an observable value
     * @param {HTMLInputElement} input The element to bind to 
     * @param {Observable} observable The observable instance to bind to 
     */
    bindValue(input, observable) {
        let initialValue = observable.value;
        input.value = initialValue;
        observable.subscribe(() => input.value = observable.value);
        /**
         * Converts the values 
         * @param {object} value 
         */
        let converter = value => value;
        if (typeof initialValue === "number") {
            converter = num => isNaN(num = parseFloat(num)) ? 0 : num;
        }
        input.onkeyup = () => {
            observable.value = converter(input.value);
        };
    }

    /**
     * Searches for "repeat" attribute to data-bind lists
     * @param {HTMLElement} elem The parent element to search 
     * @param {object} context The context to use for binding 
     */
    bindLists(elem, context) {
        const listBinding = elem.querySelectorAll("[repeat]");
        listBinding.forEach(elem => {
            const parent = elem.parentElement;
            const expression = elem.getAttribute("repeat");
            elem.removeAttribute("repeat");
            const template = elem.outerHTML;
            parent.removeChild(elem);
            context[expression].forEach(item => {
                let newTemplate = `${template}`;
                const matches = newTemplate.match(/\{\{([^\}]*?)\}\}/g);
                if (matches) {
                    matches.forEach(match => {
                        match = match.replace("{{", "").replace("}}", "");
                        const value = this.executeInContext(`this.${match}`, { item });
                        newTemplate = newTemplate.replace(`{{${match}}}`, value);
                    });
                    parent.innerHTML += newTemplate;
                }
            });
        });
    }
}

