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

# This is the sequence of how this widget works:
# checkState() is the heart of the state machine; it is called to get things going, and is called whenevr a state ends
# checkState() checks first against what the client thinks about the state of the JamTrack;
#   if it on the disk then the state machine may enter one of:
#     * synchronized
#     * keying
#
#   if it's still on the server, then the state machine may be:
#     * packaging
#     * downloading
#
#   errored state can be entered from @jamTrack.jam_track_right_id
#
# other state; you augment the error to the user by suppling @errorMessage before transitioning
#
#   no-client is the way the widget behaves when you are in a normal browser (i.e., nothing happens other than tell the user to use the client)
#
# Discussion of the different states:
# There are different states that a JamTrack can be in.
# The final success state is that the JamTrack is on disk and loadable. (show synchronized state)
# But there are others until you get there:
# The JamTrack does not exist on the server, so we will create it (packaging state)
# The JamTrack exists on the server, but not on disk, so we will download it (downloading state)
# The JamTrack is on the disk, but does not yet have keys, so we will fetch them (keying)

context.JK.DownloadJamTracks = {}
context.JK.DownloadJamTrack = class DownloadJamTrack
  constructor: (@app, jamTrack, size = 'large') ->
    @EVENTS = context.JK.EVENTS
    @rest = context.JK.Rest()
    @logger = context.JK.logger
    @jamTrack = jamTrack
    @size = size
    @attemptedEnqueue = false
    @errorReason = null
    @errorMessage = null
    @transitionTimer = null
    @downloadTimer = null
    @trackDetail = null
    @stateHolder = null
    @active = false
    @startTime = null
    @attempts = 0
    @tracked = false
    @ajaxEnqueueAborted = false
    @ajaxGetJamTrackRightAborted = false
    @currentPackagingStep = null
    @totalSteps = null

    throw "no JamTrack specified" unless @jamTrack?
    throw "invalid size" if @size != 'large' && @size != 'small'
    throw "no JamTrack version" unless @jamTrack.version?

    @path = []
    @states = {
      no_client:    { name: 'no-client',    show: @showNoClient, leaf: true },
      synchronized: { name: 'synchronized', show: @showSynchronized, leaf: true},
      packaging:    { name: 'packaging',    show: @showPackaging },
      downloading:  { name: 'downloading',  show: @showDownloading },
      keying:       { name: 'keying',       show: @showKeying, max_time: 10000 },
      initial:      { name: 'initial',      show: @showInitial },
      quiet:        { name: 'quiet',        show: @showQuiet },
      errored:      { name: 'errored',      show: @showError, leaf: true}
    }

    context.JK.DownloadJamTracks[@jamTrack.id] = this
    downloadJamTrackTemplate = $('#template-download-jamtrack')
    throw "no download jamtrack template" if not downloadJamTrackTemplate.exists()

    @root = $(downloadJamTrackTemplate.html())
    @stateHolder = @root.find('.state')
    @root.on('remove', this.destroy) # automatically destroy self when removed from DOM

    # populate in template and visual transition functions
    for state, data of @states
      data.template = $("#template-download-jamtrack-state-#{data.name}")

    # start off in quiet state, but don't do it through transition system.   The transition system expects a change, not initial state
    @state = @states.quiet

    this.showState()


  # after you've created the DownloadJamTrack widget, call synchronize which will begin ensuring that the jamtrack
  # is downloaded and ready to open
  init: () =>
    @active = true
    @root.addClass('active')
    this.reset()

    # check if we are in a browser or client
    if !gon.isNativeClient
      this.transition(@states.no_client)
    else
      this.transition(@states.initial)

  # when done with the widget, call destroy; this ensures it's not still active, and tracks final metrics
  destroy: () =>
    $(this).off()
    @active = false
    @root.removeClass('active')
    this.trackProgress()
      # since we are not in a leave node, we need to report a state since this is effectively our end state
    this.reset()

  clear: () =>
    # reset attemptedEnqueue to false, to allow one attempt to enqueue
    @attemptedEnqueue = false
    this.clearDownloadTimer()
    this.clearTransitionTimer()
    this.abortEnqueue()
    this.abortGetJamTrackRight()
    for state, data of @states
      if data.timer?
        clearInterval(data.timer)
        data.timer = null

  reset: () =>
    @path = []
    @attempts = 0
    @tracked = false
    @startTime = new Date()
    this.clear()


  abortEnqueue: () =>
    if @ajaxEnqueueAborted
      @logger.debug("DownloadJamTrack: aborting ajax enqueue")
      # we need to clear out @ajaxEnqueue *before* calling abort(), because the .fail callback fires inline
      ajax = @ajaxEnqueueAborted
      @ajaxEnqueueAborted = true
      ajax.abort()

  abortGetJamTrackRight: () =>
    if @ajaxGetJamTrackRightAborted
      @logger.debug("DownloadJamTrack: aborting ajax GetJamTrackRight")
      # we need to clear out @ajaxEnqueue *before* calling abort(), because the .fail callback fires inline
      ajax = @ajaxGetJamTrackRightAborted
      @ajaxGetJamTrackRightAborted = true
      ajax.abort()

  showState: () =>
    @state.stateStartTime = new Date();
    @stateHolder.children().remove()
    @stateHolder.append(context._.template(@state.template.html(), @jamTrack, { variable: 'data' }))
    @stateHolder.find('.' + @size).removeClass('hidden')
    @state.show()

  # report a stat now that we've reached the end of this widget's journey
  trackProgress: () =>

    # do not double-report
    if @tracked
      return

    if @path.length == 0
      return

    unless @state.leaf
      # we've been asked to report at a non-leaf node, meaning the user must have cancelled
      @path.push('user-cancelled')

    flattened_path = @path.join('-')

    data = {
      value: 1,
      path: flattened_path,
      duration: (new Date().getTime() - @startTime.getTime()) / 1000,
      attempts: @attempts,
      user_id: context.JK.currentUserId,
      user_name: context.JK.currentUserName}
    if @state == @states.errored
      data.result = 'error'
      data.detail = @errorReason
      @rest.createAlert("JamTrack Sync failed for #{context.JK.currentUserName}", data)
    else
      data.result = 'success'

    context.stats.write('web.jamtrack.downloader', data)
    @tracked = true

  showPackaging: () =>
    @logger.debug("showing #{@state.name}")
    this.expectTransition()

  showDownloading: () =>
    @logger.debug("showing #{@state.name}")
    # while downloading, we don't run the transition timer, because the download API is guaranteed to call success, or failure, eventually
    context.jamClient.JamTrackDownload(@jamTrack.id, null, context.JK.currentUserId,
      this.makeDownloadProgressCallback(),
      this.makeDownloadSuccessCallback(),
      this.makeDownloadFailureCallback())

  showKeying: () =>
    @logger.debug("showing #{@state.name}")
    context.jamClient.JamTrackKeysRequest()
    this.waitForState()

  showQuiet: () =>
    @logger.debug("showing #{@state.name}")

  showInitial: () =>
    @logger.debug("showing #{@state.name}")
    @sampleRate = context.jamClient.GetSampleRate()
    @fingerprint = context.jamClient.SessionGetMacHash()
    logger.debug("fingerprint: ", @fingerprint)
    @sampleRateForFilename = if @sampleRate == 48 then '48' else '44'
    @attempts = @attempts + 1
    this.expectTransition()
    context.JK.SubscriptionUtils.subscribe('jam_track_right', @jamTrack.jam_track_right_id).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, this.onJamTrackRightEvent)
    this.checkState()

  showError: () =>
    @logger.debug("showing #{@state.name}")
    context.JK.SubscriptionUtils.unsubscribe('jam_track_right', @jamTrack.jam_track_right_id)

    if @size == 'large'
      @stateHolder.find('.msg').text(@errorMessage)
      @stateHolder.find('.retry-button').click(this.retry)
    else
      @stateHolder.find('.msg').text(@jamTrack.name + ' (error)')
      @stateHolder.find('.errormsg').text(@errorMessage)
      @stateHolder.find('.retry-button').on('click', this.retry)

    retryMsg = ''
    if @attempts > 1
      retryMsg = 'Continue retrying or contact support@jamkazam.com'

    @stateHolder.find('.retry').text(retryMsg)

  showSynchronized: () =>
    @logger.debug("showing #{@state.name}")
    context.JK.SubscriptionUtils.unsubscribe('jam_track_right', @jamTrack.jam_track_right_id)

  showNoClient: () =>
    @logger.debug("showing #{@state.name}")

  downloadCheck: () =>
    @logger.debug "downloadCheck"

  retry: () =>
    @logger.debug "user initiated retry"
    @path = []
    @path.push('retry')
    this.clear()
    # just switch to the initial state again, causing the loop to start again
    this.transition(@states.initial)
    return false

  clearStateTimer: () =>
    if @state.timer?
      clearInterval(@state.timer)
      @state.timer = null

  stateIntervalCheck: () =>
    this.checkState()

    # if the timer is null now, then it must have been whacked due to a state change
    # if not, then let's see if we have timed out
    if @state.timer?
      if (new Date()).getTime() - @state.stateStartTime.getTime()   > @state.max_time
        @logger.debug("The current step (#{@state.name}) took too long")

        if @state == @states.keying
          # specific message
          this.transitionError("#{@state.name}-timeout", "It took too long for the JamTrack to be keyed.")
        else
          # generic message
          this.transitionError("#{@state.name}-timeout", "The current step (#{@state.name}) took too long")


  # sets an interval timer for every second, waiting for the status to change
  waitForState: () =>
    unless @active
      @logger.error("DownloadJamTrack: ignoring waitForState because we are not active")

    @state.timer =  setInterval(this.stateIntervalCheck, 1000)


  # unused atm; the backend is good about always signalling. we still should though
  expectDownload: () =>
    unless @active
      @logger.error("DownloadJamTrack: ignoring expectDownload because we are not active")

    # every 10 seconds, wake up and check the server and see if we missed a state transition
    this.clearDownloadTimer()
    @downloadTimer = setTimeout(this.downloadCheck, 10000)

  clearDownloadTimer: () =>
    if @downloadTimer?
      clearTimeout(@downloadTimer)
      @downloadTimer = null

  transitionError: (reasonCode, errorMessage) =>
    @errorReason = reasonCode
    @errorMessage = errorMessage
    this.transition(@states.errored)

  transitionCheck: () =>
    this.checkState()

  # this should be called every time something changes statefully, to restart a 12 second timer to hit the server for update.
  # if everything is moving snappily, we won't have to query the server much, because we are also getting subscription events
  # about any changes to the status of the jam track.  But, we could miss a message or there could be a path in the server where
  # we don't get an event, so that's why, after 12 seconds, we'll still go to the server and check.
  # exception: this should not be runngi
  expectTransition: () =>
    unless @active
      @logger.error("DownloadJamTrack: ignoring expectTransition because we are not active")

    # every 12 seconds, wake up and check the server and see if we missed a state transition
    this.clearTransitionTimer()
    @transitionTimer = setTimeout(this.transitionCheck, 12000)

  clearTransitionTimer: () =>
    if @transitionTimer?
      clearTimeout(@transitionTimer)
      @transitionTimer = null

  transition: (newState) =>
    unless @active
      @logger.error("DownloadJamTrack: ignoring state change because we are not active")
      return

    if newState == @state
      @logger.debug("DownloadJamTrack: ignoring state change #{@state.name}")
      return

    if @state?
      @logger.debug("DownloadJamTrack: state change: #{@state.name} => #{newState.name}")
      # make sure there is no timer running on the old state
      this.clearTransitionTimer()
      this.clearStateTimer()
      this.abortEnqueue()
      @logger.debug("aborting getJamTrack right on state change")
      this.abortGetJamTrackRight()
    else
      @logger.debug("DownloadJamTrack: initial state: #{newState.name}")

    @state = newState

    # track which states were taken
    @path.push(@state.name)

    if @state.leaf
      this.trackProgress()

    this.showState()

    $(this).triggerHandler(@EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, {state: @state})

  checkState: () =>
    # check for the success state against the local state of the client... if it's playable, then we should be OK
    fqId = "#{@jamTrack.id}-#{@sampleRateForFilename}"
    @trackDetail = context.jamClient.JamTrackGetTrackDetail (fqId)

    @logger.debug("DownloadJamTrack: JamTrackGetTrackDetail(#{fqId}).key_state: " + @trackDetail.key_state, @trackDetail)

    # first check if the version is not the same; if so, invalidate.

    if @trackDetail.version?
      if @jamTrack.version != @trackDetail.version
        @logger.info("DownloadJamTrack: JamTrack on disk is different version (stored: #{@trackDetail.version}, server: #{@jamTrack.version}. Invalidating")
        context.jamClient.InvalidateJamTrack("#{@jamTrack.id}-#{@sampleRateForFilename}")
        @trackDetail = context.jamClient.JamTrackGetTrackDetail ("#{@jamTrack.id}-#{@sampleRateForFilename}")

        if @trackDetail.version?
          @logger.error("after invalidating package, the version is still wrong!", @trackDetail)
          throw "after invalidating package, the version is still wrong! #{@trackDetail.version}"

    switch @trackDetail.key_state
      when 'pending'
        this.transition(@states.keying)
      when 'not authorized'
        # TODO: if not authorized, do we need to re-initiate a keying attempt?
        this.transition(@states.keying)
      when 'ready'
        this.transition(@states.synchronized)
      when 'unknown'
        @ajaxGetJamTrackRightAborted = false
        @rest.getJamTrackRight({id: @jamTrack.id})
          .done(this.processJamTrackRight)
          .fail(this.processJamTrackRightFail)

  # update progress indicator for packaging step
  updateSteps: () =>
    if @currentPackagingStep? and @totalSteps?
      progress = "#{Math.round(@currentPackagingStep/@totalSteps * 100)}%"
    else
      progress = '...'

    @root.find('.state-packaging .progress').text(progress)

  processSigningState: (jamTrackRight) =>
    signingState = jamTrackRight.signing_state
    @totalSteps = jamTrackRight.packaging_steps
    @currentPackagingStep = jamTrackRight.current_packaging_step

    @updateSteps()

    @logger.debug("DownloadJamTrack: processSigningState: " + signingState)

    switch signingState
      when 'QUIET'
        if @attemptedEnqueue
          # this means we've already tried to poke the server. something is wrong
          this.transitionError("enqueue-timeout", "The server has not begun building your JamTrack.")
        else
          this.expectTransition()

          this.attemptToEnqueue()
      when 'QUEUED'
        # when it's queued, there is nothing to do except wait.
        this.transition(@states.packaging)
      when 'QUEUED_TIMEOUT'
        if @attemptedEnqueue
          # this means we've already tried to poke the server. something is wrong
          this.transitionError("queued-timeout", "The server took too long to begin processing your JamTrack.")
        else
          this.expectTransition()

          this.attemptToEnqueue()
      when 'SIGNING'
        this.expectTransition()
        this.transition(@states.packaging)
      when 'SIGNING_TIMEOUT'
        if @attemptedEnqueue
          # this means we've already tried to poke the server. something is wrong
          this.transitionError("signing-timeout", "The server took too long to create your JamTrack.")
        else
          this.expectTransition()

          this.attemptToEnqueue()
      when 'SIGNED'
        this.transition(@states.downloading)
      when 'ERROR'
        if @attemptedEnqueue
          # this means we've already tried to poke the server. something is wrong
          this.transitionError("package-error", "The server failed to create your package.")
        else
          this.expectTransition()

          this.attemptToEnqueue()
      else
        @logger.error("unknown state: " + signingState)
        this.transitionError("unknown-state-#{signingState}", "The server sent an unknown state message: " + signingState)

  attemptToEnqueue: () =>
    @attemptedEnqueue = true
    @ajaxEnqueueAborted = false

    @rest.enqueueJamTrack({id: @jamTrack.id, sample_rate: @sampleRate, fingerprint: @fingerprint})
      .done(this.processEnqueueJamTrack)
      .fail(this.processEnqueueJamTrackFail)


  processJamTrackRight: (myJamTrack) =>
    @logger.debug("processJamTrackRight", myJamTrack)
    unless @ajaxGetJamTrackRightAborted
      this.processSigningState(myJamTrack)
    else
      @logger.debug("DownloadJamTrack: ignoring processJamTrackRight response")

  processJamTrackRightFail: () =>
    unless @ajaxGetJamTrackRightAborted?
      this.transitionError("status-check-error", "Unable to check with the server on the status of your JamTrack.")
    else
      @logger.debug("DownloadJamTrack: ignoring processJamTrackRightFail response")

  processEnqueueJamTrack: (enqueueResponse) =>
    unless @ajaxEnqueueAborted
      this.expectTransition() # the act of enqueuing should send down events to the client. we wait...
    else
      @logger.debug("DownloadJamTrack: ignoring processEnqueueJamTrack response")

  displayUIForGuard:(response) =>
    display = switch response.message
      when 'no user specified' then 'Please log back in.'
      when 'no fingerprint specified' then 'There was a problem communicating between client and server.  Please restart JamKazam.'
      when 'no all fingerprint specified' then 'There was a problem communicating between client and server.  Please restart JamKazam.'
      when 'no running fingerprint specified' then 'There was a problem communicating between client and server.  Please restart JamKazam.'
      when 'already redeemed another' then "It appears you have already redeemed your one free JamTrack for your household. We are sorry, but we cannot let you download this JamTrack free. If you believe this is an error, please contact us at support@jamkazam.com."
      when "other user has 'all' fingerprint" then  "It appears you have already redeemed your one free JamTrack for your household. We are sorry, but we cannot let you download this JamTrack free. If you believe this is an error, please contact us at support@jamkazam.com."
      when "other user has 'running' fingerprint" then  "It appears you have already redeemed your one free JamTrack for your household. We are sorry, but we cannot let you download this JamTrack free. If you believe this is an error, please contact us at support@jamkazam.com."
      else "Something went wrong #{response.message}.  Please restart JamKazam"

  processEnqueueJamTrackFail: (jqXHR) =>
    unless @ajaxEnqueueAborted
      if jqXHR.status == 403
        display = this.displayUIForGuard(JSON.parse(jqXHR.responseText))
        this.transitionError("enqueue-error", display)
      else
        this.transitionError("enqueue-error", "Unable to ask the server to build your JamTrack.")
    else
      @logger.debug("DownloadJamTrack: ignoring processEnqueueJamTrackFail response")

  onJamTrackRightEvent: (e, data) =>
    @logger.debug("DownloadJamTrack: subscription notification received: type:" + data.type, data)
    this.expectTransition()
    this.processSigningState(data.body)

  updateDownloadProgress: () =>

    if @bytesReceived? and @bytesTotal?
      progress = "#{Math.round(@bytesReceived/@bytesTotal * 100)}%"
    else
      progress = '0%'

    @root.find('.state-downloading .progress').text(progress)

  downloadProgressCallback: (bytesReceived, bytesTotal) =>
    @logger.debug("download #{bytesReceived}/#{bytesTotal}")

    @bytesReceived = Number(bytesReceived)
    @bytesTotal = Number(bytesTotal)

    # the reason this timeout is set is because, without it,
    # we observe that the client will hang.   So, if you remove this timeout, make sure to test with real client
    setTimeout(this.updateDownloadProgress, 100)

  downloadSuccessCallback: (updateLocation) =>
    # is the package loadable yet?
    @logger.debug("DownloadJamTrack:  download complete - on to keying")
    this.transition(@states.keying)

  downloadFailureCallback: (errorMsg) =>

    this.transitionError("download-error", errorMsg)

  # makes a function name for the backend
  makeDownloadProgressCallback: () =>
    "JK.DownloadJamTracks['#{@jamTrack.id}'].downloadProgressCallback"

  # makes a function name for the backend
  makeDownloadSuccessCallback: () =>
    "JK.DownloadJamTracks['#{@jamTrack.id}'].downloadSuccessCallback"

  # makes a function name for the backend
  makeDownloadFailureCallback: () =>
    "JK.DownloadJamTracks['#{@jamTrack.id}'].downloadFailureCallback"

