//# sourceURL=Widget.js
// This file is part of Reactor. Copyright (c) 2020-2026 Patrick H. Rigney, All Rights Reserved.
/* globals SlapDash,lexp */
// TODO??? Map using regular expressions? e.g. lionfish into map "[/(.*)fish/=$1]" would return "lion"

import Observable from "/client/Observable.js";
import Observer from "/client/Observer.js";
import Logger from './Logger.js';
import '/common/lexp.js';
import * as util from '/client/util.js';

export const version = 26157;

const layoutDef = {};
const headItems = {};

function splitFirst( s, d ) {
    const i = s.indexOf( d );
    if ( i < 0 ) {
        return [ s ];
    }
    return [ s.substring( 0, i ), s.substring( i + d.length ) ];
}

const Widget = function( elem ) {
    this.elem = elem;
    this.name = "Widget";
    this.id = this.getData( 'id' , "widget" + (++Widget.serial) );
    this.group = this.getData( 'group', this.group );
    this.entity_id = this.getData( 'entity', this.getData( 'device' ) );
    this.entity = false;

    /* All widgets support the active expression. */
    this.active_expr = this.getData( 'active', false );

    /* Create a logger for this widget, default as parent */
    this.log = Logger.getLogger( String(this), Logger.getDefaultLogger() );
    const debugLevel = this.getData('debuglevel', this.log.getLevel());
    this.log.setLevel( debugLevel );

    /* These are things we figure out later */
    this.layout = false;
    this.started = false;
    this.observed = {};

    this._ctx = this._createDefaultContext();

    if (typeof elem != "undefined") {
        $(elem).addClass("widget nodata");
        $(elem).attr("id", this.id);
    }
};

Widget.prototype.constructor = Widget;

Widget.serial = 0; // "static"

/**
 * Add an observed entity. Data binding and other features may make references
 * to entities that are not the "primary" entity of the widget. This widget can
 * call this function to add those other referenced entities as observed objects,
 * so changes in their state call notification to the widget.
 * @param {Entity} ref - The entity to be observed
 * @param {string} lvar - Name of an instance variable on which to store the
 *                        entity (optional).
 * @return {Promise} Promise used to find/load the entity, so caller can do
 *                   additional processing.
 */
Widget.prototype.addObservedEntity = function( ref, lvar ) {
    const self = this;
    return new Promise( (resolve, reject) => {
        self.log.debug( 9, "%1.addObservedEntity(%2, %3)", self.toString(), ref, lvar);
        SlapDash.promiseEntity( ref ).then( function( e ) {
            self.log.debug( 6, "%1.addObservedEntity() entity %2 for %3", self.toString(), e, ref );
            if ( lvar ) {
                self[lvar] = e;
            }
            self.subscribe( e );
            e.publish( e, 'entity-update' );
            resolve( e );
        }).catch( (err) => {
            console.error(err);
            reject( err );
        });
    });
};

/**
 * Internal function, used by SlapDash to start a Widget.
 */
Widget.prototype._start = function() {
    var self = this;

    /* Make a logger for self instance. */
    self.log = Logger.getLogger( self.toString(), Logger.getLogger( 'Widget' ) );

    self.log.debug(8, "%1: starting with entity_id %2", self, self.entity_id);

    /* Load layout. When done, load entity and do first binding. */
    self.elem.addClass( "nodata" ).removeClass( "active update-pending" );

    self.loadLayout().then( function() {
        self.start();
        if ( self.entity_id ) {
            self.log.debug( 6, "%1._start() loading %2", self.toString(), self.entity_id);
            self.addObservedEntity( self.entity_id, "entity" );
        }
        self.dispatchUpdate();
    }).catch( function( err ) {
        self.elem.empty().text(err);
        console.log(err);
    });
};

/**
 * If a widget needs to do something at initialization, it can override this method.
 */
Widget.prototype.start = function() {
    /* No default behavior */
};

Widget.prototype.stop = function() {
    /* No default behavior */
};

/**
 * This is the default update behavior (when a layout does not specify an
 * alternative. Typically, alternates and overrides should call this method,
 * though (e.g. subclass should super.update()).
 */
