(function(){

    // Save platform functions that will be modified
    var _requestMediaKeySystemAccess = navigator.requestMediaKeySystemAccess.bind( navigator ),
        _setMediaKeys = HTMLMediaElement.prototype.setMediaKeys;

    // Allow us to modify the target of Events
    Object.defineProperties( Event.prototype, {
        target: {   get: function() { return this._target || this.currentTarget; },
                    set: function( newtarget ) { this._target = newtarget; } }
    } );

    var EventTarget = function(){
        this.listeners = {};
    };

    EventTarget.prototype.listeners = null;

    EventTarget.prototype.addEventListener = function(type, callback){
      if(!(type in this.listeners)) {
        this.listeners[type] = [];
      }
      this.listeners[type].push(callback);
    };

    EventTarget.prototype.removeEventListener = function(type, callback){
      if(!(type in this.listeners)) {
        return;
      }
      var stack = this.listeners[type];
      for(var i = 0, l = stack.length; i < l; i++){
        if(stack[i] === callback){
          stack.splice(i, 1);
          return this.removeEventListener(type, callback);
        }
      }
    };

    EventTarget.prototype.dispatchEvent = function(event){
      if(!(event.type in this.listeners)) {
        return;
      }
      var stack = this.listeners[event.type];
      event.target = this;
      for(var i = 0, l = stack.length; i < l; i++) {
        stack[i].call(this, event);
      }
    };

    function MediaKeySystemAccessProxy( keysystem, access, configuration )
    {
        this._keysystem = keysystem;
        this._access = access;
        this._configuration = configuration;
    }

    Object.defineProperties( MediaKeySystemAccessProxy.prototype, {
        keysystem: { get: function() { return this._keysystem; } }
    });

    MediaKeySystemAccessProxy.prototype.getConfiguration = function getConfiguration()
    {
        return this._configuration;
    };

    MediaKeySystemAccessProxy.prototype.createMediaKeys = function createMediaKeys()
    {
        return new Promise( function( resolve, reject ) {

            this._access.createMediaKeys()
            .then( function( mediaKeys ) { resolve( new MediaKeysProxy( mediaKeys ) ); })
            .catch( function( error ) { reject( error ); } );

        }.bind( this ) );
    };

    function MediaKeysProxy( mediaKeys )
    {
        this._mediaKeys = mediaKeys;
        this._sessions = [ ];
        this._videoelement = undefined;
    }

    MediaKeysProxy.prototype._setVideoElement = function _setVideoElement( videoElement )
    {
        if ( videoElement !== this._videoelement )
        {
            this._videoelement = videoElement;
        }
    };


    MediaKeysProxy.prototype._removeSession = function _removeSession( session )
    {
        var index = this._sessions.indexOf( session );
        if ( index !== -1 ) this._sessions.splice( index, 1 );
    };

    MediaKeysProxy.prototype.createSession = function createSession( sessionType )
    {
        if ( !sessionType || sessionType === 'temporary' ) return this._mediaKeys.createSession();

        var session = new MediaKeySessionProxy( this, sessionType );
        this._sessions.push( session );

        return session;
    };

    MediaKeysProxy.prototype.setServerCertificate = function setServerCertificate( certificate )
    {
        return this._mediaKeys.setServerCertificate( certificate );
    };

    function MediaKeySessionProxy( mediaKeysProxy, sessionType )
    {
        EventTarget.call( this );

        this._mediaKeysProxy = mediaKeysProxy
        this._sessionType = sessionType;
        this._sessionId = "";

        // MediaKeySessionProxy states
        // 'created' - After initial creation
        // 'loading' - Persistent license session waiting for key message to load stored keys
        // 'active' - Normal active state - proxy all key messages
        // 'removing' - Release message generated, waiting for ack
        // 'closed' - Session closed
        this._state = 'created';

        this._closed = new Promise( function( resolve ) { this._resolveClosed = resolve; }.bind( this ) );
    }

    MediaKeySessionProxy.prototype = Object.create( EventTarget.prototype );

    Object.defineProperties( MediaKeySessionProxy.prototype, {

        sessionId:  { get: function() { return this._sessionId; } },
        expiration: { get: function() { return NaN; } },
        closed:     { get: function() { return this._closed; } },
        keyStatuses:{ get: function() { return this._session.keyStatuses; } },       // TODO this will fail if examined too early
        _kids:      { get: function() { return this._keys.map( function( key ) { return key.kid; } ); } },
    });

    MediaKeySessionProxy.prototype._createSession = function _createSession()
    {
        this._session = this._mediaKeysProxy._mediaKeys.createSession();

        this._session.addEventListener( 'message', MediaKeySessionProxy.prototype._onMessage.bind( this ) );
        this._session.addEventListener( 'keystatuseschange', MediaKeySessionProxy.prototype._onKeyStatusesChange.bind( this ) );
    };

    MediaKeySessionProxy.prototype._onMessage = function _onMessage( event )
    {
        switch( this._state )
        {
            case 'loading':
                this._session.update( toUtf8( { keys: this._keys } ) )
                .then( function() {
                    this._state = 'active';
                    this._loaded( true );
                }.bind(this)).catch( this._loadfailed );

                break;

            case 'active':
                this.dispatchEvent( event );
                break;

            default:
                // Swallow the event
                break;
        }
    };

    MediaKeySessionProxy.prototype._onKeyStatusesChange = function _onKeyStatusesChange( event )
    {
        switch( this._state )
        {
            case 'active' :
            case 'removing' :
                this.dispatchEvent( event );
                break;

            default:
                // Swallow the event
                break;
        }
    };

    MediaKeySessionProxy.prototype._queueMessage = function _queueMessage( messageType, message )
    {
        setTimeout( function() {

            var messageAsArray = toUtf8( message ).buffer;

            this.dispatchEvent( new MediaKeyMessageEvent( 'message', { messageType: messageType, message: messageAsArray } ) );

        }.bind( this ) );
    };

    function _storageKey( sessionId )
    {
        return sessionId;
    }

    MediaKeySessionProxy.prototype._store = function _store()
    {
        var data = { keys: this._keys };

        window.localStorage.setItem( _storageKey( this._sessionId ), JSON.stringify( data ) );
    };

    MediaKeySessionProxy.prototype._load = function _load( sessionId )
    {
        var store = window.localStorage.getItem( _storageKey( sessionId ) );
        if ( store === null ) return false;

        var data;
        try { data = JSON.parse( store ) } catch( error ) {
            return false;
        }

        this._sessionType = 'persistent-license';
        this._keys = data.keys;

        return true;
    };

    MediaKeySessionProxy.prototype._clear = function _clear()
    {
        window.localStorage.removeItem( _storageKey( this._sessionId ) );
    };

    MediaKeySessionProxy.prototype.generateRequest = function generateRequest( initDataType, initData )
    {
        if ( this._state !== 'created' ) return Promise.reject( new InvalidStateError() );

        this._createSession();

        this._state = 'active';

        return this._session.generateRequest( initDataType, initData )
        .then( function() {
            this._sessionId = Math.random().toString(36).slice(2);
        }.bind( this ) );
    };

    MediaKeySessionProxy.prototype.load = function load( sessionId )
    {
        if ( this._state !== 'created' ) return Promise.reject( new InvalidStateError() );

        return new Promise( function( resolve, reject ) {

            try
            {
                if ( !this._load( sessionId ) )
                {
                    resolve( false );

                    return;
                }

                this._sessionId = sessionId;


                this._createSession();

                this._state = 'loading';
                this._loaded = resolve;
                this._loadfailed = reject;

                var initData = { kids: this._kids };

                this._session.generateRequest( 'keyids', toUtf8( initData ) );
            }
            catch( error )
            {
                reject( error );
            }
        }.bind( this ) );
    };

    MediaKeySessionProxy.prototype.update = function update( response )
    {
        return new Promise( function( resolve, reject ) {

            switch( this._state ) {

                case 'active' :

                    var message = fromUtf8( response );

                    // JSON Web Key Set
                    this._keys = message.keys;

                    this._store();

                    resolve( this._session.update( response ) );

                    break;

                case 'removing' :

                    this._state = 'closed';

                    this._clear();

                    this._mediaKeysProxy._removeSession( this );

                    this._resolveClosed();

                    delete this._session;

                    resolve();

                    break;

                default:
                    reject( new InvalidStateError() );
            }

        }.bind( this ) );
    };

    MediaKeySessionProxy.prototype.close = function close()
    {
        if ( this._state === 'closed' ) return Promise.resolve();

        this._state = 'closed';

        this._mediaKeysProxy._removeSession( this );

        this._resolveClosed();

        var session = this._session;
        if ( !session ) return Promise.resolve();

        this._session = undefined;

        return session.close();
    };

    MediaKeySessionProxy.prototype.remove = function remove()
    {
        if ( this._state !== 'active' || !this._session ) return Promise.reject( new DOMException('InvalidStateError('+this._state+')') );

        this._state = 'removing';

        this._mediaKeysProxy._removeSession( this );

        return this._session.close()
        .then( function() {

            var msg = { kids: this._kids };

            this._queueMessage( 'license-release', msg );

        }.bind( this ) )
    };

    HTMLMediaElement.prototype.setMediaKeys = function setMediaKeys( mediaKeys )
    {
        if ( mediaKeys instanceof MediaKeysProxy )
        {
            mediaKeys._setVideoElement( this );
            return _setMediaKeys.call( this, mediaKeys._mediaKeys );
        }
        else
        {
            return _setMediaKeys.call( this, mediaKeys );
        }
    };

    navigator.requestMediaKeySystemAccess = function( keysystem, configurations )
    {
        // First, see if this is supported by the platform
        return new Promise( function( resolve, reject ) {

            _requestMediaKeySystemAccess( keysystem, configurations )
            .then( function( access ) { resolve( access ); } )
            .catch( function( error ) {

                if ( error instanceof TypeError ) reject( error );

                if ( keysystem !== 'org.w3.clearkey' ) reject( error );

                if ( !configurations.some( is_persistent_configuration ) ) reject( error );

                // Shallow copy the configurations, swapping out the labels and omitting the sessiontypes
                var configurations_copy = configurations.map( function( config, index ) {

                    var config_copy = copy_configuration( config );
                    config_copy.label = index.toString();
                    return config_copy;

                } );

                // And try again with these configurations
                _requestMediaKeySystemAccess( keysystem, configurations_copy )
                .then( function( access ) {

                    // Create the supported configuration based on the original request
                    var configuration = access.getConfiguration(),
                        original_configuration = configurations[ configuration.label ];

                    // If the original configuration did not need persistent session types, then we're done
                    if ( !is_persistent_configuration( original_configuration ) ) resolve( access );

                    // Create the configuration that we will return
                    var returned_configuration = copy_configuration( configuration );

                    if ( original_configuration.label )
                        returned_configuration.label = original_configuration;
                    else
                        delete returned_configuration.label;

                    returned_configuration.sessionTypes = original_configuration.sessionTypes;

                    resolve( new MediaKeySystemAccessProxy( keysystem, access, returned_configuration ) );
                } )
                .catch( function( error ) { reject( error ); } );
            } );
        } );
    };

    function is_persistent_configuration( configuration )
    {
        return configuration.sessionTypes &&
                ( configuration.sessionTypes.indexOf( 'persistent-license' ) !== -1 );
    }

    function copy_configuration( src )
    {
        var dst = {};
        [ 'label', 'initDataTypes', 'audioCapabilities', 'videoCapabilities', 'distinctiveIdenfifier', 'persistentState' ]
        .forEach( function( item ) { if ( src[item] ) dst[item] = src[item]; } );
        return dst;
    }
}());
