/*
 * Bitmunk Player
 *
 * @requires jQuery v1.3 or later (http://jquery.com/)
 * @requires SoundManager2 (http://schillmania.com/projects/soundmanager2/)
 *
 * @author David I. Lehn <dlehn@digitalbazaar.com>
 */
/*
 * This media player uses embedded RDFa information to make a playlist out of
 * available media. The RDF scan is somewhat fragile and done with CSS
 * selectors rather than a full RDF engine. The player itself is implemented as
 * a state machine that uses SoundManager2 for playback. This uses a small
 * background flash object to play sounds. The player can be written such that
 * play buttons fallback to loading a playlist when flash is not available. 
 *
 * FIXME: document required HTML, RDFa, CSS, and images
 */

// create root bitmunk object if needed
window.bitmunk = window.bitmunk || {};

// SoundManager2 callbacks and defaults
soundManager.url = '/scripts/'
soundManager.onload = function() {
   // SM2 is ready to go!
   bitmunk.player.ready = true;
   bitmunk.player.onload();
};
soundManager.onerror = function() {
   // SM2 could not start, no sound support, something broke etc.
   bitmunk.player.ready = false;
   bitmunk.player.onerror();
};
soundManager.defaultOptions.autoPlay = true;
soundManager.debugMode = false;