Widget.prototype.update = function() {
    this.setContextEntity( this.entity );
    if ( this.active_expr ) {
        const active = this.resolve( this.active_expr );
        this.log.debug( 2, "Widget.update on %3 active expression %1 returns %2", this.active_expr, active,
            this.toString() );
        $( this.elem ).toggleClass( 'active', active );
        active ? this.activate() : this.deactivate();
    }
    this.pre_update();
    this.bindValues();
    this.post_update();
};

/** Interface for widget pre-update */
Widget.prototype.pre_update = function() {};

/** Interface for widget post-update */
Widget.prototype.post_update = function() {};

/** Called when the widget closes. Get rid of every reference to other objects.
 * Try to get rid of every reference to ourselves.
 */
Widget.prototype.close = function() {
    this.log.debug(5, "Widget.close(%1) closing", String(this));
    /* this.publish( this, 'closing' ); /* Necessary??? */
    this.stop();
    this.unsubscribe();
    this.disconnect();
    this.observed = {};
    this._ctx = false;
    this.log = false;
    this.closed = true;
    delete this.entity;
};

/** Default implementation for click on widget; do nothing. */
Widget.prototype.click = function( /* event */ ) {
    // console.log('Widget default click handler');
    return true;
};

/** Interface for widget activate */
Widget.prototype.activate = function() {};

/** Interface for widget deactivate */
Widget.prototype.deactivate = function() {};

/** Interface for widget enable */
Widget.prototype.enable = function() {};

/** Interface for widget disable */
Widget.prototype.disable = function() {};

/** Called when gridster resizes widget -- PHR??? Camera widget has some dependency; needs implementation at gridster(). ??? is this comment current?*/
Widget.prototype.resized = function() {};

/** Return string representation of this object */
Widget.prototype.toString = function() {
    return '[' + this.constructor.name + '#' + this.id + ']';
};

/**
 * Return a value from the "data" attributes of the widget's DOM element.
 * @param {string} name - Data attribute name (e.g. for attribute data-bind="123", the name is "bind")
 * @param {mixed} defVal - Default value to return if attribute is not set.
 */
Widget.prototype.getData = function( name, defVal ) {
    var val = $( this.elem ).data( name );
    return "undefined" === typeof val ? defVal : val;
};

/** Get data attribute as a boolean value */
Widget.prototype.getDataBool = function( name, defVal ) {
    var val = this.getData( name, defVal );
    if ( val === undefined ) {
        return defVal;
    }
    return /1|true|enabled|yes|y|on/i.test( val );
};

/** Get base URL of the page being displayed. */
Widget.prototype.getBaseURL = function() {
    return SlapDash.config.baseurl;
};

/* LAYOUT METHODS */

/** Get the (Promise for) the loaded layout definition data */
Widget.prototype.getLayoutDef = function() {
    return layoutDef[this.name];
};

/** Get the layout data for the Widget's active layout */
Widget.prototype.getActiveLayout = function() {
    const wdef = layoutDef && layoutDef[this.name];
    if ( wdef ) {
        return wdef.layouts[this.layout];
    }
    this.log.warn("%1 call to Widget.getActiveLayout(%2) before layout loaded", String(self), this.layout);
    return false;
};

