$ = jQuery
context = window
context.JK ||= {};

context.JK.SyncViewer = class SyncViewer
  constructor: (@app) ->
    @EVENTS = context.JK.EVENTS
    @rest = context.JK.Rest()
    @logger = context.JK.logger
    @recordingUtils = context.JK.RecordingUtils
    @since = 0
    @limit = 20
    @showing = false
    @downloadCommandId = null
    @downloadMetadata = null
    @uploadCommandId = null
    @uploadMetadata = null
    @cleanupCommandId = null
    @cleanupMetadata = null


  init: () =>
    @root = $($('#template-sync-viewer').html())
    @inProgress = @root.find('.in-progress')
    @downloadProgress = @inProgress.find('.download-progress')
    @uploadProgress = @inProgress.find('.upload-progress')
    @list = @root.find('.list')
    @logList = @root.find('.log-list')
    @templateRecordedTrack = $('#template-sync-viewer-recorded-track')
    @templateRecordedBackingTrack = $('#template-sync-viewer-recorded-backing-track')
    @templateStreamMix = $('#template-sync-viewer-stream-mix')
    @templateMix = $('#template-sync-viewer-mix')
    @templateNoSyncs = $('#template-sync-viewer-no-syncs')
    @templateRecordingWrapperDetails = $('#template-sync-viewer-recording-wrapper-details')
    @templateHoverRecordedTrack = $('#template-sync-viewer-hover-recorded-track')
    @templateHoverRecordedBackingTrack = $('#template-sync-viewer-hover-recorded-backing-track')
    @templateHoverMix = $('#template-sync-viewer-hover-mix')
    @templateDownloadReset = $('#template-sync-viewer-download-progress-reset')
    @templateUploadReset = $('#template-sync-viewer-upload-progress-reset')
    @templateGenericCommand = $('#template-sync-viewer-generic-command')
    @templateRecordedTrackCommand = $('#template-sync-viewer-recorded-track-command')
    @templateRecordedBackingTrackCommand = $('#template-sync-viewer-recorded-backing-track-command')
    @templateLogItem = $('#template-sync-viewer-log-item')
    @tabSelectors = @root.find('.dialog-tabs .tab')
    @tabs = @root.find('.tab-content')
    @logBadge = @tabSelectors.find('.badge')
    @paginator = @root.find('.paginator-holder')

    @uploadStates = {
      unknown: 'unknown',
      too_many_upload_failures: 'too-many-upload-failures',
      me_upload_soon: 'me-upload-soon',
      them_upload_soon: 'them-upload-soon'
      missing: 'missing',
      me_uploaded: 'me-uploaded',
      them_uploaded: 'them-uploaded',
      not_mine: 'not-mine'
    }
    @clientStates = {
      unknown: 'unknown',
      too_many_uploads: 'too-many-downloads',
      hq: 'hq',
      sq: 'sq',
      missing: 'missing',
      discarded: 'discarded',
      not_mine: 'not-mine'
    }

    throw "no sync-viewer" if not @root.exists()
    throw "no in-progress" if not @inProgress.exists()
    throw "no list" if not @list.exists()
    throw "no recorded track template" if not @templateRecordedTrack.exists()
    throw "no stream mix template" if not @templateStreamMix.exists()
    throw "no empty syncs template" if not @templateNoSyncs.exists()

    $(document).on(@EVENTS.FILE_MANAGER_CMD_START, this.fileManagerCmdStart)
    $(document).on(@EVENTS.FILE_MANAGER_CMD_STOP, this.fileManagerCmdStop)
    $(document).on(@EVENTS.FILE_MANAGER_CMD_PROGRESS, this.fileManagerCmdProgress)
    $(document).on(@EVENTS.FILE_MANAGER_CMD_ASAP_UPDATE, this.fileManagerAsapCommandStatus)

    @tabSelectors.click((e) =>
      $tabSelected = $(e.target).closest('a.tab')
      purpose = $tabSelected.attr('purpose')

      $otherTabSelectors = @tabSelectors.not('[purpose="' + purpose + '"]')
      $otherTabs = @tabs.not('[purpose="' + purpose + '"]')
      $tab = @tabs.filter('[purpose="' + purpose + '"]')

      $otherTabs.hide();
      $tab.show()

      $otherTabSelectors.removeClass('selected');
      $tabSelected.addClass('selected');

      if purpose == 'log'
        @logBadge.hide()
    )

  onShow: () =>
    @showing = true
    this.load()

  onHide: () =>
    @showing = false
    #$(document).off(@EVENTS.FILE_MANAGER_CMD_START, this.fileManagerCmdStart)
    #$(document).off(@EVENTS.FILE_MANAGER_CMD_STOP, this.fileManagerCmdStop)
    #$(document).off(@EVENTS.FILE_MANAGER_CMD_PROGRESS, this.fileManagerCmdProgress)

  getUserSyncs: (page) =>
    @rest.getUserSyncs({since: page * @limit, limit: @limit})
      .done(this.processUserSyncs)


  load: () =>
    @list.empty()
    @since = 0

    this.renderHeader()

    this.getUserSyncs(0)
      .done((response) =>
        $paginator = context.JK.Paginator.create(response.total_entries, @limit, 0, this.getUserSyncs)
        @paginator.empty().append($paginator);
      )


  renderHeader: () =>
    recordingManagerState = context.jamClient.GetRecordingManagerState()
    if recordingManagerState.running
      @uploadProgress.removeClass('quiet paused busy')
      @downloadProgress.removeClass('quiet paused busy')

      if recordingManagerState.current_download
        @downloadProgress.addClass('busy')
      else
        @downloadProgress.addClass('quiet')
        @downloadProgress.find('.busy').empty()

      if recordingManagerState.current_upload
        @uploadProgress.addClass('busy')
      else
        @uploadProgress.addClass('quiet')
        @uploadProgress.find('.busy').empty()
    else
      @downloadProgress.removeClass('quiet paused busy').addClass('paused')
      @uploadProgress.removeClass('quiet paused busy').addClass('paused')
      @downloadProgress.find('.busy').empty()
      @uploadProgress.find('.busy').empty()

  updateMixState: ($mix) =>
    serverInfo = $mix.data('server-info')

    mixInfo = @recordingUtils.createMixInfo(serverInfo)

    $mixState = $mix.find('.mix-state')
    $mixStateMsg = $mixState.find('.msg')
    $mixStateProgress = $mixState.find('.progress')
    $mixState.removeClass('still-uploading discarded unknown mixed mixing waiting-to-mix error stream-mix').addClass(mixInfo.mixStateClass).attr('data-state', mixInfo.mixState).data('mix-state', mixInfo)
    $mixStateMsg.text(mixInfo.mixStateMsg)
    $mixStateProgress.css('width', '0')

  updateStreamMixState: ($streamMix) =>
    clientInfo = $streamMix.data('client-info')
    serverInfo = $streamMix.data('server-info')

    # determine client state
    clientStateMsg = 'UNKNOWN'
    clientStateClass = 'unknown'
    clientState = @clientStates.unknown

    if clientInfo?
      if clientInfo.local_state == 'COMPRESSED'
        clientStateMsg = 'STREAM QUALITY'
        clientStateClass = 'sq'
        clientState = @clientStates.sq
      else if clientInfo.local_state == 'UNCOMPRESSED'
        clientStateMsg = 'STREAM QUALITY'
        clientStateClass = 'sq'
        clientState = @clientStates.sq
      else if clientInfo.local_state == 'MISSING'
        clientStateMsg = 'MISSING'
        clientStateClass = 'missing'
        clientState = @clientStates.missing

    # determine upload state
    uploadStateMsg = 'UNKNOWN'
    uploadStateClass = 'unknown'
    uploadState = @uploadStates.unknown

    if !serverInfo.fully_uploaded
      if serverInfo.upload.too_many_upload_failures
        uploadStateMsg = 'UPLOAD FAILURE'
        uploadStateClass = 'error'
        uploadState = @uploadStates.too_many_upload_failures
      else
        if clientInfo?
          if clientInfo.local_state == 'UNCOMPRESSED' or clientInfo.local_state == 'COMPRESSED'
            uploadStateMsg = 'PENDING UPLOAD'
            uploadStateClass = 'upload-soon'
            uploadState = @uploadStates.me_upload_soon
          else
            uploadStateMsg = 'MISSING'
            uploadStateClass = 'missing'
            uploadState = @uploadStates.missing
        else
          uploadStateMsg = 'MISSING'
          uploadStateClass = 'missing'
          uploadState = @uploadStates.missing
    else
      uploadStateMsg = 'UPLOADED'
      uploadStateClass = 'uploaded'
      uploadState = @uploadStates.me_uploaded

    $clientState = $streamMix.find('.client-state')
    $clientStateMsg = $clientState.find('.msg')
    $clientStateProgress = $clientState.find('.progress')
    $uploadState = $streamMix.find('.upload-state')
    $uploadStateMsg = $uploadState.find('.msg')
    $uploadStateProgress = $uploadState.find('.progress')

    $clientState.removeClass('discarded missing sq hq unknown error').addClass(clientStateClass).attr('data-state', clientState).data('custom-class', clientStateClass)
    $clientStateMsg.text(clientStateMsg)
    $clientStateProgress.css('width', '0')
    $uploadState.removeClass('upload-soon error unknown missing uploaded').addClass(uploadStateClass).attr('data-state', uploadState).data('custom-class', uploadStateClass)
    $uploadStateMsg.text(uploadStateMsg)
    $uploadStateProgress.css('width', '0')

  updateTrackState: ($track) =>
    clientInfo = $track.data('client-info')
    serverInfo = $track.data('server-info')
    myTrack = serverInfo.user.id == context.JK.currentUserId

    # determine client state
    clientStateMsg = 'UNKNOWN'
    clientStateClass = 'unknown'
    clientState = @clientStates.unknown

    if serverInfo.download.should_download
      if serverInfo.download.too_many_downloads
        clientStateMsg = 'EXCESS DOWNLOADS'
        clientStateClass = 'error'
        clientState = @clientStates.too_many_uploads
      else
        if clientInfo?
          if clientInfo.local_state == 'HQ'
            clientStateMsg = 'HIGHEST QUALITY'
            clientStateClass = 'hq'
            clientState = @clientStates.hq
          else
            clientStateMsg = 'STREAM QUALITY'
            clientStateClass = 'sq'
            clientState = @clientStates.sq
        else
          clientStateMsg = 'MISSING'
          clientStateClass = 'missing'
          clientState = @clientStates.missing
    else
      clientStateMsg = 'DISCARDED'
      clientStateClass = 'discarded'
      clientState = @clientStates.discarded

    # determine upload state
    uploadStateMsg = 'UNKNOWN'
    uploadStateClass = 'unknown'
    uploadState = @uploadStates.unknown

    if !serverInfo.fully_uploaded
      if serverInfo.upload.too_many_upload_failures
        uploadStateMsg = 'UPLOAD FAILURE'
        uploadStateClass = 'error'
        uploadState = @uploadStates.too_many_upload_failures
      else
        if myTrack
          if clientInfo?
            if clientInfo.local_state == 'HQ'
              uploadStateMsg = 'PENDING UPLOAD'
              uploadStateClass = 'upload-soon'
              uploadState = @uploadStates.me_upload_soon
            else
              uploadStateMsg = 'MISSING'
              uploadStateClass = 'missing'
              uploadState = @uploadStates.missing
          else
            uploadStateMsg = 'MISSING'
            uploadStateClass = 'missing'
            uploadState = @uploadStates.missing
        else
          uploadStateMsg = 'PENDING UPLOAD'
          uploadStateClass = 'upload-soon'
          uploadState = @uploadStates.them_upload_soon
    else
      uploadStateMsg = 'UPLOADED'
      uploadStateClass = 'uploaded'
      if myTrack
        uploadState = @uploadStates.me_uploaded
      else
        uploadState = @uploadStates.them_uploaded

    $clientState = $track.find('.client-state')
    $clientStateMsg = $clientState.find('.msg')
    $clientStateProgress = $clientState.find('.progress')
    $uploadState = $track.find('.upload-state')
    $uploadStateMsg = $uploadState.find('.msg')
    $uploadStateProgress = $uploadState.find('.progress')

    $clientState.removeClass('discarded missing sq hq unknown error').addClass(clientStateClass).attr('data-state', clientState).data('custom-class', clientStateClass)
    $clientStateMsg.text(clientStateMsg)
    $clientStateProgress.css('width', '0')
    $uploadState.removeClass('upload-soon error unknown missing uploaded').addClass(uploadStateClass).attr('data-state', uploadState).data('custom-class', uploadStateClass)
    $uploadStateMsg.text(uploadStateMsg)
    $uploadStateProgress.css('width', '0')

    # this allows us to make styling decisions based on the combination of both client and upload state.
    $track.addClass("clientState-#{clientStateClass}").addClass("uploadState-#{uploadStateClass}")

    $clientRetry = $clientState.find('.retry')
    $uploadRetry = $uploadState.find('.retry')

    if gon.isNativeClient
      # handle client state

      # only show RETRY button if you have a SQ or if it's missing,  and it's been uploaded already
      if (clientState == @clientStates.sq or clientState == @clientStates.missing) and (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded)
        $clientRetry.show()
      else
        $clientRetry.hide()

      # only show RETRY button if you have the HQ track, it's your track, and the server doesn't yet have it
      if myTrack and @clientStates.hq and (uploadState == @uploadStates.error or uploadState == @uploadStates.me_upload_soon)
        $uploadRetry.show()
      else
        $uploadRetry.hide()
    else
      $clientRetry.hide()
      $uploadRetry.hide()

  updateBackingTrackState: ($track) =>
    clientInfo = $track.data('client-info')
    serverInfo = $track.data('server-info')
    myTrack = serverInfo.user.id == context.JK.currentUserId

    # determine client state
    clientStateMsg = 'UNKNOWN'
    clientStateClass = 'unknown'
    clientState = @clientStates.unknown

    if serverInfo.mine
      if serverInfo.download.should_download
        if serverInfo.download.too_many_downloads
          clientStateMsg = 'EXCESS DOWNLOADS'
          clientStateClass = 'error'
          clientState = @clientStates.too_many_uploads
        else
          if clientInfo?
            if clientInfo.local_state == 'HQ'
              clientStateMsg = 'HIGHEST QUALITY'
              clientStateClass = 'hq'
              clientState = @clientStates.hq
            else if clientInfo.local_state == 'MISSING'
              clientStateMsg = 'MISSING'
              clientStateClass = 'missing'
              clientState = @clientStates.missing
          else
            clientStateMsg = 'MISSING'
            clientStateClass = 'missing'
            clientState = @clientStates.missing
      else
        clientStateMsg = 'DISCARDED'
        clientStateClass = 'discarded'
        clientState = @clientStates.discarded
    else
      clientStateMsg = 'NOT MINE'
      clientStateClass = 'not_mine'
      clientState = @clientStates.not_mine

    # determine upload state
    uploadStateMsg = 'UNKNOWN'
    uploadStateClass = 'unknown'
    uploadState = @uploadStates.unknown

    if serverInfo.mine
      if !serverInfo.fully_uploaded
        if serverInfo.upload.too_many_upload_failures
          uploadStateMsg = 'UPLOAD FAILURE'
          uploadStateClass = 'error'
          uploadState = @uploadStates.too_many_upload_failures
        else
          if myTrack
            if clientInfo?
              if clientInfo.local_state == 'HQ'
                uploadStateMsg = 'PENDING UPLOAD'
                uploadStateClass = 'upload-soon'
                uploadState = @uploadStates.me_upload_soon
              else
                uploadStateMsg = 'MISSING'
                uploadStateClass = 'missing'
                uploadState = @uploadStates.missing
            else
              uploadStateMsg = 'MISSING'
              uploadStateClass = 'missing'
              uploadState = @uploadStates.missing
          else
            uploadStateMsg = 'PENDING UPLOAD'
            uploadStateClass = 'upload-soon'
            uploadState = @uploadStates.them_upload_soon
      else
        uploadStateMsg = 'UPLOADED'
        uploadStateClass = 'uploaded'
        if myTrack
          uploadState = @uploadStates.me_uploaded
        else
          uploadState = @uploadStates.them_uploaded
    else
      uploadStateMsg = 'NOT MINE'
      uploadStateClass = 'not_mine'
      uploadState = @uploadStates.not_mine


    $clientState = $track.find('.client-state')
    $clientStateMsg = $clientState.find('.msg')
    $clientStateProgress = $clientState.find('.progress')
    $uploadState = $track.find('.upload-state')
    $uploadStateMsg = $uploadState.find('.msg')
    $uploadStateProgress = $uploadState.find('.progress')

    $clientState.removeClass('discarded missing hq unknown error not-mine').addClass(clientStateClass).attr('data-state', clientState).data('custom-class', clientStateClass)
    $clientStateMsg.text(clientStateMsg)
    $clientStateProgress.css('width', '0')
    $uploadState.removeClass('upload-soon error unknown missing uploaded not-mine').addClass(uploadStateClass).attr('data-state', uploadState).data('custom-class', uploadStateClass)
    $uploadStateMsg.text(uploadStateMsg)
    $uploadStateProgress.css('width', '0')

    # this allows us to make styling decisions based on the combination of both client and upload state.
    $track.addClass("clientState-#{clientStateClass}").addClass("uploadState-#{uploadStateClass}")

    $clientRetry = $clientState.find('.retry')
    $uploadRetry = $uploadState.find('.retry')

    if gon.isNativeClient
      # handle client state

      # only show RETRY button if you have a SQ or if it's missing,  and it's been uploaded already
      if (clientState == @clientStates.missing) and (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded)
        $clientRetry.show()
      else
        $clientRetry.hide()

      # only show RETRY button if you have the HQ track, it's your track, and the server doesn't yet have it
      if myTrack and @clientStates.hq and (uploadState == @uploadStates.error or uploadState == @uploadStates.me_upload_soon)
        $uploadRetry.show()
      else
        $uploadRetry.hide()
    else
      $clientRetry.hide()
      $uploadRetry.hide()


  associateClientInfo: (recording) =>
    for clientInfo in recording.local_tracks
      $track = @list.find(".recorded-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']")
      $track.data('client-info', clientInfo)
      $track.data('total-size', recording.size)

    for clientInfo in recording.backing_tracks
      $track = @list.find(".recorded-backing-track[data-recording-id='#{recording.recording_id}'][data-client-track-id='#{clientInfo.client_track_id}']")
      $track.data('client-info', clientInfo)
      $track.data('total-size', recording.size)

    $track = @list.find(".mix[data-recording-id='#{recording.recording_id}']")
    $track.data('client-info', recording.mix)
    $track.data('total-size', recording.size)

    $track = @list.find(".stream-mix[data-recording-id='#{recording.recording_id}']")
    $track.data('client-info', recording.stream_mix)
    $track.data('total-size', recording.size)

  displayStreamMixHover: ($streamMix) =>
    $clientState = $streamMix.find('.client-state')
    $clientStateMsg = $clientState.find('.msg')
    clientStateClass = $clientState.data('custom-class')
    clientState = $clientState.attr('data-state')
    clientInfo = $streamMix.data('client-info')

    $uploadState = $streamMix.find('.upload-state')
    $uploadStateMsg = $uploadState.find('.msg')
    uploadStateClass = $uploadState.data('custom-class')
    uploadState = $uploadState.attr('data-state')
    serverInfo = $streamMix.data('server-info')

    # decide on special case strings first

    summary = ''
    if clientState == @clientStates.sq && uploadState == @uploadStates.me_upload_soon
      summary = "We will attempt to upload your stream mix so that others can hear the recording on the JamKazam site, even before the final mix is done.  It will upload shortly."
    else if clientState == @clientStates.sq && uploadState == @uploadStates.me_uploaded
      # we have the SQ version, and the other user has uploaded the HQ version... it's coming soon!
      summary = "We already uploaded your stream mix so that others can hear the recording on the JamKazam site, even before the final mix is done.  Since it's uploaded, there is nothing else left to do with the stream mix...  you're all done!"
    else if clientState == @clientStates.missing
      summary = "You do not have the stream mix on your computer anymore.  This can happen if you change the computer that you run JamKazam on.  It's important to note that once a final mix for the recording is available, there is no value in the stream mix."

    clientStateDefinition = switch clientState
      when @clientStates.sq then "The stream mix is always STREAM QUALITY, because it's the version of the recording that you heard in your earphones as you made the recording."
      when @clientStates.missing then "MISSING means you do not have the stream mix anymore."
      else 'There is no help for this state'

    uploadStateDefinition = switch uploadState
      when @uploadStates.too_many_upload_failures then "Failed attempts at uploading this stream mix has happened an unusually large times.  No more uploads will be attempted."
      when @uploadStates.me_upload_soon then "PENDING UPLOAD means your JamKazam application will upload this stream mix soon."
      when @uploadStates.me_uploaded then "UPLOADED means you have already uploaded this stream mix."
      when @uploadStates.missing then "MISSING means your JamKazam application does not have this stream mix, and the server does not either."

    context._.template(@templateHoverRecordedTrack.html(),
    {summary: summary,
    clientStateDefinition: clientStateDefinition,
    uploadStateDefinition: uploadStateDefinition,
    clientStateMsg: $clientStateMsg.text(),
    uploadStateMsg: $uploadStateMsg.text(),
    clientStateClass: clientStateClass,
    uploadStateClass: uploadStateClass}
    {variable: 'data'})

  displayTrackHover: ($recordedTrack) =>
    $clientState = $recordedTrack.find('.client-state')
    $clientStateMsg = $clientState.find('.msg')
    clientStateClass = $clientState.data('custom-class')
    clientState = $clientState.attr('data-state')
    clientInfo = $recordedTrack.data('client-info')

    $uploadState = $recordedTrack.find('.upload-state')
    $uploadStateMsg = $uploadState.find('.msg')
    uploadStateClass = $uploadState.data('custom-class')
    uploadState = $uploadState.attr('data-state')
    serverInfo = $recordedTrack.data('server-info')

    # decide on special case strings first

    summary = ''
    if clientState == @clientStates.sq && uploadState == @uploadStates.them_upload_soon
      # we have the SQ version, and the other user hasn't uploaded it yet
      summary = "#{serverInfo.user.name} has not yet uploaded the high-quality version of this track.  Once he or she does, JamKazam will download it and replace your stream-quality version."
    else if clientState == @clientStates.missing && uploadState == @uploadStates.them_upload_soon
      # we don't have any version of the track at all, and the other user hasn't uploaded it yet
      summary = "#{serverInfo.user.name} has not yet uploaded the high-quality version of this track.  Once he or she does, JamKazam will download it and this track will no longer be missing."
    else if clientState == @clientStates.sq && uploadState == @uploadStates.them_uploaded
      # we have the SQ version, and the other user has uploaded the HQ version... it's coming soon!
      summary = "#{serverInfo.user.name} has uploaded the high-quality version of this track.  JamKazam will soon download it and replace your stream-quality version."
    else if clientState == @clientStates.missing && uploadState == @uploadStates.them_uploaded
      # we have no version of the track at all, and the other user has uploaded the HQ version... it's coming soon!
      summary = "#{serverInfo.user.name} has uploaded the high-quality version of this track.  JamKazam will soon restore it and then this track will no longer be missing."
    else if clientState == @clientStates.sq && uploadState == @uploadStates.me_uploaded
      # we have the SQ version, and the other user has uploaded the HQ version... it's coming soon!
      summary = "You have previously uploaded the high-quality version of this track.  JamKazam will soon restore it and replace your stream-quality version."
    else if clientState == @clientStates.missing && uploadState == @uploadStates.me_uploaded
      # we have no version of the track at all, and the other user has uploaded the HQ version... it's coming soon!
      summary = "You have previously uploaded the high-quality version of this track.  JamKazam will soon restore it and then this track will no longer be missing."
    else if clientState == @clientStates.discarded && (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded)
      # we decided not to keep the recording... so it's important to clarify why they are seeing it at all
      summary = "When this recording was made, you elected to not keep it. JamKazam already uploaded your high-quality tracks for the recording, because at least one other person decided to keep the recording and needs your tracks to make a high-quality mix."
    else if clientState == @clientStates.discarded
      # we decided not to keep the recording... so it's important to clarify why they are seeing it at all
      summary = "When this recording was made, you elected to not keep it.  JamKazam will still try to upload your high-quality tracks for the recording, because at least one other person decided to keep the recording and needs your tracks to make a high-quality mix."
    else if clientState == @clientStates.hq and ( uploadState == @uploadStates.them_uploaded or uploadState == @uploadStates.me_uploaded )
      summary = "Both you and the JamKazam server have the high-quality version of this track.  Once all the other tracks for this recording are also synchronized, then the final mix can be made."

    clientStateDefinition = switch clientState
      when @clientStates.too_many_downloads then "This track has been downloaded an unusually large number of times. No more downloads are allowed."
      when @clientStates.hq then "HIGHEST QUALITY means you have the original version of this track, as recorded by the user that made it."
      when @clientStates.sq then "STREAM QUALITY means you have the version of the track that you received over the internet in real-time."
      when @clientStates.missing then "MISSING means you do not have this track anymore."
      when @clientStates.discarded then "DISCARDED means you chose to not keep this recording when the recording was over."
      else 'There is no help for this state'

    uploadStateDefinition = switch uploadState
      when @uploadStates.too_many_upload_failures then "Failed attempts at uploading this track has happened an unusually large times.  No more uploads will be attempted."
      when @uploadStates.me_upload_soon then "PENDING UPLOAD means your JamKazam application will upload this track soon."
      when @uploadStates.them_up_soon then "PENDING UPLOAD means #{serverInfo.user.name} will upload this track soon."
      when @uploadStates.me_uploaded then "UPLOADED means you have already uploaded this track."
      when @uploadStates.them_uploaded then "UPLOADED means #{serverInfo.user.name} has already uploaded this track."
      when @uploadStates.missing then "MISSING means your JamKazam application does not have this track, and the server does not either."

    context._.template(@templateHoverRecordedTrack.html(),
    {summary: summary,
    clientStateDefinition: clientStateDefinition,
    uploadStateDefinition: uploadStateDefinition,
    clientStateMsg: $clientStateMsg.text(),
    uploadStateMsg: $uploadStateMsg.text(),
    clientStateClass: clientStateClass,
    uploadStateClass: uploadStateClass}
    {variable: 'data'})

  displayBackingTrackHover: ($recordedTrack) =>
    $clientState = $recordedTrack.find('.client-state')
    $clientStateMsg = $clientState.find('.msg')
    clientStateClass = $clientState.data('custom-class')
    clientState = $clientState.attr('data-state')
    clientInfo = $recordedTrack.data('client-info')

    $uploadState = $recordedTrack.find('.upload-state')
    $uploadStateMsg = $uploadState.find('.msg')
    uploadStateClass = $uploadState.data('custom-class')
    uploadState = $uploadState.attr('data-state')
    serverInfo = $recordedTrack.data('server-info')

    # decide on special case strings first

    summary = ''
    if clientState == @clientStates.not_mine && @uploadStates.them_uploaded
      # this is not our backing track
      summary = "#{serverInfo.user.name} opened this backing track.  Due to legal concerns, we can not distribute it to you."
    else if clientState == @clientStates.not_mine && @uploadStates.them_upload_soon
      # this is not our backing track
      summary = "#{serverInfo.user.name} has not yet uploaded their backing track."
    else if clientState == @clientStates.missing && uploadState == @uploadStates.me_uploaded
      # we have no version of the track at all, and the other user has uploaded the HQ version... it's coming soon!
      summary = "You have previously uploaded the high-quality version of this track.  JamKazam will soon restore it and then this backing track will no longer be missing."
    else if clientState == @clientStates.discarded && (uploadState == @uploadStates.me_uploaded or uploadState == @uploadStates.them_uploaded)
      # we decided not to keep the recording... so it's important to clarify why they are seeing it at all
      summary = "When this recording was made, you elected to not keep it. JamKazam already uploaded your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix."
    else if clientState == @clientStates.discarded
      # we decided not to keep the recording... so it's important to clarify why they are seeing it at all
      summary = "When this recording was made, you elected to not keep it.  JamKazam will still try to upload your high-quality backing track for the recording, because at least one other person decided to keep the recording and needs your backing track to make a high-quality mix."
    else if clientState == @clientStates.hq and ( uploadState == @uploadStates.me_uploaded )
      summary = "Both you and the JamKazam server have the high-quality version of this track.  Once all the other tracks for this recording are also synchronized, then the final mix can be made."

    clientStateDefinition = switch clientState
      when @clientStates.too_many_downloads then "This backing track has been downloaded an unusually large number of times. No more downloads are allowed."
      when @clientStates.hq then "HIGHEST QUALITY means you have the original version of this backing track."
      when @clientStates.missing then "MISSING means you do not have this backing track anymore."
      when @clientStates.discarded then "DISCARDED means you chose to not keep this recording when the recording was over."
      when @clientStates.not_mine then "NOT MINE means someone else opened and played this backing track."
      else 'There is no help for this state'

    uploadStateDefinition = switch uploadState
      when @uploadStates.too_many_upload_failures then "Failed attempts at uploading this backing track has happened an unusually large times.  No more uploads will be attempted."
      when @uploadStates.me_upload_soon then "PENDING UPLOAD means your JamKazam application will upload this backing track soon."
      when @uploadStates.them_up_soon then "PENDING UPLOAD means #{serverInfo.user.name} will upload this  backing track soon."
      when @uploadStates.me_uploaded then "UPLOADED means you have already uploaded this backing track."
      when @uploadStates.them_uploaded then "UPLOADED means #{serverInfo.user.name} has already uploaded this backing track."
      when @uploadStates.missing then "MISSING means your JamKazam application does not have this backing track, and the server does not either."
      when @uploadStates.not_mine then "NOT MINE means someone else opened and played this backing track."

    context._.template(@templateHoverRecordedBackingTrack.html(),
    {summary: summary,
    clientStateDefinition: clientStateDefinition,
    uploadStateDefinition: uploadStateDefinition,
    clientStateMsg: $clientStateMsg.text(),
    uploadStateMsg: $uploadStateMsg.text(),
    clientStateClass: clientStateClass,
    uploadStateClass: uploadStateClass}
    {variable: 'data'})

  onTrackHoverOfStateIndicator: () ->
    $recordedTrack = $(this).closest('.recorded-track.sync')
    self = $recordedTrack.data('sync-viewer')
    self.displayTrackHover($recordedTrack)

  onBackingTrackHoverOfStateIndicator: () ->
    $recordedTrack = $(this).closest('.recorded-backing-track.sync')
    self = $recordedTrack.data('sync-viewer')
    self.displayBackingTrackHover($recordedTrack)

  onStreamMixHover: () ->
    $streamMix = $(this).closest('.stream-mix.sync')
    self = $streamMix.data('sync-viewer')
    self.displayStreamMixHover($streamMix)

  resetDownloadProgress: () =>
    @downloadProgress

  resetUploadProgress: () =>
    @uploadProgress

  sendCommand: ($retry, cmd) =>

    if context.SessionStore.inSession()
      context.JK.ackBubble($retry, 'sync-viewer-paused', {}, {offsetParent: $retry.closest('.dialog')})
    else
      context.jamClient.OnTrySyncCommand(cmd)
      context.JK.ackBubble($retry, 'sync-viewer-retry', {}, {offsetParent: $retry.closest('.dialog')})


  retryDownloadRecordedTrack: (e) =>
    $retry = $(e.target)
    $track = $retry.closest('.recorded-track')
    serverInfo = $track.data('server-info')

    this.sendCommand($retry, {
      type: 'recorded_track',
      action: 'download'
      queue: 'download',
      recording_id: serverInfo.recording_id
      track_id: serverInfo.client_track_id
    })

    return false

  retryUploadRecordedTrack: (e) =>
    $retry = $(e.target)
    $track = $retry.closest('.recorded-track')
    serverInfo = $track.data('server-info')

    this.sendCommand($retry, {
      type: 'recorded_track',
      action: 'upload'
      queue: 'upload',
      recording_id: serverInfo.recording_id
      track_id: serverInfo.client_track_id
    })

    return false

  retryDownloadRecordedBackingTrack: (e) =>
    $retry = $(e.target)
    $track = $retry.closest('.recorded-backing-track')
    serverInfo = $track.data('server-info')

    console.log("track serverInfo", $track, serverInfo)
    this.sendCommand($retry, {
      type: 'recorded_backing_track',
      action: 'download'
      queue: 'download',
      recording_id: serverInfo.recording_id
      track_id: serverInfo.client_track_id
    })

    return false

  retryUploadRecordedBackingTrack: (e) =>
    $retry = $(e.target)
    $track = $retry.closest('.recorded-backing-track')
    serverInfo = $track.data('server-info')

    console.log("track serverInfo", $track, serverInfo)

    this.sendCommand($retry, {
      type: 'recorded_backing_track',
      action: 'upload'
      queue: 'upload',
      recording_id: serverInfo.recording_id
      track_id: serverInfo.client_track_id
    })

    return false

  createMix: (userSync) =>
    recordingInfo = null
    if userSync == 'fake'
      recordingInfo = arguments[1]
      # sift through the recorded_tracks in here; if they are marked discarded, then we can also mark this one discarded too
      discarded = true
      for claim in recordingInfo.claimed_recordings
          if claim.user_id == context.JK.currentUserId
            discarded = false
            break

      userSync = { recording_id: recordingInfo.id, duration: recordingInfo.duration, fake:true, discarded: discarded }
      $mix = $(context._.template(@templateMix.html(), userSync, {variable: 'data'}))
    else
      $mix = $(context._.template(@templateMix.html(), userSync, {variable: 'data'}))

    $mix.data('server-info', userSync)
    $mix.data('sync-viewer', this)
    $mix.data('view-context', 'sync')
    $mixState = $mix.find('.mix-state')
    this.updateMixState($mix)
    context.JK.hoverBubble($mixState, @recordingUtils.onMixHover, {width:'450px', closeWhenOthersOpen: true, positions:['left'], trigger:['hoverIntent', 'none']})
    $mix

  createTrack: (userSync) =>
    $track = $(context._.template(@templateRecordedTrack.html(), userSync, {variable: 'data'}))
    $track.data('server-info', userSync)
    $track.data('sync-viewer', this)
    $clientState = $track.find('.client-state')
    $uploadState = $track.find('.upload-state')
    $clientStateRetry = $clientState.find('.retry')
    $clientStateRetry.click(this.retryDownloadRecordedTrack)
    $uploadStateRetry = $uploadState.find('.retry')
    $uploadStateRetry.click(this.retryUploadRecordedTrack)
    context.JK.bindHoverEvents($track)
    context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true});
    context.JK.hoverBubble($clientState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
    context.JK.hoverBubble($uploadState, this.onTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
    $clientState.addClass('is-native-client') if gon.isNativeClient
    $uploadState.addClass('is-native-client') if gon.isNativeClient
    $track

  createBackingTrack: (userSync) =>
    $track = $(context._.template(@templateRecordedBackingTrack.html(), userSync, {variable: 'data'}))
    $track.data('server-info', userSync)
    $track.data('sync-viewer', this)
    $clientState = $track.find('.client-state')
    $uploadState = $track.find('.upload-state')
    $clientStateRetry = $clientState.find('.retry')
    $clientStateRetry.click(this.retryDownloadRecordedBackingTrack)
    $uploadStateRetry = $uploadState.find('.retry')
    $uploadStateRetry.click(this.retryUploadRecordedBackingTrack)
    context.JK.bindHoverEvents($track)
    context.JK.bindInstrumentHover($track, {positions:['top'], shrinkToFit: true});
    context.JK.hoverBubble($clientState, this.onBackingTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
    context.JK.hoverBubble($uploadState, this.onBackingTrackHoverOfStateIndicator, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
    $clientState.addClass('is-native-client') if gon.isNativeClient
    $uploadState.addClass('is-native-client') if gon.isNativeClient
    $track

  createStreamMix: (userSync) =>
    $track = $(context._.template(@templateStreamMix.html(), userSync, {variable: 'data'}))
    $track.data('server-info', userSync)
    $track.data('sync-viewer', this)
    $clientState = $track.find('.client-state')
    $uploadState = $track.find('.upload-state')
    $uploadState.find('.retry').click(this.retryUploadRecordedTrack)
    context.JK.hoverBubble($clientState, this.onStreamMixHover, {width:'450px', closeWhenOthersOpen: true, positions:['left']})
    context.JK.hoverBubble($uploadState, this.onStreamMixHover, {width:'450px', closeWhenOthersOpen: true, positions:['right']})
    $clientState.addClass('is-native-client') if gon.isNativeClient
    $uploadState.addClass('is-native-client') if gon.isNativeClient
    $track

  openVideo: (e) =>
    $link = $(e.target)
    recordingId = $link.closest('.details').attr('data-recording-id')

    if !context.jamClient.openVideo(recordingId)
      @app.layout.notify({title: 'No Video Found', text: 'Did you delete this recording earlier from your computer?'})
    return false

  exportRecording: (e) =>
    $export = $(e.target)
    if context.SessionStore.inSession()
      context.JK.ackBubble($export, 'sync-viewer-paused', {}, {offsetParent: $export.closest('.dialog')})
      return

    recordingId = $export.closest('.details').attr('data-recording-id')
    if !recordingId? or recordingId == ""
      throw "exportRecording can't find data-recording-id"

    cmd =
    { type: 'export_recording',
    action: 'export',
    queue: 'upload',
    recording_id: recordingId}

    logger.debug("enqueueing export")
    context.jamClient.OnTrySyncCommand(cmd)
    return false;

  deleteRecording: (e) =>
    $delete = $(e.target)
    if context.SessionStore.inSession()
      context.JK.ackBubble($delete, 'sync-viewer-paused', {}, {offsetParent: $delete.closest('.dialog')})
      return

    $details = $delete.closest('.details')
    recordingId = $details.attr('data-recording-id')

    if !recordingId? or recordingId == ""
      throw "deleteRecording can't find data-recording-id"

    context.JK.Banner.showYesNo({
      title: "Confirm Deletion",
      html: "Are you sure you want to delete this recording?",
      yes: =>
        @rest.deleteRecordingClaim(recordingId).done((response)=>
          cmd =
          { type: 'recording_directory',
          action: 'delete',
          queue: 'cleanup',
          recording_id: recordingId}

          # now check if the sync is gone entirely, allowing us to delete it from the UI
          @rest.getUserSync({user_sync_id: recordingId}).done((userSync) =>
            # the user sync is still here. tell user it'll be done as soon as they've uploaded their files
              context.JK.ackBubble($delete, 'file-sync-delayed-deletion', {}, {offsetParent: $delete.closest('.dialog')})
          )
          .fail((xhr) =>
            if xhr.status == 404
              # the userSync is gone; remove from file manager dynamically
              $recordingHolder =  $details.closest('.recording-holder')
              $recordingHolder.slideUp()
            else
              @app.ajaxError(arguments)
          )

          context.jamClient.OnTrySyncCommand(cmd)
        )
        .fail(@app.ajaxError)
    })

    return false;

  displaySize: (size) =>
    # size is in bytes.  divide by million, anxosd round to one decimal place
    megs = Math.round(size * 10 / (1024 * 1024) ) / 10
    "#{megs}M"

  createRecordingWrapper: ($toWrap, recordingInfo) =>
    totalSize = $($toWrap.get(0)).data('total-size')
    recordingInfo.recording_landing_url = "/recordings/#{recordingInfo.id}"
    recordingInfo.totalSize = this.displaySize(totalSize)
    recordingInfo.claimedRecordingId =  recordingInfo.my?.id
    $wrapperDetails = $(context._.template(@templateRecordingWrapperDetails.html(), recordingInfo, {variable: 'data'}))
    $wrapper = $('<div class="recording-holder"></div>')
    $toWrap.wrapAll($wrapper)
    $wrapper = $toWrap.closest('.recording-holder')
    $wrapperDetails.prependTo($wrapper)
    $mix = $wrapper.find('.mix.sync')
    if $mix.length == 0
      # create a virtual mix so that the UI is consistent
      $wrapper.append(this.createMix('fake', recordingInfo))


    if recordingInfo.video
      $wrapper.find('a.open-video').click(this.openVideo).removeClass('hidden')

    $wrapper.find('a.export').click(this.exportRecording)
    $wrapper.find('a.delete').click(this.deleteRecording)

  separateByRecording: () =>
    $recordedTracks = @list.find('.sync')

    currentRecordingId = null;

    queue = $([]);

    for recordedTrack in $recordedTracks
      $recordedTrack = $(recordedTrack)
      recordingId = $recordedTrack.attr('data-recording-id')
      if recordingId != currentRecordingId
        if queue.length > 0
          this.createRecordingWrapper(queue, $(queue.get(0)).data('server-info').recording)
          queue = $([])

      currentRecordingId = recordingId
      queue = queue.add(recordedTrack)

    if queue.length > 0
      this.createRecordingWrapper(queue, $(queue.get(0)).data('server-info').recording)

  processUserSyncs: (response) =>

    @list.empty()

    # check if no entries
    if @since == 0 and response.entries.length == 0
      @list.append(context._.template(@templateNoSyncs.html(), {}, {variable: 'data'}))
    else
      recordings = {} # collect all unique recording
      for userSync in response.entries
        if userSync.type == 'recorded_track'
          @list.append(this.createTrack(userSync))
        if userSync.type == 'recorded_backing_track'
          @list.append(this.createBackingTrack(userSync))
        else if userSync.type == 'mix'
          @list.append(this.createMix(userSync))
        else if userSync.type == 'stream_mix'
          @list.append(this.createStreamMix(userSync))

        recordings[userSync.recording_id] = userSync.recording
      recordingsToResolve = []
      # resolve each track against backend data:
      for recording_id, recording of recordings
        recordingsToResolve.push(recording)

      clientRecordings = context.jamClient.GetLocalRecordingState(recordings: recordingsToResolve)

      if clientRecordings.error?
        alert(clientRecordings.error)
      else
        this.associateClientInfo(turp) for turp in clientRecordings.recordings

      for track in @list.find('.recorded-track.sync')
        this.updateTrackState($(track))
      for track in @list.find('.recorded-backing-track.sync')
        this.updateBackingTrackState($(track))
      for streamMix in @list.find('.stream-mix.sync')
        this.updateStreamMixState($(streamMix))

    this.separateByRecording()

    @since = response.next

  resolveTrack: (commandMetadata) =>
    recordingId = commandMetadata['recording_id']
    clientTrackId = commandMetadata['track_id']

    matchingTrack = @list.find(".recorded-track[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
    if matchingTrack.length == 0
      return @rest.getRecordedTrack({recording_id: recordingId, track_id: clientTrackId})
    else
      deferred = $.Deferred();
      deferred.resolve(matchingTrack.data('server-info'))
      return deferred

  resolveBackingTrack: (commandMetadata) =>
    recordingId = commandMetadata['recording_id']
    clientTrackId = commandMetadata['track_id']

    matchingTrack = @list.find(".recorded-backing-track[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
    if matchingTrack.length == 0
      return @rest.getRecordedBackingTrack({recording_id: recordingId, track_id: clientTrackId})
    else
      deferred = $.Deferred();
      deferred.resolve(matchingTrack.data('server-info'))
      return deferred

  renderFullUploadRecordedTrack: (serverInfo) =>
    $track = $(context._.template(@templateRecordedTrackCommand.html(), $.extend(serverInfo, {action:'UPLOADING'}), {variable: 'data'}))
    $busy = @uploadProgress.find('.busy')
    $busy.empty().append($track)
    @uploadProgress.find('.progress').css('width', '0%')

  renderFullDownloadRecordedTrack: (serverInfo) =>
    $track = $(context._.template(@templateRecordedTrackCommand.html(), $.extend(serverInfo, {action:'DOWNLOADING'}), {variable: 'data'}))
    $busy = @downloadProgress.find('.busy')
    $busy.empty().append($track)
    @downloadProgress.find('.progress').css('width', '0%')

  renderFullUploadRecordedBackingTrack: (serverInfo) =>
    $track = $(context._.template(@templateRecordedBackingTrackCommand.html(), $.extend(serverInfo, {action:'UPLOADING'}), {variable: 'data'}))
    $busy = @uploadProgress.find('.busy')
    $busy.empty().append($track)
    @uploadProgress.find('.progress').css('width', '0%')

  renderFullDownloadRecordedBackingTrack: (serverInfo) =>
    $track = $(context._.template(@templateRecordedBackingTrackCommand.html(), $.extend(serverInfo, {action:'DOWNLOADING'}), {variable: 'data'}))
    $busy = @downloadProgress.find('.busy')
    $busy.empty().append($track)
    @downloadProgress.find('.progress').css('width', '0%')

  # this will either show a generic placeholder, or immediately show the whole track
  renderDownloadRecordedTrack: (commandId, commandMetadata) =>
    # try to find the info in the list; if we can't find it, then resolve it
    deferred = this.resolveTrack(commandMetadata)
    if deferred.state() == 'pending'
      this.renderGeneric(commandId, 'download', commandMetadata)

    deferred.done(this.renderFullDownloadRecordedTrack).fail(()=> @logger.error("unable to fetch recorded_track info") )


  renderUploadRecordedTrack: (commandId, commandMetadata) =>
    # try to find the info in the list; if we can't find it, then resolve it
    deferred = this.resolveTrack(commandMetadata)
    if deferred.state() == 'pending'
      this.renderGeneric(commandId, 'upload', commandMetadata)

    deferred.done(this.renderFullUploadRecordedTrack).fail(()=> @logger.error("unable to fetch recorded_track info") )

  # this will either show a generic placeholder, or immediately show the whole track
  renderDownloadRecordedBackingTrack: (commandId, commandMetadata) =>
    # try to find the info in the list; if we can't find it, then resolve it
    deferred = this.resolveBackingTrack(commandMetadata)
    if deferred.state() == 'pending'
      this.renderGeneric(commandId, 'download', commandMetadata)

    deferred.done(this.renderFullDownloadRecordedBackingTrack).fail(()=> @logger.error("unable to fetch recorded_backing_track info") )

  renderUploadRecordedBackingTrack: (commandId, commandMetadata) =>
    # try to find the info in the list; if we can't find it, then resolve it
    deferred = this.resolveBackingTrack(commandMetadata)
    if deferred.state() == 'pending'
      this.renderGeneric(commandId, 'upload', commandMetadata)

    deferred.done(this.renderFullUploadRecordedBackingTrack).fail(()=> @logger.error("unable to fetch recorded_backing_track info") )

  renderGeneric: (commandId, category, commandMetadata) =>
    commandMetadata.displayType = this.displayName(commandMetadata)

    $generic = $(context._.template(@templateGenericCommand.html(), commandMetadata, {variable: 'data'}))
    if category == 'download'
      $busy = @downloadProgress.find('.busy')
      $busy.empty().append($generic)
    else if category == 'upload'
      $busy = @uploadProgress.find('.busy')
      $busy.empty().append($generic)
    else
      @logger.error("unknown category #{category}")


  renderStartCommand: (commandId, commandType, commandMetadata) =>

    #console.log("renderStartCommand", arguments)
    unless commandMetadata?
      managerState = context.jamClient.GetRecordingManagerState()
      if commandType == 'download'
        commandMetadata = managerState.current_download
      else if commandType == 'upload'
        commandMetadata = managerState.current_upload
      else
        @logger.error("unknown commandType #{commandType}")

    unless commandMetadata?
      # we still have no metadata.  we have to give up
      @logger.error("no metadata found for current command #{commandId} #{commandType}. bailing out")
      return

    if commandMetadata.queue == 'download'
      @downloadCommandId = commandId
      @downloadMetadata = commandMetadata
      @downloadProgress.removeClass('quiet paused busy')
      @downloadProgress.addClass('busy')
      if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'download'
        this.renderDownloadRecordedTrack(commandId, commandMetadata)
      else if commandMetadata.type == 'recorded_backing_track' and commandMetadata.action == 'download'
        this.renderDownloadRecordedBackingTrack(commandId, commandMetadata)
      else
        this.renderGeneric(commandId, 'download', commandMetadata)
    else if commandMetadata.queue == 'upload'
      @uploadCommandId = commandId
      @uploadMetadata = commandMetadata
      @uploadProgress.removeClass('quiet paused busy')
      @uploadProgress.addClass('busy')
      if commandMetadata.type == 'recorded_track' and commandMetadata.action == 'upload'
        this.renderUploadRecordedTrack(commandId, commandMetadata)
      else if commandMetadata.type == 'recorded_backing_track' and commandMetadata.action == 'upload'
        this.renderUploadRecordedBackingTrack(commandId, commandMetadata)
      else
        this.renderGeneric(commandId, 'upload', commandMetadata)
    else if commandMetadata.queue == 'cleanup'
      @cleanupCommandId = commandId
      @cleanupMetadata = commandMetadata


  renderSingleRecording: (userSyncs) =>
    return if userSyncs.entries.length == 0
    clientRecordings = context.jamClient.GetLocalRecordingState(recordings: [userSyncs.entries[0].recording])
    for userSync in userSyncs.entries
      if userSync.type == 'recorded_track'
        $track = @list.find(".sync[data-id='#{userSync.id}']")
        continue if $track.length == 0
        $track.data('server-info', userSync)
        this.associateClientInfo(clientRecordings.recordings[0])
        this.updateTrackState($track)
      else if userSync.type == 'recorded_backing_track'
        $track = @list.find(".sync[data-id='#{userSync.id}']")
        continue if $track.length == 0
        $track.data('server-info', userSync)
        this.associateClientInfo(clientRecordings.recordings[0])
        this.updateBackingTrackState($track)
      else if userSync.type == 'mix'
        # check if there is a virtual mix 1st; if so, update it
        $mix = @list.find(".mix.virtual[data-recording-id='#{userSync.recording.id}']")
        if $mix.length == 0
          $mix = @list.find(".sync[data-id='#{userSync.id}']")
        continue if $mix.length == 0
        $newMix = this.createMix(userSync)
        this.associateClientInfo(clientRecordings.recordings[0])
        $mix.replaceWith($newMix)
      else if userSync.type == 'stream_mix'
        $streamMix = @list.find(".sync[data-id='#{userSync.id}']")
        continue if $streamMix.length == 0
        $streamMix.data('server-info', userSync)
        this.associateClientInfo(clientRecordings.recordings[0])
        this.updateStreamMixState($streamMix)

  updateSingleRecording: (recording_id) =>
    @rest.getUserSyncs({recording_id: recording_id}).done(this.renderSingleRecording)

  updateProgressOnSync: ($track, queue, percentage) =>
    state = if queue == 'upload' then '.upload-state' else '.client-state'
    $progress = $track.find("#{state} .progress")
    $progress.css('width', percentage + '%')

  renderFinishCommand: (commandId, data) =>

    reason = data.commandReason
    success = data.commandSuccess

    if commandId == @downloadCommandId

      this.logResult(@downloadMetadata, success, reason, false)
      recordingId = @downloadMetadata['recording_id']
      this.updateSingleRecording(recordingId) if recordingId?

    else if commandId == @uploadCommandId

      this.logResult(@uploadMetadata, success, reason, false)
      recordingId = @uploadMetadata['recording_id']
      this.updateSingleRecording(recordingId) if recordingId?

    else if commandId == @cleanupCommandId
      this.logResult(@cleanupMetadata, success, reason, false)

    else
      @logger.error("unknown commandId in renderFinishCommand")

    # refresh the header when done.  we need to leave this callback to let the command fully switch to off
    #setTimeout(this.renderHeader, 1)

    this.renderHeader()

  renderPercentage: (commandId, commandType, percentage) =>

    if commandId == @downloadCommandId
      $progress = @downloadProgress.find('.progress')
      $progress.css('width', percentage + '%')

      if @downloadMetadata.type == 'recorded_track' or @downloadMetadata.type == 'recorded_backing_track'
        clientTrackId = @downloadMetadata['track_id']
        recordingId = @downloadMetadata['recording_id']
        $matchingTrack = @list.find(".track-item.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
        if $matchingTrack.length > 0
          this.updateProgressOnSync($matchingTrack, 'download', percentage)

    else if commandId == @uploadCommandId
      $progress = @uploadProgress.find('.progress')
      $progress.css('width', percentage + '%')

      if (@uploadMetadata.type == 'recorded_track' or @uploadMetadata.type == 'recorded_backing_track') and @uploadMetadata.action == 'upload'
        clientTrackId = @uploadMetadata['track_id']
        recordingId = @uploadMetadata['recording_id']
        $matchingTrack = @list.find(".track-item.sync[data-recording-id='#{recordingId}'][data-client-track-id='#{clientTrackId}']")
        if $matchingTrack.length > 0
          this.updateProgressOnSync($matchingTrack, 'upload', percentage)
      else if @uploadMetadata.type == 'stream_mix' and @uploadMetadata.action == 'upload'
        recordingId = @uploadMetadata['recording_id']
        $matchingStreamMix = @list.find(".stream-mix.sync[data-recording-id='#{recordingId}']")
        if $matchingStreamMix.length > 0
          this.updateProgressOnSync($matchingStreamMix, 'upload', percentage)
    else if commandId == @cleanupCommandId
      # ignore
    else
      @logger.error("unknown commandId in renderFinishCommand")


  fileManagerCmdStart: (e, data) =>
    #console.log("fileManagerCmdStart", data)
    commandId = data['commandId']
    commandType = data['commandType']
    commandMetadata = data['commandMetadata']

    category =  commandMetadata.queue

    if category == 'download' && (@downloadCommandId != null && @downloadCommandId != commandId)
      @logger.warn("received command-start for download but previous command did not send stop")
      this.renderFinishCommand(commandId, category)
    else if @uploadCommandId != null && @uploadCommandId != commandId
      @logger.warn("received command-start for upload but previous command did not send stop")
      this.renderFinishCommand(commandId, category)

    this.renderStartCommand(commandId, commandType, commandMetadata)

  fileManagerCmdStop: (e, data) =>
    #console.log("fileManagerCmdStop", data)
    commandId = data['commandId']

    if commandId == @downloadCommandId
      this.renderFinishCommand(commandId, data)
      @downloadCommandId = null
      @downloadMetadata = null;
    else if commandId == @uploadCommandId
      this.renderFinishCommand(commandId, data)
      @uploadCommandId = null
      @uploadMetadata = null
    else if commandId == @cleanupCommandId
      this.renderFinishCommand(commandId, data)
      @cleanupCommandId = null
      @cleanupMetadata = null

    else
      @logger.warn("received command-stop for unknown command: #{commandId} #{@downloadCommandId} #{@uploadCommandId} #{@cleanupCommandId}" )

  fileManagerCmdProgress: (e, data) =>
    #console.log("fileManagerCmdProgress", data)
    commandId = data['commandId']

    if commandId == @downloadCommandId
      category = 'download'
      this.renderPercentage(commandId, category, data.percentage)

    else if commandId == @uploadCommandId
      category = 'upload'
      this.renderPercentage(commandId, category, data.percentage)
    else if commandId == @cleanupCommandId
      # do nothing
    else
      @logger.warn("received command-percentage for unknown command")

  fileManagerAsapCommandStatus: (e, data) =>
    this.logResult(data.commandMetadata, false, data.commandReason, true)

  displayName: (metadata) =>
    if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'download'
      return 'DOWNLOADING TRACK'
    else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'upload'
      return 'UPLOADING TRACK'
    else if metadata.type == 'mix' && metadata.action == 'download'
      return 'DOWNLOADING MIX'
    else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'convert'
      return 'COMPRESSING TRACK'
    else if (metadata.type == 'recorded_track' || metadata.type == 'recorded_backing_track') && metadata.action == 'delete'
      return 'CLEANUP TRACK'
    else if metadata.type == 'stream_mix' && metadata.action == 'upload'
      return 'UPLOADING STREAM MIX'
    else if metadata.type == 'recording_directory' && metadata.action == 'delete'
      return 'DELETE RECORDING'
    else
      return "#{metadata.action} #{metadata.type}".toUpperCase()


  shownTab: () =>
    @tabSelectors.filter('.selected')

  # create a log in the Log tab
  logResult: (metadata, success, reason, isAsap) =>

    # if an error comes in, and the log tab is not already showing, increment the badge
    if not success and (!@showing || this.shownTab().attr('purpose') != 'log')
      @logBadge.css('display', 'inline-block')
      if @showing # don't do animation unless user can see it
        @logBadge.pulse({'background-color' : '#868686'}, {pulses: 2}, () => @logBadge.css('background-color', '#980006'))

    displayReason = switch reason
      when 'no-match-in-queue' then 'restart JamKazam'
      when 'already-done' then 'ignored, already done'
      when 'failed-convert' then 'failed previously'
      when 'minimum-protection-time' then 'too soon to delete'
      else reason

    displaySuccess = if success then 'yes' else 'no'
    $log = context._.template(@templateLogItem.html(), {isAsap: isAsap, command: this.displayName(metadata), success: success, displaySuccess: displaySuccess, detail: displayReason, when: new Date()}, {variable: 'data'})

    @logList.prepend($log)



