(function(context, $) {

  "use strict";

  /**
   * when session ends
   *  - in chrome:
   * onsuspend
   * onpause
   * onended
   *  - in firefox
   * onstalled
   * onplaying
   * onsuspend
   * onpause
   * onended
   */
  context.JK = context.JK || {};

  context.JK.ListenBroadcastCurrentlyPlaying = null;
  context.JK.$templateListenBroadcastState =  $('#template-listen-broadcast-state');
  context.JK.$templateListenBroadcastDetail = $('#template-listen-broadcast-detail')


  // the purpose of this code is to simplify the interaction between user and server
  // it provides methods to call on user events (primarily play/pause), and will fire
  // events (such as 'stream_gone').  This element does nothing with UI; only fires events for you to handle

  // $parentElement: the parent element needs to contain one audio element; this will take control of it.
  // pass in music_session as a 'data-music-session' attribute on the $parentElement

  // this will also interact with any REST APIs needed to
  context.JK.ListenBroadcast = function($parentElement, options){

    var WAIT_FOR_BUFFER_TIMEOUT = 5000;
    var WAIT_FOR_PLAYING_TIMEOUT = 7000;
    var RETRY_ATTEMPTS = 5; // we try 4 times, so the user will wait up until RETRY_ATTEMPTS * WAIT_FOR_BUFFER_TIMEOUTS

    var logger                  = context.JK.logger;
    var rest                    = context.JK.Rest();
    var $parent                 = $parentElement;
    var $audio                  = null;
    var audioDomElement         = null;
    var musicSessionId          = null;
    var waitForBufferingTimeout = null;
    var waitForPlayingTimeout   = null;
    var fanAccess               = null;
    var audioSrc                = null;
    var audioType               = null;
    var retryAttempts           = 0;
    var self                    = this;
    var mountInfo               = null;
    var $mountState             = null;
    var sessionInfo             = null; // stored so we can access .mount, mostly
    var lazyAudioInit           = options && options.lazyAudioInit;
    var hoverOptions            = (options && options.hoverOptions) ? options.hoverOptions : {}
    var $detailHelper           = options && options.detailHelper;

    var destroyed = false;

    var PlayStateNone               = 'none';
    var PlayStateInitializing       = 'initializing'; // user clicked play--nothing has happened yet
    var PlayStateBuffering          = 'buffering'; // user clicked play--the stream is being read, buffer is being built
    var PlayStatePlaying            = 'playing'; // we are playing
    var PlayStateStalled            = 'stalled'; // we were playing, but the stream is stalled
    var PlayStateEnded              = 'ended'; // we were playing, but the stream ended
    var PlayStateRetrying           = 'retrying_play'; // we are retrying to play.
    var PlayStateFailedStart        = 'failed_start'; // we could not start the stream.  no more events are coming
    var PlayStateFailedPlaying      = 'failed_playing'; // failed while playing.
    var PlayStateSessionOver        = 'session_over'; // session is done
    var PlayStateNetworkError       = 'network_error'; // network error
    var PlayStateServerError        = 'server_error'; // server error
    var PlayStateConcurrentStop     = 'concurrent_stop'; // stopped by another play attempt

    var playState = PlayStateNone; // tracks if the stream is actually playing

    var CANNOT_BROADCAST_TITLE = 'Unable to Broadcast Session';
    var CANNOT_BROADCAST_MSG = 'This session cannot be broadcasted. The session organizer may have configured it to be private.';

    function play(e) {
      if(e) {
        e.preventDefault();
        e.stopPropagation();
      }

      //if(destroyed) return;

      if(context.JK.ListenBroadcastCurrentlyPlaying) {
        context.JK.ListenBroadcastCurrentlyPlaying.forcedPause();
      }

      context.JK.ListenBroadcastCurrentlyPlaying = self;

      checkServer()
        .done(function(response) {

           if(!sessionInfo.mount) {
              transition(PlayStateSessionOver);
              destroy();
           }
           else {
              try {
               recreateAudioElement();

               audioDomElement.load();

               retryAttempts = 0;

               transition(PlayStateInitializing);

               // keep this after transition, because any transition clears this timer
               waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT);
               logger.debug("setting buffering timeout");
               rest.addPlayablePlay(musicSessionId, 'JamRuby::MusicSession', null, context.JK.currentUserId);
               context.JK.GA.trackJamTrackPlaySession(musicSessionId, false)
               
                if(needsCanPlayGuard()) {
                  $audio.bind('canplay', function() {
                    audioDomElement.play();
                  })
                }
                else {
                  audioDomElement.play();
                }
              }
              catch (err) {
                logger.log("Catching error = %o", err);
              }
           }
        })
    }

    function forcedPause() {
      transition(PlayStateConcurrentStop);
      pause();
    }

    function pause(e) {
      if(e) {
        e.preventDefault();
        e.stopPropagation();
      }

      //if(destroyed) return;

      transition(PlayStateNone);

      try {
        recreateAudioElement();
      }
      catch (err) {
        logger.log("Catching error = %o", err);
      }
    }

    function destroy() {
     // if(!destroyed) {
        //$audio.remove();
        //$audio = null;
        //audioDomElement = null;
        try {
          recreateAudioElement();
        }
        catch (err) {
          logger.log("Catching error = %o", err);
        }
       // destroyed = true;
      //}
    }

    function onScreenChanged(e, data) {
      if(context.JK.ListenBroadcastCurrentlyPlaying) {
        context.JK.ListenBroadcastCurrentlyPlaying.pause();
      }
    }

    function createAudioElementHtml() {
      if (sessionInfo == null) {
        logger.log("no session info");
        throw "no session info";
      }

      if (sessionInfo.mount == null) {
        logger.log("no session mount info");
        throw "no session mount info";
      }

      $audio =
        $('<audio preload="none">' +
        '<source src="' + cacheBustedSrc(sessionInfo.mount.url) + '" type="' + sessionInfo.mount.mime_type + '"/>' +
        '</audio>');
      $parent.append($audio);
      audioDomElement = $audio.get(0);
      audioBind();

    }

    // this is the only way to make audio stop buffering after the user hits pause
    function recreateAudioElement() {
      // jeez: http://stackoverflow.com/questions/4071872/html5-video-force-abort-of-buffering/13302599#13302599
      if(audioDomElement) {
        audioDomElement.pause();
        audioDomElement.src = '';
        audioDomElement.load();
      }
      $audio.remove();
      createAudioElementHtml();
      logger.log("recreated audio element ")
    }

    function clearBufferTimeout() {
      if(waitForBufferingTimeout) {
        logger.debug("clearing buffering timeout");
        clearTimeout (waitForBufferingTimeout);
        waitForBufferingTimeout = null;
      }
    }

    function clearPlayingTimeout() {
      if(waitForPlayingTimeout) {
        logger.debug("clearing playing timeout");
        clearTimeout (waitForPlayingTimeout);
        waitForPlayingTimeout = null;
      }
    }

    function transition(newState) {
      logger.log("transitioning from " + playState + " to " + newState);

      playState = newState;

      if(newState != PlayStateStalled) {
        clearBufferTimeout();
        clearPlayingTimeout();
      }

      if(newState == PlayStateBuffering) {
        // give some time after buffering is seen to let play start
        waitForPlayingTimeout = setTimeout(noPlay, WAIT_FOR_PLAYING_TIMEOUT)
      }


      if( playState == PlayStateNone ||
          playState == PlayStateEnded ||
          playState == PlayStateFailedStart ||
          playState == PlayStateFailedPlaying ||
          playState == PlayStateSessionOver ||
          playState == PlayStateNetworkError ||
          playState == PlayStateServerError ) {
        $audio.unbind('canplay');
        context.JK.ListenBroadcastCurrentlyPlaying = null;
      }

      triggerStateChange();
    }

    function noPlay() {
      noBuffer();
    }
    function noBuffer() {

      waitForBufferingTimeout = null;

      if(retryAttempts >= RETRY_ATTEMPTS) {
        logger.log("never received indication of buffering or playing");
        transition(PlayStateFailedStart);
      }
      else {
        retryAttempts++;

        clearBufferTimeout();

        checkServer()
          .done(function(response) {

            if(!sessionInfo.mount) {
              transition(PlayStateSessionOver);
              destroy();
            }
            else {
              // tell audio to stop/start, in attempt to retry
              //audioDomElement.pause();

              try {
                recreateAudioElement();

                audioDomElement.load();
                if(needsCanPlayGuard()) {
                  $audio.bind('canplay', function() {
                    audioDomElement.play();
                  })
                }
                else {
                  audioDomElement.play();
                }

                transition(PlayStateRetrying);

                waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT);
              }
              catch (err) {
                logger.log("Catching error = %o", err);
              }
            }
          });
      }
    }

    function isNoisyEvent(eventName) {
      if(playState == PlayStateNone) {
        logger.log("ignoring: " + eventName)
        return true;
      }

      return false;
    }

    function onPlay() {
      // this just confirms that the user tried to play
    }

    function onPlaying() {
      if(isNoisyEvent('playing')) return;
      logger.log("playing", arguments);

      transition(PlayStatePlaying);
    }

    function onPause() {
      if(isNoisyEvent('pause')) return;
      logger.log("pause", arguments);

      if(treatPauseLikeEnd()) {
        transition(PlayStateEnded);
        return;
      };

      transition(PlayStateStalled);
    }

    function onError() {
      if(isNoisyEvent('error')) return;
      logger.log("error", arguments);

      if(playState == PlayStatePlaying || playState == PlayStateStalled) {
          transition(PlayStateFailedPlaying);
      }
      else {
          transition(PlayStateFailedStart);
      }
    }

    function onEnded() {
      if(isNoisyEvent('ended')) return;
      logger.log("ended", arguments);

      transition(PlayStateEnded);
    }

    function onEmptied() {
      if(isNoisyEvent('emptied')) return;
      logger.log("emptied", arguments);
    }

    function onAbort() {
      if(isNoisyEvent('abort')) return;
      logger.log("abort", arguments);
    }

    function onStalled() {
      if(isNoisyEvent('stalled')) return;
      logger.log("stalled", arguments);

      if(arguments[0].target !== audioDomElement) {
        logger.log("ignoring stalled msg for non-active audio element")
        return;
      }

      if(playState == PlayStateBuffering || playState == PlayStatePlaying) {
        transition(PlayStateStalled);
      }
    }

    function onSuspend() {
      if(isNoisyEvent('suspend')) return;
      logger.log("onsuspend", arguments);

      // fires in FF on page load

      if(ignoreSuspend()) return;

      transition(PlayStateStalled);
    }

    function onTimeUpdate() {
      //logger.log("ontimeupdate", arguments);
    }

    function onProgress() {
      if(isNoisyEvent('progress')) return;

      if(playState == PlayStateInitializing)  {
        transition(PlayStateBuffering);
      }
    }

    function checkServer() {
      return rest.getSession(musicSessionId)
        .done(function(response) {
          logger.log("assigning sessionInfo")
          sessionInfo = response;
        })
        .fail(function(jqXHR) {
          if(jqXHR.status == 404) {
            transition(PlayStateSessionOver);
            destroy();
          }
          else if (jqXHR.status == 403) {
            logger.debug("session is private");
            context.JK.app.notify({"title": CANNOT_BROADCAST_TITLE, "text": CANNOT_BROADCAST_MSG});
          }
          else if(jqXHR.status >= 500 && jqXHR.status <= 599){
            transition(PlayStateServerError);
          }
          else {
            transition(PlayStateNetworkError);
          }
        });
    }

    function triggerStateChange() {

      var isEnd = false;
      var isSessionOver = false;
      var displayText = null;
      var refresh = false;

      if(playState == 'none')  {
        //$status.text('SESSION IN PROGRESS');
      }
      else if(playState == 'initializing') {
        displayText = 'PREPARING AUDIO';
      }
      else if(playState == 'buffering') {

      }
      else if(playState == 'playing') {
        displayText = 'SESSION IN PROGRESS';
      }
      else if(playState == 'stalled') {

        // in Chrome, we've observed that a stalled can occur very early, before you've received any audio
        // it's better to leave the display text as what it would be if juts retrying
        if(!waitForBufferingTimeout) {
          displayText = 'RECONNECTING';
        }
        else {
          if(retryAttempts == 2) {
            displayText = 'STILL TRYING, HANG ON';
          }
        }
      }
      else if(playState == 'ended') {
        displayText = 'STREAM DISCONNECTED';
        isEnd = true;
        refresh = true;
      }
      else if(playState == 'session_over') {
        displayText = 'SESSION ENDED';
        isEnd = true;
        isSessionOver = true;
      }
      else if(playState == 'retrying_play') {
        if(retryAttempts == 2) {
          displayText = 'STILL TRYING, HANG ON';
        }
      }
      else if(playState == 'failed_start') {
        displayText = 'AUDIO DID NOT START';
        isEnd = true;
      }
      else if(playState == 'failed_playing') {
        displayText = 'STREAM DISCONNECTED';
        isEnd = true;
        refresh = true;
      }
      else if(playState == 'network_error') {
        displayText = 'NO NETWORK';
        isEnd = true;
      }
      else if(playState == 'server_error') {
        displayText = 'SERVER ERROR';
        isEnd = true;
      }
      else if(playState == 'concurrent_stop') {
        displayText = 'SESSION IN PROGRESS';
        isEnd = true;
      }
      else {
        logger.error("unknown state: " + playState)
      }

      $parent.data('listenbroadcast-playstate', playState);

      $parent.triggerHandler('statechange.listenBroadcast',
      {
        element: $parent,
        state: playState,
        displayText: displayText,
        isEnd: isEnd,
        isSessionOver: isSessionOver
      })

      // we have cause to believe the session is done; check against the server
      if(refresh) {
        checkServer()
          .done(function(response) {
          if(!sessionInfo.mount) {
            transition(PlayStateSessionOver);
            destroy();
          }});
      }
    }

    function treatPauseLikeEnd() {
      // never see a pause event until the session is over, in the rich client
      return playState == PlayStatePlaying && navigator.userAgent.toLowerCase().indexOf('jamkazam') > -1;
    }
    function ignoreSuspend() {
      // client always does a 'suspend' after playing. bleh
      return playState == PlayStatePlaying && navigator.userAgent.toLowerCase().indexOf('jamkazam') > -1;
    }

    function needsCanPlayGuard() {
      var ua = navigator.userAgent.toLowerCase();

      if (ua.indexOf('safari') != -1) {
        if (ua.indexOf('chrome') > -1) {
        } else {
          if (ua.indexOf('ipad') > -1 || ua.indexOf('iphone') > -1) {
            return false; // not sure yet which way this should go
          }
          return true;
        }
      }
      else if(ua.indexOf('jamkazam') > -1) {
        return true;
      }
      return false;
    }


    function audioBind() {
      $audio.bind('play', onPlay);
      $audio.bind('playing', onPlaying);
      $audio.bind('error', onError);
      $audio.bind('emptied', onEmptied);
      $audio.bind('abort', onAbort);
      $audio.bind('ended', onEnded);
      $audio.bind('pause', onPause);
      $audio.bind('suspend', onSuspend);
      $audio.bind('stalled', onStalled);
      //$audio.bind('timeupdate', onTimeUpdate);
      $audio.bind('progress', onProgress);
    }

    function broadcastDetailPreShow(box) {
      var $box = $(box)
      $mountState = $box.find('.listen-broadcast-state')

      if(mountInfo) {
        updateMountInfo(mountInfo)
        updateMountDetails(mountInfo);
      }
    }

    function updateMountInfo(mount) {
      var $summary = $mountState.find('.summary')
      var $status = $summary.find('.status.' + mount.state.reason)

      if ($status.length == 0) {
        // can't find this state
        $status = $summary.find('.status.unknown')
        $summary.attr('data-status', 'unknown')
        $status.find('.msg').text ("unknown state: " + mount.state.reason)
        return;
      }

      //var $msg = $status.find('.msg');

      $summary.attr('data-status', mount.state.reason)
    }


    function updateMountDetails(mountInfo) {

      var $detailHolder = $mountState.find('.detail-items')

      context._.each(mountInfo.source_changes, function(sourceChange) {

        var $detail = addSourceChange($detailHolder, sourceChange);
        if($detail) {
          $detailHolder.append($detail);
        }
      })
    }

    function onDetailEvent(e, data) {
      if($mountState == null) return;

      logger.debug("subscription notification received: type:" + data.type)

      if(data.body.mount) {
        updateMountInfo(data.body.mount);
      }

      var id = data.id
      var type = data.type

      if(type == 'mount') {

        var $detailHolder = $mountState.find('.detail-items')
        var msgType = data.body.type;

        var sourceChange = data.body;
        if(sourceChange) {
          var $detail = addSourceChange($detailHolder, sourceChange)
          if ($detail) {
            $detailHolder.prepend($detail)
            $detail.hide().slideDown()
          }
        }
      }
    }


    function addSourceChange($detailHolder, sourceChange) {
      var $detail= $(context._.template($('#template-listen-broadcast-detail').html(), {}, {variable: 'data'}));

      // if this is already in the dom, don't add it
      if($detailHolder.find('.listen-broadcast-detail[data-id="' + sourceChange.id + '"]').length > 0) {
        return null;
      }

      $detail.attr('data-id', sourceChange.id)

      var $detailText = $detail.find('.detail');
      var $who = $detail.find('.who')

      if(sourceChange.change_type == 'source_up_request') {
        $who.html('<span>Server</span>')
        $detailText.text('Start Broadcast Requested')
      }
      else if(sourceChange.change_type == 'source_down_request') {
        $who.html('<span>Server</span>')
        $detailText.text('Stop Broadcast Requested')
      }
      else {

        $who.attr('hoveraction', 'musician').attr('user-id', sourceChange.user.id).addClass('avatar-tiny')
        $who.html('<img src="' + context.JK.resolveAvatarUrl(sourceChange.user.photo_url) + '" />');
        context.JK.bindHoverEvents($who);

        if (sourceChange.source_direction) {
          // up
          if (sourceChange.success) {
            $detailText.text(sourceChange.user.name + ' Started Broadcasting')
          }
          else {
            if (sourceChange.reason == 'no_client') {
              $detailText.text('No Client Responded')
            }
            else if (sourceChange.reason == 'initialize_singleton') {
              $detailText.text('Init Failure (S)')
            }
            else if (sourceChange.reason == 'initialize_thread') {
              $detailText.text('Init Failure (T)')
            }
            else if (sourceChange.reason == 'initialize_ogg') {
              $detailText.text('Init Failure (O)')
            }
            else if (sourceChange.reason == 'initialize_mp3') {
              $detailText.text('Init Failure (M)')
            }
            else if (sourceChange.reason == 'initialize_socket') {
              $detailText.text('Init Failure (Client)')
            }
            else if (sourceChange.reason == 'icecast_response') {
              $detailText.text('Init Failure (Server)')
            }
            else {
              $detailText.text('Unknown Failure')
            }
          }
        }
        else {
          $detailText.text(sourceChange.user.name + ' Stopped Broadcasting');
        }
      }
      return $detail;
    }

    function cleanupDetails() {
      var mountId = $parent.data('mount-id')
      if(mountId) {
        context.JK.SubscriptionUtils.unsubscribe('mount', mountId)
      }
      $mountState = null;
      mountInfo = null;
    }

    function openBubble() {
      checkServer()
        .done(function(response) {

          var mountId = sessionInfo.mount ? sessionInfo.mount.id : null;

          if(mountId) {
            rest.getMount({id: mountId})
              .done(function (mount) {
                mountInfo = mount;
                $parent.data('mount-id', mountId);
                context.JK.SubscriptionUtils.subscribe('mount', mountId).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, onDetailEvent);
                $parent.btOn();
              })
              .fail(context.JK.app.ajaxError)
          }
          else {
            mountInfo = null;
            destroy();
            context.JK.app.notify({"title": CANNOT_BROADCAST_TITLE, "text": CANNOT_BROADCAST_MSG});
          }
        })
        .fail(function(response) {
          if (response.status == 404) {
            logger.debug("session is over");
          }
        })
    }

    function bindHoverDetail() {

      var stateHolderHtml = context._.template($('#template-listen-broadcast-state').html(), {}, {variable: 'data'});
      var btOptions = $.extend({}, {trigger: 'none', preShow: broadcastDetailPreShow,  postHide: cleanupDetails, closeWhenOthersOpen: true, positions:['bottom', 'right', 'top', 'left'], width:'300px', height:'250px'}, hoverOptions)
      context.JK.hoverBubble($parent, stateHolderHtml, btOptions)

      // in case the bt is destroyed
      $parent.on('remove', cleanupDetails);

      if($detailHelper) {
        $detailHelper.click(function() { openBubble(); return false; })
      }
      else {
        $parent.hoverIntent({
          over: function() {
            openBubble();
          },
          out: function() {

          }
        })
      }
    }

    function cacheBustedSrc(src) {
      return src + '?cache-buster=' + new Date().getTime();
    }

    function initialize() {

      musicSessionId = $parent.attr('data-music-session');
      if(!musicSessionId) throw "data-music-session must be specified on $parentElement";

      fanAccess = $parent.attr('fan-access');
      if(fanAccess === null) throw 'fan-access must be specified in $parentElement';
      fanAccess = $parent.attr('fan-access') === 'true' // coerce to boolean

      bindHoverDetail();

      $audio = $('audio', $parent);

      if($audio.length == 0 && !lazyAudioInit) {
        return;
      }

      if($audio.length > 1) {
        throw "more than one <audio> element found";
      }

      $parent.bind('play.listenBroadcast', play);
      $parent.bind('pause.listenBroadcast', pause);
      $parent.bind('destroy.listenBroadcast', destroy);

      $(document).on(context.JK.EVENTS.SCREEN_CHANGED, onScreenChanged);
    }

    initialize();

    this.play = play;
    this.pause = pause;
    this.forcedPause = forcedPause; // meant only be called by this code; not from external

    return this;
  }

  $.fn.listenBroadcast = function(options) {
    new context.JK.ListenBroadcast(this, options);
  }
})(window, jQuery);