Widget.prototype._install_layout = async function() {
    const self = this; // ??? TODO: cleanup

    this.layout = false;
    if ( "undefined" === typeof this.elem.data( 'layout' ) ) {
        /* No layout specified; see if there's body content. */
        if ( ! (this.elem.html() || "").match( /^\s*$/ ) ) {
            /* Widget body is layout */
            this.layout = null;
        }
    }

    let ldata;
    if ( null !== self.layout ) {
        const wdef = await this.getLayoutDef(); /* layouts for this widget type */

        /* Select the layout. The usual selector for layout is "data-layout", but each layout can specify
         * something else. */
        self.layout = self.getData( wdef.layoutselector || "layout", wdef.defaultlayout || "default" );
        self.log.debug(5, '%1 selector is data-%2, default layout %3', String(self), wdef.layoutselector,
            wdef.defaultlayout);

        /* Find the selected layout */
        ldata = wdef.layouts[self.layout];
        if ( !ldata ) {
            self.log.debug(1,"Cannot load layout %1, falling back to default.", self.layout);
            self.layout = wdef.defaultlayout || "default";
            ldata = wdef.layouts[self.layout];
        }

        /* Load the selected layout into the widget body */
        if ( ldata ) {
            ldata.id = self.layout;
            /* Layout may specify active expression; active_expr on widget element
             * supercedes, though.
             */
            if ( "" === ( self.active_expr || "" ) && ldata.active_expression ) {
                self.active_expr = ldata.active_expression;
            }
            /* Apply HEAD data if present */
            if ( "" !== ( ldata.head || "" ) ) {
                if ( ! headItems[ldata.head] ) {
                    $("head").append(ldata.head);
                    headItems[ldata.head] = true;
                }
            }
            /* Handle styles */
            if ( "" !== ( ldata.styles || "" ) ) {
                let s = " " + ldata.styles;
                /* ??? TO-DO: Right now, I'm just using IDs to identify the widget directly for the dynamic
                   styles. This version injects the styles for the layout for each widget/ID (ugh).
                   Plan is to use class "widget-kind-layoutName" later, and inject those
                   styles only once. */
                if (s.search("@ ") >= 0) {
                    // Using old, deprecated syntax (@ for widget ID, @@ for a single @)
                    self.log.warn( "%1.loadLayout() layout %2 uses deprecated syntax. Please update it.",
                        self.toString(), self.layout );
                    s = s.replace(/([^@])[@]([^@])/g, "$1#" + self.id + "$2");
                    s = s.replace(/@@/g, "@");
                } else // New style is to use $
                    s = s.replace(/[$]/g, "#" + self.id);

                s = s.replace(/{{([^}]+)}}/g, function( match, p1 ) {
                    const [data,dflt] = splitFirst( p1, ':' );
                    return self.getData(data, dflt||"");
                });

                $("head").append("<style>" + s + "</style>");
                $(self.elem).addClass( "widget-" + self.name.toLowerCase() + "-" + self.layout.toLowerCase() ); // this is the class
            }
            /* Expression context additions */
            if ( "object" === typeof ldata.expression_context ) {
                self.log.debug( 5, "%1 layout %2 adding expression context %3", self, self.layout, ldata.expression_context );
                self._ctx = util.combine( self._ctx, ldata.expression_context );
                self.log.debug( 7, "modified context is %1", util.dump(self._ctx) );
            }
            /* Load the content block */
            if ( !ldata.content ) {
                self.log.info( "%1: selected layout %2 has no content", self, self.layout );
            } else {
                /* Replace any element IDs in the layout content with the ID prefixed with
                 * the widget ID, to make it unique (we hope; unless the layout content has
                 * an error). */
                const $newb = $( ldata.content );
        /* ??? Need to modify CSS as well...
                $( '[id]', $newb ).each( ( ix, el ) => {
                    $(el).attr( 'id', self.id + '-' + $(el).attr( 'id' ) );
                });
        */
                $(self.elem).empty().append( $newb );
            }
        } else {
            self.elem.empty().text( "Failed to find layout " + JSON.stringify( self.layout ) );
        }
    }

    /* If the content is not wrapped in a div.widget-content, do so now */
    if ( false && 0 === $( 'div.widget-content', self.elem ).length ) {
        const $content = self.elem.children();
        const $wc = $( '<div class="widget-content"></div>' );
        $wc.append( $content );
        self.elem.empty().append( $wc );
    }

    /* Handle value expressions from element data. Use {{value}} syntax,
       for example, to get the data-value element of the widget. This is
       mostly used in content data-bind's, where we want to pass an expression
       from the element into the layout to use as the source. Sensor is a good
       example. */
    const html = self.elem.html().replace(/{{([^}]+)}}/g, function( match, p1 ) {
        const [data,dflt] = splitFirst( p1, ':' );
        return self.getData(data, dflt||"");
    });
    self.elem.html( html );

    /* Hook all declared actions to our handler. */
    $('[data-action]', self.elem).each( function( ix, obj ) {
        const tag = $(obj).attr("data-action");
        self.log.debug(5, "Widget.loadLayout(%1) setting up action %1 for %2",
            self.toString(), tag);
        $(obj).on("click.slapdash",
            function( event ) { return self._actionclick( tag, event ); } );
    });

    if ( ldata && ldata.default_action ) {
        $(self.elem).on("click.slapdash",
            function( event ) { return self._actionclick( 'default_action', event ); } );
    }

    /* Let subclasses know that we're done setting up the layout, and they can do
     * whatever else they need to do. */
    self.layoutInit( self.layout, ldata );
};