(function($)
{
   // action events during state transitions for player and playlist items
   var PAUSE = 'pause.player';
   var PLAY = 'play.player';
   var RESUME = 'resume.player';
   var STOP = 'stop.player';
   // either PLAY or RESUME
   var PLAY_OR_RESUME = [PLAY, RESUME].join(' ');
   
   // states
   var PAUSED = 'paused';
   var PLAYING = 'playing';
   var STOPPED = 'stopped';
   
   // init global player data
   bitmunk.player =
   {
      // simple ready flag
      ready: false,
      // test mode that logs to console rather than playing sound
      test: false,
      // print debug messages to console
      debug: false,
      // user function called on successful player load
      onload: function() {},
      // user function called on failure to load player
      onerror: function() {},
      // Generic CSS based callbacks
      initCssPlayer: function(player)
      {
         player.player()
            // set player as enabled so fallback CSS can hide controls
            .addClass('playerEnabled')
            // initialized as stopped
            .addClass('playerStopped')
            // while stopped set at first and last position to hide ff/rw
            .addClass('playerFirst playerLast')
            // bind state change events
            .bind(STOP, function(event) {
               $(this)
                  .addClass('playerStopped')
                  .removeClass('playerPaused')
                  .removeClass('playerPlaying');
               // while stopped set at first and last position to hide ff/rw
               $(this).addClass('playerFirst playerLast');
            })
            .bind(PAUSE, function(event) {
               $(this)
                  .addClass('playerPaused')
                  .removeClass('playerStopped')
                  .removeClass('playerPlaying');
            })
            .bind(PLAY_OR_RESUME, function(event) {
               $(this)
                  .addClass('playerPlaying')
                  .removeClass('playerStopped')
                  .removeClass('playerPaused');
            })
            .bind(PLAY, function(event) {
               $(this).toggleClass('playerFirst', player.atFirst());
               $(this).toggleClass('playerLast', player.atLast());
            });
      },
      initCssPlaylistItem: function(playlistItem)
      {
         playlistItem
            .bind(STOP, function(event) {
               $(this)
                  .addClass('playerStopped')
                  .removeClass('playerPaused')
                  .removeClass('playerPlaying');
            })
            .bind(PAUSE, function(event) {
               $(this)
                  .addClass('playerPaused')
                  .removeClass('playerStopped')
                  .removeClass('playerPlaying');
            })
            .bind(PLAY_OR_RESUME, function(event) {
               $(this)
                  .addClass('playerPlaying')
                  .removeClass('playerStopped')
                  .removeClass('playerPaused');
            });
      }
   };
   
   /**
    * Create a new player.
    * 
    * @param options:
    *    about: the RDF subject of this player. (defaults to '')
    *    player: jQuery object that contains the player UI. Defaults to the
    *       the result of the following query:
    *          $('.player,[about="' + options.about + '"]');
    *       element with about="(about param)" and class="player".
    *    playlist: jQuery array that contains the playlist items. Defaults to
    *       the result of the following query:
    *       $('[about="' + options.about + '"] ' +
    *       [typeof="audio:Recording"] [rel="media:sample"]');
    *       Or if the above is empty then trys the following query:
    *       $('[about="' + options.about + '"][rel="media:sample"]');
    *    playlistControls: jQuery array that contains playlist control elements.
    *       Defaults to the playlist minus any objects that are already part
    *       of the player. (ie, the playlist
    *    translate: function that takes a sample URL and translates it to
    *       another URL. Can be used to change fallback playlist URLs into
    *       raw media URLs.
    *    initPlayer(player): function to initialize the UI player.
    *    initPlaylistItem(item): function to initiailze a playlist item.
    *    debug: flag to set debug mode.
    */
   bitmunk.player.make = function(options)
   {
      options = $.extend({
         // default to main URI
         about: '',
         player: null,
         playlist: null,
         translate: null,
         initPlayer: bitmunk.player.initCssPlayer,
         initPlaylistItem: bitmunk.player.initCssPlaylistItem,
         debug: bitmunk.player.debug
      }, options);
      
      // state one of [STOPPED, PAUSED, PLAYING]
      var state = STOPPED;
      // current index in the playlist
      var index = -1;
      // current sound object
      var current = null;
      // jQuery array of playlist elements
      var playlist = options.playlist;
      // jQuery object for the player controls
      var player = options.player;
      // Flag to avoid double-binding the playlist when it is the player
      // This can happen when the player controls have a fallback to a playlist
      // and that is the only media on the page.
      // FIXME: Improve how this is detected.  Right now it's just a simple
      // FIXME: flag set below when we know we are in single vs collection
      // FIXME: mode.
      var bindPlaylist = true;
      
      if(player == null)
      {
         player = $('.player,[about="' + options.about + '"]');
      }
      if(playlist === null)
      {
         playlist = $(
            '[about="' + options.about + '"] ' +
            '[typeof="audio:Recording"] [rel="media:sample"]');
         if(playlist.length == 0)
         {
            // no samples for sub-recordings so check for sample of subject
            playlist = $(
               '[about="' + options.about + '"] [rel="media:sample"]');
            // FIXME: (see above) avoid double binding events
            bindPlaylist = false;
         }
      }
      if(options.debug)
      {
         console.log('player:', player);
         console.log('playlist:', playlist);
      }
      
      var _play = function(playOptions)
      {
         playOptions = $.extend({
            // default to first playlist item
            index: 0,
            // function to call when finished
            finished: function() {}
         }, playOptions);
         if(options.debug)
         {
            console.log('%s:play idx:%d', state, playOptions.index);
         }
         index = playOptions.index;
         var uri = playlist.get(index).href;
         if(options.translate !== null)
         {
            uri = options.translate(uri);
         }
         _stopPlaying();
         if(!bitmunk.player.test)
         {
            try
            {
               current = soundManager.createSound({
                  id: 'player-' + options.about + '-' + index,
                  url: uri,
                  onfinish: playOptions.finished,
                  autoPlay: true
               });
               state = PLAYING;
            }
            catch(e)
            {
               if(options.debug)
               {
                  console.error(
                     'bitmunk.player: createSound exception:', e);
               }
            }
         }
         else
         {
            console.log('TEST PLAY', uri);
         }
      };
      var _stopPlaying = function()
      {
         if(!bitmunk.player.test)
         {
            if(current !== null)
            {
               try
               {
                  current.destruct();
               }
               catch(e)
               {
                  if(options.debug)
                  {
                     console.error(
                        'bitmunk.player: destruct exception:', e);
                  }
               }
               current = null;
            }
         }
         else
         {
            console.log('TEST STOP PLAYING');
         }
      };
      var _stop = function()
      {
         state = STOPPED;
         index = -1;
         _stopPlaying();
      };
      var _pause = function()
      {
         state = PAUSED;
         if(!bitmunk.player.test)
         {
            try
            {
               current.pause();
            }
            catch(e)
            {
               if(options.debug)
               {
                  console.error(
                     'bitmunk.player: pause exception:', e);
               }
            }
         }
         else
         {
            console.log('TEST PAUSE');
         }
      };
      var _resume = function()
      {
         state = PLAYING;
         if(!bitmunk.player.test)
         {
            try
            {
               current.resume();
            }
            catch(e)
            {
               if(options.debug)
               {
                  console.error(
                     'bitmunk.player: resume exception:', e);
               }
            }
         }
         else
         {
            console.log('TEST RESUME');
         }
      };
      
      var _player =
      {
         init: function()
         {
            _stop();
         },
         /**
          * Get the player jQuery object.
          */
         player: function()
         {
            return player;
         },
         /**
          * Get the playlist jQuery object.
          */
         playlist: function()
         {
            return playlist;
         },
         atFirst: function()
         {
            return index == 0;
         },
         atLast: function()
         {
            return index == (playlist.length - 1);
         },
         play: function(idx, single)
         {
            // default to first item and not single
            if(index == -1 && typeof(idx) === 'undefined')
            {
               idx = 0;
            }
            if(typeof(single) === 'undefined')
            {
               single = false;
            }
            if(options.debug)
            {
               console.log('%s:play idx:%d i:%d s:%d',
                  state, idx, index, single);
            }
            switch(state) {
               case PLAYING:
                  break;
               case STOPPED:
                  if(idx < playlist.length)
                  {
                     _play({
                        index: idx,
                        finished: function()
                        {
                           // stop current playlist item
                           playlist.eq(index).triggerHandler(STOP);
                           if(single)
                           {
                              _player.stop();
                           }
                           else
                           {
                              _player.next();
                           }
                        }
                     });
                     playlist.eq(idx).triggerHandler(PLAY);
                     player.triggerHandler(PLAY);
                  }
                  break;
               case PAUSED:
                  _player.resume();
                  break;
            }
         },
         stop: function()
         {
            if(options.debug)
            {
               console.log('%s:stop', state);
            }
            switch(state) {
               case STOPPED:
                  break;
               case PAUSED:
               case PLAYING:
                  playlist.eq(index).triggerHandler(STOP);
                  player.triggerHandler(STOP);
                  _stop();
                  break;
            }
         },
         pause: function()
         {
            if(options.debug)
            {
               console.log('%s:pause', state);
            }
            switch(state) {
               case STOPPED:
               case PAUSED:
                  break;
               case PLAYING:
                  playlist.eq(index).triggerHandler(PAUSE);
                  player.triggerHandler(PAUSE);
                  _pause();
                  break;
            }
         },
         resume: function()
         {
            if(options.debug)
            {
               console.log('%s:resume', state);
            }
            switch(state) {
               case STOPPED:
                  break;
               case PAUSED:
                  _resume();
                  playlist.eq(index).triggerHandler(RESUME);
                  player.triggerHandler(RESUME);
                  break;
               case PLAYING:
                  break;
            }
         },
         prev: function()
         {
            if(options.debug)
            {
               console.log('%s:prev', state);
            }
            switch(state) {
               case STOPPED:
               case PAUSED:
                  break;
               case PLAYING:
                  if(!_player.atFirst())
                  {
                     playlist.eq(index).triggerHandler(STOP);
                     index = index - 1;
                     _play({
                        index: index,
                        finished: _player.next
                     });
                     playlist.eq(index).triggerHandler(PLAY);
                     player.triggerHandler(PLAY);
                  }
                  break;
            }
         },
         next: function()
         {
            if(options.debug)
            {
               console.log('%s:next', state);
            }
            switch(state) {
               case STOPPED:
               case PAUSED:
                  break;
               case PLAYING:
                  if(!_player.atLast())
                  {
                     playlist.eq(index).triggerHandler(STOP);
                     index = index + 1;
                     _play({
                        index: index,
                        finished: _player.atLast() ? _player.stop : _player.next
                     });
                     playlist.eq(index).triggerHandler(PLAY);
                     player.triggerHandler(PLAY);
                  }
                  break;
            }
         }
      };
      
      // resend clicks as player events
      $('.playerPlay', player)
         .bind('click.player', function(event) {
            _player.play();
            return false;
         });
      $('.playerStop', player)
         .bind('click.player', function(event) {
            _player.stop();
            return false;
         });
      $('.playerPause', player)
         .bind('click', function(event) {
            _player.pause();
            return false;
         });
      $('.playerPrev', player)
         .bind('click.player', function(event) {
            _player.prev();
            return false;
         });
      $('.playerNext', player)
         .bind('click.player', function(event) {
            _player.next();
            return false;
         });
      
      options.initPlayer(_player);
      if(bindPlaylist)
      {
         $.each(playlist, function(i, e) {
            $(e)
               .bind('click.player', function(event) {
                  var idx = playlist.index(this);
                  if(options.debug)
                  {
                     console.log('playlist click',
                        $(this), event, index, idx);
                  }
                  if(idx == index)
                  {
                     // current track playing or paused, change state
                     switch(state)
                     {
                        case PLAYING:
                           _player.pause();
                           break;
                        case PAUSED:
                           _player.resume();
                           break;
                     }
                  }
                  else
                  {
                     // new track requested, stop playing if needed
                     switch(state)
                     {
                        case PLAYING:
                        case PAUSED:
                           playlist.eq(index).triggerHandler(STOP);
                           _stop();
                           break;
                     }
                     // signal player to play a single specific index
                     _player.play(idx, true);
                  }
                  return false;
               });
            options.initPlaylistItem($(e));
         });
      }
      // init
      _player.init();
      
      return _player;
   };
})(jQuery);


