/**
 * Common utility functions.
 */
(function (context, $) {

  "use strict";
  context.JK = context.JK || {};
  var logger = context.JK.logger;
  var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR;

  var ALERT_NAMES = context.JK.ALERT_NAMES;
  var ALERT_TYPES = context.JK.ALERT_TYPES;
  var EVENTS = context.JK.EVENTS;

  var days = new Array("Sun", "Mon", "Tue",
    "Wed", "Thu", "Fri", "Sat");

  var months = new Array("January", "February", "March",
    "April", "May", "June", "July", "August", "September",
    "October", "November", "December");

  var os = null;
  var client_type = null;

  var reactHovers = []
  context.JK.getGenreList = function() {
    return context.JK.Rest().getGenres();
  }

  context.JK.stringToBool = function (s) {
    switch (s.toLowerCase()) {
      case "true":
      case "yes":
      case "1":
        return true;
      case "false":
      case "no":
      case "0":
      case null:
        return false;
      default:
        return Boolean(s);
    }
  };

  // http://stackoverflow.com/a/8809472/834644
  context.JK.generateUUID = function () {
    var d = new Date().getTime();
    var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
      var r = (d + Math.random() * 16) % 16 | 0;
      d = Math.floor(d / 16);
      return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16);
    });
    return uuid;
  };

  // Build up two maps of images, for each instrument id.
  // This map is a simple base map of instrument id to the basic image name.
  // Below, a loop goes through this and builds two size-specific maps.
  // In the future, we should test Whether having a single larger image
  // available, and allowing the browser to resize offers better quality.
  var icon_map_base = {
    "accordion": "accordion",
    "acoustic guitar": "acoustic_guitar",
    "banjo": "banjo",
    "bass guitar": "bass_guitar",
    "cello": "cello",
    "clarinet": "clarinet",
    "computer": "computer",
    "_default": "default",
    "drums": "drums",
    "percussion": "drums",
    "electric guitar": "electric_guitar",
    "euphonium": "euphonium",
    "flute": "flute",
    "french horn": "french_horn",
    "harmonica": "harmonica",
    "keyboard": "keyboard",
    "mandolin": "mandolin",
    "oboe": "oboe",
    "other": "other",
    "piano": "piano",
    "saxophone": "saxophone",
    "trombone": "trombone",
    "trumpet": "trumpet",
    "tuba": "tuba",
    "ukulele": "ukulele",
    "upright bass": "upright_bass",
    "double bass": "double_bass",
    "viola": "viola",
    "violin": "violin",
    "voice": "voice"
  };

  var instrumentIconMap24 = {};
  var instrumentIconMap45 = {};
  var instrumentIconMap256 = {};
  var notSpecifiedText = "Not specified";

  $.each(icon_map_base, function (instrumentId, icon) {
    instrumentIconMap24[instrumentId] = {asset: "/assets/content/icon_instrument_" + icon + "24.png", name: instrumentId};
    instrumentIconMap45[instrumentId] = {asset: "/assets/content/icon_instrument_" + icon + "45.png", name: instrumentId};
    instrumentIconMap256[instrumentId] = {asset: "/assets/content/icon_instrument_" + icon + "256.png", name: instrumentId};
  });

  context.JK.helpBubbleFunctionHelper = function(originalFunc) {
    var helpText = originalFunc.apply(this);
    var holder = $('<div class="hover-bubble help-bubble"></div>');
    holder.append(helpText);
    return holder;
  }

  /** same as helpBubble(), but doesnt' go away until the user clicks somewhere */
  context.JK.helpBubblePersist = function ($element, templateName, data, options) {
    if(!options) options = {};
    options['trigger'] = 'none';
    options['clickAnywhereToClose'] = true
    options['closeWhenOthersOpen']= true
    options['persist'] = true
    context.JK.helpBubble($element, templateName, data, options)
  }

  /** shows up once.   Up to you to call btOff() when appropriate */
  context.JK.onceBubble = function ($element, templateName, data, options) {
    if(!options) options = {};
    options['trigger'] = 'none';
    options['clickAnywhereToClose'] = false
    options['closeWhenOthersOpen']= false
    context.JK.helpBubble($element, templateName, data, options)
    $element.btOn()
  }

  /**
   * Associates a help bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips)
   * @param $element The element that should show the help when hovered
   * @param templateName the name of the help template (without the '#template-help' prefix). Add to _help.html.slim
   * @param data (optional) data for your template, if applicable
   * @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips
   */
  context.JK.helpBubble = function ($element, templateName, data, options) {
    if (!data) {
      data = {};
    }
    if(!options) {
      options = {};
    }

    var originalPostShow = options.postShow;
    options.postShow = function(container) {
      context.JK.popExternalLinks($(container))

      if (originalPostShow) {
        originalPostShow($(container));
      }
    }


    if(options.persist) {

      var timeout = null;

      $element.hoverIntent({
        over: function() {
          $element.btOn();
        },
        out: function() {

        }});
    }
    $element.on('remove', function() {
      $element.btOff(); // if the element goes away for some reason, get rid of the bubble too
    })

    var holder = null;
    if (context._.isFunction(templateName)) {
      holder = function wrapper() {
        return context.JK.helpBubbleFunctionHelper.apply(this, [templateName]);
      }
    }
    else {
      try {
        var $template = $('#template-help-' + templateName)
        if ($template.length == 0) {
          var helpText = templateName;
        }
        else {
          var helpText = context._.template($template.html(), data, { variable: 'data' });
        }
      }
      catch(e) {
       var helpText = templateName;
      }

      holder = $('<div class="hover-bubble help-bubble"></div>');
      holder = holder.append(helpText).html()
    }

    context.JK.hoverBubble($element, holder, options);
  }

  /**
   * Meant to show a little bubble to confirm something happened, in a way less obtrusive than a app.notify
   * @param $element The element that should show the help when hovered
   * @param templateName the name of the help template (without the '#template-help' prefix). Add to _help.html.slim
   * @param data (optional) data for your template, if applicable
   * @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips
   */
  context.JK.ackBubble = function($element, templateName, data, options) {
    if(!options) options = {};
    options.spikeGirth = 0;
    options.spikeLength = 0;
    options.shrinkToFit = true
    options.duration = 3000
    options.cornerRadius = 5

    return context.JK.prodBubble($element, templateName, data, options);
  }
  /**
   * Associates a help bubble immediately with the specified $element, using jquery.bt.js (BeautyTips)
   * By 'prod' it means to literally prod the user, to make them aware of something important because they did something else
   *
   * This will open a bubble immediately and show it for 4 seconds,
   * if you call it again before the 4 second timer is up, it will renew the 4 second timer.
   * @param $element The element that should show the help when hovered
   * @param templateName the name of the help template (without the '#template-help' prefix). Add to _help.html.slim
   * @param data (optional) data for your template, if applicable
   * @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips
   */
  context.JK.prodBubble = function($element, templateName, data, options) {
    if(!options) options = {};
    options['trigger'] = 'none';
    options['clickAnywhereToClose'] = false
    if(!options['duration']) options['duration'] = 6000;

    var existingTimer = $element.data("prodTimer");
    if(existingTimer) {
      clearTimeout(existingTimer);
      $element.btOn();
    }
    else {
      context.JK.helpBubble($element, templateName, data, options);
      $element.btOn();
    }
    $element.data("prodTimer", setTimeout(function() {
      $element.data("prodTimer", null);
      $element.btOff();
    }, options['duration']));

    $element.on('remove', function() {
      var timer = $element.data('prodTimer')
      if(timer) {
        clearTimeout(timer);
        $element.data("prodTimer", null);
        $element.btOff();
      }
    })
    return $element;
  }

  /** Creates a hover element that does not dissappear when the user mouses over the hover.
   *
   * @param $element
   * @param text
   * @param options
   */
  context.JK.interactReactBubble = function($element, reactElementName, reactPropsCallback, options) {

    if(!options) options = {};

    context._.each(reactHovers, function(react) {
      reactHovers.btOff();
    })
    reactHovers = []
    var reactElement = null
    var reactDomNode = null;

    function cleanupReact() {
      if(reactDomNode) {
        logger.debug()
        React.unmountComponentAtNode(reactDomNode)
      }
    }
    function waitForBubbleHover($bubble) {
      $bubble.hoverIntent({
        over: function() {
          if(timeout) {
            clearTimeout(timeout);
            timeout = null;
          }
        },
        out: function() {
          //$element.btOff();
        }});
    }

    var timeout = null;

    options.postHide = cleanupReact;
    options.trigger = 'none'
    options.clickAnywhereToClose = true
    options.closeWhenOthersOpen = true
    options.preShow = function(container) {
      var reactElement = context[reactElementName]
      if(!reactElementName) {
        throw "unknown react element" + reactElementName
      }
      reactElement= React.createElement(reactElement, reactPropsCallback());
      var $container = $(container)
      reactDomNode = $container.find('.react-holder').get(0)
      $(reactDomNode).data('bt', $element)
      React.render(reactElement, reactDomNode)
    }
    options.postShow = function(container) {

      if(timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      waitForBubbleHover($(container))
      timeout = setTimeout(function() {/**$element.btOff()*/}, 3000)
    }

    $element.hoverIntent({
      over: function() {
        $element.btOn();
      },
      out: function() {

      }});

    options.cssStyles = {}
    options.padding = 0;
    var extra = options.extraClasses || ''
    context.JK.hoverBubble($element, '<div class="react-holder ' + reactElementName + ' ' + extra + '"></div>', options)
    return $element;
  }

  /**
   * Associates a bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips)
   * @param $element  The element that should show the bubble when hovered
   * @param text the text or jquery element that should be shown as contents of the bubble
   * @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips
   */
  context.JK.hoverBubble = function ($element, text, options) {
    if (!text) {
      logger.error("hoverBubble: no text to attach to $element %o", $element);
      return;
    }

    $element.off('remove').on('remove', function() {
     $element.btOff();
    })

    if ($element instanceof jQuery) {
      if ($element.length == 0) {
        logger.error("hoverBubble: no element specified with text %o", text);
        return;
      }
    }

    var defaultOpts = {
      fill: '#333',
      strokeStyle: '#ED3618',
      spikeLength: 10,
      spikeGirth: 10,
      padding: 8,
      cornerRadius: 0,
      cssStyles: {
        fontSize: '11px',
        color: '#cccccc',
        whiteSpace: 'normal'
      }
    };

    if (options) {
      options = $.extend(false, defaultOpts, options);
    }
    else {
      options = defaultOpts;
    }

    if(typeof text != 'string') {
      options.contentSelector = text;
      $element.bt(options);
    }
    else {
      $element.bt(text, options);
    }

  }

  context.JK.groupIdDisplay = function(mixer) {
    if(mixer && mixer.group_id) {
      return context.JK.ChannelGroupLookup[mixer.group_id]
    }
    else {
      return "?group?"
    }
  }

  context.JK.bindProfileClickEvents = function($parent, dialogsToClose) {
    if (!$parent) {
      $parent = $('body');
    }

    function closeDialogs() {
      if (dialogsToClose) {
        $.each(dialogsToClose, function(index, val) {
          JK.app.layout.closeDialog(val);
        });
      }
    }

    $("[profileaction='band']", $parent).unbind('click');
    $("[profileaction='band']", $parent).click(function(evt) {
      closeDialogs();
      window.location = "/client#/bandProfile/" + $(this).attr('band-id');
    });

    $("[profileaction='musician']", $parent).unbind('click');
    $("[profileaction='musician']", $parent).click(function(evt) {
      closeDialogs();
      window.location = "/client#/profile/" + $(this).attr('user-id');
    });
  }

  context.JK.bindInstrumentHover = function($parent, options) {
    context._.each($parent.find("[hoveraction='instrument']"), function(element) {
      var $element = $(element);

      var instrumentId = $element.attr('data-instrument-id');

      if(instrumentId) {
        if (instrumentId === "null") {
          instrumentId = "not specified";
        }
        context.JK.hoverBubble($element, instrumentId, options);
      }
      else {
        logger.warn("no instrument-id on hoveraction=instrument element")
      }
    });
  }

  context.JK.calculateHoverPosition = function(mouseX, mouseY, hoverWidth, hoverHeight, $hoverElement) {
    var mouseLeft = mouseX < (window.innerWidth / 2);
    var mouseTop = mouseY < (window.innerHeight / 2);

    var hoverElementX = $hoverElement.offset().left;
    var hoverElementY = $hoverElement.offset().top;
    var hoverElementWidth = $hoverElement.width();
    var hoverElementHeight = $hoverElement.height();

    var css = {};
    if (mouseLeft) {
      css.left =  hoverElementX + hoverElementWidth + 5 + 'px';
    }
    else {
      css.left = hoverElementX - hoverWidth - 5 + 'px';
    }
    if (mouseTop) {
      css.top = hoverElementY + 'px';
    }
    else {
      css.top = hoverElementY - hoverHeight + 'px';
    }

    return css;
  }

  context.JK.bindHoverEvents = function ($parent, prefixData) {
    var timeout = 300;
    var fadeoutValue = 100;
    var sensitivity = 3;
    var interval = 500;

    var hoveractionAttr;
    var userIdAttr;
    var bandIdAttr;
    var recordingIdAttr;
    var sessionIdAttr;

    if(prefixData) {
      hoveractionAttr = 'data-hoveraction'
      userIdAttr = 'data-user-id'
      bandIdAttr = 'data-band-id'
      recordingIdAttr = 'data-recording-id'
      sessionIdAttr = 'data-session-id'
    }
    else {
      hoveractionAttr = 'hoveraction'
      userIdAttr = 'user-id'
      bandIdAttr = 'band-id'
      recordingIdAttr = 'recording-id'
      sessionIdAttr = 'session-id'
    }

    if (!$parent) {
      $parent = $('body');
    }

    function showBubble(bubble, $hoverElement) {
      $hoverElement.attr("bubble-id", bubble.id);
      bubble.showBubble($hoverElement)
        .done(function() {

        $(bubble.id()).hover(
          function () {
            $(this).data('hovering', true)
            // do nothing when entering the bubble
          },
          function () {
            $(this).data('hovering', false).fadeOut(100);
          }
        );

      })
    }

    function hideBubble($hoverElement) {

      var bubbleSelector = $hoverElement.attr("bubble-id");

      // first check to see if the user isn't hovering over the hover bubble
      if (!$(bubbleSelector).data('hovering')) {
        $(bubbleSelector).fadeOut(fadeoutValue);
      }
    }

    // MUSICIAN
    $("[" + hoveractionAttr + "='musician']", $parent).hoverIntent({
      over: function(e) {
        var bubble = new JK.MusicianHoverBubble($(this).attr(userIdAttr), e.pageX, e.pageY);

        showBubble(bubble, $(this));


      },
      out: function () { // this registers for leaving the hoverable element
        hideBubble($(this));
      },
      sensitivity: sensitivity,
      interval: interval,
      timeout: timeout
    });

    // FAN
    $("[" + hoveractionAttr + "='fan']", $parent).hoverIntent({
      over: function(e) {
        var bubble = new JK.FanHoverBubble($(this).attr(userIdAttr), e.pageX, e.pageY);
        showBubble(bubble, $(this));
      },
      out: function () { // this registers for leaving the hoverable element
        hideBubble($(this));
      },
      sensitivity: sensitivity,
      interval: interval,
      timeout: timeout
    });

    // BAND
    $("[" + hoveractionAttr +"='band']", $parent).hoverIntent({
      over: function(e) {
        var bubble = new JK.BandHoverBubble($(this).attr(bandIdAttr), e.pageX, e.pageY);
        showBubble(bubble, $(this));
      },
      out: function () { // this registers for leaving the hoverable element
        hideBubble($(this));
      },
      sensitivity: sensitivity,
      interval: interval,
      timeout: timeout
    });

    // SESSION
    $("[" + hoveractionAttr + "='session']", $parent).hoverIntent({
      over: function(e) {
        var bubble = new JK.SessionHoverBubble($(this).attr(sessionIdAttr), e.pageX, e.pageY);
        showBubble(bubble, $(this));
      },
      out: function () { // this registers for leaving the hoverable element
        hideBubble($(this));
      },
      sensitivity: sensitivity,
      interval: interval,
      timeout: timeout
    });

    // RECORDING
    $("[" + hoveractionAttr + "='recording']", $parent).hoverIntent({
      over: function(e) {
        var bubble = new JK.RecordingHoverBubble($(this).attr(recordIdAttr), e.pageX, e.pageY);
        showBubble(bubble, $(this));
      },
      out: function () { // this registers for leaving the hoverable element
        hideBubble($(this));
      },
      sensitivity: sensitivity,
      interval: interval,
      timeout: timeout
    });
  }

  context.JK.fetchUserNetworkOrServerFailure = function () {
    JK.app.notify({
      title: "Unable to communicate with server",
      text: "Please try again later",
      icon_url: "/assets/content/icon_alert_big.png"
    });
  }

  context.JK.entityNotFound = function (type) {
    JK.app.notify({
      title: type + " Deleted",
      text: "The " + type + " no longer exists.",
      icon_url: "/assets/content/icon_alert_big.png"
    });
  }

  // http://stackoverflow.com/questions/4878756/javascript-how-to-capitalize-first-letter-of-each-word-like-a-2-word-city
  context.JK.toTitleCase = function(str) {
    return str.replace(/\w\S*/g, function(txt){
      return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
    });
  }


  // Uber-simple templating
  // var template = "Hey {name}";
  // var vals = { name: "Jon" };
  // _fillTemplate(template, vals);
  // --> "Hey Jon"
  //
  // use context._.template for something more powerful
  context.JK.fillTemplate = function (template, vals) {
    if (template == null) throw 'null template in fillTemplate'
    for (var val in vals)
      template = template.replace(new RegExp('{' + val + '}', 'g'), vals[val]);
    return template;
  };

  context.JK.resolveAvatarUrl = function (photo_url) {
    var client_type = context.JK.clientType();
    if(client_type == 'browser') {
      return photo_url ? photo_url : "/assets/shared/avatar_generic.png";
    }
    else {
      return photo_url ? photo_url.replace('https://', 'http://') : "/assets/shared/avatar_generic.png";
    }

  };

  context.JK.resolveBandAvatarUrl = function (photo_url) {
    var client_type = context.JK.clientType();
    if(client_type == 'browser') {
      return photo_url ? photo_url : "/assets/shared/avatar_generic_band.png";
    }
    else {
      return photo_url ? photo_url.replace('https://', 'http://') : "/assets/shared/avatar_generic_band.png";
    }

  };

  context.JK.getInstrumentIconMap24 = function () {
    return instrumentIconMap24;
  };

  context.JK.getInstrumentIconMap45 = function () {
    return instrumentIconMap45;
  };

  context.JK.getInstrumentIconMap256 = function () {
    return instrumentIconMap256;
  };

  context.JK.getInstrumentIcon24 = function (instrument) {
    if (instrument in instrumentIconMap24) {
      return instrumentIconMap24[instrument].asset;
    }

    return instrumentIconMap24["_default"].asset;
  };

  context.JK.getInstrumentIcon45 = function (instrument) {
    if (instrument in instrumentIconMap45) {
      return instrumentIconMap45[instrument].asset;
    }

    return instrumentIconMap45["_default"].asset;
  };

  context.JK.getInstrumentIcon256 = function (instrument) {
    if (instrument in instrumentIconMap256) {
      return instrumentIconMap256[instrument].asset;
    }

    return instrumentIconMap256["_default"].asset;
  };

  context.JK.getInstrumentId = function(instrumentId) {
    return instrumentId ? instrumentId : notSpecifiedText;
  }

  // meant to pass in a bunch of images with an instrument-id attribute on them.

  context.JK.setInstrumentAssetPath = function ($elements) {
    $.each($elements, function (index, item) {
      var $element = $(this);
      if (!$element.is('img')) {
        throw "expected to receive an <img> in setInstrumentAssetPath"
      }

      var instrument = $element.attr('instrument-id');

      $element.attr('src', context.JK.getInstrumentIcon24(instrument))
    })
  }

  // creates an array with entries like [{ id: "drums", description: "Drums"}, ]
  context.JK.listInstruments = function () {
    var instrumentArray = [];
    $.each(context.JK.server_to_client_instrument_map, function (key, val) {
      instrumentArray.push({"id": context.JK.server_to_client_instrument_map[key].client_id, "description": key});
    });
    return instrumentArray;
  }

  context.JK.convertClientInstrumentToServer = function(clientId) {
    var serverInstrument = context.JK.client_to_server_instrument_map[clientId]

    if (!serverInstrument)
      return 'other'
    else
      return serverInstrument.server_id
  }

  context.JK.showErrorDialog = function (app, msg, title) {
    app.layout.showDialog('error-dialog');
    $('#error-msg', 'div[layout-id="error-dialog"]').html(msg);
    $('#error-summary', 'div[layout-id="error-dialog"]').html(title);
  }

  // TODO: figure out how to handle this in layout.js for layered popups
  context.JK.showOverlay = function () {
    $('.dialog-overlay').show();
  }

  context.JK.hideOverlay = function () {
    $('.dialog-overlay').hide();
  }

  // usage: context.JK.onBackendEvent(ALERT_NAMES.SOME_EVENT)
  context.JK.onBackendEvent = function(type, namespace, callback) {
    var alertData = ALERT_TYPES[type];
    if(!alertData) {throw "onBackendEvent: unknown alert type " + type};
    logger.debug("onBackendEvent: " + alertData.name + '.' + namespace)
    $(document).on(alertData.name + '.' + namespace, callback);
  }

  context.JK.offBackendEvent = function(type, namespace, callback) {
    var alertData = ALERT_TYPES[type];
    if(!alertData) {throw "offBackendEvent: unknown alert type " + type};
    logger.debug("offBackendEvent: " + alertData.name + '.' + namespace, alertData)
    $(document).off(alertData.name + '.' + namespace);
  }

  /*
   * Loads a listbox or dropdown with the values in input_array, setting the option value
   * to the id_field and the option text to text_field. It will preselect the option with
   * value equal to selected_id.
   */
  context.JK.loadOptions = function (templateHtml, listbox_id, input_array, id_field, text_field, selected_id) {
    $.each(input_array, function (index, val) {
      var isSelected = "";
      if (val[id_field] === selected_id) {
        isSelected = "selected";
      }
      var html = context.JK.fillTemplate(templateHtml, {
        value: val[id_field],
        label: val[text_field],
        selected: isSelected
      });

      listbox_id.append(html);
    });
  }

  context.JK.trimString = function (str) {
    return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
  };

  context.JK.padString = function (str, max) {
    var retVal = '' + str;
    while (retVal.length < max) {
      retVal = '0' + retVal;
    }

    return retVal;
  }

  context.JK.formatDateTime = function (dateString) {
    var date = dateString instanceof Date ? dateString : new Date(dateString);
    return context.JK.padString(date.getMonth() + 1, 2) + "/" + context.JK.padString(date.getDate(), 2) + "/" + date.getFullYear() + " - " + date.toLocaleTimeString();
  }

  context.JK.formatDateShort = function (dateString) {
    var date = dateString instanceof Date ? dateString : new Date(dateString);
    return months[date.getMonth()] + ' ' + date.getDate() + ', ' +  date.getFullYear();
  }

  // returns Fri May 20, 2013
  context.JK.formatDate = function (dateString, suppressDay) {
    if (!dateString) {
      return 'N/A'
    }

    var date = new Date(dateString);
    return (suppressDay ? '' : (days[date.getDay()] + ' ')) + months[date.getMonth()] + ' ' + context.JK.padString(date.getDate(), 2) + ', ' + date.getFullYear();
  }

  // returns June for months 0-11
  context.JK.getMonth = function(monthNumber) {
    return months[monthNumber];
  }

  context.JK.formatDateYYYYMMDD = function(dateString) {
    var date = dateString instanceof Date ? dateString : new Date(dateString);
    return date.getFullYear() + '-' + context.JK.padString((date.getMonth() + 1).toString(), 2) + '-' + context.JK.padString(date.getDate(), 2);
  }

  context.JK.formatTime = function (dateString) {
    var date = new Date(dateString);
    return date.toLocaleTimeString();
  }

  context.JK.iconMapBase = function() {
    return icon_map_base
  }

  context.JK.formatUtcTime = function(date, change) {
    if (change) {
      date.setMinutes(Math.ceil(date.getMinutes() / 30) * 30);
    }
    var h12h = date.getHours();
    var m12h = date.getMinutes();
    var ampm;
    if (h12h >= 0 && h12h < 12) {
      if (h12h === 0) {
        h12h = 12; // 0 becomes 12
      }
      ampm = "AM";
    }
    else {
      if (h12h > 12) {
        h12h -= 12; // 13-23 becomes 1-11
      }
      ampm = "PM";
    }
    var timeString = ("00" + h12h).slice(-2) + ":" + ("00" + m12h).slice(-2) + " " + ampm;

    return timeString;
  }

  context.JK.prettyPrintElements = function ($elements) {
    $.each($elements, function (index, item) {
      var $item = $(item);
      $item.text(context.JK.prettyPrintSeconds(parseInt($item.attr('duration'))))
    });
  }

  context.JK.prettyPrintSeconds = function (seconds) {
    // from: http://stackoverflow.com/questions/3733227/javascript-seconds-to-minutes-and-seconds

    // Minutes and seconds
    var mins = ~~(seconds / 60);
    var secs = seconds % 60;

    // Hours, minutes and seconds
    var hrs = ~~(seconds / 3600);
    var mins = ~~((seconds % 3600) / 60);
    var secs = seconds % 60;

    // Output like "1:01" or "4:03:59" or "123:03:59"
    var ret = "";

    if (hrs > 0)
      ret += "" + hrs + ":" + (mins < 10 ? "0" : "");

    ret += "" + mins + ":" + (secs < 10 ? "0" : "");
    ret += "" + secs;
    return ret;
  }

    context.JK.alertSupportedNeeded = function(additionalContext) {
    var $item = context.JK.Banner.showAlert(additionalContext + '<br/><br/>Please <a href="https://jamkazam.freshdesk.com" rel="external">contact support</a>.');
    context.JK.popExternalLinks($item);
    return $item;
  }

  // returns:
  // * Win32
  // * MacOSX
  // * Unix
  context.JK.GetOSAsString = function() {
    if(!os) {
      if(context.jamClient && context.jamClient.IsNativeClient()) {
        os = context.jamClient.GetOSAsString();
      }
      else {
        os = context.JK.detectOS();
      }
    }
    return os;
  }

  context.JK.search = function (query, app, callback) {
    //logger.debug("search:  "+ query)
    $.ajax({
      type: "GET",
      dataType: "json",
      contentType: 'application/json',
      url: "/api/search?query=" + query,
      processData: false,
      success: function (response) {
        callback(response);
      },
      error: app.ajaxError
    });
  };

  /*
   * Get the length of a dictionary
   */
  context.JK.dlen = function (d) {
    var count = 0;
    for (var i in d) {
      if (d.hasOwnProperty(i)) {
        count++;
      }
    }
    return count;
  };

  /*
   * Get the keys of a dictionary as an array (same as Object.keys, but works in all browsers)
   */
  context.JK.dkeys = function (d) {
    var keys = []
    for (var i in d) {
      if (d.hasOwnProperty(i)) {
        keys.push(i);
      }
    }
    return keys;
  };

  /**
   * Finds the first error associated with the field.
   * @param fieldName the name of the field
   * @param errors_data response from a 422 response in ajax
   * @returns null if no error for the field name, or the 1st error associated with that field
   */
  context.JK.get_first_error = function (fieldName, errors_data) {
    var errors = errors_data["errors"];
    if (errors == null) return null;

    if (errors[fieldName] && errors[fieldName].length > 0) {
      return errors[fieldName][0]
    }
    else {
      return null;
    }
  }

  context.JK.getFullFirstError = function(fieldName, errors, field_mapper) {

    if (errors == null) return null;

    if (errors[fieldName] && errors[fieldName].length > 0) {
      var displayField = fieldName
      if (field_mapper) {
        displayField = field_mapper[fieldName]
        if (!displayField) {
          // in case mapper has no data for this field
          displayField = fieldName;
        }
      }
      return displayField + ' ' + errors[fieldName][0]
    }
    else {
      return null;
    }
  }
  /**
   * Returns a ul with an li per field name.
   * @param fieldName the name of the field
   * @param errors_data error data return by a 422 ajax response
   * @returns null if no error for the field name; otherwise a ul/li
   */
  context.JK.format_errors = function (fieldName, errors_data) {
    var errors = errors_data["errors"];
    if (errors == null) return null;

    var field_errors = errors[fieldName];
    if (field_errors == null) {
      return null;
    }

    var ul = $('<ul class="error-text"></ul>');

    $.each(field_errors, function (index, item) {
      ul.append(context.JK.fillTemplate("<li>{message}</li>", {message: item}))
    })

    return ul;
  }

  context.JK.reset_errors = function($container) {
    $container.find('.error-text').remove()
    $container.find('.error').removeClass("error")
  }

  context.JK.append_errors = function($field, fieldName, errors_data) {
    var $ul = context.JK.format_errors(fieldName, errors_data);
    if($ul != null) {
      delete errors_data['errors'][fieldName];
      $field.closest('div.field').addClass('error').end().after($ul);
    }
  }

  context.JK.format_all_errors = function (errors_data) {
    var errors = errors_data["errors"];
    if (errors == null) return  $('<ul class="error-text"><li>unknown error</li></ul>');

    var ul = $('<ul class="error-text"></ul>');

    $.each(errors, function (fieldName, field_errors) {

      $.each(field_errors, function (index, item) {
        ul.append(context.JK.fillTemplate("<li>{field} {message}</li>", {field: fieldName, message: item}))
      });
    });

    return ul;
  }

  context.JK.reactErrors = function (errors_data, fieldMapper) {
    var errors = errors_data && errors_data["errors"];
    if (errors == null) return  null;
    var items = []

    $.each(errors, function (fieldName, field_errors) {
      var displayName = fieldMapper && fieldMapper[fieldName]
      if (!displayName) {
        displayName = fieldName;
      }
      $.each(field_errors, function (index, item) {
        items.push(React.DOM.li({key: fieldName + item}, displayName + ' ' + item))
      });
    });

    return React.DOM.ul({className: 'error-text'}, null, items)
  }

  context.JK.reactSingleFieldErrors = function(fieldName, error_data) {
    if (!error_data) {
      return null;
    }

    if (!_.isArray(error_data)) {
      if (error_data.errors) {
        error_data = error_data.errors
      }

      error_data = error_data[fieldName]
      if (!error_data) {
        return null;
      }
    }

    var items = []

    $.each(error_data, function (i, error) {
        items.push(React.DOM.li({key: error}, error))
    });

    return React.DOM.ul({className: 'error-text'}, null, items)
  }

  /**
   * Way to verify that a number of parallel tasks have all completed.
   * Provide a function to evaluate completion, and a callback to
   * invoke when that function evaluates to true.
   * NOTE: this does not pause execution, it simply ensures that
   * when the test function evaluates to true, the callback will
   * be invoked.
   */
  context.JK.joinCalls = function (completionTestFunction, callback, interval) {
    function doneYet() {
      if (completionTestFunction()) {
        callback();
      } else {
        context.setTimeout(doneYet, interval);
      }
    }

    doneYet();
  };

  context.JK.initJamClient = function(app) {
    // If no jamClient (when not running in native client)
    // create a fake one.
    if (!(window.jamClient)) {
      var p2pMessageFactory = new JK.FakeJamClientMessages();
      window.jamClient = new JK.FakeJamClient(app, p2pMessageFactory);
      window.jamClient.SetFakeRecordingImpl(new JK.FakeJamClientRecordings(app, jamClient, p2pMessageFactory));
    }
    else if(false) { // set to true to time long running bridge calls
      var originalJamClient = window.jamClient;
      var interceptedJamClient = {};
      $.each(Object.keys(originalJamClient), function(i, key) {
        if(key.indexOf('(') > -1) {
          // this is a method. time it
          var jsKey = key.substring(0, key.indexOf('('))
          console.log("replacing " + jsKey)
          interceptedJamClient[jsKey] = function() {
            var original = originalJamClient[key]
            var start = new Date();
            if(key == "FTUEGetDevices()") {
              var returnVal = eval('originalJamClient.FTUEGetDevices(' + arguments[0] + ')');
            }
            else {
              var returnVal = original.apply(originalJamClient, arguments);
            }
            var time = new Date().getTime() - start.getTime();
            if(time >= 0) { // if 0, you'll see ALL bridge calls. If you set it to a higher value, you'll only see calls that are beyond that threshold
              console.error(time + "ms jamClient." + jsKey + ' returns=', returnVal);
            }

            return returnVal;
          }
        }
        else {
          // we need to intercept properties... but how?
        }
      });


      window.jamClient = interceptedJamClient;
    }

  }

  // pass in 'arguments' in a fail callback of a $.ajax
  context.JK.isNetworkError = function(failArgs) {
    if(failArgs.length != 3) throw "expected 3 arguments from .fail of $ajax in isNetworkError"
    var xhr = failArgs[0];

    return xhr.status == 0;
    }

  context.JK.clientType = function () {
    if (client_type) {
      return client_type;
    }
    else {
      if (context.jamClient) {
        client_type = context.jamClient.IsNativeClient() ? 'client' : 'browser';
      }
      else {
        client_type = 'browser';
      }
      return client_type;
    }
  }
  /**
   * Returns 'MacOSX' if the os appears to be macintosh,
   * 'Win32' if the os appears to be windows,
   * 'Unix' if the OS appears to be linux,
   * and null if unknown
   * @returns {*}
   */
  context.JK.detectOS = function () {
    if (!navigator.platform) {
      return null;
    }

    if (navigator.platform.toLowerCase().indexOf('mac') !== -1) {
      return "MacOSX";
    }
    if (navigator.platform.toLowerCase().indexOf('win') !== -1) {
      return "Win32"
    }
    if (navigator.platform.toLowerCase().indexOf('linux') !== -1) {
      return "Unix"
    }

    return null;
  }

  context.JK.makeAbsolute = function(path) {
    return window.location.protocol + '//' + window.location.host + path;
  }

  context.JK.popExternalLinks = function ($parent) {

    function popOpenBrowser (evt) {

      if (!context.jamClient) {
        return;
      }

      evt.preventDefault();
      var href = $(this).attr("href");

      if (href) {
        // make absolute if not already
        if (href.indexOf('http') != 0 && href.indexOf('mailto') != 0) {
          href = window.location.protocol + '//' + window.location.host + href;
        }

        context.jamClient.OpenSystemBrowser(href);
      }
      return false;
    }

    if(!$parent) $parent = $('body');
    // Allow any a link with a rel="external" attribute to launch
    // the link in the default browser, using jamClient:
    var $links = $parent.find('a[rel="external"]')
    $links.off('click');
    $links.on('click', popOpenBrowser);
  }

  context.JK.popExternalLink = function (href, login) {
    if (!context.jamClient) {
      return;
    }

    if (href) {
      // make absolute if not already
      if (href.indexOf('http') != 0 && href.indexOf('mailto') != 0) {
        href = window.location.protocol + '//' + window.location.host + href;
      }

      if(login) {
        var rememberToken = $.cookie("remember_token");
        if(rememberToken) {
          href = window.location.protocol + '//' + window.location.host + "/passthrough?redirect-to=" + encodeURIComponent(href) + '&stoken=' + encodeURIComponent(rememberToken)
        }

      }
      context.jamClient.OpenSystemBrowser(href);
    }
    return false;
  }

  context.JK.popExternalLinkAndLogin = function(href) {

    var rememberToken = $.cookie("remember_token");

    context.JK.popExternalLink("https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-")
    if(rememberToken) {
      "https://"
    }
  }

  context.JK.checkbox = function ($checkbox, dark) {
    if (dark){
      return $checkbox.iCheck({
        checkboxClass: 'icheckbox_minimal dark',
        radioClass: 'iradio_minimal dark',
        inheritClass: true
      });
    }else {
      return $checkbox.iCheck({
        checkboxClass: 'icheckbox_minimal',
        radioClass: 'iradio_minimal',
        inheritClass: true
      });
    }

  }

  context.JK.dropdown = function ($select, options) {

    var opts = options || {}

    opts = $.extend({}, {nativeTouch: !(context.jamClient && context.jamClient.IsNativeClient()) && gon.global.env != "test", cutOff: 7}, opts)
    $select.each(function (index) {
      var $item = $(this);

      if ($item.data('easydropdown-select')) {
        // if this has already been initialized, re-init it so it picks up any new <options>
        $item.easyDropDown('destroy')
      }
      $item.easyDropDown(opts);
    })
  }

  context.JK.currentTimezone = function() {
    var tz = window.jstz.determine().name()

    if (tz == 'America/Chicago') {
      tz = 'US Central Time'
    }
    else if(tz == 'America/Los_Angeles' || tz == 'America/Los Angeles') {
      tz = 'US Pacific Time'
    }
    else if(tz == 'America/New_York' || tz == 'America/New York') {
      tz = 'US Eastern Time'
    }
    else if (tz == 'America/Arizona') {
      tz = 'US Mountain Time'
    }
    return tz;
  }

  context.JK.flash = function(msg, options) {
    options = options || {}

    var $flash = $(context._.template($('#template-flash-notice').html(), {}, { variable: 'data' }));
    $flash.find('.flash-content').html(msg);
    $('body').prepend($flash)
    if(options.hide) {
      // slide down (take 1 sec to do it), sit for 5, then leave over 1 second
      setTimeout(function() {$flash.slideUp(1000, 'swing') }, options.hide * 1000)
    }
  }

  context.JK.hasFlash = function () {
    var hasFlash = false;

    if (!context.jamClient || !context.jamClient.IsNativeClient()) {
      try {
        var fo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash');
        if (fo) hasFlash = true;
      } catch (e) {
        if (navigator.mimeTypes ["application/x-shockwave-flash"] != undefined) hasFlash = true;
      }
    }

    return hasFlash;
  }

  context.JK.getNameOfFile = function(filename) {

    var index = filename.lastIndexOf('/');
    if(index == -1) {
      index = filename.lastIndexOf('\\');
    }
    return index == -1 ? filename : filename.substring(index + 1, filename.length)
  }

  context.JK.hasOneConfiguredDevice = function () {
    var result = context.jamClient.FTUEGetGoodConfigurationList();
    logger.debug("hasOneConfiguredDevice: ", result);
    return result.length > 0;
  };

  context.JK.getGoodAudioConfigs = function () {
    var result = context.jamClient.FTUEGetGoodAudioConfigurations();
    logger.debug("goodAudioConfigs=%o", result);
    return result;
  };

  context.JK.getGoodConfigMap = function () {
    var goodConfigMap = [];
    var goodConfigs = context.JK.getGoodAudioConfigs();
    $.each(goodConfigs, function (index, profileKey) {
      var friendlyName = context.jamClient.FTUEGetConfigurationDevice(profileKey);
      goodConfigMap.push({key: profileKey, name: friendlyName});
    });

    return goodConfigMap;
  }

  context.JK.getBadAudioConfigs = function () {
    var badAudioConfigs = [];
    var allAudioConfigs = context.jamClient.FTUEGetAllAudioConfigurations();
    var goodAudioConfigs = context.JK.getGoodAudioConfigs();

    for (var i = 0; i < allAudioConfigs.length; i++) {
      if ($.inArray(allAudioConfigs[i], goodAudioConfigs) === -1) {
        badAudioConfigs.push(allAudioConfigs[i]);
      }
    }
    logger.debug("badAudioConfigs=%o", badAudioConfigs);
    return badAudioConfigs;
  };

  context.JK.getBadConfigMap = function () {
    var badConfigMap = [];
    var badConfigs = context.JK.getBadAudioConfigs();
    $.each(badConfigs, function (index, profileKey) {
      var friendlyName = context.jamClient.FTUEGetConfigurationDevice(profileKey);
      badConfigMap.push({key: profileKey, name: friendlyName});
    });

    return badConfigMap;
  }

  context.JK.getFirstGoodDevice = function (preferredDeviceId) {
    var badConfigs = context.JK.getBadAudioConfigs();

    function getGoodDevice() {
      var goodConfigs = context.JK.getGoodAudioConfigs();
      if (goodConfigs && goodConfigs.length > 0) {
        deviceId = goodConfigs[0];
      }
      else {
        deviceId = null;
      }
      return deviceId;
    }

    var deviceId = null;

    if (preferredDeviceId) {

      // if the preferred device is not in the list of invalid configs, use it
      if ($.inArray(preferredDeviceId, badConfigs) === -1) {
        deviceId = preferredDeviceId;
      }
      else {
        deviceId = getGoodDevice();
      }
    }
    else {
      deviceId = getGoodDevice();
    }
    return deviceId;
  }

  // returns /client#/home for http://www.jamkazam.com/client#/home
  context.JK.locationPath = function () {
    var bits = context.location.href.split('/');
    return '/' + bits.slice(3).join('/');
  }

  context.JK.nowUTC = function () {
    var d = new Date();
    return new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds());
  }

  context.JK.guardAgainstBrowser = function(app, args) {
    if(!gon.isNativeClient) {
      logger.debug("guarding against normal browser on screen that requires native client")
      app.layout.showDialog('launch-app-dialog', args)
        .one(EVENTS.DIALOG_CLOSED, function() {
          if(args && args.goHome) {
            window.location = '/client#/home';
          }
        })

      return false;
    }
    return true;
  }


  context.JK.createSession = function(app, data) {

    // auto pick an 'other' instrument
    var otherId = context.JK.server_to_client_instrument_map.Other.server_id; // get server ID
    var otherInstrumentInfo = context.JK.instrument_id_to_instrument[otherId]; // get display name
    var beginnerLevel  = 1; // default to beginner
    var instruments = [ {id: otherId, name: otherInstrumentInfo.display, level: beginnerLevel} ];
    $.each(instruments, function(index, instrument) {
      var slot = {};
      slot.instrument_id = instrument.id;
      slot.proficiency_level = instrument.level;
      slot.approve = true;
      data.rsvp_slots.push(slot);
    });

    data.isUnstructuredRsvp = true;

    return rest.createScheduledSession(data)
  }

  context.JK.privateSessionSettings = function(createSessionSettings) {
    createSessionSettings.genresValues = ['Pop'];
    createSessionSettings.genres = ['pop'];
    createSessionSettings.timezone = 'Central Time (US & Canada),America/Chicago'
    createSessionSettings.name = "Private Test Session";
    createSessionSettings.description = "Private session set up just to test things out in the session interface by myself.";
    createSessionSettings.notations = [];
    createSessionSettings.language = 'eng'
    createSessionSettings.legal_policy = 'Standard';
    createSessionSettings.musician_access = false
    createSessionSettings.fan_access = false
    createSessionSettings.fan_chat = false
    createSessionSettings.approval_required = false
    createSessionSettings.legal_terms = true
    createSessionSettings.recurring_mode = 'once';
    createSessionSettings.start = new Date().toDateString() + ' ' + context.JK.formatUtcTime(new Date(), false);
    createSessionSettings.duration = "60";
    createSessionSettings.open_rsvps = false
    createSessionSettings.rsvp_slots = [];
  }
  /*
   * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
   * Digest Algorithm, as defined in RFC 1321.
   * Copyright (C) Paul Johnston 1999 - 2000.
   * Updated by Greg Holt 2000 - 2001.
   * See http://pajhome.org.uk/site/legal.html for details.
   */

  /*
   * Convert a 32-bit number to a hex string with ls-byte first
   */
  var hex_chr = "0123456789abcdef";

  function rhex(num) {
    var str, j;
    str = "";
    for (j = 0; j <= 3; j++) {
      str += hex_chr.charAt((num >> (j * 8 + 4)) & 0x0F) +
        hex_chr.charAt((num >> (j * 8)) & 0x0F);
    }
    return str;
  }

  /*
   * Convert a string to a sequence of 16-word blocks, stored as an array.
   * Append padding bits and the length, as described in the MD5 standard.
   */
  function str2blks_MD5(str) {
    var nblk, blks, i;
    nblk = ((str.length + 8) >> 6) + 1;
    blks = new Array(nblk * 16);
    for (i = 0; i < nblk * 16; i++) blks[i] = 0;
    for (i = 0; i < str.length; i++)
      blks[i >> 2] |= str.charCodeAt(i) << ((i % 4) * 8);
    blks[i >> 2] |= 0x80 << ((i % 4) * 8);
    blks[nblk * 16 - 2] = str.length * 8;
    return blks;
  }

  /*
   * Add integers, wrapping at 2^32. This uses 16-bit operations internally
   * to work around bugs in some JS interpreters.
   */
  function add(x, y) {
    var lsw = (x & 0xFFFF) + (y & 0xFFFF);
    var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
    return (msw << 16) | (lsw & 0xFFFF);
  }

  /*
   * Bitwise rotate a 32-bit number to the left
   */
  function rol(num, cnt) {
    return (num << cnt) | (num >>> (32 - cnt));
  }

  /*
   * These functions implement the basic operation for each round of the
   * algorithm.
   */
  function cmn(q, a, b, x, s, t) {
    return add(rol(add(add(a, q), add(x, t)), s), b);
  }

  function ff(a, b, c, d, x, s, t) {
    return cmn((b & c) | ((~b) & d), a, b, x, s, t);
  }

  function gg(a, b, c, d, x, s, t) {
    return cmn((b & d) | (c & (~d)), a, b, x, s, t);
  }

  function hh(a, b, c, d, x, s, t) {
    return cmn(b ^ c ^ d, a, b, x, s, t);
  }

  function ii(a, b, c, d, x, s, t) {
    return cmn(c ^ (b | (~d)), a, b, x, s, t);
  }

  /*
   * Take a string and return the hex representation of its MD5.
   */
  context.JK.calcMD5 = function (str) {
    var x, a, b, c, d, i,
      olda, oldb, oldc, oldd;
    x = str2blks_MD5(str);
    a = 1732584193;
    b = -271733879;
    c = -1732584194;
    d = 271733878;

    for (i = 0; i < x.length; i += 16) {
      olda = a;
      oldb = b;
      oldc = c;
      oldd = d;

      a = ff(a, b, c, d, x[i + 0], 7, -680876936);
      d = ff(d, a, b, c, x[i + 1], 12, -389564586);
      c = ff(c, d, a, b, x[i + 2], 17, 606105819);
      b = ff(b, c, d, a, x[i + 3], 22, -1044525330);
      a = ff(a, b, c, d, x[i + 4], 7, -176418897);
      d = ff(d, a, b, c, x[i + 5], 12, 1200080426);
      c = ff(c, d, a, b, x[i + 6], 17, -1473231341);
      b = ff(b, c, d, a, x[i + 7], 22, -45705983);
      a = ff(a, b, c, d, x[i + 8], 7, 1770035416);
      d = ff(d, a, b, c, x[i + 9], 12, -1958414417);
      c = ff(c, d, a, b, x[i + 10], 17, -42063);
      b = ff(b, c, d, a, x[i + 11], 22, -1990404162);
      a = ff(a, b, c, d, x[i + 12], 7, 1804603682);
      d = ff(d, a, b, c, x[i + 13], 12, -40341101);
      c = ff(c, d, a, b, x[i + 14], 17, -1502002290);
      b = ff(b, c, d, a, x[i + 15], 22, 1236535329);

      a = gg(a, b, c, d, x[i + 1], 5, -165796510);
      d = gg(d, a, b, c, x[i + 6], 9, -1069501632);
      c = gg(c, d, a, b, x[i + 11], 14, 643717713);
      b = gg(b, c, d, a, x[i + 0], 20, -373897302);
      a = gg(a, b, c, d, x[i + 5], 5, -701558691);
      d = gg(d, a, b, c, x[i + 10], 9, 38016083);
      c = gg(c, d, a, b, x[i + 15], 14, -660478335);
      b = gg(b, c, d, a, x[i + 4], 20, -405537848);
      a = gg(a, b, c, d, x[i + 9], 5, 568446438);
      d = gg(d, a, b, c, x[i + 14], 9, -1019803690);
      c = gg(c, d, a, b, x[i + 3], 14, -187363961);
      b = gg(b, c, d, a, x[i + 8], 20, 1163531501);
      a = gg(a, b, c, d, x[i + 13], 5, -1444681467);
      d = gg(d, a, b, c, x[i + 2], 9, -51403784);
      c = gg(c, d, a, b, x[i + 7], 14, 1735328473);
      b = gg(b, c, d, a, x[i + 12], 20, -1926607734);

      a = hh(a, b, c, d, x[i + 5], 4, -378558);
      d = hh(d, a, b, c, x[i + 8], 11, -2022574463);
      c = hh(c, d, a, b, x[i + 11], 16, 1839030562);
      b = hh(b, c, d, a, x[i + 14], 23, -35309556);
      a = hh(a, b, c, d, x[i + 1], 4, -1530992060);
      d = hh(d, a, b, c, x[i + 4], 11, 1272893353);
      c = hh(c, d, a, b, x[i + 7], 16, -155497632);
      b = hh(b, c, d, a, x[i + 10], 23, -1094730640);
      a = hh(a, b, c, d, x[i + 13], 4, 681279174);
      d = hh(d, a, b, c, x[i + 0], 11, -358537222);
      c = hh(c, d, a, b, x[i + 3], 16, -722521979);
      b = hh(b, c, d, a, x[i + 6], 23, 76029189);
      a = hh(a, b, c, d, x[i + 9], 4, -640364487);
      d = hh(d, a, b, c, x[i + 12], 11, -421815835);
      c = hh(c, d, a, b, x[i + 15], 16, 530742520);
      b = hh(b, c, d, a, x[i + 2], 23, -995338651);

      a = ii(a, b, c, d, x[i + 0], 6, -198630844);
      d = ii(d, a, b, c, x[i + 7], 10, 1126891415);
      c = ii(c, d, a, b, x[i + 14], 15, -1416354905);
      b = ii(b, c, d, a, x[i + 5], 21, -57434055);
      a = ii(a, b, c, d, x[i + 12], 6, 1700485571);
      d = ii(d, a, b, c, x[i + 3], 10, -1894986606);
      c = ii(c, d, a, b, x[i + 10], 15, -1051523);
      b = ii(b, c, d, a, x[i + 1], 21, -2054922799);
      a = ii(a, b, c, d, x[i + 8], 6, 1873313359);
      d = ii(d, a, b, c, x[i + 15], 10, -30611744);
      c = ii(c, d, a, b, x[i + 6], 15, -1560198380);
      b = ii(b, c, d, a, x[i + 13], 21, 1309151649);
      a = ii(a, b, c, d, x[i + 4], 6, -145523070);
      d = ii(d, a, b, c, x[i + 11], 10, -1120210379);
      c = ii(c, d, a, b, x[i + 2], 15, 718787259);
      b = ii(b, c, d, a, x[i + 9], 21, -343485551);

      a = add(a, olda);
      b = add(b, oldb);
      c = add(c, oldc);
      d = add(d, oldd);
    }
    return rhex(a) + rhex(b) + rhex(c) + rhex(d);
  };

  /** validates that no changes are being made to tracks while recording */
  context.JK.verifyNotRecordingForTrackChange = function (app) {
    if (context.RecordingStore.recordingModel.isRecording()) {
      app.notify({
        title: "Currently Recording",
        text: "Tracks cannot be modified while recording.",
        icon_url: "/assets/content/icon_alert_big.png"});
      return false;
    }

    return true;
  }


})(window, jQuery);