/**
 * Load the layout named in the layout descriptor. If successfully loaded,
 * the callback function is called with the layout name and the descriptor
 * object.
 * @param {callback} callback - callback (clear now?)
 */
Widget.prototype.loadLayout = async function( /* callback */ ) {
    const self = this;
    const promise = self._get_layout_data();
    promise.then( result => {
        self.log.debug( 6, "%1.loadLayout() layouts loaded, installing layout", String(self));
        self._install_layout();
        return result;
    }).catch( e => {
        /* ??? supply an internal default layout? */
        console.log(e);
        self.log.err("%1 failed to load layout data: %2", String(self), e);
        throw e;
    });
    return promise;
};

/** Called after layout is loaded and set. Subs can override for their own work. */
Widget.prototype.layoutInit = function( /* layout, descObj */ ) {};

/** ??? What does this do? See getLayoutValue */
Widget.prototype.getDescriptorValue = async function( name, deflt ) {
    const wdef = await this.getLayoutDef();
    if ( ! wdef ) return deflt;
    const p = name.split('.');
    let n = wdef[p.shift()];
    while ( p.length ) {
        if ( n === undefined ) return deflt;
        n = n[p.shift()];
    }
    return ( "undefined" === typeof n ) ? deflt : n;
};

/** And this? See Clock.js */
Widget.prototype.getLayoutValue = function( name, deflt ) {
    if ( ! this.layout ) {
        return deflt;
    }
    const ldata = this.getActiveLayout();
    if ( ldata && ldata[name] ) {
        return ldata[name];
    }
    return deflt;
};

Widget.prototype._get_layout_yaml = function( name ) {
    var self = this;
    return new Promise( (resolve,reject) => {
        var fn = self.getBaseURL() + '/slapdash/widgets/' + name + '.yaml';
        $.ajax({
            dataType: "text",
            cache: false,
            url: fn,
            r: Math.random()
        }).done( data => {
            try {
                const d = jsyaml.load( data );
                self.log.debug( 5, "%1 loaded YAML layout %2", String(self), fn);
                resolve( d );
                return data;
            } catch( e ) {
                console.log( "Failed to parse YAML in", fn );
                console.error( e );
                reject( e );
            }
        }).fail( ( e ) => {
            console.error( e );
            reject( e );
        });
    });
};

Widget.prototype._get_layout_json = function( name ) {
    var self = this;
    return new Promise( (resolve,reject) => {
        var fn = self.getBaseURL() + '/slapdash/widgets/' + name + '.json';
        $.ajax({
            dataType: "json",
            cache: false,
            url: fn,
            r: Math.random()
        }).done( data => {
            self.log.debug( 5, "%1 loaded JSON layout %2", String(self), fn);
            resolve( data );
            return data;
        }).fail( ( e ) => {
            reject( e );
        });
    });
};

/** Returns a Promise for loading layout data for the named Widget */
Widget.prototype._get_layout_data = async function() {
    const self = this;
    if ( ! layoutDef[self.name] ) {
        layoutDef[self.name] = new Promise( (resolve,reject) => {
            self._get_layout_yaml( self.name ).then( ( data ) => {
                /* Fix-ups */
                data.layoutselector = data.layoutselector || "layout";
                data.defaultlayout = data.defaultlayout || "default";
                layoutDef[self.name] = data;
                resolve( data );
            }).catch( ( e ) => {
                self._get_layout_json( self.name ).then( ( data ) => {
                    /* Fix-ups */
                    data.layoutselector = data.layoutselector || "layout";
                    data.defaultlayout = data.defaultlayout || "default";
                    layoutDef[self.name] = data;
                    resolve( data );
                }).catch( e => {
                    reject( e );
                });
            });
        });
    }
    return layoutDef[self.name];
};

/* ACTION METHODS */

/**
 * Internal method to dispatch click on action object in widget layout.
 * @param {string} act - name of the action
 * @param {Object] event - The DOM event for the click
 */
