$ = jQuery
context = window
logger = context.JK.logger
rest = context.JK.Rest()
EVENTS = context.JK.EVENTS
MIX_MODES = context.JK.MIX_MODES
CLIENT_ROLE = context.JK.CLIENT_ROLE

JamTrackActions = @JamTrackActions
SessionActions = @SessionActions
RecordingActions = @RecordingActions
NotificationActions = @NotificationActions
VideoActions = @VideoActions
ConfigureTracksActions = @ConfigureTracksActions

@SessionStore = Reflux.createStore(
  {
    listenables: SessionActions

    userTracks: null # comes from the backend
    currentSessionId: null
    currentSession: null
    currentOrLastSession: null
    sessionRules: null
    subscriptionRules: null
    startTime: null
    currentParticipants: {}
    participantsEverSeen: {}
    users: {} # // User info for session participants
    requestingSessionRefresh: false
    pendingSessionRefresh: false
    sessionPageEnterTimeout: null
    sessionPageEnterDeferred: null
    gearUtils: null
    sessionUtils: null
    joinDeferred: null
    recordingModel: null
    currentTrackChanges: 0
    isRecording: false
    previousAllTracks: {userTracks: [], backingTracks: [], metronomeTracks: []}
    webcamViewer: null
    openBackingTrack: null
    helper: null
    downloadingJamTrack: false

    init: ->
      # Register with the app store to get @app
      this.listenTo(context.AppStore, this.onAppInit)
      this.listenTo(context.RecordingStore, this.onRecordingChanged)
      this.listenTo(context.VideoStore, this.onVideoChanged)


    onAppInit: (@app) ->
      @gearUtils = context.JK.GearUtilsInstance
      @sessionUtils = context.JK.SessionUtils
      @recordingModel = new context.JK.RecordingModel(@app, rest, context.jamClient);
      RecordingActions.initModel(@recordingModel)
      @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack, @enableVstTimeout?, @sessionRules, @subscriptionRules)

    onSessionJoinedByOther: (payload) ->
      clientId = payload.client_id

      #parentClientId = context.jamClient.getParentClientId()
      #if parentClientId? && parentClientId != ''
        #if parentClientId == clientId
      # auto nav to session
      if context.jamClient.getClientParentChildRole && context.jamClient.getClientParentChildRole() == CLIENT_ROLE.CHILD && payload.source_user_id == context.JK.currentUserId
        logger.debug("autonav to session #{payload.session_id}")
        context.SessionActions.navToSession(payload.session_id)

    onNavToSession: (sessionId) ->
      context.location = '/client#/session/' + sessionId

    onMixdownActive: (mixdown) ->
      if @currentSession?.jam_track?
        @currentSession.jam_track.mixdown = mixdown
        @issueChange()


    onVideoChanged: (@videoState) ->

    issueChange: () ->
      @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack, @enableVstTimeout?, @sessionRules, @subscriptionRules)
      this.trigger(@helper)

    onWindowBackgrounded: () ->
      #@app.user()
      #.done((userProfile) =>
        #if userProfile.show_whats_next &&
        #    window.location.pathname.indexOf(gon.client_path) == 0 &&
        #    !@app.layout.isDialogShowing('getting-started')
        #  @app.layout.showDialog('getting-started')
      #)

      return unless @inSession()

      # the window was closed; just attempt to nav to home, which will cause all the right REST calls to happen
      logger.debug("leaving session because window was closed")
      SessionActions.leaveSession({location: '/client#/home'})

    onBroadcastFailure: (text) ->
      logger.debug("SESSION_LIVEBROADCAST_FAIL alert. reason:" + text);

      if @currentSession? && @currentSession.mount?
        rest.createSourceChange({
          mount_id: @currentSession.mount.id,
          source_direction: true,
          success: false,
          reason: text,
          client_id: @app.clientId
        })
      else
        logger.debug("unable to report source change because no mount seen on session")

    onBroadcastSuccess: (text) ->
      logger.debug("SESSION_LIVEBROADCAST_ACTIVE alert. reason:" + text);

      if @currentSession? && @currentSession.mount?
        rest.createSourceChange({
          mount_id: @currentSession.mount.id,
          source_direction: true,
          success: true,
          reason: text,
          client_id: @app.clientId
        })
      else
        logger.debug("unable to report source change because no mount seen on session")

    onBroadcastStopped: (text) ->
      logger.debug("SESSION_LIVEBROADCAST_STOPPED alert. reason:" + text);

      if @currentSession? && @currentSession.mount?
        rest.createSourceChange({
          mount_id: @currentSession.mount.id,
          source_direction: false,
          success: true,
          reason: text,
          client_id: @app.clientId
        })
      else
        logger.debug("unable to report source change because no mount seen on session")

    onShowNativeMetronomeGui: () ->
      context.jamClient.SessionShowMetronomeGui()

    onOpenMetronome: () ->
      unstable = @unstableNTPClocks()
      if @participants().length > 1 && unstable.length > 0
        names = unstable.join(", ")
        logger.debug("Unstable clocks: ", names, unstable)
        context.JK.Banner.showAlert("Couldn't open metronome", context._.template($('#template-help-metronome-unstable').html(), {names: names}, { variable: 'data' }));
      else
        data =
          value: 1
          session_size: @participants().length
          user_id: context.JK.currentUserId
          user_name: context.JK.currentUserName

        context.stats.write('web.metronome.open', data)
        rest.openMetronome({id: @currentSessionId})
        .done((response) =>
          MixerActions.openMetronome()
          @updateSessionInfo(response, true)
        )
        .fail((jqXHR) =>
          @app.notify({
            "title": "Couldn't open metronome",
            "text": "Couldn't inform the server to open metronome. msg=" + jqXHR.responseText,
            "icon_url": "/assets/content/icon_alert_big.png"
          })
        )

    onMetronomeCricketChange: (isCricket) ->
      context.jamClient.setMetronomeCricketTestState(isCricket);

    unstableNTPClocks: () ->
      unstable = []
      # This should be handled in the below loop, actually:
      myState = context.jamClient.getMyNetworkState()
      map = null
      for participant in @participants()
        isSelf = participant.client_id == @app.clientId

        if isSelf
          isStable = myState.ntp_stable
        else
          map = context.jamClient.getPeerState(participant.client_id)
          isStable = map.ntp_stable

        if !isStable
          name = participant.user.name

          if isSelf
            name += " (this computer)"

          unstable.push(name)
      unstable



    onDownloadingJamTrack: (downloading) ->
      @downloadingJamTrack = downloading

      @issueChange()

    onToggleSessionVideo: () ->

      if @videoState?.videoEnabled
        logger.debug("toggle session video")
        VideoActions.toggleVideo()
      else
        context.JK.Banner.showAlert({
          title: "Video Is Disabled",
          html: "To re-enable video, you must go your video settings in your account settings and enable video.",
        })


    onAudioResync: () ->
      logger.debug("audio resyncing")
      response = context.jamClient.SessionAudioResync()
      if response?
        @app.notify({
          "title": "Error",
          "text": response,
          "icon_url": "/assets/content/icon_alert_big.png"})

    onSyncWithServer: () ->
      @refreshCurrentSession(true)

    onWatchedInputs: (inputTracks) ->

      logger.debug("obtained tracks at start of session")
      @sessionPageEnterDeferred.resolve(inputTracks);
      @sessionPageEnterDeferred = null

    onLog: (detail) ->
      logger.debug("SessionStore: OnLog", detail)

    # codeInitiated means the user did not initiate this
    onCloseMedia: (codeInitiated) ->

      logger.debug("SessionStore: onCloseMedia", codeInitiated)
      if @helper.recordedTracks()
        @closeRecording()
      else if @helper.jamTracks() || @downloadingJamTrack
        @closeJamTrack()
      else if @helper.backingTrack() && @helper.backingTrack().path
        @closeBackingTrack()
      else if @helper.isMetronomeOpen()
        @closeMetronomeTrack()
      else
        logger.error("don't know how to close open media", @helper) unless codeInitiated


    closeJamTrack: () ->
      logger.debug("closing jam track");

      if @isRecording
        logger.debug("can't close jamtrack while recording")
        @app.notify({title: 'Can Not Close JamTrack', text: 'A JamTrack can not be closed while recording.'})
        return

      unless @selfOpenedJamTracks()
        logger.debug("can't close jamtrack if not the opener")
        @app.notify({title: 'Can Not Close JamTrack', text: 'Only the person who opened the JamTrack can close it.'})
        return

      rest.closeJamTrack({id: @currentSessionId})
      .done(() =>
        @downloadingJamTrack = false
        @refreshCurrentSession(true)
      )
      .fail((jqXHR) =>
        @app.notify({
          "title": "Couldn't Close JamTrack",
          "text": "Couldn't inform the server to close JamTrack. msg=" + jqXHR.responseText,
          "icon_url": "/assets/content/icon_alert_big.png"
        })
      )

      context.jamClient.JamTrackStopPlay()
      JamTrackActions.close()


    onOpenBackingTrack: (result) ->
      unless @inSession()
        logger.debug("ignoring backing track selected callback (not in session)")
        return

      if result.success
        logger.debug("backing track selected: " + result.file);

        rest.openBackingTrack({id: @currentSessionId, backing_track_path: result.file})
        .done(() =>

          openResult = context.jamClient.SessionOpenBackingTrackFile(result.file, false);

          if openResult
            # storing session state in memory, not in response of Session server response. bad.
            @openBackingTrack = result.file
          else
            @app.notify({
              "title": "Couldn't Open Backing Track",
              "text": "Is the file a valid audio file?",
              "icon_url": "/assets/content/icon_alert_big.png"
            });
            @closeBackingTrack()
        )
        .fail((jqXHR) =>
          @app.notifyServerError(jqXHR, "Unable to Open Backing Track For Playback");
        )

    closeRecording: () ->
      logger.debug("closing recording");

      rest.stopPlayClaimedRecording({id: @currentSessionId, claimed_recording_id: @currentSession.claimed_recording.id})
      .done((response) =>
        #sessionModel.refreshCurrentSession(true);
        # update session info
        @onUpdateSession(response)
      )
      .fail((jqXHR) =>
        @app.notify({
          "title": "Couldn't Stop Recording Playback",
          "text": "Couldn't inform the server to stop playback. msg=" + jqXHR.responseText,
          "icon_url": "/assets/content/icon_alert_big.png"
        })
      )

      context.jamClient.CloseRecording()

    closeMetronomeTrack:() ->
      logger.debug("SessionStore: closeMetronomeTrack")
      rest.closeMetronome({id: @currentSessionId})
      .done(() =>
        context.jamClient.SessionCloseMetronome()
        @refreshCurrentSession(true)
      )
      .fail((jqXHR) =>
        @app.notify({
          "title": "Couldn't Close MetronomeTrack",
          "text": "Couldn't inform the server to close MetronomeTrack. msg=" + jqXHR.responseText,
          "icon_url": "/assets/content/icon_alert_big.png"
        })
      )

    closeBackingTrack: () ->
      if @isRecording
        logger.debug("can't close backing track while recording")
        return


      rest.closeBackingTrack({id: @currentSessionId})
      .done(() =>
      )
      .fail(() =>
        @app.notify({
          "title": "Couldn't Close Backing Track",
          "text": "Couldn't inform the server to close Backing Track. msg=" + jqXHR.responseText,
          "icon_url": "/assets/content/icon_alert_big.png"
        });
      )

      # '' closes all open backing tracks
      context.jamClient.SessionStopPlay();
      context.jamClient.SessionCloseBackingTrackFile('');


    onMixersChanged: (type, text, trackInfo) ->

      return unless @inSession()

      if text == 'RebuildAudioIoControl'

        if @backendMixerAlertThrottleTimer
          clearTimeout(@backendMixerAlertThrottleTimer)

        @backendMixerAlertThrottleTimer =
        setTimeout(() =>
          @backendMixerAlertThrottleTimer = null
          if @sessionPageEnterDeferred
            # this means we are still waiting for the BACKEND_MIXER_CHANGE that indicates we have user tracks built-out/ready

            # we will get at least one BACKEND_MIXER_CHANGE that corresponds to the backend doing a 'audio pause', which won't matter much
            # so we need to check that we actaully have userTracks before considering ourselves done
            if trackInfo.userTracks.length > 0
              logger.debug("obtained tracks at start of session")
              @sessionPageEnterDeferred.resolve(trackInfo.userTracks)
              @sessionPageEnterDeferred = null

            return

          # wait until we are fully in session before trying to sync tracks to server
          if @joinDeferred
            @joinDeferred
            .done(()=>
              MixerActions.syncTracks()
            )
        , 100)
      else if text == 'Midi-Track Update'
        logger.debug('midi track sync')
        MixerActions.syncTracks()
      else if text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl'

        backingTracks = trackInfo.backingTracks
        previousBackingTracks = @previousAllTracks.backingTracks
        metronomeTracks = trackInfo.metronomeTracks
        previousMetronomeTracks = @previousAllTracks.metronomeTracks

        # the way we know if backing tracks changes, or recordings are opened, is via this event.
        # but we want to report to the user when backing tracks change; so we need to detect change on our own
        if !(previousBackingTracks.length == 0 && backingTracks.length == 0) && previousBackingTracks != backingTracks
          logger.debug("backing tracks changed", previousBackingTracks, backingTracks)
          MixerActions.syncTracks()
        else if !(previousMetronomeTracks.length == 0 && metronomeTracks.length == 0) && previousMetronomeTracks != metronomeTracks
          #logger.debug("metronome state changed ", previousMetronomeTracks, metronomeTracks)
          MixerActions.syncTracks()
        else
          @refreshCurrentSession(true)

        @previousAllTracks = trackInfo

      else if text == 'Global Peer Input Mixer Mode'
        MixerActions.mixerModeChanged(MIX_MODES.MASTER)

      else if text == 'Local Peer Stream Mixer Mode'
        MixerActions.mixerModeChanged(MIX_MODES.PERSONAL)

    onRecordingChanged: (details) ->
      logger.debug("SessionStore.onRecordingChanged: " + details.cause)
      @isRecording = details.isRecording

      switch details.cause
        when 'started'

          if details.reason
            reason = details.reason;
            detail = details.detail;
            title = "Could Not Start Recording";

            switch reason
              when 'client-no-response'
                @notifyWithUserInfo(title, 'did not respond to the start signal.', detail)
              when 'empty-recording-id'
                @app.notifyAlert(title, "No recording ID specified.")
              when 'missing-client'
                @notifyWithUserInfo(title, 'could not be signalled to start recording.', detail)
              when 'already-recording'
                @app.notifyAlert(title, 'Already recording. If this appears incorrect, try restarting JamKazam.')
              when 'recording-engine-unspecified'
                @notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail)
              when 'recording-engine-create-directory'
                @notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail)
              when 'recording-engine-create-file'
                @notifyWithUserInfo(title, 'had a problem creating a recording file.', detail)
              when 'recording-engine-sample-rate'
                @notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail)
              when 'rest'
                jqXHR = detail[0];
                @app.notifyServerError(jqXHR);
              else
                @notifyWithUserInfo(title, 'Error Reason: ' + reason)
          else
            @displayWhoCreatedRecording(details.clientId)

        when 'stopped'
          if @selfOpenedJamTracks()
            timeline = context.jamClient.GetJamTrackTimeline();

            rest.addRecordingTimeline(details.recordingId, timeline)
            .fail(()=>
              @app.notify({
                title: "Unable to Add JamTrack Volume Data",
                text: "The volume of the JamTrack will not be correct in the recorded mix."
              }, null, true)
            )

          if details.reason
            logger.warn("Recording Discarded: ", details)
            reason = details.reason
            detail = details.detail
            title = "Recording Discarded"

            switch reason
              when 'client-no-response'
                @notifyWithUserInfo(title, 'did not respond to the stop signal.', detail)
              when 'missing-client'
                @notifyWithUserInfo(title, 'could not be signalled to stop recording.', detail)
              when 'empty-recording-id'
                @app.notifyAlert(title, "No recording ID specified.")
              when 'wrong-recording-id'
                @app.notifyAlert(title, "Wrong recording ID specified.")
              when 'not-recording'
                @app.notifyAlert(title, "Not currently recording.")
              when 'already-stopping'
                @app.notifyAlert(title, "Already stopping the current recording.")
              when 'start-before-stop'
                @notifyWithUserInfo(title, 'asked that we start a new recording; cancelling the current one.', detail)
              else
                @app.notifyAlert(title, "Error reason: " + reason)
          else
            @promptUserToSave(details.recordingId, timeline);

        when 'abortedRecording'
          reason = details.reason
          detail = details.detail

          title = "Recording Cancelled"

          switch reason
            when 'client-no-response'
              @notifyWithUserInfo(title, 'did not respond to the start signal.', detail)
            when 'missing-client'
              @notifyWithUserInfo(title, 'could not be signalled to start recording.', detail)
            when 'populate-recording-info'
              @notifyWithUserInfo(title, 'could not synchronize with the server.', detail)
            when 'recording-engine-unspecified'
              @notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail)
            when 'recording-engine-create-directory'
              @notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail)
            when 'recording-engine-create-file'
              @notifyWithUserInfo(title, 'had a problem creating a recording file.', detail)
            when 'recording-engine-sample-rate'
              @notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail)
            else
              @app.notifyAlert(title, "Error reason: " + reason)

      @issueChange()

    notifyWithUserInfo: (title , text, clientId) ->
      @findUserBy({clientId: clientId})
      .done((user)=>
        @app.notify({
          "title": title,
          "text": user.name + " " + text,
          "icon_url": context.JK.resolveAvatarUrl(user.photo_url)
        });
      )
      .fail(()=>
        @app.notify({
          "title": title,
          "text": 'Someone ' + text,
          "icon_url": "/assets/content/icon_alert_big.png"
        })
      )

    findUserBy: (finder) ->
      if finder.clientId
        foundParticipant = null
        for participant in @participants()
          if participant.client_id == finder.clientId
            foundParticipant = participant
            break

        if foundParticipant
          return $.Deferred().resolve(foundParticipant.user).promise();

      # TODO: find it via some REST API if not found?
      return $.Deferred().reject().promise();

    findParticipantByUserId: (userId) ->
      foundParticipant = null
      for participant in @participants()
        if participant.user.id == userId
          foundParticipant = participant
          break

      foundParticipant

    displayWhoCreatedRecording: (clientId) ->
      if @app.clientId != clientId # don't show to creator
        @findUserBy({clientId: clientId})
        .done((user) =>
          @app.notify({
            "title": "Recording Started",
            "text": user.name + " started a recording",
            "icon_url": context.JK.resolveAvatarUrl(user.photo_url)
          })
        )
        .fail(() =>
          @app.notify({
            "title": "Recording Started",
            "text": "Oops! Can't determine who started this recording",
            "icon_url": "/assets/content/icon_alert_big.png"
          })
        )

    promptUserToSave: (recordingId, timeline) ->
      rest.getRecording( {id: recordingId} )
      .done((recording) =>
        if timeline
          recording.timeline = timeline.global

        context.JK.recordingFinishedDialog.setRecording(recording)
        @app.layout.showDialog('recordingFinished').one(EVENTS.DIALOG_CLOSED, (e, data) =>
          if data.result && data.result.keep
            context.JK.prodBubble($('#recording-manager-viewer'), 'file-manager-poke', {}, {positions:['top', 'left', 'right', 'bottom'], offsetParent: $('#session-screen').parent()})
        )
      )
      .fail(@app.ajaxError)

    onEnterSession: (sessionId) ->
      if !context.JK.guardAgainstBrowser(@app)
        return false;

      window.location.href = '/client#/session/' + sessionId

    onJoinSession: (sessionId) ->

      # poke ShareDialog
      shareDialog = new JK.ShareDialog(@app, sessionId, "session");
      shareDialog.initialize(context.JK.FacebookHelperInstance);

      # initialize webcamViewer
      VideoActions.stopVideo();

      # double-check that we are connected to the server via websocket

      return unless @ensureConnected()

      # just make double sure a previous session state is cleared out
      @sessionEnded(true)

      # update the session data to be empty
      @updateCurrentSession(null)

      # start setting data for this new session
      @currentSessionId = sessionId
      @startTime = new Date().getTime()

      # let's find out the public/private nature of this session,
      # so that we can decide whether we need to validate the audio profile more aggressively
      rest.getSessionHistory(@currentSessionId)
      .done((musicSession)=>
        musicianAccessOnJoin = musicSession.musician_access

        shouldVerifyNetwork = musicSession.musician_access;

        # old client protection
        if !context.jamClient.getClientParentChildRole?
          clientRole = CLIENT_ROLE.PARENT
        else
          clientRole = context.jamClient.getClientParentChildRole()

        if clientRole == CLIENT_ROLE.CHILD
          logger.debug("client is configured to act as child. skipping all checks. assuming 0 tracks")
          @userTracks = []
          @joinSession()
          return

        @gearUtils.guardAgainstInvalidConfiguration(@app, shouldVerifyNetwork).fail(() =>
          SessionActions.leaveSession.trigger({location: '/client#/home'})
        ).done(() =>
          result = @sessionUtils.SessionPageEnter();

          @gearUtils.guardAgainstActiveProfileMissing(@app, result)
          .fail((data) =>
            leaveBehavior = {}

            if data && data.reason == 'handled'
              if data.nav == 'BACK'
                leaveBehavior.location = -1
              else
                leaveBehavior.location = data.nav
            else
              leaveBehavior.location = '/client#/home';

            SessionActions.leaveSession.trigger(leaveBehavior)
          ).done(() =>
            @waitForSessionPageEnterDone()
            .done((userTracks) =>
              @userTracks = userTracks

              @ensureAppropriateProfile(musicianAccessOnJoin)
              .done(() =>
                logger.debug("user has passed all session guards")
                @joinSession()
              )
              .fail((result) =>
                unless result.controlled_location
                  SessionActions.leaveSession.trigger({location: "/client#/home"})
              )
            ).fail((data) =>
              if data == "timeout"
                context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.')
              else if data == 'session_over'
                # do nothing; session ended before we got the user track  info. just bail
                logger.debug("session is over; bailing")
              else
                context.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data)

              SessionActions.leaveSession.trigger({location: '/client#/home'})
            )
          )
        )
      )
      .fail((e) =>
        logger.error("unable to fetch session history", e)
      )

    waitForSessionPageEnterDone: () ->
      @sessionPageEnterDeferred = $.Deferred()

      # see if we already have tracks; if so, we need to run with these
      inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient)

      logger.debug("isNoInputProfile", @gearUtils.isNoInputProfile())
      if inputTracks.length > 0 || @gearUtils.isNoInputProfile()
        logger.debug("on page enter, tracks are already available")
        @sessionPageEnterDeferred.resolve(inputTracks)
        deferred = @sessionPageEnterDeferred
        @sessionPageEnterDeferred = null
        return deferred

      @sessionPageEnterTimeout = setTimeout(()=>
        if @sessionPageEnterTimeout
          if @sessionPageEnterDeferred
            @sessionPageEnterDeferred.reject('timeout')
            @sessionPageEnterDeferred = null
          @sessionPageEnterTimeout = null
      , 5000)

      @sessionPageEnterDeferred

    ensureAppropriateProfile: (musicianAccess) ->
      deferred = new $.Deferred();
      if musicianAccess
        deferred = context.JK.guardAgainstSinglePlayerProfile(@app)
      else
        deferred.resolve();
      deferred

    openBrowserToPayment: () ->
      context.JK.popExternalLink("/client#/account/subscription", true)

    openBrowserToPlanComparison: () ->
      context.JK.popExternalLink("https://jamkazam.freshdesk.com/support/solutions/articles/66000122535-what-are-jamkazam-s-free-vs-premium-features-")
      return 'noclose'

    joinSession: () ->
      context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2");
      context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted");
      context.jamClient.SessionSetConnectionStatusRefreshRate(1000);
      clientRole = context.jamClient.getClientParentChildRole();
      parentClientId = context.jamClient.getParentClientId();
      logger.debug("role when joining session: #{clientRole}, parent client id #{parentClientId}")
      #context.JK.HelpBubbleHelper.jamtrackGuideSession($screen.find('li.open-a-jamtrack'), $screen)

      if clientRole == 0
        clientRole = 'child'
      else if clientRole == 1
        clientRole = 'parent'

      if clientRole == '' || !clientRole
        clientRole = null

      # subscribe to events from the recording model
      @recordingRegistration()

      # tell the server we want to join

      @joinDeferred = rest.joinSession({
        client_id: @app.clientId,
        ip_address: context.JK.JamServer.publicIP,
        as_musician: true,
        tracks: @userTracks,
        session_id: @currentSessionId,
        client_role: clientRole,
        parent_client_id: parentClientId
        audio_latency: context.jamClient.FTUEGetExpectedLatency().latency
      })
      .done((response) =>

        unless @inSession()
          # the user has left the session before they got joined.  We need to issue a leave again to the server to make sure they are out
          logger.debug("user left before fully joined to session.  telling server again that they have left")
          @leaveSessionRest(@currentSessionId)
          return

        @updateSessionInfo(response, true)
        @issueChange()

        logger.debug("calling jamClient.JoinSession");
        # on temporary disconnect scenarios, a user may already be in a session when they enter this path
        # so we avoid double counting
        unless @alreadyInSession()
          if @participants().length == 1
            context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.create);
          else
            context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join);

        @recordingModel.reset(@currentSessionId);

        joinSessionMsg = {sessionID: @currentSessionId, music_session_id_int: response.music_session_id_int}
        context.jamClient.JoinSession(joinSessionMsg);

        #@refreshCurrentSession(true);

        context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, @trackChanges);
        context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, @trackChanges);
        context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TRACKS_CHANGED, @trackChanges);
        context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, @trackChanges);

        $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: @currentSessionId, lesson_session: response.lesson_session}}) if document

        @handleAutoOpenJamTrack()

        @watchBackendStats()

        ConfigureTracksActions.reset(true)
        @delayEnableVst()
      )
      .fail((xhr) =>
        @updateCurrentSession(null)

        if xhr.status == 404
          # we tried to join the session, but it's already gone.  kick user back to join session screen
          leaveBehavior =
            location: "/client#/findSession"
            notify:
              title: "Unable to Join Session",
              text: " The session you attempted to join is over."
          SessionActions.leaveSession.trigger(leaveBehavior)
        else if xhr.status == 422
          response = JSON.parse(xhr.responseText);
          if response["errors"] && response["errors"]["tracks"] && response["errors"]["tracks"][0] == "Please select at least one track"
            @app.notifyAlert("No Inputs Configured", $('<span>You will need to reconfigure your audio device.</span>'))

          else if response["errors"] && response["errors"]["music_session"] && response["errors"]["music_session"][0] == ["is currently recording"]
            leaveBehavior =
              location: "/client#/findSession"
              notify:
                title: "Unable to Join Session"
                text: "The session is currently recording."
            SessionActions.leaveSession.trigger(leaveBehavior)
          else if response["errors"] && response["errors"]["remaining_session_play_time"]
            leaveBehavior =
              location: "/client#/findSession"
            buttons = []
            buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'})
            buttons.push({name: 'COMPARE PLANS', buttonStyle: 'button-grey', click: (() => (@openBrowserToPlanComparison()))})
            buttons.push({
              name: 'UPGRADE PLAN',
              buttonStyle: 'button-orange',
              click: (() => (@openBrowserToPayment()))
            })
            context.JK.Banner.show({
              title: "Out of Time For This Session",
              html: context._.template($('#template-no-remaining-session-play-time').html(), {}, { variable: 'data' }),
              buttons: buttons});
            SessionActions.leaveSession.trigger(leaveBehavior)
          else if response["errors"] && response["errors"]["remaining_month_play_time"]
            leaveBehavior =
              location: "/client#/findSession"
            buttons = []
            buttons.push({name: 'CLOSE', buttonStyle: 'button-grey'})
            buttons.push({name: 'COMPARE PLANS', buttonStyle: 'button-grey', click: (() => (@openBrowserToPlanComparison()))})
            buttons.push({
              name: 'UPGRADE PLAN',
              buttonStyle: 'button-orange',
              click: (() => (@openBrowserToPayment()))
            })
            context.JK.Banner.show({
              title: "Out of Time for the Month",
              html: context._.template($('#template-no-remaining-month-play-time').html(), {}, { variable: 'data' }),
              buttons: buttons});
            SessionActions.leaveSession.trigger(leaveBehavior)
          else
            @app.notifyServerError(xhr, 'Unable to Join Session');
        else
          @app.notifyServerError(xhr, 'Unable to Join Session');
      )

    delayEnableVst: () ->
      if @enableVstTimeout?
        clearTimeout(@enableVstTimeout)
        @enableVstTimeout = null

      isVstLoaded = context.jamClient.IsVstLoaded()
      hasVstAssignment = context.jamClient.hasVstAssignment()

      if hasVstAssignment && !isVstLoaded
        @enableVstTimeout = setTimeout((() =>
          @enableVst()
        ), 5000)
        @issueChange()

    enableVst: () ->
      @enableVstTimeout = null

      if @inSession()
        ConfigureTracksActions.enableVst()
      else
        logger.debug("no longer in session; not enabling VSTs at this time")
      @issueChange()

    watchBackendStats: () ->
      @backendStatsInterval = window.setInterval((() => (@updateBackendStats())), 1000)

    updateBackendStats: () ->
      connectionStats = window.jamClient.getConnectionDetail('', false)
      parentConnectionStats = window.jamClient.getConnectionDetail('', true)
      #console.log("CONNECTION STATES", connectionStats)
      #console.log("PARENT STATES", parentConnectionStats)
      SessionStatsActions.pushStats(connectionStats, parentConnectionStats)

    trackChanges: (header, payload) ->
      if @currentTrackChanges < payload.track_changes_counter
        # we don't have the latest info. try and go get it
        logger.debug("track_changes_counter = stale. refreshing...")
        @refreshCurrentSession();

      else
        if header.type != 'HEARTBEAT_ACK'
          # don't log if HEARTBEAT_ACK, or you will see this log all the time
          logger.info("track_changes_counter = fresh. skipping refresh...", header, payload)

    handleAutoOpenJamTrack: () ->
      jamTrack = @sessionUtils.grabAutoOpenJamTrack();
      if jamTrack
        # give the session to settle just a little (call a timeout of 1 second)
        setTimeout(()=>
          # tell the server we are about to open a jamtrack
          rest.openJamTrack({id: @currentSessionId, jam_track_id: jamTrack.id})
          .done((response) =>
            logger.debug("jamtrack opened")
            # now actually load the jamtrack
            context.SessionActions.updateSession.trigger(response);
            # context.JK.CurrentSessionModel.updateSession(response);
            # loadJamTrack(jamTrack);
            JamTrackActions.open(jamTrack)
          )
          .fail((jqXHR) =>
            @app.notifyServerError(jqXHR, "Unable to Open JamTrack For Playback")
          )
        , 1000)

    inSession: () ->
      !!@currentSessionId

    alreadyInSession: () ->
      inSession = false
      for participant in @participants()
        if participant.user.id == context.JK.currentUserId
          inSession = true
          break

    participants: () ->
      if @currentSession
        @currentSession.participants;
      else
        []

    refreshCurrentSession: (force) ->
      logger.debug("refreshCurrentSession(force=true)") if force

      @refreshCurrentSessionRest(force)

    refreshCurrentSessionRest: (force) ->
      unless @inSession()
        logger.debug("refreshCurrentSession skipped: ")
        return

      if @requestingSessionRefresh
        # if someone asks for a refresh while one is going on, we ask for another to queue up
        logger.debug("queueing refresh")
        @pendingSessionRefresh = true;
      else
        @requestingSessionRefresh = true
        rest.getSession(@currentSessionId)
        .done((response) =>
          try
            @updateSessionInfo(response, force)
          catch e
            logger.error("unable to updateSessionInfo in session refresh", e)
          #setTimeout(() =>
          #  @updateSessionInfo(response, force)
          #, 5000)
        )
        .fail((jqXHR) =>
          if jqXHR.status != 404
            @app.notifyServerError(jqXHR, "Unable to refresh session data")
          else
            logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone")
        )
        .always(() =>
          @requestingSessionRefresh = false
          if @pendingSessionRefresh
            # and when the request is done, if we have a pending, fire it off again
            @pendingSessionRefresh = false
            @refreshCurrentSessionRest(force)
        )

    onUpdateSession: (session) ->
      @updateSessionInfo(session, true)

    updateSessionInfo: (session, force) ->
      if force == true || @currentTrackChanges < session.track_changes_counter
        logger.debug("updating current track changes from %o to %o", @currentTrackChanges, session.track_changes_counter)
        @currentTrackChanges = session.track_changes_counter
        @sendClientParticipantChanges(@currentSession, session)
        logger.debug('update current session')
        @updateCurrentSession(session);
        #if(callback != null) {
        #  callback();
        #}
      else
        logger.info("ignoring refresh because we already have current: " + @currentTrackChanges + ", seen: " + session.track_changes_counter);


    leaveSessionRest: () ->
      rest.deleteParticipant(@app.clientId);

    sendClientParticipantChanges: (oldSession, newSession) ->
      joins = []
      leaves = []
      leaveJoins = []; # Will hold JamClientParticipants

      oldParticipants = []; # will be set to session.participants if session
      oldParticipantIds = {};
      newParticipants = [];
      newParticipantIds = {};

      if oldSession && oldSession.participants
        for oldParticipant in oldSession.participants
          oldParticipantIds[oldParticipant.client_id] = oldParticipant

      if newSession && newSession.participants
        for newParticipant in newSession.participants
          newParticipantIds[newParticipant.client_id] = newParticipant

      for client_id, participant of newParticipantIds
        # grow the 'all participants seen' list
        unless (client_id of @participantsEverSeen)
          @participantsEverSeen[client_id] = participant;


        if client_id of oldParticipantIds
          # if the participant is here now, and here before, there is still a chance we missed a
          # very fast leave/join. So check if joined_session_at is different
          if oldParticipantIds[client_id].joined_session_at != participant.joined_session_at
            leaveJoins.push(participant)
        else
          # new participant id that's not in old participant ids: Join
          joins.push(participant);

      for client_id, participant of oldParticipantIds
        unless (client_id of newParticipantIds)
          # old participant id that's not in new participant ids: Leave
          leaves.push(participant);

      for i, v of joins
        if v.client_id != @app.clientId
          @participantJoined(newSession, v)

      for i,v of leaves
        if v.client_id != @app.clientId
          @participantLeft(newSession, v)

      for i,v of leaveJoins
        if v.client_id != @app.clientId
          logger.debug("participant had a rapid leave/join")
          @participantLeft(newSession, v)
          @participantJoined(newSession, v)

    participantJoined: (newSession, participant) ->
      logger.debug("jamClient.ParticipantJoined", participant.client_id)
      context.jamClient.ParticipantJoined(newSession, @toJamClientParticipant(participant));
      @currentParticipants[participant.client_id] = {server: participant, client: {audio_established: null}}

    participantLeft: (newSession, participant) ->
      logger.debug("jamClient.ParticipantLeft", participant.client_id)
      context.jamClient.ParticipantLeft(newSession, @toJamClientParticipant(participant));
      delete @currentParticipants[participant.client_id]

    toJamClientParticipant: (participant) ->
      {
        userID: "",
        clientID: participant.client_id,
        client_id_int: participant.client_id_int,
        tcpPort: 0,
        udpPort: 0,
        localIPAddress: participant.ip_address, # ?
        globalIPAddress: participant.ip_address, # ?
        latency: 0,
        natType: ""
      }

    recordingRegistration: () ->
      logger.debug("recording registration not hooked up yet")

    updateCurrentSession: (sessionData) ->
      if sessionData != null
        @currentOrLastSession = sessionData

        if sessionData.session_rules
          @sessionRules = sessionData.session_rules
          # TESTING:
          #@sessionRules.remaining_session_play_time = 60 * 15 + 15 # 15 minutes and 15 seconds

          # compute timestamp due time
          if @sessionRules.remaining_session_play_time?
            until_time = new Date()
            until_time = new Date(until_time.getTime() + @sessionRules.remaining_session_play_time * 1000)
            console.log("subscription: session has remaining play time", until_time)
            @sessionRules.remaining_session_until = until_time


        if sessionData.subscription
          # for the backend - it looks here
          #sessionData.subscription = sessionData.subscription_rules
          # let the backend know
          #context.jamClient.applySubscriptionPolicy()
          @subscriptionRules = sessionData.subscription
          # TESTING:
          #@subscriptionRules.remaining_month_play_time = 60 * 15 + 15 # 15 minutes and 15 seconds

          if @subscriptionRules.remaining_month_play_time?
            until_time = new Date()
            until_time = new Date(until_time.getTime() + @subscriptionRules.remaining_month_play_time * 1000)
            #until_time.setSeconds(until_time.getSeconds() + @subscriptionRules.remaining_month_play_time)
            console.log("subscription: month has remaining play time", until_time)
            @subscriptionRules.remaining_month_until = until_time

      @currentSession = sessionData
      if context.jamClient.UpdateSessionInfo?
        if @currentSession?
          context.jamClient.UpdateSessionInfo(@currentSession)
        else
          context.jamClient.UpdateSessionInfo({})
      #logger.debug("session changed")

      logger.debug("issue change")
      @issueChange()

    ensureConnected: () ->
      unless context.JK.JamServer.connected
        leaveBehavior =
          location: '/client#/home'
          notify:
            title: "Not Connected"
            text: 'To create or join a session, you must be connected to the server.'

        SessionActions.leaveSession.trigger(leaveBehavior)

      context.JK.JamServer.connected

    # called by anyone wanting to leave the session with a certain behavior
    onLeaveSession: (behavior) ->
      logger.debug("attempting to leave session", behavior)

      if behavior.notify
        @app.layout.notify(behavior.notify)

      SessionActions.allowLeaveSession.trigger()

      if behavior.location
        if jQuery.isNumeric(behavior.location)
          window.history.go(behavior.location)
        else
          window.location = behavior.location
      else if behavior.hash
          window.location.hash = behavior.hash
      else
        logger.warn("no location specified in leaveSession action", behavior)
        window.location = '/client#/home'

      #VideoActions.stopVideo()


      if @currentSession?.lesson_session?
        isTeacher = context.JK.currentUserId == @currentSession.lesson_session.teacher_id

        tempSession = @currentSession
        rest.ratingDecision({
          as_student: !isTeacher,
          teacher_id: @currentSession.lesson_session.teacher_id,
          student_id: @currentSession.lesson_session.teacher_id
        }).done(((decision) =>
          showDialog = !decision.rating || showDialog = decision.lesson_count % 6 == 0

          if showDialog
            if isTeacher
              @app.layout.showDialog('rate-user-dialog', {d1: 'student_' + tempSession.lesson_session.student_id})
            else
              @app.layout.showDialog('rate-user-dialog', {d1: 'teacher_' + tempSession.lesson_session.teacher_id})
          else
            unless @rateSessionDialog?
              @rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app);
              @rateSessionDialog.initialize();

              @rateSessionDialog.showDialog();
        ))



      else
        unless @rateSessionDialog?
          @rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app);
          @rateSessionDialog.initialize();

        @rateSessionDialog.showDialog();

      @leaveSession()

      @sessionUtils.SessionPageLeave()



    leaveSession: () ->

      if !@joinDeferred? || @joinDeferred?.state() == 'resolved'
        deferred = new $.Deferred()

        @recordingModel.stopRecordingIfNeeded()
        .always(()=>
          @performLeaveSession(deferred)
        )

    performLeaveSession: (deferred) ->

      logger.debug("SessionModel.leaveCurrentSession()")
      # TODO - sessionChanged will be called with currentSession = null\

      # leave the session right away without waiting on REST.  Why?  If you can't contact the server, or if it takes a long
      # time, for that entire duration you'll still be sending voice data to the other users.
      # this may be bad if someone decides to badmouth others in the left-session during this time
      logger.debug("performLeaveSession: calling jamClient.LeaveSession for clientId=" + @app.clientId)
      context.jamClient.LeaveSession({ sessionID: @currentSessionId })
      @leaveSessionRest(@currentSessionId)
      .done(=>
        deferred.resolve(arguments[0], arguments[1], arguments[2]))
      .fail(=>
        deferred.reject(arguments[0], arguments[1], arguments[2]);
      )

      # 'unregister' for callbacks
      context.jamClient.SessionRegisterCallback("");
      #context.jamClient.SessionSetAlertCallback("");
      context.jamClient.SessionSetConnectionStatusRefreshRate(0);

      @sessionEnded()

      @issueChange()

    selfOpenedJamTracks: () ->
      @currentSession && (@currentSession.jam_track_initiator_id == context.JK.currentUserId)

    sessionEnded: (onJoin) ->
      # cleanup

      context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN,   @trackChanges);
      context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_DEPART, @trackChanges);
      context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.TRACKS_CHANGED, @trackChanges);
      context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.HEARTBEAT_ACK,  @trackChanges);

      if @sessionPageEnterDeferred?
        @sessionPageEnterDeferred.reject('session_over')
        @sessionPageEnterDeferred = null

      if @backendMixerAlertThrottleTimer
        clearTimeout(@backendMixerAlertThrottleTimer)
        @backendMixerAlertThrottleTimer = null

      @userTracks = null;
      @startTime = null;

      if @backendStatsInterval?
        window.clearInterval(@backendStatsInterval)
        @backendStatsInterval = null

      if @joinDeferred?.state() == 'resolved'
        $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: @currentSessionId}})

      @currentTrackChanges = 0
      @currentSession = null
      @joinDeferred = null
      @isRecording = false
      @currentSessionId = null
      @sessionRules = null
      @subscriptionRules = null
      @currentParticipants = {}
      @previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []}
      @openBackingTrack = null
      @shownAudioMediaMixerHelp = false
      @controlsLockedForJamTrackRecording = false
      @openBackingTrack = null
      @downloadingJamTrack = false
      @sessionUtils.setAutoOpenJamTrack(null) unless onJoin

      JamTrackActions.close()
      NotificationActions.sessionEnded()

      $(context.AppStore).triggerHandler('SessionEnded')

    id: () ->
      @currentSessionId

    canRecord: () ->
      if @subscriptionRules?
        console.log("can record? rules:", @subscriptionRules.can_record_audio )
        return @subscriptionRules.can_record_audio
      else
        console.log("can record? no rules; allow")
        return true

    canVideo: () ->
      if @subscriptionRules?
        console.log("can video? rules:", @subscriptionRules.can_use_video)
        return @subscriptionRules.can_use_video
      else
        console.log("can video? no rules; allow")
        return true

    getCurrentOrLastSession: () ->
      @currentOrLastSession

    handleJoinLeaveRequestCallback: (data) ->
      op = data["op"]
      logger.debug("client asks #{op} for #{data["id"]}")
      if op == "join"
        sessionId = data["id"]
        if sessionId != @currentSessionId
          window.location = "/client#/session/" + sessionId
        else
          logger.debug("dropped #{op} because sessionId #{sessionId} matches currentSessionId")
      else if op == "leave"
        sessionId = data["id"]
        if sessionId == @currentSessionId
          @onLeaveSession({location: '/client#/home'})
        else
          logger.debug("dropped #{op} because sessionId #{sessionId} does not match currentSessionId #{@currentSessionId}")


  }
)