(function( Popcorn ) {

  // combines calls of two function calls into one
  var combineFn = function( first, second ) {

    first = first || Popcorn.nop;
    second = second || Popcorn.nop;

    return function() {

      first.apply( this, arguments );
      second.apply( this, arguments );
    };
  };

  //  ID string matching
  var rIdExp  = /^(#([\w\-\_\.]+))$/;

  Popcorn.player = function( name, player ) {

    // return early if a player already exists under this name
    if ( Popcorn[ name ] ) {

      return;
    }

    player = player || {};

    var playerFn = function( target, src, options ) {

      options = options || {};

      // List of events
      var date = new Date() / 1000,
          baselineTime = date,
          currentTime = 0,
          readyState = 0,
          volume = 1,
          muted = false,
          events = {},

          // The container div of the resource
          container = typeof target === "string" ? Popcorn.dom.find( target ) : target,
          basePlayer = {},
          timeout,
          popcorn;

      if ( !Object.prototype.__defineGetter__ ) {

        basePlayer = container || document.createElement( "div" );
      }

      // copies a div into the media object
      for( var val in container ) {

        // don't copy properties if using container as baseplayer
        if ( val in basePlayer ) {

          continue;
        }

        if ( typeof container[ val ] === "object" ) {

          basePlayer[ val ] = container[ val ];
        } else if ( typeof container[ val ] === "function" ) {

          basePlayer[ val ] = (function( value ) {

            // this is a stupid ugly kludgy hack in honour of Safari
            // in Safari a NodeList is a function, not an object
            if ( "length" in container[ value ] && !container[ value ].call ) {

              return container[ value ];
            } else {

              return function() {

                return container[ value ].apply( container, arguments );
              };
            }
          }( val ));
        } else {

          Popcorn.player.defineProperty( basePlayer, val, {
            get: (function( value ) {

              return function() {

                return container[ value ];
              };
            }( val )),
            set: Popcorn.nop,
            configurable: true
          });
        }
      }

      var timeupdate = function() {

        date = new Date() / 1000;

        if ( !basePlayer.paused ) {

          basePlayer.currentTime = basePlayer.currentTime + ( date - baselineTime );
          basePlayer.dispatchEvent( "timeupdate" );
          timeout = setTimeout( timeupdate, 10 );
        }

        baselineTime = date;
      };

      basePlayer.play = function() {

        this.paused = false;

        if ( basePlayer.readyState >= 4 ) {

          baselineTime = new Date() / 1000;
          basePlayer.dispatchEvent( "play" );
          timeupdate();
        }
      };

      basePlayer.pause = function() {

        this.paused = true;
        basePlayer.dispatchEvent( "pause" );
      };

      Popcorn.player.defineProperty( basePlayer, "currentTime", {
        get: function() {

          return currentTime;
        },
        set: function( val ) {

          // make sure val is a number
          currentTime = +val;
          basePlayer.dispatchEvent( "timeupdate" );

          return currentTime;
        },
        configurable: true
      });

      Popcorn.player.defineProperty( basePlayer, "volume", {
        get: function() {

          return volume;
        },
        set: function( val ) {

          // make sure val is a number
          volume = +val;
          basePlayer.dispatchEvent( "volumechange" );
          return volume;
        },
        configurable: true
      });

      Popcorn.player.defineProperty( basePlayer, "muted", {
        get: function() {

          return muted;
        },
        set: function( val ) {

          // make sure val is a number
          muted = +val;
          basePlayer.dispatchEvent( "volumechange" );
          return muted;
        },
        configurable: true
      });

      Popcorn.player.defineProperty( basePlayer, "readyState", {
        get: function() {

          return readyState;
        },
        set: function( val ) {

          readyState = val;
          return readyState;
        },
        configurable: true
      });

      // Adds an event listener to the object
      basePlayer.addEventListener = function( evtName, fn ) {

        if ( !events[ evtName ] ) {

          events[ evtName ] = [];
        }

        events[ evtName ].push( fn );
        return fn;
      };

      // Removes an event listener from the object
      basePlayer.removeEventListener = function( evtName, fn ) {

        var i,
            listeners = events[ evtName ];

        if ( !listeners ){

          return;
        }

        // walk backwards so we can safely splice
        for ( i = events[ evtName ].length - 1; i >= 0; i-- ) {

          if( fn === listeners[ i ] ) {

            listeners.splice(i, 1);
          }
        }

        return fn;
      };

      // Can take event object or simple string
      basePlayer.dispatchEvent = function( oEvent ) {

        var evt,
            self = this,
            eventInterface,
            eventName = oEvent.type;

        // A string was passed, create event object
        if ( !eventName ) {

          eventName = oEvent;
          eventInterface  = Popcorn.events.getInterface( eventName );

          if ( eventInterface ) {

            evt = document.createEvent( eventInterface );
            evt.initEvent( eventName, true, true, window, 1 );
          }
        }

        if ( events[ eventName ] ) {

          for ( var i = events[ eventName ].length - 1; i >= 0; i-- ) {

            events[ eventName ][ i ].call( self, evt, self );
          }
        }
      };

      // Attempt to get src from playerFn parameter
      basePlayer.src = src || "";
      basePlayer.duration = 0;
      basePlayer.paused = true;
      basePlayer.ended = 0;

      options && options.events && Popcorn.forEach( options.events, function( val, key ) {

        basePlayer.addEventListener( key, val, false );
      });

      // true and undefined returns on canPlayType means we should attempt to use it,
      // false means we cannot play this type
      if ( player._canPlayType( container.nodeName, src ) !== false ) {

        if ( player._setup ) {

          player._setup.call( basePlayer, options );
        } else {

          // there is no setup, which means there is nothing to load
          basePlayer.readyState = 4;
          basePlayer.dispatchEvent( "loadedmetadata" );
          basePlayer.dispatchEvent( "loadeddata" );
          basePlayer.dispatchEvent( "canplaythrough" );
        }
      } else {

        // Asynchronous so that users can catch this event
        setTimeout( function() {
          basePlayer.dispatchEvent( "error" );
        }, 0 );
      }

      popcorn = new Popcorn.p.init( basePlayer, options );

      if ( player._teardown ) {

        popcorn.destroy = combineFn( popcorn.destroy, function() {

          player._teardown.call( basePlayer, options );
        });
      }

      return popcorn;
    };

    playerFn.canPlayType = player._canPlayType = player._canPlayType || Popcorn.nop;

    Popcorn[ name ] = Popcorn.player.registry[ name ] = playerFn;
  };

  Popcorn.player.registry = {};

  Popcorn.player.defineProperty = Object.defineProperty || function( object, description, options ) {

    object.__defineGetter__( description, options.get || Popcorn.nop );
    object.__defineSetter__( description, options.set || Popcorn.nop );
  };

  // player queue is to help players queue things like play and pause
  // HTML5 video's play and pause are asynch, but do fire in sequence
  // play() should really mean "requestPlay()" or "queuePlay()" and
  // stash a callback that will play the media resource when it's ready to be played
  Popcorn.player.playerQueue = function() {

    var _queue = [],
        _running = false;

    return {
      next: function() {

        _running = false;
        _queue.shift();
        _queue[ 0 ] && _queue[ 0 ]();
      },
      add: function( callback ) {

        _queue.push(function() {

          _running = true;
          callback && callback();
        });

        // if there is only one item on the queue, start it
        !_running && _queue[ 0 ]();
      }
    };
  };

  // Popcorn.smart will attempt to find you a wrapper or player. If it can't do that,
  // it will default to using an HTML5 video in the target.
  Popcorn.smart = function( target, src, options ) {
    var node = typeof target === "string" ? Popcorn.dom.find( target ) : target,
        i, srci, j, media, mediaWrapper, popcorn, srcLength, 
        // We leave HTMLVideoElement and HTMLAudioElement wrappers out
        // of the mix, since we'll default to HTML5 video if nothing
        // else works.  Waiting on #1254 before we add YouTube to this.
        wrappers = "HTMLYouTubeVideoElement HTMLVimeoVideoElement HTMLSoundCloudAudioElement HTMLNullVideoElement".split(" ");

    if ( !node ) {
      Popcorn.error( "Specified target `" + target + "` was not found." );
      return;
    }

    // If our src is not an array, create an array of one.
    src = typeof src === "string" ? [ src ] : src;

    // Loop through each src, and find the first playable.
    for ( i = 0, srcLength = src.length; i < srcLength; i++ ) {
      srci = src[ i ];

      // See if we can use a wrapper directly, if not, try players.
      for ( j = 0; j < wrappers.length; j++ ) {
        mediaWrapper = Popcorn[ wrappers[ j ] ];
        if ( mediaWrapper && mediaWrapper._canPlaySrc( srci ) === "probably" ) {
          media = mediaWrapper( node );
          popcorn = Popcorn( media, options );
          // Set src, but not until after we return the media so the caller
          // can get error events, if any.
          setTimeout( function() {
            media.src = srci;
          }, 0 );
          return popcorn;
        }
      }

      // No wrapper can play this, check players.
      for ( var key in Popcorn.player.registry ) {
        if ( Popcorn.player.registry.hasOwnProperty( key ) ) {
          if ( Popcorn.player.registry[ key ].canPlayType( node.nodeName, srci ) ) {
            // Popcorn.smart( player, src, /* options */ )
            return Popcorn[ key ]( node, srci, options );
          }
        }
      }
    }

    // If we don't have any players or wrappers that can handle this,
    // Default to using HTML5 video.  Similar to the HTMLVideoElement
    // wrapper, we put a video in the div passed to us via:
    // Popcorn.smart( div, src, options )
    var videoHTML,
        videoElement,
        videoID = Popcorn.guid( "popcorn-video-" ),
        videoHTMLContainer = document.createElement( "div" );

    videoHTMLContainer.style.width = "100%";
    videoHTMLContainer.style.height = "100%";

    // If we only have one source, do not bother with source elements.
    // This means we don't have the IE9 hack,
    // and we can properly listen to error events.
    // That way an error event can be told to backup to Flash if it fails.
    if ( src.length === 1 ) {
      videoElement = document.createElement( "video" );
      videoElement.id = videoID;
      node.appendChild( videoElement );
      setTimeout( function() {
        // Hack to decode html characters like &amp; to &
        var decodeDiv = document.createElement( "div" );
        decodeDiv.innerHTML = src[ 0 ];

        videoElement.src = decodeDiv.firstChild.nodeValue;
      }, 0 );
      return Popcorn( '#' + videoID, options );
    }

    node.appendChild( videoHTMLContainer );
    // IE9 doesn't like dynamic creation of source elements on <video>
    // so we do it in one shot via innerHTML.
    videoHTML = '<video id="' +  videoID + '" preload=auto autobuffer>';
    for ( i = 0, srcLength = src.length; i < srcLength; i++ ) {
      videoHTML += '<source src="' + src[ i ] + '">';
    }
    videoHTML += "</video>";
    videoHTMLContainer.innerHTML = videoHTML;

    if ( options && options.events && options.events.error ) {
      node.addEventListener( "error", options.events.error, false );
    }
    return Popcorn( '#' + videoID, options );
  };
})( Popcorn );