Widget.prototype._actionclick = function( tag, event, ui ) {
    this.log.debug(5, "Widget._actionclick(%1) action %2", String(this), tag);
    const ldata = this.getActiveLayout();
    if ( ldata ) {
        this.log.debug(9, "Widget._actionclick(%1) layout %2", String(this), ldata.id);
        let action = false;
        if ( 'default_action' === tag ) {
            action = ldata.default_action;
        } else if ( (ldata.actions || {})[tag] ) {
            action = ldata.actions[tag];
            this.log.debug( 6, "Widget._actionclick(%1) found %2 action in layout", this.toString(), tag );
        } else {
            /* If a tag wasn't found, assume the value of data-action on the clicked
             * object is a direct reference to an action (in capability.action form).
             */
            action = tag;
        }
        this.log.debug( 6, "Widget._actionclick(%1) action determined %2", String(this), action);
        let ret;
        if ( action ) {
            if ( "object" !== typeof action ) {
                /* Simple text form action */
                this.performOnEntity( this.entity, action, {} );
                return false;
            } else if ( action.call || action.function ) {
                const func = action.call || action.function;
                this.log.debug(6, "Widget._actionclick(%1) calling %2 for %3", String(this), func, tag);
                try {
                    if ( this.performTimer ) {
                        clearTimeout( this.performTimer );
                    }
                    const self = this;
                    this.performTimer = setTimeout( () => {
                        delete self.performTimer;
                        $( self.elem ).removeClass( "update-pending" );
                    }, 3000 );
                    $( this.elem ).addClass( "update-pending" );
                    ret = this[func].call( this, action, event, ui );
                    return "undefined" === typeof ret ? false : ret;
                } catch ( e ) {
                    console.log(e);
                    this.log.err( "Widget._actionclick(%1) action function %2 threw an exception",
                        String(this), func );
                    this.log.exception(e);
                    return false;
                }
            } else if ( action.action ) {
                const p = {};
                Object.keys( action.parameters || {} ).forEach( (name) => {
                    const pm = action.parameters[name];
                    if ( pm.value ) {
                        /* Constant/fixed */
                        p[name] = pm.value;
                    } else if ( pm.source_element ) {
                        /* ??? idSelector -- escaping needed here later. Also prefixing??? */
                        p[name] = $( '#' + pm.source_element, this.elem ).val();
                    } else if ( pm.expression ) {
                        /* TBD */
                        console.error("**** TBD **** Widget._actionclick()");
                        p[name] = null;
                    } else {
                        this.log.warn("Widget._actionclick(%1) layout %1 action %3 parameter %2 value unresolvable",
                            this.toString(), this.layout, name, tag);
                        p[name] = null;
                    }
                    if ( null === p[name] && pm.default ) {
                        p[name] = pm.default;
                    }
                });
                let target = this.entity;
                if ( action.target ) {
                    target = SlapDash.findEntity( action.target );
                    if ( !target ) {
                        this.log.err("Widget._actionclick(%1) target %2 not found!",
                            this.toString(), action.target);
                        return false;
                    }
                }
                this.performOnEntity( target, action.action, p );
                return false;
            }
        }
    } else {
        this.log.warn("Widget._actionclick(%1) no layout data", String(this));
    }
    return true;
};

Widget.prototype.performOnEntity = function( target, action, params ) {
    const self = this;
    target = target || this.entity;
    $( this.elem ).addClass( "update-pending" );
    if ( this.performTimer ) {
        clearTimeout( this.performTimer );
    }
    this.performTimer = setTimeout( () => {
        delete self.performTimer;
        $( self.elem ).removeClass( "update-pending" );
    }, 3000 );
    target.perform( action, params || {} );
};

/* OBSERVABLE IMPLEMENTATION */

/**
 * Dispatch update based on layout specification. The default update method
 * is update(), but the layout may specify an alternate based on the current
 * layout's requirement.
 */
