/*

*   SoundCloud Custom Player jQuery Plugin

*   Author: Matas Petrikas, matas@soundcloud.com

*   Copyright (c) 2009  SoundCloud Ltd.

*   Licensed under the MIT license:

*   http://www.opensource.org/licenses/mit-license.php

*

*   Usage:

*   <a href="http://soundcloud.com/matas/hobnotropic" class="sc-player">My new dub track</a>

*   The link will be automatically replaced by the HTML based player

*/

(function($) {

  // Convert milliseconds into Hours (h), Minutes (m), and Seconds (s)

  var timecode = function(ms) {

    var hms = function(ms) {

          return {

            h: Math.floor(ms/(60*60*1000)),

            m: Math.floor((ms/60000) % 60),

            s: Math.floor((ms/1000) % 60)

          };

        }(ms),

        tc = []; // Timecode array to be joined with '.'



    if (hms.h > 0) {

      tc.push(hms.h);

    }



    tc.push((hms.m < 10 && hms.h > 0 ? "0" + hms.m : hms.m));

    tc.push((hms.s < 10  ? "0" + hms.s : hms.s));



    return tc.join('.');

  };

  // shuffle the array

  var shuffle = function(arr) {

    arr.sort(function() { return Math.round(Math.random()); } );

    return arr;

  };



  var debug = true,

      useSandBox = false,

      $doc = $(document),

      log = function(args) {

        if(debug && window.console && window.console.log){

          window.console.log.apply(window.console, arguments);

        }

      },

      domain = useSandBox ? 'sandbox-soundcloud.com' : 'soundcloud.com',

      scApiUrl = function(url, apiKey) {

        return (/api\./.test(url) ? url + '?' : 'http://api.' + domain +'/resolve?url=' + url + '&') + 'format=json&consumer_key=' + apiKey +'&callback=?';

      };





  var audioEngine = function() {

    var html5AudioAvailable = function() {

        var state = false;

        try{

          var a = new Audio();

          state = a.canPlayType && (/maybe|probably/).test(a.canPlayType('audio/mpeg'));

          // let's enable the html5 audio on selected mobile devices first, unlikely to support Flash

          // the desktop browsers are still better with Flash, e.g. see the Safari 10.6 bug

          // comment the following line out, if you want to force the html5 mode

          state = state && (/iPad|iphone|mobile|pre\//i).test(navigator.userAgent);

        }catch(e){

          // there's no audio support here sadly

        }

        return state;

    }(),

    callbacks = {

      onReady: function() {

        $doc.trigger('scPlayer:onAudioReady');

      },

      onPlay: function() {

        $doc.trigger('scPlayer:onMediaPlay');

      },

      onPause: function() {

        $doc.trigger('scPlayer:onMediaPause');

      },

      onEnd: function() {

        $doc.trigger('scPlayer:onMediaEnd');

      },

      onBuffer: function(percent) {

        $doc.trigger({type: 'scPlayer:onMediaBuffering', percent: percent});

      }

    };



    var html5Driver = function() {

      var player = new Audio(),

          onTimeUpdate = function(event){

            var obj = event.target,

                buffer = ((obj.buffered.length && obj.buffered.end(0)) / obj.duration) * 100;

            // ipad has no progress events implemented yet

            callbacks.onBuffer(buffer);

            // anounce if it's finished for the clients without 'ended' events implementation

            if (obj.currentTime === obj.duration) { callbacks.onEnd(); }

          },

          onProgress = function(event) {

            var obj = event.target,

                buffer = ((obj.buffered.length && obj.buffered.end(0)) / obj.duration) * 100;

            callbacks.onBuffer(buffer);

          };



      $('<div class="sc-player-engine-container"></div>').appendTo(document.body).append(player);



      // prepare the listeners

      player.addEventListener('play', callbacks.onPlay, false);

      player.addEventListener('pause', callbacks.onPause, false);

      player.addEventListener('ended', callbacks.onEnd, false);

      player.addEventListener('timeupdate', onTimeUpdate, false);

      player.addEventListener('progress', onProgress, false);





      return {

        load: function(track, apiKey) {

          player.pause();

          player.src = track.stream_url + '?consumer_key=' + apiKey;

          player.load();

          player.play();

        },

        play: function() {

          player.play();

        },

        pause: function() {

          player.pause();

        },

        stop: function(){

          player.currentTime = 0;

          player.pause();

        },

        seek: function(relative){

          player.currentTime = player.duration * relative;

          player.play();

        },

        getDuration: function() {

          return player.duration;

        },

        getPosition: function() {

          return player.currentTime;

        },

        setVolume: function(val) {

          if(a){

            a.volume = val / 100;

          }

        }

      };



    };







    var flashDriver = function() {

      var engineId = 'scPlayerEngine',

          player,

          flashHtml = function(url) {

            var swf = 'http://player.' + domain +'/player.swf?url=' + url +'&amp;enable_api=true&amp;player_type=engine&amp;object_id=' + engineId;

            if ($.browser.msie) {

              return '<object height="100%" width="100%" id="' + engineId + '" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" data="' + swf + '">'+

                '<param name="movie" value="' + swf + '" />'+

                '<param name="allowscriptaccess" value="always" />'+

                '</object>';

            } else {

              return '<object height="100%" width="100%" id="' + engineId + '">'+

                '<embed allowscriptaccess="always" height="100%" width="100%" src="' + swf + '" type="application/x-shockwave-flash" name="' + engineId + '" />'+

                '</object>';

            }

          };







      // listen to audio engine events

      // when the loaded track is ready to play

      soundcloud.addEventListener('onPlayerReady', function(flashId, data) {

        player = soundcloud.getPlayer(engineId);

        callbacks.onReady();

      });



      // when the loaded track finished playing

      soundcloud.addEventListener('onMediaEnd', callbacks.onEnd);



      // when the loaded track is still buffering

      soundcloud.addEventListener('onMediaBuffering', function(flashId, data) {

        callbacks.onBuffer(data.percent);

      });



      // when the loaded track started to play

      soundcloud.addEventListener('onMediaPlay', callbacks.onPlay);



      // when the loaded track is was paused

      soundcloud.addEventListener('onMediaPause', callbacks.onPause);



      return {

        load: function(track) {

          var url = track.permalink_url;

          if(player){

            player.api_load(url);

          }else{

            // create a container for the flash engine (IE needs this to operate properly)

            $('<div class="sc-player-engine-container"></div>').appendTo(document.body).html(flashHtml(url));

          }

        },

        play: function() {

          player && player.api_play();

        },

        pause: function() {

          player && player.api_pause();

        },

        stop: function(){

          player && player.api_stop();

        },

        seek: function(relative){

          player && player.api_seekTo((player.api_getTrackDuration() * relative));

        },

        getDuration: function() {

          return player && player.api_getTrackDuration && player.api_getTrackDuration() * 1000;

        },

        getPosition: function() {

          return player && player.api_getTrackPosition && player.api_getTrackPosition() * 1000;

        },

        setVolume: function(val) {

          if(player && player.api_setVolume){

            player.api_setVolume(val);

          }

        }



      };

    };



    return html5AudioAvailable? html5Driver() : flashDriver();



  }();







  var apiKey,

      didAutoPlay = false,

      players = [],

      updates = {},

      currentUrl,

      loadTracksData = function($player, links, key) {

        var index = 0,

            playerObj = {node: $player, tracks: []},

            loadUrl = function(link) {

              $.getJSON(scApiUrl(link.url, apiKey), function(data) {

                // log('data loaded', link.url, data);

                index += 1;

                if(data.tracks){

                  // log('data.tracks', data.tracks);

                  playerObj.tracks = playerObj.tracks.concat(data.tracks);

                }else if(data.duration){

                  // a secret link fix, till the SC API returns permalink with secret on secret response

                  data.permalink_url = link.url;

                  // if track, add to player

                  playerObj.tracks.push(data);

                }else if(data.username){

                  // if user, get his tracks or favorites

                  if(/favorites/.test(link.url)){

                    links.push({url:data.uri + '/favorites'});

                  }else{

                    links.push({url:data.uri + '/tracks'});

                  }

                }else if($.isArray(data)){

                  playerObj.tracks = playerObj.tracks.concat(data);

                }

                if(links[index]){

                  // if there are more track to load, get them from the api

                  loadUrl(links[index]);

                }else{

                  // if loading finishes, anounce it to the GUI

                  playerObj.node.trigger({type:'onTrackDataLoaded.scPlayer', playerObj: playerObj});

                }

             });

           };

        // update current API key

        apiKey = key;

        // update the players queue

        players.push(playerObj);

        // load first tracks

        loadUrl(links[index]);

      },

      artworkImage = function(track, usePlaceholder) {

        if(usePlaceholder){

          return '<div class="sc-loading-artwork">Loading Artwork</div>';

        }else if (track.artwork_url) {

          return '<img src="' + track.artwork_url.replace('-large', '-t300x300') + '"/>';

        }else{

          return '<div class="sc-no-artwork"> </div>';

        }

      },

      updateTrackInfo = function($player, track) {

        // update the current track info in the player

        // log('updateTrackInfo', track);

        $('.sc-info', $player).each(function(index) {

          $('h3', this).html('<a href="' + track.permalink_url +'">' + track.title + '</a>');

          $('h4', this).html('by <a href="' + track.user.permalink_url +'">' + track.user.username + '</a>');

          $('p', this).html(track.description || 'no Description');

        });

        // update the artwork

        $('.sc-artwork-list li', $player).each(function(index) {

          var $item = $(this),

              itemTrack = $item.data('sc-track');



          if (itemTrack === track) {

            // show track artwork

            $item

              .addClass('active')

              .find('.sc-loading-artwork')

                .each(function(index) {

                  // if the image isn't loaded yet, do it now

                  $(this).removeClass('sc-loading-artwork').html(artworkImage(track, false));

                });

          }else{

            // reset other artworks

            $item.removeClass('active');

          }

        });

        // update the track duration in the progress bar

        $('.sc-duration', $player).html(timecode(track.duration));

        // put the waveform into the progress bar

        $('.sc-waveform-container', $player).html('<img src="' + track.waveform_url +'" />');



        $player.trigger('onPlayerTrackSwitch.scPlayer', [track]);

      },

      play = function(track) {

        var url = track.permalink_url;

        if(currentUrl === url){

          // log('will play');

          audioEngine.play();

        }else{

          currentUrl = url;

          // log('will load', url);

          audioEngine.load(track, apiKey);

        }

      },

      getPlayerData = function(node) {

        return players[$(node).data('sc-player').id];

      },

      updatePlayStatus = function(player, status) {

        if(status){

          // reset all other players playing status

          $('div.sc-player.playing').removeClass('playing');

        }

        $(player)

          .toggleClass('playing', status)

          .trigger((status ? 'onPlayerPlay' : 'onPlayerPause') + '.scPlayer');

      },

      onPlay = function(player, id) {

        var track = getPlayerData(player).tracks[id || 0];

        updateTrackInfo(player, track);

        // cache the references to most updated DOM nodes in the progress bar

        updates = {

          $buffer: $('.sc-buffer', player),

          $played: $('.sc-played', player),

          position:  $('.sc-position', player)[0]

        };

        updatePlayStatus(player, true);

        play(track);

      },

      onPause = function(player) {

        updatePlayStatus(player, false);

        audioEngine.pause();

      },

      onFinish = function() {

        var $player = updates.$played.closest('.sc-player'),

            $nextItem;

        // update the scrubber width

        updates.$played.css('width', '0%');

        // show the position in the track position counter

        updates.position.innerHTML = timecode(0);

        // reset the player state

        updatePlayStatus($player, false);

        // stop the audio

        audioEngine.stop();

        // continue playing through all players

        // TODO create a nicer auto-play flow

        log('track finished get the next one');

        $nextItem = $('.sc-trackslist li.active', $player).next('li');

        // try to find the next track in other player

        if(!$nextItem.length){

          $nextItem = $player.nextAll('div.sc-player:first').find('.sc-trackslist li.active');

        }

        $nextItem.click();

      },

      onSeek = function(player, relative) {

        audioEngine.seek(relative);

      },

      soundVolume = function() {

        var vol = 80,

            cooks = document.cookie.split(';'),

            volRx = new RegExp('scPlayer_volume=(\\d+)');

        for(var i in cooks){

          if(volRx.test(cooks[i])){

            vol = parseInt(cooks[i].match(volRx)[1], 10);

            break;

          }

        }

        return vol;

      }(),

      onVolume = function(volume) {

        var vol = Math.floor(volume);

        // save the volume in the cookie

        var date = new Date();

        date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000));

        soundVolume = vol;

        document.cookie = ['scPlayer_volume=', vol, '; expires=', date.toUTCString(), '; path="/"'].join('');

        // update the volume in the engine

        audioEngine.setVolume(soundVolume);

      },

      positionPoll;



    // listen to audio engine events

    $doc

      .bind('scPlayer:onAudioReady', function(event) {

        log('onPlayerReady: audio engine is ready');

        audioEngine.play();

        // set initial volume

        onVolume(soundVolume);

      })

      // when the loaded track started to play

      .bind('scPlayer:onMediaPlay', function(event) {

        clearInterval(positionPoll);

        positionPoll = setInterval(function() {

          var duration = audioEngine.getDuration(),

              position = audioEngine.getPosition(),

              relative = (position / duration);



          // update the scrubber width

          updates.$played.css('width', (100 * relative) + '%');

          // show the position in the track position counter

          updates.position.innerHTML = timecode(position);

          // announce the track position to the DOM

          $doc.trigger({

            type: 'onMediaTimeUpdate.scPlayer',

            duration: duration,

            position: position,

            relative: relative

          });

        }, 500);

      })

      // when the loaded track is was paused

      .bind('scPlayer:onMediaPause', function(event) {

        clearInterval(positionPoll);

        positionPoll = null;

      })

      // change the volume

      .bind('scPlayer:onVolumeChange', function(event) {

        onVolume(event.volume);

      })

      .bind('scPlayer:onMediaEnd', function(event) {

        onFinish();

      })

      .bind('scPlayer:onMediaBuffering', function(event) {

        updates.$buffer.css('width', event.percent + '%');

      });





  // Generate custom skinnable HTML/CSS/JavaScript based SoundCloud players from links to SoundCloud resources

  $.scPlayer = function(options, node) {

    var opts = $.extend({}, $.scPlayer.defaults, options),

        playerId = players.length,

        $source = node && $(node),

        sourceClasses = $source[0].className.replace('sc-player', ''),

        links = opts.links || $.map($('a', $source).add($source.filter('a')), function(val) { return {url: val.href, title: val.innerHTML}; }),

        $player = $('<div class="sc-player loading"></div>').data('sc-player', {id: playerId}),

        $artworks = $('<ol class="sc-artwork-list"></ol>').appendTo($player),

        //$info = $('<div class="sc-info"><h3></h3><h4></h4><p></p><a href="#" class="sc-info-close">X</a></div>').appendTo($player),

        $controls = $('<div class="sc-controls"></div>').appendTo($player),

        $list = $('<ol class="sc-trackslist"></ol>').appendTo($player);



        // add the classes of the source node to the player itself

        // the players can be indvidually styled this way

        if(sourceClasses || opts.customClass){

          $player.addClass(sourceClasses).addClass(opts.customClass);

        }





        // adding controls to the player

        $player

          .find('.sc-controls')

            .append('<a href="#play" class="sc-play">Play</a> <a href="#pause" class="sc-pause hidden">Pause</a>')

          .end()

          //.append('<a href="#info" class="sc-info-toggle">Info</a>')

          .append('<div class="sc-scrubber"></div>')

            .find('.sc-scrubber')

              .append('<div class="sc-volume-slider"><span class="sc-volume-status" style="width:' + soundVolume +'%"></span></div>')

              .append('<div class="sc-time-span"><div class="sc-waveform-container"></div><div class="sc-buffer"></div><div class="sc-played"></div></div>')

              .append('<div class="sc-time-indicators"><span class="sc-position"></span> | <span class="sc-duration"></span></div>');



        // load and parse the track data from SoundCloud API

        loadTracksData($player, links, opts.apiKey);

        // init the player GUI, when the tracks data was laoded

        $player.bind('onTrackDataLoaded.scPlayer', function(event) {

          // log('onTrackDataLoaded.scPlayer', event.playerObj, playerId, event.target);

          var tracks = event.playerObj.tracks;

          if (opts.randomize) {

            tracks = shuffle(tracks);

          }

          // create the playlist

          $.each(tracks, function(index, track) {

            var active = index === 0;

            // create an item in the playlist

            $('<li><a href="' + track.permalink_url +'">' + track.title + '</a><span class="sc-track-duration">' + timecode(track.duration) + '</span></li>').data('sc-track', {id:index}).toggleClass('active', active).appendTo($list);

            // create an item in the artwork list

            $('<li></li>')

              .append(artworkImage(track, index >= opts.loadArtworks))

              .appendTo($artworks)

              .toggleClass('active', active)

              .data('sc-track', track);

          });

          $player

            .removeClass('loading')

            .trigger('onPlayerInit.scPlayer');



          // update the element before rendering it in the DOM

          $player.each(function() {

            if($.isFunction(opts.beforeRender)){

              opts.beforeRender.call(this, tracks);

            }

          });

          // set the first track's duration

          $('.sc-duration', $player)[0].innerHTML = timecode(tracks[0].duration);

          $('.sc-position', $player)[0].innerHTML = timecode(0);

          // set up the first track info

          updateTrackInfo($player, tracks[0]);

          // if auto play is enabled and it's the first player, start playing

          if(opts.autoPlay && !didAutoPlay){

            onPlay($player);

            didAutoPlay = true;

          }

        });





    // replace the DOM source (if there's one)

    $source.each(function(index) {

      $(this).replaceWith($player);

    });



    return $player;

  };



  // stop all players, might be useful, before replacing the player dynamically

  $.scPlayer.stopAll = function() {

    $('.sc-player.playing a.sc-pause').click();

  };



  // plugin wrapper

  $.fn.scPlayer = function(options) {

    // reset the auto play

    didAutoPlay = false;

    // create the players

    this.each(function() {

      $.scPlayer(options, this);

    });

    return this;

  };



  // default plugin options

  $.scPlayer.defaults = $.fn.scPlayer.defaults = {

    customClass: null,

    // do something with the dom object before you render it, add nodes, get more data from the services etc.

    beforeRender  :   function(tracksData) {

      var $player = $(this);

    },

    // initialization, when dom is ready

    onDomReady  : function() {

      $('a.sc-player, div.sc-player').scPlayer();

    },

    autoPlay: false,

    randomize: false,

    loadArtworks: 5,

    // the default Api key should be replaced by your own one

    // get it here http://soundcloud.com/you/apps/new

    apiKey: 'htuiRd1JP11Ww0X72T1C3g'

  };





  // the GUI event bindings

  //--------------------------------------------------------



  // toggling play/pause

  $('a.sc-play, a.sc-pause').live('click', function(event) {

    var $list = $(this).closest('.sc-player').find('ol.sc-trackslist');

    // simulate the click in the tracklist

    $list.find('li.active').click();

    return false;

  });



  // displaying the info panel in the player

  $('a.sc-info-toggle, a.sc-info-close').live('click', function(event) {

    var $link = $(this);

    $link.closest('.sc-player')

      .find('.sc-info').toggleClass('active').end()

      .find('a.sc-info-toggle').toggleClass('active');

    return false;

  });



  // selecting tracks in the playlist

  $('.sc-trackslist li').live('click', function(event) {

    var $track = $(this),

        $player = $track.closest('.sc-player'),

        trackId = $track.data('sc-track').id,

        play = $player.is(':not(.playing)') || $track.is(':not(.active)');

    if (play) {

      onPlay($player, trackId);

    }else{

      onPause($player);

    }

    $track.addClass('active').siblings('li').removeClass('active');

    $('.artworks li', $player).each(function(index) {

      $(this).toggleClass('active', index === trackId);

    });

    return false;

  });



  var scrub = function(node, xPos) {

    var $scrubber = $(node).closest('.sc-time-span'),

        $buffer = $scrubber.find('.sc-buffer'),

        $available = $scrubber.find('.sc-waveform-container img'),

        $player = $scrubber.closest('.sc-player'),

        relative = Math.min($buffer.width(), (xPos  - $available.offset().left)) / $available.width();

    onSeek($player, relative);

  };



  var onTouchMove = function(ev) {

    if (ev.targetTouches.length === 1) {

      scrub(ev.target, ev.targetTouches && ev.targetTouches.length && ev.targetTouches[0].clientX);

      ev.preventDefault();

    }

  };





  // seeking in the loaded track buffer

  $('.sc-time-span')

    .live('click', function(event) {

      scrub(this, event.pageX);

      return false;

    })

    .live('touchstart', function(event) {

      this.addEventListener('touchmove', onTouchMove, false);

      event.originalEvent.preventDefault();

    })

    .live('touchend', function(event) {

      this.removeEventListener('touchmove', onTouchMove, false);

      event.originalEvent.preventDefault();

    });



  // changing volume in the player

  var startVolumeTracking = function(node, startEvent) {

    var $node = $(node),

        originX = $node.offset().left,

        originWidth = $node.width(),

        getVolume = function(x) {

          return Math.floor(((x - originX)/originWidth)*100);

        },

        update = function(event) {

          $doc.trigger({type: 'scPlayer:onVolumeChange', volume: getVolume(event.pageX)});

        };

    $node.bind('mousemove.sc-player', update);

    update(startEvent);

  };



  var stopVolumeTracking = function(node, event) {

    $(node).unbind('mousemove.sc-player');

  };



  $('.sc-volume-slider')

    .live('mousedown', function(event) {

      startVolumeTracking(this, event);

    })

    .live('mouseup', function(event) {

      stopVolumeTracking(this, event);

    });



  $doc.bind('scPlayer:onVolumeChange', function(event) {

    $('span.sc-volume-status').css({width: event.volume + '%'});

  });

  // -------------------------------------------------------------------



  // the default Auto-Initialization

  $(function() {

    if($.isFunction($.scPlayer.defaults.onDomReady)){

      $.scPlayer.defaults.onDomReady();

    }

  });



})(jQuery);


