(function(context,$) {

  "use strict";

  context.JK = context.JK || {};
  context.JK.NotificationPanel = function(app) {
    var EVENTS = context.JK.EVENTS;
    var logger = context.JK.logger;
    var friends = [];
    var rest = context.JK.Rest();
    var missedNotificationsWhileAway = false;
    var $panel = null;
    var $expanded = null;
    var $contents = null;
    var $count = null;
    var $list = null;
    var $notificationTemplate = null;
    var sidebar = null;
    var darkenedColor = '#0D7B89';
    var highlightedColor = 'white'
    var textMessageDialog = null;
    var queuedNotification = null;
    var queuedNotificationCreatedAt = null;
    var sessionUtils = context.JK.SessionUtils;
    var notificationBatchSize = 20;
    var currentNotificationPage = 0;
    var didLoadAllNotifications = false, isLoading = false;
    var ui = new context.JK.UIHelper(JK.app);

    function isNotificationsPanelVisible() {
      return $contents.is(':visible')
    }

    function incrementNotificationCount() {
      var count = parseInt($count.text());
      setCount(count + 1);
    }

    function decrementNotificationCount() {
      var count = parseInt($count.text());
      if(count > 0) {
        count = count - 1;
        setCount(count);
        if(count == 0) {
          lowlightCount();
          missedNotificationsWhileAway = false;
        }
      }
    }

    // set the element to white,  and pulse it down to the un-highlighted value 2x, then set
    function pulseToDark() {
      logger.debug("pulsing notification badge")
      lowlightCount();
      $count.pulse({'background-color' : highlightedColor}, {pulses: 2}, function() {
        $count.removeAttr('style')
        setCount(0);
      })
    }

    function setCount(count) {
      $count.text(count);
    }

    function lowlightCount() {
      $count.removeClass('highlighted');
    }

    function highlightCount() {
      $count.addClass('highlighted');
    }

    function queueNotificationSeen(notificationId, notificationCreatedAt, type) {

        var time = new Date(notificationCreatedAt);

        if(!notificationCreatedAt) {
          throw 'invalid value passed to queuedNotificationCreatedAt'
        }

        if(!queuedNotificationCreatedAt) {
          queuedNotification = notificationId;
          queuedNotificationCreatedAt = notificationCreatedAt;
          logger.debug("updated queuedNotificationCreatedAt with: " + notificationCreatedAt);
        }
        else if(time.getTime() > new Date(queuedNotificationCreatedAt).getTime()) {
          queuedNotification = notificationId;
          queuedNotificationCreatedAt = notificationCreatedAt;
          logger.debug("updated queuedNotificationCreatedAt with: " + notificationCreatedAt);
        }
        else {
          logger.debug("ignored queuedNotificationCreatedAt for: " + notificationCreatedAt);
        }
    }

    function onNotificationOccurred(payload) {
      if(userCanSeeNotifications(payload)) {
        app.updateNotificationSeen(payload.notification_id, payload.created_at);
      }
      else {
        if(app.layout.isNoisyNotification(payload) && !missedNotificationsWhileAway) {
          // this handles a special case--if a notification is too noisy while away, then don't bother
          // incrementing anything on the sidebar or otherwise distracting the user
          app.updateNotificationSeen(payload.notification_id, payload.created_at);
        }
        else {
          queueNotificationSeen(payload.notification_id, payload.created_at, payload.description);
          highlightCount();
          incrementNotificationCount();
          missedNotificationsWhileAway = true;
        }
      }
    }

    function userCameBack() {
      if(isNotificationsPanelVisible()) {
        if(missedNotificationsWhileAway) {
          // catch user's eye, then put count to 0
          pulseToDark();
          if(queuedNotificationCreatedAt) {
            app.updateNotificationSeen(queuedNotification, queuedNotificationCreatedAt);
          }
        }
      }

      queuedNotification = null;
      queuedNotificationCreatedAt = null;
      missedNotificationsWhileAway = false;
    }

    function opened() {
      queuedNotification = null;
      queuedNotificationCreatedAt = null;
      rest.updateUser({notification_seen_at: 'LATEST'})
        .done(function(response) {
          lowlightCount();
          setCount(0);
        })
        .fail(app.ajaxError)
    }

    function windowBlurred() {

    }

    function events() {
      $(context).on(EVENTS.DIALOG_CLOSED, function(e, data) {if(data.dialogCount == 0) userCameBack(); });
      $(window).focus(userCameBack);
      $(window).blur(windowBlurred);
      app.user()
        .done(function(user) {
          setCount(user.new_notifications);
          if(user.new_notifications > 0) {
            highlightCount();
          }
        });

      $panel.on('open', opened);

      // friend notifications
      registerFriendRequest();
      registerFriendRequestAccepted();
      registerNewUserFollower();
      registerNewBandFollower();

      // session notifications
      registerSessionInvitation();
      registerSessionEnded();
      registerJoinRequest();
      registerJoinRequestApproved();
      registerJoinRequestRejected();
      registerMusicianSessionJoin();
      registerBandSessionJoin();

      // scheduled sessions
      registerScheduledSessionInvitation();
      registerScheduledSessionRsvp();
      registerScheduledSessionRsvpApproved();
      registerScheduledSessionRsvpCancelled();
      registerScheduledSessionRsvpCancelledOrg();
      registerScheduledSessionCancelled();
      registerScheduledSessionRescheduled();
      registerScheduledSessionReminder();
      registerScheduledSessionComment();

      // jamclass invitation
      registerScheduledJamclassInvitation();

      // recording notifications
      registerMusicianRecordingSaved();
      registerBandRecordingSaved();
      registerRecordingMasterMixComplete();
      registerRecordingStreamMixComplete();

      // band notifications
      registerBandInvitation();
      registerBandInvitationAccepted();

      // register text messages
      registerTextMessage();

      registerLessonMessage();
    }

    function buildParams() {
      return { offset: currentNotificationPage * notificationBatchSize, limit: notificationBatchSize};
    }

    function populate() {
      if (isLoading || didLoadAllNotifications) return;
      isLoading = true;
      // retrieve pending notifications for this user
      rest.getNotifications(buildParams())
        .done(function(response) {
            updateNotificationList(response);
            isLoading = false;
        })
        .fail(function() {
            isLoading = false;
            app.ajaxError(arguments);
        })
    }

    function updateNotificationList(response) {
      $.each(response, function(index, val) {

        // this means the session no longer exists
        //if (response.fan_access == null && response.musician_access == null) {
        //  return;
        //}

        if(val.description == context.JK.MessageType.TEXT_MESSAGE) {
          val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html();
        }
        else if(val.description == context.JK.MessageType.LESSON_MESSAGE && val.message) {
          val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html();
        }

        // fill in template for Connect pre-click
        var template = $notificationTemplate.html();
        var notificationHtml = context.JK.fillTemplate(template, {
          notificationId: val.notification_id,
          sessionId: val.session_id,
          hoveraction: val.session_id ? "session" : "",
          avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
          text: val.formatted_msg,
          date: $.timeago(val.created_at),
          userId: val.source_user_id
        });
        
        $list.append(notificationHtml);

        // val.description contains the notification record's description value from the DB (i.e., type)
        initializeActions(val, val.description);
      });

      if(response.length < notificationBatchSize) {
        didLoadAllNotifications = true;
      }
      currentNotificationPage++;
    }

    function initializeActions(payload, type) {

      var $notification = $('li[notification-id=' + payload.notification_id + ']');
      var $btnNotificationAction = '#btn-notification-action';

      // wire up "x" button to delete notification
      $notification.find('#img-delete-notification').click(deleteNotificationHandler);

      // customize action buttons based on notification type
      if (type === context.JK.MessageType.FRIEND_REQUEST) {
        var $action_btn = $notification.find($btnNotificationAction);
        $action_btn.text('ACCEPT');
        $action_btn.click(function() {
          acceptFriendRequest(payload);
        });
      }

      else if (type === context.JK.MessageType.FRIEND_REQUEST_ACCEPTED) {
        $notification.find('#div-actions').hide();
      }

      else if (type === context.JK.MessageType.NEW_USER_FOLLOWER || type === context.JK.MessageType.NEW_BAND_FOLLOWER) {
        $notification.find('#div-actions').hide();
      }

      else if (type === context.JK.MessageType.SESSION_INVITATION) {
        var $action_btn = $notification.find($btnNotificationAction);
        $action_btn.text('JOIN');
        $action_btn.click(function() {
          openTerms(payload);
        });
      }

      else if (type === context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE) {
        var $action_btn = $notification.find($btnNotificationAction);
        $action_btn.text('SHARE');
        $action_btn.click(function() {

          rest.getRecording({id: payload.recording_id})
            .done(function(recording) {
              if(recording.my)  {
                ui.launchShareDialog(recording.my.id, 'recording');
              }
              else {
                context.JK.Banner.showAlert('Unable to share recording')
              }
            })
            .fail(app.ajaxError)

        });
      }

      else if (type === context.JK.MessageType.RECORDING_STREAM_MIX_COMPLETE) {
        var $action_btn = $notification.find($btnNotificationAction);
        $action_btn.text('SHARE');
        $action_btn.click(function() {

          rest.getRecording({id: payload.recording_id})
            .done(function(recording) {
              if(recording.my)  {
                ui.launchShareDialog(recording.my.id, 'recording');
              }
              else {
                context.JK.Banner.showAlert('Unable to share recording')
              }
            })
            .fail(app.ajaxError)

        });
      }


      else if (type === context.JK.MessageType.JOIN_REQUEST) {
        var $action_btn = $notification.find($btnNotificationAction);
        $action_btn.text('APPROVE');
        $action_btn.click(function() {
          approveJoinRequest(payload);
        });
      }

      else if (type === context.JK.MessageType.JOIN_REQUEST_APPROVED) {
        var $action_btn = $notification.find($btnNotificationAction);
        $action_btn.text('JOIN');
        $action_btn.click(function() {
          openTerms(payload);
        });
        context.SessionsActions.updateSession(payload.session_id)
      }

      else if (type === context.JK.MessageType.JOIN_REQUEST_REJECTED) {
        $notification.find('#div-actions').hide();
      }

      else if (type === context.JK.MessageType.MUSICIAN_SESSION_JOIN || type === context.JK.MessageType.BAND_SESSION_JOIN) {

        var actionText = '';
        var callback;
        if (context.JK.currentUserMusician) {
          // user is MUSICIAN; musician_access = TRUE
          if (payload.musician_access) {
            actionText = "JOIN";
            callback = joinSession;
          }
          // user is MUSICIAN; fan_access = TRUE
          else if (payload.fan_access) {
            actionText = "LISTEN";
            callback = listenToSession;
          }
        }
        else {
          // user is FAN; fan_access = TRUE
          if (payload.fan_access) {
            actionText = "LISTEN";
            callback = listenToSession;
          }
        }

        var $action_btn = $notification.find($btnNotificationAction);
        if (actionText === '') {
          $action_btn.hide();
        }
        else {
          $action_btn.text(actionText);
          $action_btn.click(function() {
            callback(payload);
          });
        }
      }

      else if (type === context.JK.MessageType.MUSICIAN_RECORDING_SAVED || type === context.JK.MessageType.BAND_RECORDING_SAVED) {
        var $action_btn = $notification.find($btnNotificationAction);
        $action_btn.text('LISTEN');
        $action_btn.click(function() {
          listenToRecording(payload);
        });
      }

      else if (type === context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE) {
        $notification.find('#div-actions').hide();
        logger.debug("context.jamClient.OnDownloadAvailable!")
        context.jamClient.OnDownloadAvailable(); // poke backend, letting it know a download is available
      }

      else if (type === context.JK.MessageType.JAM_TRACK_SIGN_COMPLETE) {
        $notification.find('#div-actions').hide();
        logger.debug("context.jamClient.OnDownloadAvailable!")
        context.jamClient.OnDownloadAvailable(); // poke backend, letting it know a download is available
      }

      else if (type === context.JK.MessageType.BAND_INVITATION) {
        var $action_btn = $notification.find($btnNotificationAction);
        $action_btn.text('ACCEPT');
        $action_btn.click(function() {
          acceptBandInvitation(payload);
        });
      }
      else if (type === context.JK.MessageType.BAND_INVITATION_ACCEPTED) {
        $notification.find('#div-actions').hide();
      }
      else if (type === context.JK.MessageType.TEXT_MESSAGE) {
        var $action_btn = $notification.find($btnNotificationAction);
        $action_btn.text('REPLY');
        $action_btn.click(function() {
          var userId = $notification.find('.more-text-available').attr('data-sender-id');
          app.layout.showDialog('text-message', { d1: userId });
        });

        var moreTextLink = $notification.find('.more-text-available');
        var textMessage = $notification.find('.text-message');
        var clipped_msg = textMessage.attr('data-is-clipped') === 'true';

        if(clipped_msg) {
          moreTextLink.text('more').show();
          moreTextLink.click(function(e) {
            var userId = $(this).attr('data-sender-id');
            app.layout.showDialog('text-message', { d1: userId });
            return false;
          });
        }
        else {
          moreTextLink.hide();
        }
      }
      else if (type === context.JK.MessageType.SCHEDULED_SESSION_INVITATION) {
        linkSessionInfoNotification(payload, $notification, $btnNotificationAction);
      }
      else if (type === context.JK.MessageType.LESSON_MESSAGE) {
        if (payload.purpose == 'chat') {
          linkLessonChatNotification(payload, $notification, $btnNotificationAction);
        }
        else
        {
          linkLessonInfoNotification(payload, $notification, $btnNotificationAction);
        }
      }
      else if (type === context.JK.MessageType.SCHEDULED_JAMCLASS_INVITATION) {
        linkLessonInfoNotification(payload, $notification, $btnNotificationAction);
      }
      else if (type === context.JK.MessageType.SCHEDULED_SESSION_RSVP) {
        var $action_btn = $notification.find($btnNotificationAction);
        $action_btn.text('MANAGE RSVP');
        $action_btn.click(function() {
          context.location = "/client#/account/sessionDetail/" + payload.session_id;
        });
      }
      else if (type === context.JK.MessageType.SCHEDULED_SESSION_RSVP_APPROVED) {
        linkSessionInfoNotification(payload, $notification, $btnNotificationAction);
      }
      else if (type === context.JK.MessageType.SCHEDULED_SESSION_RSVP_CANCELLED) {
        linkSessionInfoNotification(payload, $notification, $btnNotificationAction);
      }
      else if (type === context.JK.MessageType.SCHEDULED_SESSION_RSVP_CANCELLED_ORG) {
        linkSessionInfoNotification(payload, $notification, $btnNotificationAction);
      }
      else if (type === context.JK.MessageType.SCHEDULED_SESSION_CANCELLED) {
        linkSessionInfoNotification(payload, $notification, $btnNotificationAction);
      }
      else if (type === context.JK.MessageType.SCHEDULED_SESSION_RESCHEDULED) {
        linkSessionInfoNotification(payload, $notification, $btnNotificationAction);
      }
      else if (type === context.JK.MessageType.SCHEDULED_SESSION_REMINDER) {
        linkSessionInfoNotification(payload, $notification, $btnNotificationAction);
      }
      else if (type === context.JK.MessageType.SCHEDULED_SESSION_COMMENT) {
        linkSessionInfoNotification(payload, $notification, $btnNotificationAction);
      }

      context.JK.bindHoverEvents($("#sidebar-div"));
    }

    function linkSessionInfoNotification(payload, $notification, $btnNotificationAction) {
      var $action_btn = $notification.find($btnNotificationAction);
      $action_btn.text('SESSION DETAILS');
      $action_btn.click(function() {
        openSessionInfoWebPage({"session_id": payload.session_id});
      });
    }

    function linkLessonInfoNotification(payload, $notification, $btnNotificationAction) {
      var $action_btn = $notification.find($btnNotificationAction);
      $action_btn.text('LESSON DETAILS');
      $action_btn.click(function() {
        gotoLessonBookingPage({"lesson_session_id": payload.lesson_session_id});
      });
    }

    function linkLessonChatNotification(payload, $notification, $btnNotificationAction) {
      var $action_btn = $notification.find($btnNotificationAction);
      $action_btn.text('LESSON CONVERSATION');
      $action_btn.click(function() {
        gotoLessonChatPage({"lesson_session_id": payload.lesson_session_id});
      });
    }


    function acceptBandInvitation(args) {
      rest.updateBandInvitation(
          args.band_id,
          args.band_invitation_id,
          true
        ).done(function(response) {
          deleteNotification(args.notification_id);  // delete notification corresponding to this friend request
        }).error(app.ajaxError);
    }


    function deleteNotification(notificationId) {
      var url = "/api/users/" + context.JK.currentUserId + "/notifications/" + notificationId;
      $.ajax({
        type: "DELETE",
        dataType: "json",
        contentType: 'application/json',
        url: url,
        processData: false,
        success: function(response) {
          var notification = $('li[notification-id=' + notificationId + ']');
          if(notification.length > 0) {
            decrementNotificationCount();
          }
          notification.remove();
        },
        error: app.ajaxError
      });
    }


    function listenToSession(args) {
      deleteNotification(args.notification_id);
      context.JK.popExternalLink('/sessions/' + args.session_id);
    }

    function joinSession(args) {
      sessionUtils.joinSession(args.session_id);
      deleteNotification(args.notification_id);
    }

    function registerJoinRequestApproved() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST_APPROVED, function(header, payload) {
        logger.debug("Handling JOIN_REQUEST_APPROVED msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Join Request Approved",
          "text": payload.msg,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-join",
              text: "JOIN SESSION",
              "layout-action": "close",
              href: "#",
              "class": "button-orange",
              callback: openTerms,
              callback_args: {
                "session_id": payload.session_id,
                "notification_id": payload.notification_id
              }
            }]
        );
      });
    }

    function registerJoinRequestRejected() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST_REJECTED, function(header, payload) {
        logger.debug("Handling JOIN_REQUEST_REJECTED msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Join Request Rejected",
          "text": payload.msg,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        });
      });
    }


    function registerJoinRequest() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST, function(header, payload) {
        logger.debug("Handling JOIN_REQUEST msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "New Join Request",
          "text": payload.msg,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-approve",
              text: "APPROVE",
              "layout-action": "close",
              href: "#",
              "class": "button-orange",
              callback: approveJoinRequest,
              callback_args: {
                "join_request_id": payload.join_request_id,
                "notification_id": payload.notification_id
              }
            },
            {
              id: "btn-reject",
              text: "REJECT",
              "layout-action": "close",
              href: "#",
              "class": "button-grey",
              callback: rejectJoinRequest,
              callback_args: {
                "join_request_id": payload.join_request_id,
                "notification_id": payload.notification_id
              }
            }]
        );
      });
    }


    function registerFriendRequestAccepted() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_REQUEST_ACCEPTED, function(header, payload) {
        logger.debug("Handling FRIEND_REQUEST_ACCEPTED msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        sidebar.refreshFriends();

        app.notify({
          "title": "Friend Request Accepted",
          "text": payload.msg,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        });
      });
    }

    function registerNewUserFollower() {

      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.NEW_USER_FOLLOWER, function(header, payload) {
        logger.debug("Handling NEW_USER_FOLLOWER msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "New Follower",
          "text": payload.msg,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        });
      });
    }


    function registerNewBandFollower() {

      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.NEW_BAND_FOLLOWER, function(header, payload) {
        logger.debug("Handling NEW_BAND_FOLLOWER msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "New Band Follower",
          "text": payload.msg,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        });
      });
    }


    function registerFriendRequest() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_REQUEST, function(header, payload) {
        logger.debug("Handling FRIEND_REQUEST msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "New Friend Request",
          "text": payload.msg,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-accept",
              text: "ACCEPT",
              "layout-action": "close",
              href: "#",
              "class": "button-orange",
              callback: acceptFriendRequest,
              callback_args: {
                "friend_request_id": payload.friend_request_id,
                "notification_id": payload.notification_id
              }
            }]
        );
      });
    }

    function acceptFriendRequest(args) {

      rest.acceptFriendRequest({
        status: 'accept',
        friend_request_id: args.friend_request_id
      }).done(function(response) {
        deleteNotification(args.notification_id);  // delete notification corresponding to this friend request
        sidebar.refreshFriends();  // refresh friends panel when request is accepted
      }).error(app.ajaxError);
    }

    function registerSessionEnded() {
      // TODO: this should clean up all notifications related to this session
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_ENDED, function(header, payload) {
        logger.debug("Handling SESSION_ENDED msg " + JSON.stringify(payload));
        deleteSessionNotifications(payload.session_id);
      });
    }

    // remove all notifications for this session
    function deleteSessionNotifications(sessionId) {
      $('li[session-id=' + sessionId + ']').hide();
      //decrementNotificationCount();
    }

    function registerSessionKick() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_KICK, function(header, payload) {
        logger.debug("Handling SESSION_KICK msg " + JSON.stringify(payload));

        context.SessionAction.leaveSession({
          location: '/client#/home',
        })

        var buttons = []
        buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'})
        buttons.push({name: 'COMPARE PLANS', buttonStyle: 'button-grey', click: function() {
          context.JK.popExternalLink("https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-")
          return 'noclose'
        }})
        buttons.push({
          name: 'UPGRADE PLAN',
          buttonStyle: 'button-orange',
          click: function() {
            context.JK.popExternalLink("/client#/account/subscription", true)
          }
        })
        context.JK.Banner.show({
          title: "Session Too Big For Current Plan",
          html: context._.template($('#template-session-too-big-kicked').html(), {}, { variable: 'data' }),
          buttons: buttons});
      })
    }

    function registerSessionInvitation() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_INVITATION, function(header, payload) {
        logger.debug("Handling SESSION_INVITATION msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        context.SessionsActions.updateSession(payload.session_id)

        app.notify({
            "title": "Session Invitation",
            "text": payload.msg
          }, [{
                id: "btn-join",
                text: "JOIN SESSION",
                "layout-action": "close",
                href: "#",
                "class": "button-orange",
                callback: openTerms,
                callback_args: {
                  "session_id": payload.session_id,
                  "notification_id": payload.notification_id
                }
              }]
          );

      // THERE IS A RACE CONDITION THAT CAUSES AN ERROR WHEN INVOKING THE CODE BELOW
      // THE ACTIVEMUSICSESSION HAS NOT YET BEEN FULLY CREATED B/C THE CREATOR'S CLIENT IS
      // STILL SETTING UP THE SESSION WHEN THE NOTIFICATION IS SENT

      // SEE ActiveMusicSession#participant_create

      //   var participants = [];
      //   rest.getSession(payload.session_id)
      //   .done(function(response) {
      //     $.each(response.participants, function(index, val) {
      //       participants.push({"photo_url": context.JK.resolveAvatarUrl(val.user.photo_url), "name": val.user.name});
      //     });

      //     var participantHtml = "You have been invited to join a session with: <br/><br/>";
      //     participantHtml += "<table><tbody>";

      //     $.each(participants, function(index, val) {
      //       if (index < 4) {
      //         participantHtml += "<tr><td><img class='avatar-small' src='" + context.JK.resolveAvatarUrl(val.photo_url) + "' /></td><td>" + val.name + "</td></tr>";
      //       }
      //     });

      //     participantHtml += "</tbody></table>";

      //     app.notify({
      //       "title": "Session Invitation",
      //       "text": participantHtml
      //     }, [{
      //           id: "btn-join",
      //           text: "JOIN SESSION",
      //           "layout-action": "close",
      //           href: "#",
      //           "class": "button-orange",
      //           callback: openTerms,
      //           callback_args: {
      //             "session_id": payload.session_id,
      //             "notification_id": payload.notification_id
      //           }
      //         }]
      //     );
      //   }).error(app.ajaxError);
      });
    }


    function registerMusicianSessionJoin() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, function(header, payload) {
        logger.debug("Handling MUSICIAN_SESSION_JOIN msg " + JSON.stringify(payload));

        var okText = '';
        var showNotification = false;
        var callback;
        if (context.JK.currentUserMusician) {

          context.SessionsActions.updateSession(payload.session_id)

          showNotification = true;
          okText = "JOIN";
          callback = joinSession;
        }
        else {
          // user is FAN; fan_access = TRUE
          if (payload.fan_access) {
            showNotification = true;
            okText = "LISTEN";
            callback = listenToSession;
          }
        }

        if (showNotification) {
          handleNotification(payload, header.type);

          app.notify({
            "title": "Musician Joined Session",
            "text": payload.msg,
            "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
          }, [{
                id: "btn-action",
                text: okText,
                "layout-action": "close",
                href: "#",
                "class": "button-orange",
                callback: callback,
                callback_args: {
                  "session_id": payload.session_id,
                  "fan_access": payload.fan_access,
                  "musician_access": payload.musician_access,
                  "approval_required": payload.approval_required,
                  "notification_id": payload.notification_id
                }
              }]
          );
        }
      });
    }


    function registerBandSessionJoin() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_SESSION_JOIN, function(header, payload) {
        logger.debug("Handling BAND_SESSION_JOIN msg " + JSON.stringify(payload));

        var okText = '';
        var showNotification = false;
        var callback;
        if (context.JK.currentUserMusician) {
          // user is MUSICIAN; musician_access = TRUE
          if (payload.musician_access) {
            showNotification = true;
            okText = "JOIN";
            callback = joinSession;
          }
          // user is MUSICIAN; fan_access = TRUE
          else if (payload.fan_access) {
            showNotification = true;
            okText = "LISTEN";
            callback = listenToSession;
          }
        }
        else {
          // user is FAN; fan_access = TRUE
          if (payload.fan_access) {
            showNotification = true;
            okText = "LISTEN";
            callback = listenToSession;
          }
        }

        if (showNotification) {
          handleNotification(payload, header.type);

          app.notify({
            "title": "Band Joined Session",
            "text": payload.msg,
            "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
          }, [{
                id: "btn-listen",
                text: okText,
                "layout-action": "close",
                href: "#",
                "class": "button-orange",
                callback: callback,
                callback_args: {
                  "session_id": payload.session_id,
                  "fan_access": payload.fan_access,
                  "musician_access": payload.musician_access,
                  "approval_required": payload.approval_required,
                  "notification_id": payload.notification_id
                }
              }]
          );
        }
      });
    }


    function registerScheduledSessionInvitation() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_SESSION_INVITATION, function(header, payload) {
        logger.debug("Handling SCHEDULED_SESSION_INVITATION msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Session Invitation",
          "text": payload.msg + "<br/><br/>" + payload.session_name + "<br/>" + payload.session_date,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-more-info",
              text: "MORE INFO",
              rel: "external",
              "class": "button-orange",
              callback: openSessionInfoWebPage,
              callback_args: {
                "session_id": payload.session_id
              }
            }]
        );
      });
    }

    function registerScheduledJamclassInvitation() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_JAMCLASS_INVITATION, function(header, payload) {
        logger.debug("Handling SCHEDULED_JAMCLASS_INVITATION msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
            "title": "JamClass Scheduled",
            "text": payload.msg + "<br/><br/>" + payload.session_name + "<br/>" + payload.session_date,
            "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
          }, [{
            id: "btn-more-info",
            text: "MORE INFO",
            rel: "external",
            "class": "button-orange",
            callback: gotoLessonBookingPage,
            callback_args: {
              "lesson_session_id": payload.lesson_session_id
            }
          }]
        );
      });
    }

    function registerScheduledSessionRsvp() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_SESSION_RSVP, function(header, payload) {
        logger.debug("Handling SCHEDULED_SESSION_RSVP msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Session RSVP",
          "text": payload.msg + "<br/><br/>" + payload.session_name + "<br/>" + payload.session_date,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-manage-rsvp",
              text: "MANAGE RSVP",
              "class": "button-orange",
              callback: navigateToSessionDetails,
              callback_args: {
                "session_id": payload.session_id
              }
            }]
        );
      });
    }

    function registerScheduledSessionRsvpApproved() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_SESSION_RSVP_APPROVED, function(header, payload) {
        logger.debug("Handling SCHEDULED_SESSION_RSVP_APPROVED msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Session RSVP Approved!",
          "text": payload.msg + "<br/><br/>" + payload.session_name + "<br/>" + payload.session_date,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-session-details",
              text: "SESSION DETAILS",
              "layout-action": "close",
              rel: "external",
              "class": "button-orange",
              callback: openSessionInfoWebPage,
              callback_args: {
                "session_id": payload.session_id
              }
            }]
        );
      });
    }

    function registerScheduledSessionRsvpCancelled() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_SESSION_RSVP_CANCELLED, function(header, payload) {
        logger.debug("Handling SCHEDULED_SESSION_RSVP_CANCELLED msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Session RSVP Cancelled",
          "text": payload.msg + "<br/><br/>" + payload.session_name + "<br/>" + payload.session_date,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-session-details",
              text: "SESSION DETAILS",
              rel: "external",
              "class": "button-orange",
              callback: openSessionInfoWebPage,
              callback_args: {
                "session_id": payload.session_id
              }
            }]
        );
      });
    }

    function registerScheduledSessionRsvpCancelledOrg() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_SESSION_RSVP_CANCELLED_ORG, function(header, payload) {
        logger.debug("Handling SCHEDULED_SESSION_RSVP_CANCELLED_ORG msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Session RSVP Cancelled",
          "text": payload.msg + "<br/><br/>" + payload.session_name + "<br/>" + payload.session_date,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-session-details",
              text: "SESSION DETAILS",
              rel: "external",
              "class": "button-orange",
              callback: openSessionInfoWebPage,
              callback_args: {
                "session_id": payload.session_id
              }
            }]
        );
      });
    }

    function registerScheduledSessionCancelled() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_SESSION_CANCELLED, function(header, payload) {
        logger.debug("Handling SCHEDULED_SESSION_CANCELLED msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Session Cancelled",
          "text": payload.msg + "<br/><br/>" + payload.session_name + "<br/>" + payload.session_date,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-session-details",
              text: "SESSION DETAILS",
              rel: "external",
              "class": "button-orange",
              callback: openSessionInfoWebPage,
              callback_args: {
                "session_id": payload.session_id
              }
            }]
        );
      });
    }

    function registerScheduledSessionRescheduled() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_SESSION_RESCHEDULED, function(header, payload) {
        logger.debug("Handling SCHEDULED_SESSION_RESCHEDULED msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Session Rescheduled",
          "text": payload.msg + "<br/><br/>" + payload.session_name + "<br/>" + payload.session_date,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-session-details",
              text: "SESSION DETAILS",
              rel: "external",
              "class": "button-orange",
              callback: openSessionInfoWebPage,
              callback_args: {
                "session_id": payload.session_id
              }
            }]
        );
      });
    }

    function registerScheduledSessionReminder() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_SESSION_REMINDER, function(header, payload) {
        logger.debug("Handling SCHEDULED_SESSION_REMINDER msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Session Reminder",
          "text": payload.msg + "<br/><br/>" + payload.session_name + "<br/>" + payload.session_date,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-session-details",
              text: "SESSION DETAILS",
              rel: "external",
              "class": "button-orange",
              callback: openSessionInfoWebPage,
              callback_args: {
                "session_id": payload.session_id
              }
            }]
        );
      });

    }

    function registerScheduledSessionComment() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SCHEDULED_SESSION_COMMENT, function(header, payload) {
        logger.debug("Handling SCHEDULED_SESSION_COMMENT msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Session Comment",
          "text": payload.msg + "<br/><br/>" + payload.session_name + "<br/>" + payload.session_date,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-session-details",
              text: "SESSION DETAILS",
              rel: "external",
              "class": "button-orange",
              callback: openSessionInfoWebPage,
              callback_args: {
                "session_id": payload.session_id
              }
            }]
        );
      });

    }

    function registerMusicianRecordingSaved() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_RECORDING_SAVED, function(header, payload) {
        logger.debug("Handling MUSICIAN_RECORDING_SAVED msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Musician Recording Saved",
          "text": payload.msg,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-listen",
              text: "LISTEN",
              "layout-action": "close",
              href: "#",
              "class": "button-orange",
              callback: listenToRecording,
              callback_args: {
                "recording_id": payload.recording_id,
                "notification_id": payload.notification_id
              }
            }]
        );
      });
    }

    function registerBandRecordingSaved() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_RECORDING_SAVED, function(header, payload) {
        logger.debug("Handling BAND_RECORDING_SAVED msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Band Recording Saved",
          "text": payload.msg,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-listen",
              text: "LISTEN",
              "layout-action": "close",
              href: "#",
              "class": "button-orange",
              callback: listenToRecording,
              callback_args: {
                "recording_id": payload.recording_id,
                "notification_id": payload.notification_id
              }
            }]
        );
      });
    }


    function registerRecordingMasterMixComplete() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE, function(header, payload) {
        logger.debug("Handling RECORDING_MASTER_MIX_COMPLETE msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Recording Master Mix Complete",
          "text": payload.msg,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-share",
              text: "SHARE",
              "layout-action": "close",
              href: "#",
              "class": "button-orange",
              callback: shareRecording,
              callback_args: {
                "claimed_recording_id": payload.claimed_recording_id
              }
            }]
        );
      });
    }

    function registerRecordingStreamMixComplete() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_STREAM_MIX_COMPLETE, function(header, payload) {
        logger.debug("Handling RECORDING_STREAM_MIX_COMPLETE msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
            "title": "Recording Stream Mix Complete",
            "text": payload.msg,
            "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
          }, [{
          id: "btn-share",
          text: "SHARE",
          "layout-action": "close",
          href: "#",
          "class": "button-orange",
          callback: shareRecording,
          callback_args: {
            "claimed_recording_id": payload.claimed_recording_id
          }
        }]
        );
      });
    }

    function shareRecording(args) {
      var claimedRecordingId = args.claimed_recording_id;

      ui.launchShareDialog(claimedRecordingId, 'recording');
    }

    function registerBandInvitation() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION, function(header, payload) {
        logger.debug("Handling BAND_INVITATION msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Band Invitation",
          "text": payload.msg,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        }, [{
              id: "btn-accept",
              text: "ACCEPT",
              "layout-action": "close",
              href: "#",
              "class": "button-orange",
              callback: acceptBandInvitation,
              callback_args: {
                "band_invitation_id": payload.band_invitation_id,
                "band_id": payload.band_id,
                "notification_id": payload.notification_id
              }
            }]
        );
      });
    }


    function registerTextMessage() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TEXT_MESSAGE, function(header, payload) {
        logger.debug("Handling TEXT_MESSAGE msg " + JSON.stringify(payload));

        textMessageDialog.messageReceived(payload);

        handleNotification(payload, header.type);
      });
    }

    function registerLessonMessage() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.LESSON_MESSAGE, function(header, payload) {
        logger.debug("Handling LESSON_MESSAGE msg " + JSON.stringify(payload));

        //context.ChatActions.msgReceived(payload);

        // only show if chat dialog is not showing or if the focused lesson session is not the one specified in the payload
        console.log("context.ChatStore.lessonSessionId",  app.layout.isDialogShowing('chat-dialog'), context.ChatStore.lessonSessionId, payload.lesson_session_id)
        if (!app.layout.isDialogShowing('chat-dialog') || context.ChatStore.lessonSessionId != payload.lesson_session_id) {
          app.notify({
              "title": "Lesson Message",
              "text": payload.msg,
              "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
            }, [{
              id: "btn-lesson-chat",
              text: "SEE LESSON CONVERSATION",
              "layout-action": "close",
              href: "#",
              "class": "button-orange",
              callback: gotoLessonChatPage,
              callback_args: {
                "lesson_session_id": payload.lesson_session_id
              }
            }]
          );
        }

        handleNotification(payload, header.type);
      });
    }

    function registerBandInvitationAccepted() {
      context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION_ACCEPTED, function(header, payload) {
        logger.debug("Handling BAND_INVITATION_ACCEPTED msg " + JSON.stringify(payload));

        handleNotification(payload, header.type);

        app.notify({
          "title": "Band Invitation Accepted",
          "text": payload.msg,
          "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
        });
      });
    }


    // one important limitation; if the user is focused on an iframe, this will be false
    // however, if they are doing something with Facebook or the photo picker, this may actually still be desirable
    function userCanSeeNotifications(payload) {
      return document.hasFocus() && !app.layout.dialogObscuredNotification(payload);
    }

    // default handler for incoming notification
    function handleNotification(payload, type) {

      // on a load of notifications, it is possible to load a very new notification,
      // and get a websocket notification right after for that same notification,
      // so we need to protect against such duplicates
      if($list.find('li[notification-id="' + payload.notification_id + '"]').length > 0) {
        return false;
      }

      // add notification to sidebar
      var template = $notificationTemplate.html();
      var notificationHtml = context.JK.fillTemplate(template, {
        notificationId: payload.notification_id,
        sessionId: payload.session_id,
        hoveraction: payload.session_id ? "session" : "",
        avatar_url: context.JK.resolveAvatarUrl(payload.photo_url),
        text: payload.msg instanceof jQuery ? payload.msg.html() : payload.msg ,
        date: $.timeago(payload.created_at)
      });

      $list.prepend(notificationHtml);

      onNotificationOccurred(payload);

      initializeActions(payload, type);

      return true;
    }

    function approveJoinRequest(args) {
      rest.updateJoinRequest(args.join_request_id, true)
        .done(function(response) {
          deleteNotification(args.notification_id);
        }).error(app.ajaxError);
    }

    function openTerms(args) {
      var termsDialog = new context.JK.TermsDialog(app, args, onTermsAccepted);
      termsDialog.initialize();
      app.layout.showDialog('terms');
    }

    function onTermsAccepted(args) {
      deleteNotification(args.notification_id);
      context.location = '/client#/session/' + args.session_id;
    }

    function rejectJoinRequest(args) {
      rest.updateJoinRequest(args.join_request_id, false)
        .done(function(response) {
          deleteNotification(args.notification_id);
        }).error(app.ajaxError);
    }

    function listenToRecording(args) {
      deleteNotification(args.notification_id);
      context.JK.popExternalLink('/recordings/' + args.recording_id);
    }

    function navigateToSessionDetails(args) {
      context.location = '/client#/account/sessionDetail/' + args.session_id;
    }

    function openSessionInfoWebPage(args) {
      context.JK.popExternalLink('/sessions/' + args.session_id + '/details');
    }

    function gotoLessonBookingPage(args) {
      window.location.href = "/client#/jamclass/lesson-booking/" + args.lesson_session_id
    }

    function gotoLessonChatPage(args) {
      app.layout.showDialog('chat-dialog', {d1: "lesson_" + args.lesson_session_id})
      //window.location.href = "/client#/jamclass/chat-dialog/d1=lesson_" + args.lesson_session_id
    }

    function deleteNotificationHandler(evt) {
      evt.stopPropagation();
      var notificationId = $(this).attr('notification-id');
      deleteNotification(notificationId);
    }

    function registerNotificationScroll() {
        $('.notification-scroller').scroll(function() {
            var wintop = $('.notification-scroller').scrollTop();
            var winheight = $('.notification-scroller').height();
            var docheight = $('#sidebar-notification-list').height();
            var scrollTrigger = 0.90;
            // console.log("notification scroll:  wintop="+wintop+" docheight="+docheight+" winheight="+winheight+" ratio="+(wintop / (docheight - winheight)));
            if ((wintop / (docheight - winheight)) > scrollTrigger) {
                populate();
            }
        });
    }

    function initialize(sidebarInstance, textMessageDialogInstance) {
      sidebar = sidebarInstance;
      textMessageDialog = textMessageDialogInstance;
      $panel = $('[layout-id="panelNotifications"]');
      $expanded = $panel.find('.panel.expanded');
      $contents = $panel.find('.panelcontents');
      $count = $panel.find('#sidebar-notification-count');
      $list = $panel.find('#sidebar-notification-list');
      $notificationTemplate = $('#template-notification-panel');
      if($panel.length == 0) throw "notifications panel not found"
      if($expanded.length == 0) throw "notifications expanded content not found"
      if($contents.length == 0) throw "notifications contents not found"
      if($count.length == 0) throw "notifications count element not found";
      if($list.length == 0) throw "notification list element not found";
      if($notificationTemplate.length == 0) throw "notification template not found";
      registerNotificationScroll();

      events();

      app.user().done(function(){
        populate();
      })
    };

    this.initialize = initialize;
    this.onNotificationOccurred = onNotificationOccurred;
  };
})(window, jQuery);