Widget.prototype.dispatchUpdate = function( ...args ) {
    // Have good data for device. Controller state first, then entity if set.
    let valid = true; // ??? !this.controller.system.getAttributeBoolean("error");
    if ( valid && this.entity_id && !( this.entity ) )
        valid = false;
    if ( valid ) {
        this.enable();
        $(this.elem).removeClass("nodata update-pending");
    } else {
        this.disable();
        $(this.elem).addClass("nodata").removeClass('active update-pending');
    }

    let f;
    const ldef = this.getActiveLayout();
    if ( ldef ) {
        /* Try update function specified in layout. */
        /* ??? Seems like now that we have pre- and post-update methods, this
         * may fall to disuse? */
        f = ldef.update;
    }
    if ( !f ) {
        /* Use default update */
        this.update();
    } else if ( "function" === typeof this[f] ) {
        try {
            this[f].apply( this, ...args );
        } catch ( e ) {
            this.log.err("%1.dispatchUpdate() exception thrown by layout-specific update method %2",
                String(this), f);
            this.log.exception( e );
        }
    } else {
        this.log.debug( 2, 'Update handler %1 not found or not function', f );
    }
};

/**
 * Fulfills the Observer contract--receive notice for an observed object.
 * Calls dispatchUpdate() to dispatch it.
 */
Widget.prototype.notify = function( msg ) {
    // We ignore message because we only register for one
    const val = msg.data;
    const sender = msg.sender;
    console.log("in notify, this is", this);
    this.log.debug(8, "messageNotify from %1 data %2", sender, val );

    this.dispatchUpdate( val, sender );
};

/* BINDING FOR LAYOUT */

/**
 * Create default context
 * @return {Object} context for lexp
 */
Widget.prototype._createDefaultContext = function() {
    const self = this;
    const context = lexp.get_context();
    lexp.define_func_impl( context, 'strftime', ( ctx, fmt, t ) => util.strftime( fmt, t ) );
    lexp.define_func_impl( context, 'data', ( ctx, name, dflt ) => { console.log("data fetch", name); return self.getData( name, dflt ); } );
    lexp.define_func_impl( context, 'deref', ( ctx, obj, path ) => util.deref( obj, path ) );
    lexp.define_func_impl( context, '$', ( ctx, name ) => {
        var e = SlapDash.findEntity( name );
        if ( e ) {
            self.subscribe( e );
            return e.extract();
        }
        return null;
    });
    lexp.define_var( context, 'system', {} );
    return context;
};

/**
 * Assign default entity to this Widget's evaluation context.
 */
Widget.prototype.setContextEntity = function( entity ) {
    if ( entity ) {
        if ( ! this._ctx._env ) { /* WHY??? */
            this._ctx._env = entity.getCanonicalID();
        }
        lexp.define_var( this._ctx, 'entity', entity.extract() );
    } else {
        if ( ! this._ctx._env ) {
            this._ctx._env = this.entity_id || null;
        }
        lexp.define_var( this._ctx, 'entity', null );
    }
    console.log("entity context",this._ctx);
};

/**
 * Resolve an expression to its value. An optional context for lexp
 * may also be passed in.
 * @param {string} expr - Expression to be parsed
 * @param {Object} context - Additional context for the evaluation (required)
 * @return {mixed} Result of the search/expression evaluation.
 */
Widget.prototype.resolve = function( exprList, context ) {
    context = context || this._ctx;
    const exprs = exprList.split(';');
    let value;
    const nexpr = exprs.length;
    for (let k=0; k<nexpr; ++k) {
        const expr = exprs[k];
        if ( expr.match( /^\s*$/ ) ) {
            continue;
        }

        /* Constant string expression? */
        if ( '#' === expr.substr(0,1) ) {
            value = expr.substr(1);
            break;
        }

        /* Try to compile and run the expression */
        let ce;
        try {
            ce = lexp.compile( expr );
        }
        catch (err) {
            this.log.warn( "%1 expression compilation of %2 failed: %3", this, expr, err);
            this.log.warn("Source is %1", exprList);
            continue;
        }

        try {
            this.log.debug( 8, "%1 attempting expression %2 with context %3", this, expr, context );
            value = lexp.run( ce, context );
            // console.log( `${this.toString()} expr ${expr} result`, typeof value, value );
            this.log.debug( 8, "%1 expression %2 result %3", this, expr, JSON.stringify( value ) );
        }
        catch (err) {
            this.log.warn( "%1 expression evaluation of %2 failed: %3", this, expr, err);
            console.error(err);
            continue;
        }

        /* If we have a value, return it. Otherwise, next expression. */
        if ( "undefined" !== typeof value && null !== value ) {
            break;
        }
    }

    return value;
};

