/*
 * View framework for JamKazam.
 *
 * Processes proprietary attributes in markup to convert a set of HTML elements
 * into the JamKazam screen layout. This module is only responsible for size
 * and position. All other visual aspects should be elsewhere.
 *
 * See the layout-example.html file for a simple working example.
 */
(function (context, $) {

  "use strict";

  context.JK = context.JK || {};

  // Static function to hide the 'curtain' which hides the underlying
  // stuff until we can get it laid out. Called from both the main
  // client as well as the landing page.
  context.JK.hideCurtain = function (duration) {
    context.setTimeout(function () {
      $('.curtain').fadeOut(2 * duration);
    }, duration);
  };

  context.JK.Layout = function () {

    // privates
    var logger = context.JK.logger;

    var EVENTS = context.JK.EVENTS;
    var NOT_HANDLED = "not handled";

    var me = null;  // Reference to this instance for context sanity.

    var opts = {
      headerHeight: 75,
      sidebarWidth: 300,
      notifyHeight: 150,
      notifyGutter: 10,
      collapsedSidebar: 30,
      panelHeaderHeight: 36,
      alwaysOpenPanelHeaderHeight:78,       // for the search bar
      gutter: 60,                           // Margin around the whole UI
      screenMargin: 0,                     // Margin around screens (not headers/sidebar)
      gridOuterMargin: 6,                   // Outer margin on Grids (added to screenMargin if screen)
      gridPadding: 8,                       // Padding around grid cells. Added to outer margin.
      animationDuration: 400,
      allowBodyOverflow: false,         // Allow tests to disable the body-no-scroll policy
      sizeOverlayToContent: false         // if true, use the size of <body> tag to decide overlay size everytime overlay is shown. should be used in non-client settings
    };

    var width = $(context).width();
    var height = $(context).height();
    var resizing = null;
    var sidebarVisible = true;
    var expandedPanel = null;
    var previousScreen = null;
    var currentScreen = null;
    var currentHash = null;

    var screenBindings = {};
    var dialogBindings = {};
    var wizardShowFunctions = {};

    var openDialogs = []; // FIFO stack

    var resettingHash = false;

    function setup() {
      requiredStyles();
      hideAll();
      setInitialExpandedSidebarPanel();
      sizeScreens(width, height, '[layout="screen"]', true);
      positionOffscreens(width, height);
      $('[layout="sidebar"]').show();
      $('[layout="panel"]').show();
      layout();
    }

    function setInitialExpandedSidebarPanel() {
      if (gon.global.chat_opened_by_default) {
        expandedPanel = 'panelChat';
      }
      else {
        expandedPanel = 'panelFriends';
      }

    }

    function layout() {
      width = $(context).width();
      height = $(context).height();
      // TODO
      // Work on naming. File is layout, class is Layout, this method
      // is layout and every other method starts with 'layoutX'. Perhaps
      // a little redundant?
      layoutCurtain(width, height);
      layoutDialogOverlay(width, height);
      layoutScreens(width, height);
      layoutSidebar(width, height);
      layoutHeader(width, height);
      layoutNotify(width, height);
      layoutFooter(width, height);

      $(document).triggerHandler('layout_resized');
    }

    function layoutCurtain(screenWidth, screenHeight) {
      var curtainStyle = {
        position: 'absolute',
        width: screenWidth + 'px',
        height: screenHeight + 'px'
      };
      $('.curtain').css(curtainStyle);
    }

    function layoutDialogOverlay(screenWidth, screenHeight) {
      var style = {
        position: 'absolute',
        width: screenWidth + 'px',
        height: screenHeight + 'px'
      };
      $('.dialog-overlay').css(style);
    }

    function layoutScreens(screenWidth, screenHeight) {
      var previousScreenSelector = '[layout-id="' + previousScreen + '"]';
      var currentScreenSelector = '[layout-id="' + currentScreen + '"]';
      $(currentScreenSelector).show();

      var width = screenWidth - (2 * opts.gutter + 2 * opts.screenMargin);
      var left = -1 * width - 100;

      if (currentScreenSelector === previousScreenSelector) {
        left = $(currentScreenSelector).css("left");
        if (left) {
          left = left.split("px")[0];
        }
      }
      $(previousScreenSelector).animate({left: left}, {duration: opts.animationDuration, queue: false});
      sizeScreens(screenWidth, screenHeight, '[layout="screen"]');
      positionOffscreens(screenWidth, screenHeight);
      positionOnscreen(screenWidth, screenHeight);
    }

    function sizeScreens(screenWidth, screenHeight, selector, immediate) {
      var duration = opts.animationDuration;
      if (immediate) {
        duration = 0;
      }

      var width = screenWidth - (2 * opts.gutter + 2 * opts.screenMargin);
      if (sidebarVisible) {
        width -= (opts.sidebarWidth + 2 * opts.gridPadding);
      } else {
        width -= opts.collapsedSidebar + 2 * opts.gridPadding;
        width += opts.gutter; // Add back in the right gutter width.
      }
      var height = screenHeight - opts.headerHeight - (2 * opts.gutter + 2 * opts.screenMargin);
      var css = {
        width: width,
        height: height
      };
      var $screens = $(selector);
      $screens.animate(css, {duration: duration, queue: false});
      layoutHomeScreen(width, height);
    }

    /**
     * Postition all screens that are not the current screen.
     */
    function positionOffscreens(screenWidth, screenHeight) {
      var top = opts.headerHeight + opts.gutter + opts.screenMargin;
      var left = -1 * (screenWidth + 2 * opts.gutter);
      var $screens = $('[layout="screen"]').not('[layout-id="' + currentScreen + '"]');
      $screens.css({
        top: top,
        left: left
      });
    }

    /**
     * Position the current screen
     */
    function positionOnscreen(screenWidth, screenHeight, immediate) {
      var duration = opts.animationDuration;
      if (immediate) {
        duration = 0;
      }
      var top = opts.headerHeight + opts.gutter + opts.screenMargin;
      var left = opts.gutter + opts.screenMargin;
      var $screen = $('[layout-id="' + currentScreen + '"]');
      $screen.animate({
        top: top,
        left: left,
        overflow: 'auto'
      }, duration);
    }

    function layoutHomeScreen(homeScreenWidth, homeScreenHeight) {
      var $grid = $('[layout-grid]');
      var gridWidth = homeScreenWidth;
      var gridHeight = homeScreenHeight;
      $grid.css({width: gridWidth, height: gridHeight});
      var layout = $grid.attr('layout-grid');
      if (!layout)
        return;
      var gridRows = layout.split('x')[0];
      var gridCols = layout.split('x')[1];

      var gutterWidth = 0;

      var findCardLayout;
      var feedCardLayout;

      $grid.find('.homecard').each(function () {
        var childPosition = $(this).attr("layout-grid-position");
        var childRow = childPosition.split(',')[1];
        var childCol = childPosition.split(',')[0];
        var childRowspan = $(this).attr("layout-grid-rows");
        var childColspan = $(this).attr("layout-grid-columns");
        var childLayout = me.getCardLayout(gridWidth, gridHeight, gridRows, gridCols,
          childRow, childCol, childRowspan, childColspan);

        if($(this).is('.jamtrack')) {
          feedCardLayout = childLayout;
        }
        else if($(this).is('.findsession')) {
          findCardLayout = childLayout;
        }

        $(this).animate({
          width: childLayout.width,
          height: childLayout.height,
          top: childLayout.top,
          left: childLayout.left
        }, opts.animationDuration);
      });

      var broadcastWidth = findCardLayout.width + feedCardLayout.width; //+ opts.gridPadding * 2;

      //layoutBroadcast(broadcastWidth, findCardLayout.left);
      layoutTopMessage(broadcastWidth, findCardLayout.left)
    }

    /**
    function layoutBroadcast(width, left) {
      var css = {
        width:width + opts.gridPadding * 2,
        left:left
      }
      $('[data-react-class="BroadcastHolder"]').animate(css, opts.animationDuration)
    }*/

    function layoutTopMessage(width, left) {
      var css = {
        width:width + opts.gridPadding * 2,
        left:left
      }
      $('[data-react-class="TopMessageHolder"]').animate(css, opts.animationDuration)
    }

    function layoutSidebar(screenWidth, screenHeight) {
      var width = opts.sidebarWidth;
      var expanderHeight = $('[layout-sidebar-expander]').height();
      var height = screenHeight - opts.headerHeight - 2 * opts.gutter + expanderHeight;
      var right = opts.gutter;
      if (!sidebarVisible) {
        // Negative right to hide most of sidebar
        right = (0 - opts.sidebarWidth) + opts.collapsedSidebar;
      }
      var top = opts.headerHeight + opts.gutter - expanderHeight;
      var css = {
        width: width,
        height: height,
        top: top,
        right: right
      };
      $('[layout="sidebar"]').animate(css, opts.animationDuration);
      layoutPanels(width, height);
      if (sidebarVisible) {
        $('[layout-panel="collapsed"]').hide();
        $('[layout-panel="expanded"]').show();
        $('[layout-sidebar-expander="hidden"]').hide();
        $('[layout-sidebar-expander="visible"]').show();
      } else {
        $('[layout-panel="collapsed"]').show();
        $('[layout-panel="expanded"]').hide();
        $('[layout-sidebar-expander="hidden"]').show();
        $('[layout-sidebar-expander="visible"]').hide();
      }
    }

    function layoutPanels(sidebarWidth, sidebarHeight) {
      // TODO - don't like the accordian - poor usability. Requires longest mouse
      // reach when switching panels. Probably better to do tabs.
      if (!sidebarVisible) {
        return;
      }
      var $expandedPanel = $('[layout-id="' + expandedPanel + '"]');
      var $expandedPanelContents = $expandedPanel.find('[layout-panel="contents"]');
      var combinedHeaderHeight = ($('[layout-panel="contents"]').length - 1) * opts.panelHeaderHeight + opts.alwaysOpenPanelHeaderHeight;
      var expanderHeight = $('[layout-sidebar-expander]').height();
      var expandedPanelHeight = sidebarHeight - (combinedHeaderHeight + expanderHeight);
      $('[layout-panel="contents"]').hide();
      $('[layout-panel="contents"]').css({"height": "1px"});
      $expandedPanelContents.show();
      $expandedPanel.triggerHandler('open')
      $expandedPanelContents.animate({"height": expandedPanelHeight + "px"}, opts.animationDuration, function() {
        $expandedPanel.triggerHandler('fullyOpen')
      });
    }

    function layoutHeader(screenWidth, screenHeight) {
      var width = screenWidth - 2 * opts.gutter;
      var height = opts.headerHeight - opts.gutter;
      var top = opts.gutter;
      var left = opts.gutter;
      var css = {
        width: width + "px",
        height: height + "px",
        top: top + "px",
        left: left + "px"
      };
      $('[layout="header"]').css(css);
    }

    function layoutNotify(screenWidth, screenHeight) {
      var $notify = $('[layout="notify"]');
      var nHeight = $notify.height();
      var notifyStyle = {
        bottom: '0px',
        position: 'fixed'
      };
      $notify.css(notifyStyle);
    }

    function layoutFooter(screenWidth, screenHeight) {

      if (!opts.layoutFooter) {
        return;
      }
      var $footer = $('#footer');
      $footer.show();
      var nHeight = $footer.height();
      var footerStyle = {
        top: (screenHeight - 80) + 'px'
      };
      var width = screenWidth - (2 * opts.gutter + 2 * opts.screenMargin);
      var left = -1 * width - 100;
      $footer.animate({ "left": opts.gutter, "width": width, "top": (screenHeight - 78) + "px"}, opts.animationDuration);
    }

    function requiredLayoutStyles() {
      var layoutStyle = {
        position: 'absolute',
        margin: '0px',
        padding: '0px'
      };
      $('[layout]').css(layoutStyle);
      // JW: Setting z-index of notify to 1001, so it will appear above the dialog overlay.
      // This allows dialogs to use the notification.
      $('[layout="notify"]').css({"z-index": "1001", "padding": "20px"});
      $('[layout="panel"]').css({position: 'relative'});
      var $headers = $('[layout-panel="expanded"] [layout-panel="header"]');
      context._.each($headers, function($header) {
        $header = $($header);
        var isAlwaysOpenHeader = $header.is('.always-open');
        $header.css({
          margin: "0px",
          padding: "0px",
          height: (isAlwaysOpenHeader ? opts.alwaysOpenPanelHeaderHeight : opts.panelHeaderHeight) + "px"
        });
      })
      $('[layout-grid]').css({
        position: "relative"
      });
      $('[layout-grid]').children().css({
        position: "absolute"
      });
    }

    function requiredStyles() {
      var bodyStyle = {
        margin: '0px',
        padding: '0px',
        overflow: 'hidden'
      };
      if (opts.allowBodyOverflow) {
        delete bodyStyle.overflow;
      }
      $('body').css(bodyStyle);

      requiredLayoutStyles();

      var curtainStyle = {
        position: "absolute",
        margin: '0px',
        padding: '0px',
        overflow: 'hidden',
        zIndex: 100
      };
      $('.curtain').css(curtainStyle);
    }

    function hideAll() {
      $('[layout]').hide();
      $('[layout="header"]').show();
    }

    function showSidebar() {
      sidebarVisible = true;
      layout();
    }

    function hideSidebar() {
      sidebarVisible = false;
      layout();
    }

    function toggleSidebar() {
      if (sidebarVisible) {
        hideSidebar();
      } else {
        showSidebar();
      }
    }

    function hideDialogs() {
      // TODO - may need dialogEvents here for specific dialogs.
      $('[layout="dialog"]').hide();
      $('.dialog-overlay').hide();
    }

    function tabClicked(evt) {
      evt.preventDefault();
      var destination = $(evt.currentTarget).attr('tab-target');
      $('[tab-target]').removeClass('selected');
      $(evt.currentTarget).addClass('selected');
      $('.tab').hide();
      $('[tab-id="' + destination + '"]').show();
    }

    function linkClicked(evt) {
      evt.preventDefault();
      var $currentTarget = $(evt.currentTarget);

      // allow links to be disabled
      if ($currentTarget.hasClass("disabled")) {
        return;
      }

      var destination = $(evt.currentTarget).attr('layout-link');
      var $destination = $('[layout-id="' + destination + '"]');

      var destinationType = $destination.attr("layout");
      if (destinationType === "screen") {
        if(!context.JK.currentUserId && !$destination.is('.no-login-required')) {
          // there is no user, and this item does not support 'no-login', so warn user
          showDialog('login-required-dialog');
          return;
        }
        context.location = '/client#/' + destination;
      } else if (destinationType === "dialog") {
        showDialog(destination);
      }
    }

    function close(evt) {
      var $target = $(evt.currentTarget).closest('[layout]');
      var layoutId = $target.attr('layout-id');
      var isDialog = ($target.attr('layout') === 'dialog');
      if (isDialog) {
        closeDialog(layoutId);
      } else {
        $target.hide();
      }
      return false;
    }

    function cancel(evt) {
      var $target = $(evt.currentTarget).closest('[layout]');
      var layoutId = $target.attr('layout-id');
      var isDialog = ($target.attr('layout') === 'dialog');
      if (isDialog) {
        cancelDialog(layoutId);
      } else {
        // ?
        logger.warn("unable to handle cancel layout-action for %o", $target)
      }
      return false;
    }

    function cancelDialog(dialog) {
      logger.debug("cancelling dialog: " + dialog);
      var $dialog = $('[layout-id="' + dialog + '"]');
      var result = dialogEvent(dialog, 'onCancel');
      if(result !== false) {
        closeDialog(dialog, true);
      }
      else {
        logger.debug("dialog refused cancel");
      }
    }

    function closeDialog(dialog, canceled) {
      logger.debug("closing dialog: " + dialog);
      var $dialog = $('[layout-id="' + dialog + '"]');
      dialogEvent(dialog, 'beforeHide');
      var $overlay = $('.dialog-overlay');
      unstackDialogs($overlay);
      $dialog.hide();
      $dialog.triggerHandler(EVENTS.DIALOG_CLOSED,    {name: dialog, dialogCount: openDialogs.length, result: $dialog.data('result'), canceled: canceled });
      $(context).triggerHandler(EVENTS.DIALOG_CLOSED, {name: dialog, dialogCount: openDialogs.length, result: $dialog.data('result'), canceled: canceled });
      dialogEvent(dialog, 'afterHide');
      $.btOffAll(); // add any prod bubbles if you close a dialog
    }

    function screenProperty(screen, property) {
      if (screen && screen in screenBindings) {
        return screenBindings[screen][property]
      }
      return null;
    }
    function screenEvent(screen, evtName, data) {
      if (screen && screen in screenBindings) {
        if (evtName in screenBindings[screen]) {
          return screenBindings[screen][evtName].call(me, data);
        }
      }
      return NOT_HANDLED;
    }

    function dialogEvent(dialog, evtName, data) {
      if (dialog && dialog in dialogBindings) {
        if (evtName in dialogBindings[dialog]) {
          return dialogBindings[dialog][evtName].call(me, data);
        }
      }
      return NOT_HANDLED;
    }

    function activeElementEvent(evtName, data) {
      var result = {};
      var currDialog = currentDialog();
      if(currDialog) {
        result.dialog = dialogEvent(currDialog.attr('layout-id'), evtName, data);
      }

      if(currentScreen) {
        result.screen = screenEvent(currentScreen, evtName, data);
      }
      return result;
    }

    function onHashChange(e, postFunction) {

      if(currentHash == context.location.hash) { return }

      if(resettingHash) {
        resettingHash = false;
        e.preventDefault();
        return false;
      }

      try {
        var location = context.RouteMap.parse(context.location.hash);
      }
      catch (e) {
        // this is nowhere in the rich client; just let it go through
        return postFunction(e);
      }

      var screen = location.page.substring(1); // remove leading slash
      var accepted = screenEvent(currentScreen, 'beforeLeave', {screen:screen, hash: context.location.hash});
      if(accepted === false) {
        logger.debug("navigation to " + context.location.hash + " rejected by " + currentScreen);
        //resettingHash = true;
        // reset the hash to where it just was
        context.location.hash = currentHash;
      }
      else {(screen)
        return postFunction(e);
      }
    }

    function changeToScreen(screen, data) {
      changeScreen(screen, data);
    }

    function changeScreen(screen, data) {
      previousScreen = currentScreen;
      currentScreen = screen;
      currentHash = context.location.hash;

      var accepted = screenEvent(previousScreen, 'beforeHide', data);
      if(accepted === false) return;

      logger.debug("layout: changing screen to " + currentScreen);

      // notify everyone
      $(document).triggerHandler(EVENTS.SCREEN_CHANGED, {previousScreen: previousScreen, newScreen: currentScreen})
      window.NavActions.screenChanged(currentScreen, screenProperty(currentScreen, 'navName'))
      context.JamTrackPreviewActions.screenChange()

      screenEvent(currentScreen, 'beforeShow', data);

      // For now -- it seems we want it open always.
      // TODO - support user preference here? Remember how they left it?
      sidebarVisible = true;
      /*
       var openSidebarScreens = [
       'home', 'session', 'createSession',
       'findSession', 'searchResults'
       ];
       $.each(openSidebarScreens, function() {
       logger.debug("comparing " + this + " to " + currentScreen);
       if (this === currentScreen) {
       sidebarVisible = true;
       return false;
       }
       });
       */
      layout();

      // add an attribute to any dialogs, which let's it know it's current screen (useful for contextual styling)
      context._.each(openDialogs, function(dialog) {
        addScreenContextToDialog($(dialog));
      })

      screenEvent(previousScreen, 'afterHide', data);
      screenEvent(currentScreen, 'afterShow', data);
      jQuery.btOffAll(); // add any prod bubbles if you change screens

      // Show any requested dialog
      if ("d" in data) {
        showDialog(data.d, data);
      }
    }

    // if no arguments passed, then see if any dialog is showing
    // if string passed, see if dialog is showing (even if buried) of a given name
    function isDialogShowing() {
      if(arguments.length == 1) {
        // user passed in dialog id
        var dialogId = arguments[0];
        var result = false
        context._.each(openDialogs, function(dialog) {
          if($(dialog).attr('layout-id') == dialogId) {
            result = true;
            return false;
          }
        })
        return result;
      }
      else  {
        // user passed in nothing
        return openDialogs.length > 0;
      }
    }

    function currentDialog() {
      if(openDialogs.length == 0) return null;

      return openDialogs[openDialogs.length - 1];
    }

    // payload is a notification event from websocket gateway
    function dialogObscuredNotification(payload) {
        var openDialog = currentDialog();
        if(!openDialog) return false;

        if(typeof openDialog.handledNotification === 'function') {
          return !openDialog.handledNotification(payload);
        }
        else {
          return true;
        }
    }

    // payload is a notification event from websocket gateway
    function isNoisyNotification(payload) {
      var openDialog = currentDialog();
      if(!openDialog) return false;

      if(typeof openDialog.isNoisyNotification === 'function') {
        return !openDialog.isNoisyNotification(payload);
      }
      else {
        return true;
      }
    }

    /**
     * Responsible for keeping N dialogs in correct stacked order,
     * also moves the .dialog-overlay such that it hides/obscures all dialogs except the highest one
     */
    function stackDialogs($dialog, $overlay) {
      // don't push a dialog on the stack that is already on there; remove it from where ever it is currently
      // and the rest of the code will make it end up at the top
      var layoutId = $dialog.attr('layout-id');
      for (var i = openDialogs.length - 1; i >= 0; i--) {
        if (openDialogs[i].attr('layout-id') === layoutId) {
          openDialogs.splice(i, 1);
        }
      }

      // pull out a topmost one, if present
      var topMost = null;
      for (var i = openDialogs.length - 1; i >= 0; i--) {
        if (openDialogs[i].attr('topmost') === 'true') {
          topMost = openDialogs[i];
          openDialogs.splice(i, 1);
        }
      }

      openDialogs.push($dialog);
      if(topMost) openDialogs.push(topMost);

      var zIndex = 1000;
      for (var i in openDialogs) {
        var $openDialog = openDialogs[i];
        $openDialog.css('zIndex', zIndex);
        zIndex++;
      }
      $overlay.css('zIndex', zIndex - 1);
    }

    function unstackDialogs($overlay) {
      if (openDialogs.length > 0) {
        openDialogs.pop();
      }

      var zIndex = 1000 + openDialogs.length;
      $overlay.css('zIndex', zIndex - 1);

      if (openDialogs.length == 0) {
        $overlay.hide();
      }
    }

    function addScreenContextToDialog($dialog) {
      $dialog.attr('current-screen', currentScreen); // useful for contextual styling of dialogs
    }

    function showDialog(dialog, options) {
      if (dialogEvent(dialog, 'beforeShow', options) === false) {
        return null;
      }
      logger.debug("opening dialog: " + dialog)

      var $dialog = $('[layout-id="' + dialog + '"]');
      if($dialog.length == 0) {
        logger.debug("unknown dialog encountered: " + dialog)
        return
      }


      var $overlay = $('.dialog-overlay')

      if (opts.sizeOverlayToContent) {
        var $body = $('body')
        $('.dialog-overlay').css({
          width: $body.width() + 'px',
          height: $body.height() + 'px'
        });
      }

      $overlay.show();
      centerDialog(dialog);
      stackDialogs($dialog, $overlay);
      addScreenContextToDialog($dialog)
      $dialog.show();

      // maintain center (un-attach previous sensor if applicable, then re-add always)

      window.ResizeSensor.detach($dialog.get(0))
      new window.ResizeSensor($dialog, function(){
        centerDialog(dialog);
      });

      dialogEvent(dialog, 'afterShow', options);
      $.btOffAll(); // add any prod bubbles if you open a dailog
      return $dialog;
    }

    function centerDialog(dialog) {
      var $dialog = $('[layout-id="' + dialog + '"]');
      $dialog.css({
        left: width / 2 - ($dialog.width() / 2) + "px",
        top: height / 2 - ($dialog.height() / 2) + "px"
      });
    }


    function panelHeaderClicked(evt) {
      evt.preventDefault();

      if(!context.JK.currentUserId) {
        showDialog('login-required-dialog');
        return false;
      }

      expandedPanel = $(evt.currentTarget).closest('[layout="panel"]').attr("layout-id");
      layout();
      return false;
    }

    function wizardLinkClicked(evt) {
      evt.preventDefault();
      var targetStepId = $(evt.currentTarget).attr("layout-wizard-link");
      setWizardStep(targetStepId);
      return false;
    }

    function startNewFtue() {
      // var step = 0;
      //setWizardStep(step);
      //wizardShowFunctions[step]();
      return showDialog('gear-wizard');
    }

    function setWizardStep(targetStepId) {
      var selector = '[layout-wizard-step="' + targetStepId + '"]';
      var $targetStep = $(selector);
      var stepDialogTitle = $targetStep.attr("dialog-title");
      if (stepDialogTitle) {
        var $myDialog = $targetStep.closest('[layout="dialog"]');
        var $myTitle = $('.content-head h1', $myDialog);
        $myTitle.html(stepDialogTitle);
      }
      // Hide all steps:
      // Invoke the 'show' function, if present prior to actually showing.
      if (context._.contains(context._.keys(wizardShowFunctions), targetStepId)) {
        wizardShowFunctions[targetStepId]();
      }
      $('[layout-wizard-step]').hide();
      $targetStep.show();

      var ftuePurpose = $targetStep.attr("dialog-purpose");
      context.JK.GA.trackFTUECompletion(ftuePurpose, context.JK.detectOS());
    }

    function trackLocationChange(e) {
      context.JK.GA.virtualPageView(location.pathname + location.search + location.hash);
    }

    function onHandleKey(e) {

      if (e.keyCode == 27 /** esc */)
      {
          if(isDialogShowing()) {
            var $dialog = currentDialog();
            if(!$dialog)  {
              logger.error("unable to find current dialog on ESC");
              return;
            }

            cancelDialog($dialog.attr('layout-id'));
          }
      }
    }

    function handleDialogState() {
      var rawDialogState = $.cookie('dialog_state');
      try {
        var dialogState = JSON.parse(rawDialogState);
        if (!dialogState) {
          $.removeCookie('dialog_state');
          return;
        }
      }
      catch (e) {
        $.removeCookie('dialog_state');
        return;
      }

      var dialogName = dialogState['name'];
      if (dialogName) {
        setTimeout(function () {
          // TODO: we need a 'everything is initialized' event
          showDialog(dialogName);
        }, 0);
      }
      $.removeCookie('dialog_state');
    }

    // on next page load, a dialog of this name will show
    function queueDialog(name) {
      $.cookie('dialog_state', JSON.stringify({name: name}))
    }

    function events() {
      $(context).resize(function () {
        if (resizing) {
          context.clearTimeout(resizing);
        }
        resizing = context.setTimeout(layout, 80);
      });
      $('body').on('click', '[layout-link]', linkClicked);
      $('[layout-action="close"]').on('click', close);
      $('[layout-action="cancel"]').on('click', cancel);
      $('[layout-sidebar-expander]').on('click', toggleSidebar);
      $('[layout-panel="expanded"] [layout-panel="header"]').on('click', panelHeaderClicked);
      $('[layout-wizard-link]').on('click', wizardLinkClicked);
      $('[tab-target]').on('click', tabClicked);
      $(context).on('hashchange', trackLocationChange);
      $(document).keyup(onHandleKey);
    }

    // public functions
    this.getOpts = function () {
      return opts;
    };

    // used for concurrent notifications
    var notifyQueue = [];
    var firstNotification = false;
    var notifyDetails;

    var okButton = {
      id: "btn-okay",
      text: "OKAY",
      "layout-action": "close",
      href: "#",
      "class": "button-orange"
    };

    var cancelButton = {
      id: "btn-cancel",
      text: "CANCEL",
      "layout-action": "close",
      href: "#",
      "class": "button-grey"
    };

    var defaultButtons = [
      okButton,
      cancelButton
    ];

    this.notify = function (message, buttons, noCancel) {
      if (!buttons) {
        if (noCancel) {
          buttons = [okButton];
        }
        else {
          buttons = defaultButtons;
        }
      }

      // this allows clients to just specify the important action button without having to repeat the cancel descripton everywhere
      if (buttons.length === 1) {
        // jkolyer:  changed default to remove cancel as this is used as an alert, not a confirmation (see ConfirmDialog)
        // buttons.push(cancelButton);
      }

      var $notify = $('[layout="notify"]');

      if (notifyQueue.length === 0) {
        firstNotification = true;
        setNotificationInfo(message, buttons, $notify);
      }

      notifyQueue.push({message: message, descriptor: buttons});
      // JW - speeding up the in/out parts of notify. Extending non-moving time.
      $notify.slideDown(250)
        .delay(4000)
        .slideUp({
          duration: 400,
          queue: true,
          complete: function () {
            notifyDetails = notifyQueue.shift();

            // shift 1 more time if this is first notification being displayed
            if (firstNotification) {
              notifyDetails = notifyQueue.shift();
              firstNotification = false;
            }

            if (notifyDetails !== undefined) {
              setNotificationInfo(notifyDetails.message, notifyDetails.descriptor, $notify);
            }

            notifyDetails = {};
          }
        });
    };

    function setNotificationInfo(message, buttons, notificationSelector) {
      var $notify = $('[layout="notify"]');
      $('h2', $notify).text(message.title);
      $('p', $notify).empty();
      if (message.text instanceof jQuery) {
        $('p', $notify).append(message.text)
      }
      else {
        $('p', $notify).html(message.text);
      }

      if (message.icon_url) {
        $('#avatar', $notify).attr('src', message.icon_url);
        $('#notify-avatar', $notify).show();
      }
      else {
        $('#notify-avatar', $notify).hide();
      }

      if (message.detail) {
        $('div.detail', $notify).html(message.detail).show();
      }
      else {
        $('div.detail', $notify).hide();
      }

      var $buttonDiv = $('#buttons', $notify);
      $buttonDiv.empty();

      $.each(buttons, function(index, val) {

        // build button HTML based on KV pairs
        var keys = Object.keys(val);
        var buttonHtml = '<a';

        for (var i=0; i < keys.length; i++) {
          //console.log("keys[i]=%o, val[keys[i]]=%o", keys[i], val[keys[i]]);

          // ignore button text and callback info
          if (keys[i] !== 'text' && keys[i] !== 'callback' && keys[i] !== 'callback_args') {
            buttonHtml += ' ' + keys[i] + '="' + val[keys[i]] + '"';
          }
        }

        buttonHtml += '>' + val['text'] + '</a>&nbsp;&nbsp;';
        $buttonDiv.append(buttonHtml);

        // ensure it doesn't fire twice
        $('#' + val.id, $notify).unbind('click');

        $('#' + val.id, $notify).click(function() {
          if (val.callback !== undefined) {
            if (val.callback_args) {
              val.callback(val.callback_args);
            }
            else {
              val.callback();
            }
            return false;
          }
          else {
            notificationSelector.hide();
            return false;
          }
        });
      });
    }

    this.setWizardStep = setWizardStep;
    this.startNewFtue = startNewFtue;

    this.changeToScreen = function (screen, data) {
      changeToScreen(screen, data);
    };

    this.onHashChange = function(e, postFunction) {
      return onHashChange(e, postFunction);
    }

    this.showDialog = function (dialog, options) {
      return showDialog(dialog, options);
    };

    this.dialogObscuredNotification = function(payload) {
      return dialogObscuredNotification(payload);
    }

    this.isNoisyNotification= function(payload) {
      return isNoisyNotification(payload);
    }

    this.shouldFreezeAppOnDisconnect = function() {
      return shouldFreezeAppOnDisconnect();
    }

    this.isDialogShowing = function() {
      return isDialogShowing(arguments[0]);
    }

    this.activeElementEvent = function(evtName, data) {
      return activeElementEvent(evtName, data);
    }

    this.getCurrentScreen = function() {
      return currentScreen; // will be a string of the layout-id of the active screen
    }
    this.close = function (evt) {
      close(evt);
    };

    this.beforeDisconnect = function() {
      fireEvents();
    }

    this.afterReconnect = function() {
      fireEvents();
    }

    this.closeDialog = closeDialog;
    this.cancelDialog = cancelDialog;

    this.handleDialogState = handleDialogState;
    this.queueDialog = queueDialog;

    /**
     * Given information on a grid, and a given card's grid settings, use the
     * margin options and return a list of [top, left, width, height]
     * for the cell.
     */
    this.getCardLayout = function (gridWidth, gridHeight, gridRows, gridCols, row, col, rowspan, colspan) {

      var _gridWidth = gridWidth + 3 * opts.gridPadding;
      var _gridHeight = gridHeight + 3 * opts.gridPadding;
      var cellWidth, cellHeight, top, left, width, height;

      cellWidth = Math.floor((_gridWidth - 2 * opts.gridOuterMargin) / gridCols);
      cellHeight = Math.floor((_gridHeight - 2 * opts.gridOuterMargin) / gridRows);
      width = colspan * cellWidth - 2 * opts.gridPadding;
      height = rowspan * cellHeight - 2 * opts.gridPadding;
      top = row * cellHeight;//  + opts.gridOuterMargin; // + opts.gridPadding;
      left = col * cellWidth;//  + opts.gridOuterMargin; // + opts.gridPadding;

      return {
        top: top,
        left: left,
        width: width,
        height: height
      };
    };

    this.bindScreen = function (screen, handler) {
      screenBindings[screen] = handler;
    };

    this.bindDialog = function (dialog, handler) {
      dialogBindings[dialog] = handler;
    };

    this.registerWizardStepFunction = function (stepId, showFunction) {
      wizardShowFunctions[stepId] = showFunction;
    };

    this.initialize = function (inOpts) {
      me = this;
      opts = $.extend(opts, inOpts);
      setup();
      events();
    };

    return this;

  };

}(window, jQuery));