//# sourceURL=SlapDash.js
// This file is part of Reactor. Copyright (c) 2020-2026 Patrick H. Rigney, All Rights Reserved.
/* globals require */

import api from "/client/ClientAPI.js";
import * as util from '/client/util.js';
import { getAuthInfo } from "/reactor/en-US/lib/js/reactor-ui-common.js";
import Logger from './Logger.js';
// import * as cf from './config.js';

const version = 26158;

const SlapDash = (function() {
    'use strict';

    var cf = {};
    var cfPromise = false;
    var logger = Logger.getLogger("SlapDash");

    var fwrap = function( name, path, cbf ) {
        return function( data, status, jqxhr ) {
            logger.debug( 4, 'require %1', path );
            require( [ path ], cbf );
        };
    };

    var loadWidget = function( name, callback ) {
        try {
            $.ajax({
                url: '/dashboard/slapdash/widgets/' + name + '.js',
                type: 'HEAD'
            })
            .done( function() {
                require( [ 'widgets/' + name ], callback );
            })
            .fail( function() {
                require( [ 'local/widgets/' + name ], callback );
            });
        } catch (e) {
            logger.err( "Error while attempting to load widget %1: %2", name, e );
        }
    };

    var registerWidget = function( elem ) {
        var typ = $(elem).attr('data-view');
        var i = $(elem).data('sl_po_inst');
        if (i !== undefined) return; // already registered

        logger.debug( 4, 'Loading widget %1 for %2', typ, elem );
        import( './widgets/' + typ + '.js' ).then( function( mod ) {
            /* Create a new instance of our widget, pass it the DOM element it will be working with */
            let widgetClass = mod.default;
            try {
                var inst = new widgetClass( $( elem ) );
                /* Leave a handle on the document element back to our new Widget instance */
                $( elem ).data("sl_po_inst", inst);
                /* We handle clicks on Widgets */
                $( elem ).on( 'click', handleWidgetClick );
                inst._start();
            } catch (e) {
                logger.err( "Failed to instantiate or start %1 widget in %2: %3", typ, elem, e );
                logger.err(e.stack);
                $(elem).addClass("widget-failed nodata").removeClass("widget");
                console.log(e);
                // debugger;
            }
        }).catch( function( e ) {
            logger.err( "Can't load Widget subclass " + typ );
            logger.err( e.stack );
            $(elem).addClass('widget-failed nodata');
            console.log(e);
        });
    };

    var clearWidgets = function( $gs ) {
        $( 'ul', $gs ).data('gridster').destroy();

        /* ??? probably leaking memory here, as existing widgets are still linked
         * to subscriptions that will keep them from being garbage-collected. */
        /* For all *attempted* widgets... */
        $( 'div[data-view]', $gs ).each( function() {
            var inst = $(this).data( 'sl_po_inst' );
            console.log($(this), String(inst));
            if ( inst ) {
                /* It got widgeted, so close the widget */
                try {
                    console.log("SlapDash.clearWidgets closing", inst.toString());
                    inst.close();
                } catch( e ) {
                    console.log(e);
                }
            }
        });
        return $gs.empty();
    };

    var flashObjects = function() {
        $(".flash").each( function( ix ) {
            if ($(this).hasClass( "flashoff" )) {
                $(this).show().removeClass( "flashoff" );
            } else {
                $(this).hide().addClass( "flashoff" );
            }
        });

        //setTimeout( flashObjects, 1000 );
    };

    var handleAutoClick = function( event ) {
        var $el = $( event.currentTarget );
        var $gs = $el.closest( 'div.dashboard-auto' );
        var eid = $el.data( 'entity' ) || "";
        if ( eid ) {
            var e = api.getEntity( eid );
            console.log("SlapDash: handling auto-click on ",e);
            if ( "Group" === e.getType() ) {
                event.preventDefault();
                event.stopPropagation();
                let unwind = $gs.data( 'unwind' ) || $gs.attr( 'data-unwind' ) || "";
                let loc = $gs.data( 'panel' ) || "";
                if ( "" === loc ) {
                    loc = $gs.data( 'group' ) || "";
                    if ( "" !== loc ) {
                        loc = 'Group=' + loc;
                    } else {
                        loc = "";
                    }
                } else {
                    loc = 'Panel=' + loc;
                }
                unwind = unwind.split( /;/ );
                if ( loc != "" || unwind != "" ) {
                    unwind.unshift( loc );
                }
                clearWidgets( $gs )
                    .attr( 'data-group', e.getCanonicalID() ).data( 'group', e.getCanonicalID() )
                    .attr( 'data-panel', '' ).data( 'panel', '' )
                    .attr( 'showback', 'true' ).data( 'showback', true )
                    .attr( 'data-unwind', unwind.join(';') ).data( 'unwind', unwind.join(';') );
                start( $gs );
                return false;
            }
        } else if ( $el.hasClass( 'widget-text' ) || $el.hasClass( 'widget-image' ) ) {
            console.log("Handling back?");
            if ( "" !== ( $el.data('link') || "" ) ) {
                event.preventDefault();
                event.stopPropagation();
                clearWidgets( $gs );
                window.location.href = $el.data('link');
                return false;
            }
            let unwind = $gs.data( 'unwind' ) || "";
            console.log("unwind", unwind);
            if ( "" !== unwind ) {
                unwind = unwind.split( /;/ );
                let loc = unwind.shift();
                clearWidgets( $gs ).data( 'unwind', unwind.join(';') ).attr( 'data-unwind', unwind.join(';') );
                let m = loc.split( /=/ );
                if ( "Panel" === m[0] ) {
                    $gs.data( 'panel', m[1] ).attr( 'data-panel', m[1] )
                        .data( 'group', "" ).attr( 'data-group', "" );
                } else {
                    $gs.data( 'group', m[1] ).attr( 'data-group', m[1] )
                        .data( 'panel', "" ).attr( 'data-panel', "" );
                }
            } else {
                clearWidgets( $gs )
                    .data( 'group', "" ).attr( 'data-group', "" )
                    .data( 'panel', "" ).attr( 'data-panel', "" );
            }
            start( $gs );
            return;
        }
        console.log("SlapDash.handleAutoClick() passing on click", event);
    };

    /* ??? TODO: Pagination. Like, next. */
    var handleAuto = function( $ct ) {
        let $ul = $ct.children( 'ul:first' );
        let panel = $ct.attr( 'data-panel' ) || $ct.data( 'panel' ) || "";
        if ( "" === panel ) {
            /* Group? */
            let group = $ct.attr( 'data-group' ) || $ct.data( 'group' ) || "";
            if ( "" === group ) {
                /* Show all groups */
                let elist = api.getEntities();
                let groups = [];
                elist.forEach( (e) => {
                    /* ??? filter from data-filter expression? */
                    if ( "Group" === e.getType() && e.getAttribute( 'sys_group.visible', true ) ) {
                        groups.push( e );
                    }
                });
                /* Sort backwards, because the way we have to insert gridster cells is at
                 * the head (so last in is first displayed) */
                groups.sort( function( a, b ) {
                    let n1 = a.getName();
                    let n2 = b.getName();
                    if ( n1 === n2 ) {
                        return 0;
                    }
                    return n1 > n2 ? -1 : 1;
                });
                groups.forEach( (e) => {
                    let cid = e.getCanonicalID();
                    let name = e.getName();
                    const $item = $( '<li></li>' )
                        .addClass( 'color0' )
                        .data( { col: 1, row: 1, sizex: 1, sizey: 1 } )
                        .attr( { "data-col": 1, "data-row": 1, "data-sizex": 1, "data-sizey": 1 } )
                        .appendTo( $ul );
                    $( '<div></div>' )
                        .data( { view: 'Text', entity: cid, title: name, layout: 'bi-icon', icon: 'bi-grid-3x3-gap', showback: true } )
                        .attr( { 'data-view': 'Text', 'data-entity': cid, 'data-title': name, 'data-layout': 'bi-icon',
                            'data-icon': 'bi-grid-3x3-gap', 'showback': true } )
                        .on( 'click.dashauto', handleAutoClick )
                        .appendTo( $item );
                });
            } else {
                let e = api.getEntity( group );
                let elist = e.listMembers();
                let targets = [];
                elist.forEach( (mem) => {
                    let mem_e = api.getEntity( mem );
                    if ( mem_e ) {
                        /* ??? filter */
                        targets.push( mem_e );
                    }
                });
                targets.sort( ( a, b ) => {
                    let n1 = a.getName();
                    let n2 = b.getName();
                    if ( n1 === n2 ) {
                        return 0;
                    }
                    return n1 > n2 ? -1 : 1;
                });
                if ( $ct.data('showback') ) {
                    const $item = $( '<li></li>' );
                    $item.addClass( 'color2' )
                        .data( { col: 1, row: 1, sizex: 1, sizey: 1 } )
                        .attr( { "data-col": 1, "data-row": 1, "data-sizex": 1, "data-sizey": 1 } )
                        .appendTo( $ul );
                    $( '<div></div>' )
                        .data( { view: 'Text', title: 'Back', layout: 'bi-icon', icon: 'bi-back' } )
                        .attr( { "data-view": "Text", "data-title": "Back", "data-layout": "bi-icon", "data-icon": "bi-back" } )
                        .on( 'click.dashauto', handleAutoClick )
                        .appendTo( $item );
                }
                for ( const mem_e of targets ) {
                    const cid = mem_e.getCanonicalID();
                    const name = mem_e.getName();
                    const caps = mem_e.getCapabilities();
                    // mem_e.getType()
                    // mem_e.getPrimaryAttribute()
                    console.log( "Examining", mem_e, "capabilities", caps );
                    for ( const cap of caps ) {
                        const $item = $( '<li></li>' )
                            .addClass( 'color0' )
                            .data( { col: 1, row: 1, sizex: 1, sizey: 1 } )
                            .attr( { "data-col": 1, "data-row": 1, "data-sizex": 1, "data-sizey": 1 } )
                        const $widg = $( '<div></div>' )
                            .data( { entity: cid, title: name } )
                            .attr( { "data-entity": cid, "data-title": name } )
                            .on( 'click.dashauto', handleAutoClick )
                            .appendTo( $item );

                        switch ( cap ) {
                            case 'lock':
                                $widg.data( { view: 'Level', layout: 'lock' } )
                                    .attr( { "data-view": "Level", "data-layout": "lock" } );
                                break;
                            case "power_switch":
                                if ( mem_e.hasCapability( 'dimming' ) ) continue;
                                $widg.data( { view: 'Level', layout: 'switch-icon' } )
                                    .attr( { 'data-view': 'Level', 'data-layout': 'switch-icon' } );
                                break;
                            case "dimming":
                                $widg.data( { view: 'Level', layout: 'step' } )
                                    .attr( { "data-view": "Level", "data-layout": "step" } );
                                break;
                           case "volume":
                                {
                                    const attrs = {
                                        'scale': 100,
                                        'round': 0,
                                        'level-expr': 'entity.attributes.volume.level',
                                        'change-action': 'volume.set',
                                        'change-action-param': 'value',
                                        'prec': 1,
                                        'step': 0.1,
                                    };
                                    $widg.data( { view: 'Level', layout: "updown", subtitle: "Volume", ...attrs } )
                                        .attr( { "data-view": "Level", "data-layout": "updown", ...attrs } );
                                }
                                break;
                            case "cover":
                                $widg.data( { view: 'Cover' } ).attr( 'data-view', 'Cover' );
                                break;
                            case "valve":
                                $widg.data( { view: 'Level', layout: 'valve' } )
                                   .attr( { 'data-view': 'Level', 'data-layout': 'valve' } );
                                break;
                            case "binary_sensor":
                                if ( mem_e.hasCapability( 'door_sensor' )
                                    || mem_e.hasCapability( 'window_sensor' )
                                    || mem_e.hasCapability( 'motion_sensor' )
                                    || mem_e.hasCapability( 'leak_detector' )
                                    || mem_e.hasCapability( 'glass_break_detector' )
                                    || mem_e.hasCapability( 'security_zone' )
                                    ) continue;  // skip binary_sensor likely duplicate/redundant status
                                $widg.data( { view: 'Sensor', layout: 'binary-check' } )
                                    .attr( { 'data-view': 'Sensor', 'data-layout': 'binary-check' } );
                                break;
                            case "motion_sensor":
                            case "window_sensor":
                            case "glass_break_detector":
                            case "leak_detector":
                                $widg.data( { view: 'Sensor', layout: 'binary-check-security' } )
                                    .attr( { 'data-view': 'Sensor', 'data-layout': 'binary-check-security' } );
                                break;
                            case "door_sensor":
                                {
                                    const attrs = {
                                        active: "entity.attributes.door_sensor.state",
                                        'icon-active': `"\\f308"`,
                                        'icon-inactive': `"\\f306"`,
                                    };
                                    $widg.data( { view: 'Sensor', layout: 'binary-icon', ...attrs } )
                                        .attr( { 'data-view': 'Sensor', 'data-layout': 'binary-icon', ...attrs } );
                                }
                                break;
                            case "value_sensor":
                            case "temperature_sensor":
                            case "humidity_sensor":
                                {
                                    const attrs = { primary: `${cap}.value`, scale: 1, round: 1, subtitle: cap };
                                    if ( mem_e.hasAttribute( `${cap}.units` ) ) {
                                        attrs.units = mem_e.getAttribute( `${cap}.units`, "" );
                                    }
                                    $widg.data( { view: 'Sensor', layout: "primary-large", ...attrs } )
                                        .attr( { "data-view": "Sensor", "data-layout": "primary-large", ...attrs } );
                                }
                                break;
                            case "security_zone":
                                {
                                    const attrs = {
                                        primary: "security_zone.state",
                                        active: "entity.attributes.security_zone.state !== 'normal'"
                                    };
                                    $widg.data( { view: 'Sensor', layout: "binary-check-security", ...attrs } )
                                        .attr( { "data-view": "Sensor", "data-layout": "binary-check-security", ...attrs } );
                                }
                                break;
                            case "string_sensor":
                                {
                                    const attrs = { primary: "string_sensor.value" };
                                    $widg.data( { view: 'Sensor', layout: "primary-elastic", ...attrs } )
                                        .attr( { "data-view": "Sensor", "data-layout": "primary-elastic", ...attrs } );
                                }
                                break;
                            case "battery_power":
                                {
                                    const attrs = { primary: `${cap}.level`, units: "%", scale: 100, round: 0, subtitle: "Battery" };
                                    $widg.data( { view: 'Sensor', layout: "primary-large", ...attrs } )
                                        .attr( { "data-view": "Sensor", "data-layout": "primary-large", ...attrs } );
                                }
                                break;
                            case "volume":
                                {
                                    const attrs = { primary: `${cap}.level`, units: "%", scale: 100, round: 0 };
                                    $widg.data( { view: 'Sensor', layout: "primary-large", ...attrs } )
                                        .attr( { "data-view": "Sensor", "data-layout": "primary-large", ...attrs } );
                                }
                                break;
                            case "button":
                                $widg.data( { view: 'Button' } ).attr( { 'data-view': 'Button' } );
                                break;
                            case "script":
                                $widg.data( { view: 'Scene', layout: 'bi-icon' } )
                                    .attr( { 'data-view': 'Scene', 'data-layout': 'bi-icon' } );
                                break;
                            case "media_player":
                                $widg.data( { view: 'MediaPlayer' } ).attr( 'data-view', 'MediaPlayer' );
                                break;
                            case "wx":
                                $widg.data( { view: 'CurrentWeather' } ).attr( 'data-view', 'CurrentWeather' );
                                break;
                            case "hvac_cooling_unit":
                                $widg.data( {
                                    view: 'Level',
                                    layout: 'updown-horizontal',
                                    subtitle: 'Cooling',
                                    'level-expr': 'entity.attributes.hvac_cooling_unit.setpoint',
                                    'step': 1,
                                    'change-action': "hvac_cooling_unit.set_setpoint",
                                    'change-action-param': "setpoint"
                                } ).attr( {
                                    "data-view": "Level",
                                    "data-layout": "updown",
                                    'data-level-expr': 'entity.attributes.hvac_cooling_unit.setpoint',
                                    'data-change-action': "hvac_cooling_unit.set_setpoint",
                                    'data-change-action-param': "setpoint"
                                } );
                                break;
                            case "hvac_heating_unit":
                                $widg.data( {
                                    view: 'Level',
                                    layout: 'updown-horizontal',
                                    subtitle: 'Heating',
                                    'level-expr': 'entity.attributes.hvac_heating_unit.setpoint',
                                    'step': 1,
                                    'change-action': "hvac_heating_unit.set_setpoint",
                                    'change-action-param': "setpoint"
                                } ).attr( {
                                    "data-view": "Level",
                                    "data-layout": "updown",
                                    'data-level-expr': 'entity.attributes.hvac_heating_unit.setpoint',
                                    'data-change-action': "hvac_heating_unit.set_setpoint",
                                    'data-change-action-param': "setpoint"
                                } );
                                break;
                            default:
                                // ignore this capability
                                continue;
                        }

                        $item.appendTo( $ul );
                        console.log( "Added widget for", mem_e, "capability", cap );
                    }  // cap of caps
                } // mem_e of targets
            }
        } else {
            $ct.append("<p>panel TBD!</p>");
        }
    };

    var measureScroll = function() {
        var $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
        var dim = {
            width: $c.width() - $c[0].clientWidth,
            height: $c.height() - $c[0].clientHeight
        };
        $c.remove();
        return dim;
    };

    var start = function( gs ) {
        // Special care to suppress click to widget when dragging. See https://stackoverflow.com/a/25431791 (with thanks to Alastair Maw)
        gs.addClass( "gridster" );
        var $grid = gs.children( 'ul' );
        if ( 0 === $grid.length ) {
            $grid = $( '<ul></ul>' );
            $grid.appendTo( gs );
        }

        const auth = getAuthInfo( "dashboard" );
        api.off( 'auth_fail.dashboard' ).on( 'auth_fail.dashboard', () => {
            console.log("LOST AUTH");
            window.location.href = "/reactor/login.html?redir=" + encodeURIComponent( window.location.pathname );
        });
        api.start( { authorization: auth?.token } ).then( function() {
            console.log("SlapDash: Reactor wsapi connected");

            if ( gs.hasClass( 'dashboard-auto' ) ) {
                handleAuto( gs );
            }

            // Set up our widgets. Look for div elements that define a view. The view is the Widget type (subclass of Widget).
            var $widgets = $( 'div[data-view]', $grid );

            /* Register widgets */
            $widgets.each( function( ix ) {
                // console.log('RegisterWidgets found ' + this + ' with data-view attribute, registering as widget...' );
                registerWidget( this );
            });

            /* If auto, try to guess pleasant layout. Do this after registering widgets, so we get accurate measurements
             * as presented.
             */
            var colw, rowh;
            var ncol = gs.data('cols') || getConfig( "auto.default_columns", 5 );        // number of grid columns
            var nrow = gs.data('rows') || getConfig( "auto.default_rows", 3 );        // number of grid rows
            var w = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
            var h = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
            if ( h > w ) {
                /* Rotate */
                var temp = ncol;
                ncol = nrow;
                nrow = temp;
            }
            var spacing = gs.data('spacing') || getConfig( "auto.default_spacing", 3 );  // margins (pixels)
            if ( gs.hasClass( 'dashboard-autosize' ) ) {
                // alert( w + " x " + h + "; " + navigator.userAgent);
                var nwidg = $widgets.length;
                var dpi_x = 96, dpi_y = 96;
                if ( false ) {
                    /* Crashes mobile browsers??? */
                    try {
                        var $tel = $( '<div id="testdiv" style="height: 1in; width: 1in;"></div>' ).appendTo( 'body' );
                        var devicePixelRatio = window.devicePixelRatio || 1;
                        dpi_x = document.getElementById('testdiv').offsetWidth * devicePixelRatio;
                        dpi_y = document.getElementById('testdiv').offsetHeight * devicePixelRatio;
                        $tel.remove();
                    } catch ( err ) {
                        console.log( err );
                    }
                    // alert(dpi_x + " x " + dpi_y);
                }
                var count = 9;
                while ( count-- > 0 ) {
                    colw = Math.floor((w - spacing*(ncol+1)) / ncol);
                    if ( colw < dpi_x ) {
                        ncol -= 1;
                        continue;
                    } else if ( colw > 3*dpi_x ) {
                        ncol += 1;
                        continue;
                    }
                    nrow = Math.floor(h / colw);
                    console.log("widgets",nwidg,"ncol",ncol,"nrow",nrow);
                    if ( nwidg > ( ncol * nrow ) ) {
                        /* There will be scroll bars. */
                        var dim = measureScroll();
                        colw = Math.floor(( w - dim.width - spacing*(ncol+1)) / ncol );
                        rowh = colw;
                        nrow = Math.round(h / rowh);
                        break;
                    } else {
                        rowh = Math.floor((h - spacing*(nrow+1)) / nrow);
                        break;
                    }
                }
                console.log("Auto col,row",ncol,nrow,colw,rowh,"of",w,h);
            } else {
                colw = Math.floor((w - spacing*(ncol*2)) / ncol);
                rowh = Math.floor((h - spacing*(nrow*2)) / nrow);
                console.log("cols="+ncol+", rows="+nrow+", colw="+colw+", rowh="+rowh);
                console.log("w=" + w + ", h = " + h);

            }

            // Adjust document default font size to frac of cell height. All styles should use units relative to this (e.g. rem).
            var lows = rowh < colw ? rowh : colw;
            // $('html').css( 'font-size', Math.floor(lows / 16) + "px" );
            $('html').css( 'font-size', Math.floor( lows / 10.0 ) + 'px' );

            /* Start gridster */
            var preventClick = function( e ) { e.stopPropagation(); e.preventDefault(); };
            (function( colw, rowh, ncol, nrow, spacing ) {
                var moar = Math.ceil( $widgets.length / ncol );
                console.log( "Widgets:",$widgets.length,"rows", moar );
                console.log("Auto col,row",ncol,nrow,colw,rowh,"of",w,h);
                $grid.gridster({
                    widget_selector: "li",
                    widget_margins: [spacing, spacing],
                    widget_base_dimensions: [colw, rowh],
                    min_cols: ncol,
                    max_cols: ncol,
                    min_rows: nrow,
                    max_rows: Math.max( nrow, moar ),
                    extra_rows: Math.max( 0, moar - nrow ),
                    draggable: {
                        start: function( e, ui ) {
                            ui.$player[0].addEventListener('click', preventClick, true);
                        },
                        stop: function( e, ui ) {
                            var player = ui.$player;
                            setTimeout( function() { player[0].removeEventListener('click', preventClick, true); }, 0 ); // AM didn't list time in his timeout; assuming UI thread ready time
                        }
                    },
                    resize: {
                        enabled: false,
                        max_size: [ncol, nrow],
                        stop: function( e, ui, $widget ) {
                            // $widget is the jQuery listitem, find our div and dispatch
                            var target = $widget.find('div.widget');
                            var inst = $(target).data('sl_po_inst');
                            // console.log('gridster resize stop, inst is ' + inst);
                            if ( inst ) {
                                inst.resized();
                            }
                        }
                    }
                }).data('gridster').disable(); // prevent dragging (use data to get actual gridster object for API access);
            })( colw, rowh, ncol, nrow, spacing );

            //setTimeout(flashObjects, 1000 );
        });

        if (document.addEventListener) {
            document.addEventListener("fullscreenchange", function() {
                // alert("fs");
            }, false);
            document.addEventListener("webkitfullscreenchange", function() {
                // alert("mozfs");
                return true;
            }, false);
        }
    };

    var handleWidgetClick = function( event ) {
        var target = $( event.target );
        // util.debug( 4, "click landed on", event.target.nodeName);

        // Find parent widget. Get our wrapper object for it, and run its click method, if (1) not disabled, (2) not dragging, (3) has click()
        target = $(target).closest('.widget');
        if (target.length > 0 && !$(target).hasClass( "disabled" ) ) {
            // util.debug( 3, "handle the click for ", $(target).get(0).nodeName );
            var listItem = $(target).closest('li.gs-w');
            if ( $(listItem).hasClass('dragging') ) {
                // util.debug( 4, 'You are being dragged, no clicks for you!' );
                return true;
            }

            // find the object
            var inst = $(target).data("sl_po_inst");
            // util.assert( inst instanceof Widget );
            // util.debug( 4, "Click target Widget is " + inst );

            // see if it implements the click() method (it should; console message if it doesn't, as any .clickable should implement click()
            if (inst && typeof inst.click === "function") {
                // run the click method
                // util.debug( 4, 'Dispatching click to ' + inst );
                inst.click( event );
            } // else util.debug( 4, "Doesn't click()");
        } else {
            // util.debug( 4, event.target.nodeName,  "got clicked, but we don't care (not in an enabled Widget subclass)." );
        }
    };

    var lookupCache = {};

    var findEntity = function( eid ) {
        if ( lookupCache[eid] ) {
            return lookupCache[eid];
        }
        if ( !api.isConnected() ) {
            return false;
        }
        let id, eclass;
        let m = eid.match( /^([^=]+)=(.*)/ );
        if ( m && m.length > 2 ) {
            id = m[2];
            eclass = m[1];
        } else {
            eclass = false;
            id = eid;
        }
        /* Easy way? */
        let e = api.getEntity( id );
        if ( e && ( !eclass || eclass === e.type ) ) {
            lookupCache[eid] = e;
            return e;
        }
        /* Hard way. */
        let el = api.getEntities();
        let nel = el.length;
        let leid = id.toLowerCase();
        for ( let k=0; k<nel; ++k ) {
            if ( eclass && el[k].getType() !== eclass ) {
                continue;
            }
            if ( el[k].getID() === id ) {
                lookupCache[eid] = el[k];
                return el[k];
            }
            if ( el[k].getName().toLowerCase() == leid ) {
                lookupCache[eid] = el[k];
                return el[k];
            }
        }
        console.log("SlapDash.promiseEntity() ", eid, " not found yet");
        return false;
    };

    var promiseEntity = function( eid, timeoutms ) {
        var e = this.findEntity( eid );
        if ( !e ) {
            return Promise.reject( "Entity not found: " + eid );
        }
        return Promise.resolve( e );
    };

    var toggleFullscreen = function() {
        var doc = window.document;
        var docEl = doc.documentElement;
        var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
        var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
        if (!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
            console.log("Requesting full screen...");
            requestFullScreen.call(docEl);
        } else {
            cancelFullScreen.call(doc);
        }
    };

    var cameraImageError = function( source ) {
        // source.onerror = ""; // don't do this, prevents future handling when camera attempts refreshes
        if ( source ) {
            if ( source.parentNode ) {
                source.parentNode.style.backgroundImage = "url('images/nocamera.png')";
            }
            source.src = "images/nocamera.png";
        }
        return true;
    };

    var getConfig = function( name, dflt ) {
        var res = cf;
        var a = name.split( '.' );
        while ( a.length > 0 ) {
            var n = a.shift();
            res = res[n];
            if ("undefined" === typeof res || null === res) {
                return dflt;
            }
        }
        return res;
    };

    var getAllConfig = function() {
        /* Load configuration */
        if ( !cfPromise ) {
            cfPromise = new Promise( function(resolve,reject) {
                $.ajax({
                    url: "/dashboard/config/dashboard.json",
                    method: "GET",
                    timeout: 10000,
                    dataType: "json"
                }).done( function( data ) {
                    console.log("SlapDash: configuration loaded");
                    Object.keys( data ).forEach( function( k ) {
                        cf[k] = data[k];
                    });
                    resolve( cf );
                }).fail( function( err ) {
                    console.warn("Slapdash: configuration could not be loaded; proceeding with default config");
                    console.log("default config", cf);
                    reject();
                });
            });
        }
        return cfPromise;
    };

    // Configuration defaults
    cf.baseurl = cf.baseurl || window.location.href.replace(/\/dashboard\/.*$/, "/dashboard/");

    // Public Methods and Properties
    return {
        config: cf,
        getConfig: getConfig,
        getAllConfig: getAllConfig,
        toggleFullscreen: toggleFullscreen,
        start: start,
        promiseEntity: promiseEntity,
        findEntity: findEntity,
        cameraImageError: cameraImageError,
        getClientAPI: function() { return api; }
    };
})();

export default SlapDash;