/**
 * This method is used to update displayed widgets. It loops through all "data-bind"-carrying
 * elements within the widget and resolves those references/expressions to values. The returned
 * value may be put through an optional map, specified using "[this=that,this2=that2...]"
 * notation at the end (expression value matching this is translated to that).
 * The data-bind values can be a list of expressions to be evaluated, the first of which to
 * return something other than undefined determining the returned value (after mapping).
 * Once a value is produced, it is bound to the containing element, either by setting its text
 * content (not html), or if the expression is of the form "attribute=expression", the value
 * is set on the element attribute named (see third example below). If the expression evaluates
 * to an array, the bound element is repeated for each array element returned.
 *
 * Detailed examples:
 *
 *     <h1 data-bind="data(title);entity.name;#Unknown"></h1>
 *         This h1 element's content is set to the value of data-title of the parent <div>, if provided,
 *         and otherwise the entity's name. This effectively allows the heading to have a
 *         dynamic name (a scene name, whatever it may be) by default, or the user's markup to specify a
 *         title that is used instead. Also in this example, if neither a parent data-title nor
 *         entity.name yields a value, the literal string "Unknown" is set on the element.
 *
 *     <img data-bind="<src>entity.attributes.imageurl;#images/placeholder.jpg"></img>
 *         The target of binding for this element is to its src attribute rather than the element's inner HTML.
 *         All other semantics are identical.
 *
 *     <ul id="somelist"><li data-bind="entity.attributes"></li></ul> ???
 *         In this case, it is presumed that the expression entity.attributes returns an array
 *         of results. The <li> element will be cloned for each element in the result array.
 *
 */
Widget.prototype.bindValues = function() {
    var self = this;

    $('[data-bind]', this.elem).each( function( /* ix */ ) {
        // Note: 'this' is now the specific element matched
        let refstr = $(this).attr('data-bind');

        /* Binding to attribute or content? Determine target. */
        let target = false; // assume target will be innerHTML of our element
        const m = refstr.match( /^<([^>]+)>(.*)/ );
        if ( m ) {
            target = m[1];
            refstr = m[2];
        }
        self.log.debug( 8, "bindValues expr %1 target %2", refstr, target);

        let value;
        try {
            self.log.debug( 8, "bindValues attempting resolve of %1 with context %2", refstr, self._ctx);
            value = self.resolve( refstr, self._ctx );
        } catch(e) {
            self.log.debug(Logger.ERR, "Expression %1 threw an error during evaluation: %2",
                refstr, e);
        }

        self.log.debug(9, "binding value for %1 to value %2 (%3) on %4", refstr, value, typeof value, target ? target : "content" );

        /* If value is an array, iterate over the array, repeating the containing
         * element. Otherwise, simply sub the text of the existing containing element.
         * First, remove all previously-cloned (if any) elements from prior updates.
         * The original from the layout is left (it is not given the "cloned" class. */
        $('.slapdash-cloned', $(this).parent()).remove();
        if ( Array.isArray(value) ) {
            // Apply the first array element to the original content element.
            $(this).text( value[0] ).attr( 'id', '1' );

            // Now iterate over the array remainder and insert new copies of the content element, with the correct value.
            let last = this;
            const nvals = value.length;
            for (let ix=1; ix<nvals; ++ix) {
                const newElement = $(this).clone().removeAttr('data-bind')
                    .addClass( 'slapdash-cloned' ).attr( 'id', ix+1 );
                if (target === false) {
                    $( newElement ).text( value[ix] );
                } else {
                    $( newElement ).attr( target, value[ix] );
                }
                $(newElement).insertAfter( last );
                last = newElement;
            }
        } else {
            if ( ! target ) {
                self.log.debug( 7, "%1(Widget).resolve() resolved %2 to %3, setting element text", self.toString(), refstr, value );
                $(this).text( value );
            } else if ( "class" === target ) {
                self.log.debug( 7, "%1(Widget).resolve() resolved %2 to %3, adding as class", self.toString(), refstr, value );
                $(this).addClass( value );
            } else {
                self.log.debug( 7, "%1(Widget).resolve() resolved %2 to %3, setting as attr %4", self.toString(), refstr, value, target );
                $(this).attr( target, value );
            }
        }
    });
};

util.mixin( Widget, Observer );
util.mixin( Widget, Observable );

export default Widget;
