var lensUIUtils = {

    _pageURL: null,

    setPageUrl: function setPageUrl(url) {
        this._pageURL = url;
        // store the URL we use to redirect to the lenses page
    },

    showFindWhichDisplayByCol: function showFindWhichDisplayByCol() {
        const logHeader = 'lensUIUtils.showFindWhichDisplayByCol: ';
        log(logHeader + 'enter');

        const theElem = $('#divColFindWhichGroupBy');

        theElem.addClass("column");
        theElem.addClass("col-auto");

        theElem.css('display', '');
        // show the column

        log(logHeader + 'leave');
    },

    hideFindWhichDisplayByCol: function hideFindWhichDisplayByCol() {
        const logHeader = 'lensUIUtils.hideFindWhichDisplayByCol: ';
        log(logHeader + 'enter');

        const theElem = $('#divColFindWhichGroupBy');

        theElem.removeClass("column");
        theElem.removeClass("col-auto");

        theElem.css('display', 'none');
        // hide the column

        log(logHeader + 'leave');
    },

    showHideFindWhichDisplayByColAsNec: function showHideFindWhichDisplayByColAsNec() {
        const logHeader = 'lensUIUtils.showHideFindWhichDisplayByColAsNec: ';
        log(logHeader + 'enter');

        const findWhichVisible = findWhichGroupUtils.isFindWhichVisible();
        // is the "find which" group visible?

        log(logHeader + 'findWhichVisible=' + findWhichVisible);

        if (findWhichVisible) {
            // if so

            this.showFindWhichDisplayByCol();
            // show the column

            log(logHeader + 'leave');
            return;
        }

        const displayByVisible = displayByUtils.isDisplayByVisible();
        // is the "display by" group visible?

        log(logHeader + 'displayByVisible=' + displayByVisible);

        if (displayByVisible) {
            // if so

            this.showFindWhichDisplayByCol();
            // show the column

            log(logHeader + 'leave');
            return;
        }

        this.hideFindWhichDisplayByCol();
        // hide the column

        log(logHeader + 'leave');
    },

    _handleVideoClick: function _handleVideoClick(evt) {
        const logHdr = 'lensUIUtils._handleVideoClick: ';
        log(logHdr + 'enter');

        //const videoElem = evt.target;
        //// get the video element

        //const paused = videoElem.paused;
        //// is the video paused?

        //log(logHdr + 'paused=' + paused);

        //if (paused) {

        //    log(logHdr + 'playing the video');

        //    videoElem.play();
        //    // if the video is paused, play it
        //}
        //else {
        //    log('pausing the video');

        //    videoElem.pause();
        //    // if the video is playing, pause it
        //}

        log(logHdr + 'leave');
    },

    _getVideoElemSize: function _getVideoElemSize(viewWidth, viewHeight) {
        const logHdr = 'lensUIUtils._getVideoElemSize: ';
        log(logHdr + 'enter');
        log(logHdr + 'viewWidth=' + viewWidth);
        log(logHdr + 'viewHeight=' + viewHeight);

        var sizeObj = {};

        var elemWidth = 260;
        var elemHeight = 146;
        // default to smallest size

        if (viewWidth > 320) {
            elemWidth = 300;
            elemHeight = 168;
        }

        if (viewWidth > 425) {

            if (viewHeight > 414) {
                // if the view is taller than iPhone 6 in landscape mode

                elemWidth = 565;
                elemHeight = 317;
            }
        }

        sizeObj.width = elemWidth;
        sizeObj.height = elemHeight;

        log(logHdr + 'elemWidth=' + elemWidth);
        log(logHdr + 'elemHeight=' + elemHeight);
        log(logHdr + 'leave');

        return sizeObj;
    },

    _addVideoElemToTabIfNecAsync: async function _addVideoElemToTabIfNecAsync(lensId) {
        const logHdr = 'lensUIUtils._addVideoElemToTabIfNecAsync: ';
        log(logHdr + 'enter');
        log(logHdr + 'lensId=' + lensId);

        const videoType = videoUtils.getVideoType(lensId);
        // what is the video type for this lens?

        log(logHdr + 'videoType=' + videoType);

        var targetId = '';

        if (videoType === videoUtils.VIDEO_TYPE_BLOB) {
            // if the video for the lens is a video element

            targetId = elemIdUtils.getVideoElemId(lensId);
            // get the id of the video element
        }
        else {
            // if the video for the lens is an iFrame

            targetId = elemIdUtils.getVideoIFrameParentElemId(lensId);
            // get the id of the div parent of the video iFrame
        }

        log(logHdr + 'targetId=' + targetId);

        const videoSelector = '#' + targetId;

        const vidCount = $(videoSelector).length;
        // how many elements for the lens are on the page?

        log(logHdr + 'vidCount=' + vidCount);

        if (vidCount > 0) {
            // if there is already one on the page
            log(logHdr + 'there is already a video element on the page for this lens so we are done');
            log(logHdr + 'leave');
            return;
        }

        // if we need to add the video element

        log(logHdr + 'we need to add the video element');

        const videoElemSize = lensUIUtils._getVideoElemSize(viewWidth, viewHeight);
        // get the desired size of the video element given the view dimensions

        const videoElemWidth = videoElemSize.width;
        const videoElemHeight = videoElemSize.height;

        log(logHdr + 'viewWidth=' + viewWidth);
        log(logHdr + 'viewHeight=' + viewHeight);
        log(logHdr + 'video element size is ' + videoElemWidth + ' x ' + videoElemHeight);

        const vId = elemIdUtils.getElemId(lensId, elemIdUtils.TAB_PANE_VIDEO_PREFIX, undefined);
        // get the id of the element to which we will append the video HTML
        // undefined => there is only one of these elements for each lens

        log(logHdr + 'vId=' + vId);

        var videoHtml = '';

        if (videoType === videoUtils.VIDEO_TYPE_BLOB) {
            // if the video for the lens is a video element

            const blubUrl = videoUtils.getVideoUrl(lensId);
            // get the URL of the video blob

            videoHtml = videoUtils.getVideoHtml_video(lensId, videoElemWidth, videoElemHeight, blubUrl);
            // get the html for the video element

            log('videoHtml=' + videoHtml);

            $('#' + vId).append(videoHtml);
            // add the HTML for the video element to the page

            const videoElemId = elemIdUtils.getVideoElemId(lensId);
            // get the id of the video element

            lensUIUtils.subscribeToEventsForVideoElem(videoElemId);
            // subscribe to the events for the video element
        }
        else {
            // if the video for the lens is an iFrame

            const iFrameSourceUrl = videoUtils.getIFrameUrl(lensId);
            // get the URL for the iFrame source

            log(logHdr + 'iFrameSourceUrl=' + iFrameSourceUrl);

            videoHtml = videoUtils.getVideoHtml_iFrame(lensId, videoElemWidth, videoElemHeight, iFrameSourceUrl);
            // get the html for the iFrame element

            log(logHdr + 'videoHtml=' + videoHtml);

            $('#' + vId).append(videoHtml);
            // add the HTML for the iFrame element to the page

            await lensUIUtils.subscribeToEventsForIFrameVideoAsync(lensId);
            // subscribe to video events for the video in the iFrame
        }

        log(logHdr + 'leave');
    },

    _shouldShowCloseAlert: function _shouldShowCloseAlert() {
        const logHdr = 'lensUIUtils._shouldShowCloseAlert: ';
        log(logHdr + 'enter');

        const shownCount = userStateUtils.getHideAlertCount();
        // how many times have we shown the hide alert?

        if (shownCount < 2) {
            // if fewer than two

            log(logHdr + 'shownCount=' + shownCount + ', so we will return true');
            log(logHdr + 'leave');
            return true;
        }

        //const alertDate = userStateUtils.getHideAlertDate();
        //// get the date we last showed the "hide lens" alert
        //// may be undefined if we have not yet showed the alert

        //log('_shouldShowCloseAlert: alertDate=' + alertDate);

        //const shownToday = userStateUtils.isDateWithinHoursOfNow(alertDate, 24);.
        //// have we shown this alert in the past 24 hours?

        //log('_shouldShowCloseAlert: shownToday=' + shownToday);

        //const show = (!shownToday);
        //// if there are 25 lenses showing and we have NOT shown the alert today, show it

        const show = false;
        // otherwise, don't show it

        log(logHdr + 'show=' + show);
        log(logHdr + 'leave');

        return show;
    },

    _handleFullscreenChange: function _handleFullscreenChange(e) {
        const logHdr = 'lensUIUtils._handleFullscreenChange';
        log(logHdr + 'enter');

        const now = new Date();

        userStateUtils.setFullscreenDate(now);
        // store the date and time when a video was set to fullscreen

        log(logHdr + 'leave');
    },

    _handleVideoPlay: function _handleVideoPlay(e) {
        // called when the video plays

        const logHdr = 'lensUIUtils._handleVideoPlay: ';
        log(logHdr + 'enter');

        // NOTE: vides on the leses page use videoUtils, not userStateUtils to track
        // whether a video is playing

        const sender = this;
        // the object that fired the event for this handler

        const senderName = sender.constructor.name;
        // get the name of the type of object that called this handler

        log(logHdr + 'senderName=' + senderName);

        if (senderName !== "Player") {
            // if the sender is not a Vimeo player

            log(logHdr + 'the sender is NOT a Vimeo video player, so we are done');
            log(logHdr + 'leave');
            return;
        }

        log(logHdr + 'the sender is a Vimeo video player');

        const eventPlayer = sender;
        // the sender is a Vimeo video player
        
        const elemId = eventPlayer.element.id;
        // get the DOM element id of the Player object

        log(logHdr + 'event video elemId=' + elemId);

        const lensId = elemIdUtils.getLensIdFromVideoIFrameElemId(elemId);
        // get the lens id from the iFrame DOM element id

        log(logHdr + 'atc videoPlayers.stopPlayingVideosExcept');

        videoPlayers.stopPlayingVideosExcept(lensId);
        // stop any videos other than the event video (the one that fired this event)

        log(logHdr + 'bf videoPlayers.stopPlayingVideosExcept');

        videoPlayers.setPlayerPlaying(lensId, eventPlayer);
        // remember that this video is playing

        log(logHdr + 'leave');
    },

    _handleVideoStopped: function _handleVideoStopped(e) {
        // called when the video is stopped for any reason

        const logHdr = 'lensUIUtils._handleVideoStopped: ';
        log(logHdr + 'enter');

        const senderName = caller.constructor.name;
        // get the name of the type of object that called this handler

        log(logHdr + 'senderName=' + senderName);

        if (senderName === "Player") {
            // if it's a Vimeo player

            log(logHdr + 'the caller is a Vimeo video player');

            const elemId = eventPlayer.element.id;
            // get the DOM element id of the current player object

            log(logHdr + 'elemId=' + elemId);

            var lensId = elemIdUtils.getLensIdFromVideoIFrameElemId(elemId);
            // extract the lens id

            log(logHdr + 'lensId=' + lensId);

            log(logHdr + 'atc videoPlayers.setPlayerStopped');

            videoPlayers.setPlayerStopped(lensId);

            log(logHdr + 'bf videoPlayers.setPlayerStopped');
        }
        else {
            // otherwise

            log(logHdr + 'the caller is NOT a Vimeo video player');
        }

        //const now = new Date();
        //userStateUtils.setVideoStoppedDate(now);
        //// store the date and time the video was stopped
        //// we use this value as explained in the header comment for this function

        //userStateUtils.clearVideoPlayingFlag();
        //// clear the flag that indicates that a video is playing

        log(logHdr + 'leave');
    },

    subscribeToEventsForIFrameVideoAsync: async function subscribeToEventsForIFrameVideoAsync(lensId) {
        // subscribe to events for a single iFrame video element

        const logHdr = 'subscribeToEventsForIFrameVideoAsync: ';
        log(logHdr + 'enter');
        log(logHdr + 'lensId=' + lensId);

        const iFrameId = elemIdUtils.getVideoIFrameElemId(lensId);
        // get the id of the iFrame element

        log(logHdr + 'iFrameId=' + iFrameId);

        const iFrameJQ = $('#' + iFrameId);
        // get a jQuery object for the iFrame element

        const count = iFrameJQ.length;
        // how many did we find?

        log(logHdr + 'count=' + count);

        if (count < 1) {
            // if we didn't find any

            log(logHdr + 'we did NOT find the video iFrame element.  this may be an error.');
            log(logHdr + 'leave');
            return;
        }

        log(logHdr + 'found the video iFrame element');

        const iFrameObj = iFrameJQ[0];
        // get the iFrame DOM object from the jQuery object

        log(logHdr + 'read the DOM object from the jQuery object');

        const player = new Vimeo.Player(iFrameObj);
        // allocate a Vimeo player object for the iFrame

        log(logHdr + 'allocated the Vimeo player');

        videoPlayers.setPlayer(lensId, player);
        // store the player so we can use it later

        //player.getVideoTitle().then(function (title) { log(logHdr + 'videoTitle=' + title); }).catch(function (error) { log(logHdr + 'ERROR: ' + error.name); });
        //// get the title of the video, to make sure the player was allocated successfully

        //NOTE: the code for video elements subscribes to the following events that the Video player does NOT have
        // fullscreenchange
        // webkitfullscreenchange
        // mozfullscreenchange
        // msfullscreenchange
        // stalled (which we don't subscribe to for video elements)
        //     NOTE: at least on iOS, the "stalled" event gets sent fairly often while the video plays.
        //     more important, no event is sent when the video continues to play,
        //      so if we take the stalled event to mean that the video has stopped we will be incorrect.
        //     so for now we don't subscribe to the stalled event.
        // abort

        log(logHdr + 'about to subscribe to video events for the Vimeo player');

        player.on('play', lensUIUtils._handleVideoPlay);
        // call this method when the video plays

        player.on('ended', lensUIUtils._handleVideoStopped);
        player.on('error', lensUIUtils._handleVideoStopped);
        player.on('pause', lensUIUtils._handleVideoStopped);
        // subscribe to events where we consider the video not playing

        log(logHdr + 'subscribed to video events for the Vimeo player');
        log(logHdr + 'leave');
    },

    subscribeToEventsForVideoElem: function subscribeToEventsForVideoElem(videoElemId) {
        // subscribe to events for a single video element

        const logHdr = 'subscribeToEventsForVideoElem: ';
        log(logHdr + 'enter');
        log(logHdr + 'videoElemId=' + videoElemId);

        const videoJQ = $('#' + videoElemId);
        // get a jQuery object for the video element

        videoJQ.on("fullscreenchange", this._handleFullscreenChange);
        videoJQ.on("webkitfullscreenchange", this._handleFullscreenChange);
        videoJQ.on("mozfullscreenchange", this._handleFullscreenChange);
        videoJQ.on("msfullscreenchange", this._handleFullscreenChange);
        // depending on platform, any of these 4 events might occur when the user enters or exits fullscreen on a video

        videoJQ.on("play", this._handleVideoPlay);
        // call this method when the video plays

        // NOTE: at least on iOS, the "stalled" event gets sent fairly often while the video plays.
        // more important, no event is sent when the video continues to play,
        // so if we take the stalled event to mean that the video has stopped we will be incorrect.
        // so for now we don't subscribe to the stalled event.

        videoJQ.on("abort", this._handleVideoStopped);
        videoJQ.on("ended", this._handleVideoStopped);
        videoJQ.on("error", this._handleVideoStopped);
        videoJQ.on("pause", this._handleVideoStopped);
        // subscribe to events where we consider the video not playing

        log(logHdr + 'leave');
    },

    _subscribeToEventsForVideoElems: function _subscribeToEventsForVideoElems() {
        // subscribe to events for <video> elements

        const logHdr = '_subscribeToEventsForVideoElems: ';
        log(logHdr + 'enter');

        const videosJQ = $('video');
        // get a jQuery object for all the video elements on the page

        videosJQ.on("fullscreenchange", this._handleFullscreenChange);
        videosJQ.on("webkitfullscreenchange", this._handleFullscreenChange);
        videosJQ.on("mozfullscreenchange", this._handleFullscreenChange);
        videosJQ.on("msfullscreenchange", this._handleFullscreenChange);
        // depending on platform, any of these 4 events might occur when the user enters or exits fullscreen on a video

        videosJQ.on("play", this._handleVideoPlay);
        // call this method when the video plays

        videosJQ.on("abort", this._handleVideoStopped);
        videosJQ.on("ended", this._handleVideoStopped);
        videosJQ.on("error", this._handleVideoStopped);
        videosJQ.on("pause", this._handleVideoStopped);
        videosJQ.on("stalled", this._handleVideoStopped);
        // subscribe to events where we consider the video NOT playing

        //$('video').each(function () {
        //    enableInlineVideo(this);
        //});
        //// enable the iphone-inline-video functionality for all videos

        //$('video').on("click", function (evt) { lensUIUtils._handleVideoClick(evt); });
        //// called when the user clicks the video

        log(logHdr + 'leave');
    },

    _subscribeToEventsForIFrameVideos: function _subscribeToEventsForIFrameVideos() {
        // subscribe to events for iFrame videos

        const logHdr = '_subscribeToEventsForIFrameVideos: ';
        log(logHdr + 'enter');

        const iFramesJQ = $('.vimeo-iframe');
        // get a jQuery object for all video iFrames on the page

        const count = iFramesJQ.length;
        // how many?

        log(logHdr + 'count=' + count);

        if (count < 1) {
            // if none

            log(logHdr + 'NO iFrame videos found, so we are done');
            log(logHdr + 'leave');

            return;
        }

        iFramesJQ.each(function () {
            var iFrameObj = this;

            var iFrameId = iFrameObj.id;
            // get the DOM element id of the iFrame

            var lensId = elemIdUtils.getLensIdFromVideoIFrameElemId(iFrameId);
            // extract the lens id

            var player = new Vimeo.Player(iFrameObj);
            // allocate a Vimeo player object for the iFrame

            log(logHdr + 'allocated the Vimeo player');

            videoPlayers.setPlayer(lensId, player);
            // store the player so we can use it later

            //player.getVideoTitle().then(function (title) { log(logHdr + 'videoTitle=' + title); }).catch(function (error) { log(logHdr + 'ERROR: ' + error.name); });
            //// get the title of the video, to make sure the player was allocated successfully

            //NOTE: the code for video elements subscribes to the following events that the Video player does NOT have
            // fullscreenchange
            // webkitfullscreenchange
            // mozfullscreenchange
            // msfullscreenchange
            // stalled (which we don't subscribe to for video elements)
            //     NOTE: at least on iOS, the "stalled" event gets sent fairly often while the video plays.
            //     more important, no event is sent when the video continues to play,
            //      so if we take the stalled event to mean that the video has stopped we will be incorrect.
            //     so for now we don't subscribe to the stalled event.
            // abort

            log(logHdr + 'about to subscribe to video events for the Vimeo player');

            player.on('play', lensUIUtils._handleVideoPlay);
            // call this method when the video plays

            player.on('ended', lensUIUtils._handleVideoStopped);
            player.on('error', lensUIUtils._handleVideoStopped);
            player.on('pause', lensUIUtils._handleVideoStopped);
            // subscribe to events where we consider the video not playing
        });

        //$('video').on("click", function (evt) { lensUIUtils._handleVideoClick(evt); });
        //// called when the user clicks the video

        log(logHdr + 'leave');
    },

    _subscribeToEventsForLensCards: function _subscribeToEventsForLensCards(pageURL) {
        log('_subscribeToEventsForLensCards: enter');
        log('_subscribeToEventsForLensCards: pageURL=' + pageURL);

        lensUIUtils._subscribeToEventsForVideoElems();
        // subscribe to events for video elements

        lensUIUtils._subscribeToEventsForIFrameVideos();
        // subscribe to events for iFrame videos

        $('button.load-video').on("click", function (evt) { const lensId = lensUIUtils.getLensIdFromElemId(this.id); lensClickUtils.handleLoadVideoClick(lensId); });
        // subscribe to the click event for the "Load Video" buttons

        $('.s-more-tabs').on("click", function (evt) { const lensId = lensUIUtils.getLensIdFromElemId(this.id); moreLessClickUtils.handleMoreClick_Tabs(lensId); });
        // subscribe to the click event for the "more..." spans in cards with tabs

        $('.s-less-tabs').on("click", function (evt) { const lensId = lensUIUtils.getLensIdFromElemId(this.id); moreLessClickUtils.handleLessClick_Tabs(lensId); });
        // subscribe to the click event for the "less..." spans in cards with tabs


        $('.s-more').on("click", function (evt) { const lensId = lensUIUtils.getLensIdFromElemId(this.id); moreLessClickUtils.handleMoreClick_NoTabs(lensId); });
        // subscribe to the click event for the "more..." spans in cards without tabs

        $('.s-less').on("click", function (evt) { const lensId = lensUIUtils.getLensIdFromElemId(this.id); moreLessClickUtils.handleLessClick_NoTabs(lensId, true); });
        // subscribe to the click event for the "less..." spans in cards without tabs
        // true => scroll the lens card into view

        $('.card-hdr-close-btn').on("click", async function (evt) { await lensClickUtils.handleCloseBtnClickAsync(evt); });
        // subscribe to the click event for card caption close buttons

        $('.when-tab').on("click", function (evt) { lensClickUtils.handleTabClickAsync(evt); });
        $('.how-tab').on("click", function (evt) { lensClickUtils.handleTabClickAsync(evt); });
        $('.examples-tab').on("click", function (evt) { lensClickUtils.handleTabClickAsync(evt); });
        $('.video-tab').on("click", function (evt) { lensClickUtils.handleTabClickAsync(evt); });
        // subscribe click handlers for the tabs

        alloyFingerUtils.subscribeToAlloyFingerEvts();
        // subscribe to gesture events for the cards

        if (enableBookmarkFeatures) {
            $('.empty-bookmark').click(function () { bookmarkUIUtils.handleBookmarkClickAsync(this.id, pageURL); });
        }

        log('_subscribeToEventsForLensCards: leave');
    },

    getTabNameFromElemId: function getTabNameFromElemId(elemId) {
        const header = 'getTabNameFromElemId: ';
        log(header + 'enter');
        log(header + 'elemId=' + elemId);

        // examples
        //  liWhenTab1
        //  liHowTab1
        //  liExamplesTab1
        //  liVideoTab1

        var tabName = '';

        if (elemId.startsWith('liWhenTab')) {
            tabName = 'when';
        }

        if (elemId.startsWith('liHowTab')) {
            tabName = 'how';
        }

        if (elemId.startsWith('liExamplesTab')) {
            tabName = 'examples';
        }

        if (elemId.startsWith('liVideoTab')) {
            tabName = 'video';
        }

        log(header + 'tabName=' + tabName);
        log(header + 'leave');

        return tabName;
    },

    getLensIdFromElemId: function getLensIdFromElemId(elemId) {
        //const header = 'getLensIdFromElemId: ';
        //log(header + 'enter');
        //log(header + 'elemId=' + elemId);

        // examples
        //  cardTextP1
        //  lensNameS1
        //  how1
        //  examples1
        //  video1

        var idStr = '';
        var lensId = 0;

        const newFormat = elemId.includes('-');
        // is the id a new format id?

        if (newFormat) {
            const where = elemId.lastIndexOf('-');
            // find the last hyphen in the string

            idStr = elemId.substring(where + 1);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('btnLoadVideo')) {
            idStr = elemId.substring(12);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('liWhenTab')) {
            idStr = elemId.substring(9);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('liHowTab')) {
            idStr = elemId.substring(8);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('liExamplesTab')) {
            idStr = elemId.substring(13);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('liVideoTab')) {
            idStr = elemId.substring(10);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('videoTab')) {
            idStr = elemId.substring(8);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('sLess')) {
            idStr = elemId.substring(5);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('sMore')) {
            idStr = elemId.substring(5);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('btnCloseCardHdr')) {
            idStr = elemId.substring(15);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('cardTextP')) {
            idStr = elemId.substring(9);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('cardHdr')) {
            idStr = elemId.substring(7);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('imgBk')) {
            idStr = elemId.substring(5);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('lensNameS')) {
            idStr = elemId.substring(9);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('when')) {
            idStr = elemId.substring(4);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('how')) {
            idStr = elemId.substring(3);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('examples')) {
            idStr = elemId.substring(8);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        if (elemId.startsWith('video')) {
            idStr = elemId.substring(5);
            //log(header + 'idStr=' + idStr);
            lensId = parseInt(idStr, 10);
            //log(header + 'leave');
            return lensId;
        }

        //log(header + 'NOT FOUND');
        //log(header + 'leave');

        return -1;
    },

    doesLensCardHaveTabs: function doesLensCardHaveTabs(lensId) {
        log('doesLensCardHaveTabs: enter');

        const tabContentId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_TAB_CONTENT_PREFIX, undefined);
        // get the id of the tab content element

        log('doesLensCardHaveTabs: tabContentId=' + tabContentId);

        const count = $('#' + tabContentId).length;
        // how many tab content elements for the lens are contained on the page?

        const hasTabs = (count > 0);
        // if any, the card has tabs

        log('doesLensCardHaveTabs: hasTabs=' + hasTabs);
        log('doesLensCardHaveTabs: leave');

        return hasTabs;
    },

    isLensCardExpanded: function isLensCardExpanded(lensId, hasTabs) {
        log('isLensCardExpanded: enter');
        log('isLensCardExpanded: lensId=' + lensId);
        log('isLensCardExpanded: hasTabs=' + hasTabs);

        var expanded = false;

        if (hasTabs) {
            expanded = tabsCardExpStorageUtils.isCardExpanded(lensId);
        }
        else {
            expanded = noTabsCardExpStorageUtils.isCardExpanded(lensId);
        }

        log('isLensCardExpanded: expanded=' + expanded);
        log('isLensCardExpanded: leave');

        return expanded;
    },

    toggleLessMore: function toggleLessMore(lensId, scrollIntoView) {
        // lensId: id of the target lens
        // scrollIntoView: if true, scroll the lens into view after collapsing

        log('toggleLessMore: enter');
        log('toggleLessMore: lensId=' + lensId);

        const hasTabs = lensUIUtils.doesLensCardHaveTabs(lensId);
        // does the lens card have tabs?

        log('toggleLessMore: hasTabs=' + hasTabs);

        const expanded = lensUIUtils.isLensCardExpanded(lensId, hasTabs);
        // is the lens card expanded?

        log('toggleLessMore: expanded=' + expanded);

        if (hasTabs) {
            // if the lens card has tabs

            if (expanded) {
                moreLessClickUtils.handleLessClick_Tabs(lensId);
                // collapse the lens card which has tabs
            }
            else {
                moreLessClickUtils.handleMoreClick_Tabs(lensId);
                // expand the lens card which has tabs
            }
        }
        else {
            // if the lens card doesn't have tabs

            if (expanded) {
                moreLessClickUtils.handleLessClick_NoTabs(lensId, scrollIntoView);
                // collapse the lens card which doesn't have tabs
            }
            else {
                moreLessClickUtils.handleMoreClick_NoTabs(lensId);
                // expand the lens card which doesn't have tabs
            }
        }

        log('toggleLessMore: leave');
    },

    displayCurrentLensesAsync: async function displayCurrentLensesAsync() {
        // display the current set of lenses
        // the current set of lenses depends on
        //  1) the selected button in the "which to find" button group (all or bookmarked)
        //  2) hidden lenses
        //  3) display: alphabetically or by groups

        const logHdr = 'lensUIUtils.displayCurrentLensesAsync: ';
        log(logHdr + 'enter');

        var targetLenses = [];
        var targetCount = 0;
        var lensesHtml = '';

        // NOTE: for now, the Find Lens feature overrides other search-related values

        const toFindCount = lensesToFindStorageUtils.count();

        log(logHdr + 'toFindCount=' + toFindCount);

        if (toFindCount > 0) {
            // if any lenses are specified to find

            log(logHdr + 'the user specified at least one lens to find');

            const findLensIds = lensesToFindStorageUtils.getLensIds();
            // get the ids of the lenses to find

            const foundCount = findLensIds.length;

            log(logHdr + 'foundCount=' + foundCount);

            if (foundCount > 0) {

                targetLenses = lensUtils.getLensesByIds(findLensIds);
                // get the lenses to display

                targetCount = targetLenses.length;

                log(logHdr + 'targetCount=' + targetCount);

                log(logHdr + 'calculating HTML to display found lenses alphabetically');

                lensesHtml = await lensHtmlUtils.getHtmlForLensesByAlphaAsync(targetLenses);
                // get HTML for the array of lenses
                // arrange lenses alphabetically, not by group

                $('#divResults').html(lensesHtml);

                // NOTE: don't change the cache

                this._subscribeToEventsForLensCards(lensUIUtils._pageURL);

                log(logHdr + 'leave');

                return;
            }
        }

        const findAll = findWhichGroupStateUtils.getFindAll();
        // are we set to find all lenses?
        //  true => find all
        //  false => find bookmarked

        log(logHdr + 'findAll=' + findAll + ', (true => find all, false => find bookmarked)');

        var byAlpha = displayByStateUtils.getDisplayAlpha();
        // are we displaying lenses alphabetically?
        // true => alphabetically, false => by group

        log(logHdr + 'byAlpha=' + byAlpha + ', (true => alphabetically, false => by group)');

        var cached = false;

        if (findAll) {
            // if we need to find all lenses except hidden (as opposed to bookmarked)

            if (byAlpha) {
                // if displaying alphabetically

                cached = displayCurrentLensesCache.hasAllAlphaValue();
            }
            else {
                // if displaying by category

                cached = displayCurrentLensesCache.hasAllGroupedValue();
            }

            log(logHdr + 'cached=' + cached);

            if (cached) {
                // if we have the HTML for this scenario cached

                log(logHdr + 'we need all lenses and we have them cached');

                if (byAlpha) {
                    lensesHtml = displayCurrentLensesCache.getAllAlphaHtml();
                }
                else {
                    lensesHtml = displayCurrentLensesCache.getAllGroupedHtml();
                }

                $('#divResults').html(lensesHtml);

                // even though the HTML was cached, the cached is only a string, not elements in the DOM
                // after we create elements in the DOM, we need to subscribe to events

                this._subscribeToEventsForLensCards(lensUIUtils._pageURL);
            }
            else {
                // if we need all lenses and we don't have them cached

                log(logHdr + 'we need all lenses and we do NOT have them cached');
                log(logHdr + 'creating the HTML for all lenses');

                targetLenses = lensUtils.getLensDTOsToDisplay();
                // get the lenses to display

                targetCount = targetLenses.length;

                log(logHdr + 'targetCount=' + targetCount);

                if (byAlpha) {
                    // if we are displaying lenses alphabetically

                    lensesHtml = await lensHtmlUtils.getHtmlForLensesByAlphaAsync(targetLenses);
                    // get HTML for the array of lenses
                    // arrange lenses alphabetically, not by group

                    displayCurrentLensesCache.setAllAlphaHtml(lensesHtml);
                    // cache the HTML
                }
                else {
                    // otherwise

                    lensesHtml = await lensHtmlUtils.getHtmlForLensesByGroupAsync(targetLenses);
                    // get HTML for the array of lenses
                    // arrange lenses by group

                    displayCurrentLensesCache.setAllGroupedHtml(lensesHtml);
                    // cache the HTML
                }

                $('#divResults').html(lensesHtml);

                this._subscribeToEventsForLensCards(lensUIUtils._pageURL);
            }
        }
        else {
            // if we need to find bookmarked lenses

            if (byAlpha) {
                // if displaying alphabetically

                cached = displayCurrentLensesCache.hasBkAlphaValue();
            }
            else {
                // if displaying by category

                cached = displayCurrentLensesCache.hasBkGroupedValue();
            }

            log(logHdr + 'cached=' + cached);

            if (cached) {
                // if we need bookmarked lenses and we have them cached

                log(logHdr + 'we need bookmarked lenses and we have them cached');

                if (byAlpha) {
                    // if we are displaying bookmarked lenses alphabetically and we have them cached

                    lensesHtml = displayCurrentLensesCache.getBkAlphaHtml();
                    // get the HTML for bookmarked lenses displayed alphabetically
                }
                else {
                    // if we are displaying bookmarked lenses by group and we have them cached

                    lensesHtml = displayCurrentLensesCache.getBkGroupedHtml();
                    // get the HTML for bookmarked lenses displayed by group
                }

                $('#divResults').html(lensesHtml);

                // even though the HTML was cached, the cached is only a string, not elements in the DOM
                // after we create elements in the DOM, we need to subscribe to events

                this._subscribeToEventsForLensCards(lensUIUtils._pageURL);
            }
            else {
                // if we need bookmarked lenses and we don't have them cached

                log(logHdr + 'we need bookmarked lenses and we do NOT have them cached');
                log(logHdr + 'creating the HTML for bookmarked lenses');

                targetLenses = lensUtils.getLensDTOsToDisplay();
                // get the lenses to display

                targetCount = targetLenses.length;

                log(logHdr + 'targetCount=' + targetCount);

                if (byAlpha) {
                    // if we are displaying lenses alphabetically

                    log(logHdr + 'calculating HTML to display bookmarked lenses alphabetically');

                    lensesHtml = await lensHtmlUtils.getHtmlForLensesByAlphaAsync(targetLenses);
                    // get HTML for the array of lenses
                    // arrange lenses alphabetically, not by group

                    displayCurrentLensesCache.setBkAlphaHtml(lensesHtml);
                    // cache the HTML
                }
                else {
                    // otherwise

                    log(logHdr + 'calculating HTML to display bookmarked lenses by group');

                    lensesHtml = await lensHtmlUtils.getHtmlForLensesByGroupAsync(targetLenses);
                    // get HTML for the array of lenses
                    // arrange lenses by group

                    displayCurrentLensesCache.setBkGroupedHtml(lensesHtml);
                    // cache the HTML
                }

                $('#divResults').html(lensesHtml);

                this._subscribeToEventsForLensCards(lensUIUtils._pageURL);
            }
        }

        log(logHdr + 'leave');
    },

    setLensDivWidth: function setLensDivWidth(lensId, widthStr) {
        // set the width attribute of the parent div for the specified lens to the given string
        // width value can be of the form
        //  90%
        //  100px

        const logHeader = 'lensUIUtils.setLensDivWidth: ';
        log(logHeader + 'enter');
        log(logHeader + 'lensId=' + lensId);
        log(logHeader + 'widthStr=' + widthStr);

        const cardElemId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_PREFIX, undefined);
        // get the id of the card div

        const cardJQ = $('#' + cardElemId);
        // get a jQuery object for the card

        cardJQ.css('width', widthStr);
        // set the width of the card

        log(logHeader + 'leave');
    },

    clearLensDivWidth: function clearLensDivWidth(lensId) {
        // remove the width attribute of the parent div for the specified lens

        const logHeader = 'lensUIUtils.clearLensDivWidth: ';
        log(logHeader + 'enter');
        log(logHeader + 'lensId=' + lensId);

        const cardElemId = elemIdUtils.getElemId(lensId, elemIdUtils.CARD_PREFIX, undefined);
        // get the id of the card div

        const cardJQ = $('#' + cardElemId);
        // get a jQuery object for the card

        cardJQ.css('width', '');
        // remove the width

        log(logHeader + 'leave');
    }
};