// default setup for Bitmunk pages with a specific layout

// successfully loaded and can use player
bitmunk.player.onload = function()
{
   $(document).ready(function()
   {
      // URI translation from playlist to media links
      var pl2mediaRe = /(.*\/sales\/samples\/)playlist(\/.*)/;
      var pl2mediaSub = '$1media$2';
      // make a player for whole page with specialized sample URI translation
      bitmunk.player.make({
         // change playlist URIs into media URIs
         translate: function(uri)
         {
            return uri.replace(pl2mediaRe, pl2mediaSub);
         },
         initPlaylistItem: function(playlistItem)
         {
            // use default initialization
            bitmunk.player.initCssPlaylistItem(playlistItem);
            // custom handlers to highlight row that is two parents up
            playlistItem
               .bind('stop.player', function(event)
               {
                  $(this).parent().parent().removeClass('activeSample');
               })
               .bind('play.player', function(event)
               {
                  $(this).parent().parent().addClass('activeSample');
               });
         }
      });
   });
};

// failure to load player.  fallback to playlist samples.
bitmunk.player.onerror = function()
{
   $(document).ready(function()
   {
      // hide all non-play buttons
      $('a.sampleControlButton').not('.playerPlay').hide();
      // change play to single wide button class
      $('a.sampleControlButton.playerPlay')
         .addClass('singleSampleControlButton');
      // show non-flash elements
      $('.sampleNoFlash').show();
   });
};
